From 1b183474b208d4d31b01d5bce12ff23f4465fb96 Mon Sep 17 00:00:00 2001 From: Sanchit Rishi Date: Sat, 6 Jun 2026 01:58:27 +0530 Subject: [PATCH] Add repair order fields to ReturnOrder model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the existing ReturnOrder with repair-specific capabilities: - Added technician (FK→User) and is_repair (Boolean) fields - Added symptom and repair_description fields to line items - Added AWAITING_PARTS and READY_FOR_PICKUP statuses - Added await-parts and mark-ready API endpoints Ref: #12064 --- src/backend/InvenTree/order/api.py | 22 ++++++ .../0121_return_order_repair_fields.py | 56 +++++++++++++++ src/backend/InvenTree/order/models.py | 72 ++++++++++++++++++- src/backend/InvenTree/order/serializers.py | 25 +++++++ src/backend/InvenTree/order/status_codes.py | 13 +++- 5 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 src/backend/InvenTree/order/migrations/0121_return_order_repair_fields.py diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index dbe2127a33cb..78bbfcaeca3f 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -1715,6 +1715,18 @@ class ReturnOrderIssue(ReturnOrderContextMixin, CreateAPI): serializer_class = serializers.ReturnOrderIssueSerializer +class ReturnOrderAwaitParts(ReturnOrderContextMixin, CreateAPI): + """API endpoint to mark a ReturnOrder as awaiting parts.""" + + serializer_class = serializers.ReturnOrderAwaitPartsSerializer + + +class ReturnOrderMarkReady(ReturnOrderContextMixin, CreateAPI): + """API endpoint to mark a ReturnOrder as ready for pickup.""" + + serializer_class = serializers.ReturnOrderMarkReadySerializer + + class ReturnOrderReceive(ReturnOrderContextMixin, CreateAPI): """API endpoint to receive items against a ReturnOrder.""" @@ -2741,6 +2753,16 @@ def item_link(self, item): ReturnOrderIssue.as_view(), name='api-return-order-issue', ), + path( + 'await-parts/', + ReturnOrderAwaitParts.as_view(), + name='api-return-order-await-parts', + ), + path( + 'mark-ready/', + ReturnOrderMarkReady.as_view(), + name='api-return-order-mark-ready', + ), path( 'receive/', ReturnOrderReceive.as_view(), diff --git a/src/backend/InvenTree/order/migrations/0121_return_order_repair_fields.py b/src/backend/InvenTree/order/migrations/0121_return_order_repair_fields.py new file mode 100644 index 000000000000..f938bc61ff95 --- /dev/null +++ b/src/backend/InvenTree/order/migrations/0121_return_order_repair_fields.py @@ -0,0 +1,56 @@ +# Generated by Django 5.2.14 on 2026-06-06 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0120_purchaseorder_tags_returnorder_tags_salesorder_tags_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='returnorder', + name='is_repair', + field=models.BooleanField( + default=False, + help_text='Mark this order as a repair order', + verbose_name='Repair Order', + ), + ), + migrations.AddField( + model_name='returnorder', + name='technician', + field=models.ForeignKey( + blank=True, + help_text='User assigned to perform the repair', + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='repair_orders', + to=settings.AUTH_USER_MODEL, + verbose_name='Technician', + ), + ), + migrations.AddField( + model_name='returnorderlineitem', + name='symptom', + field=models.TextField( + blank=True, + help_text='Customer-reported issue or symptom', + verbose_name='Symptom', + ), + ), + migrations.AddField( + model_name='returnorderlineitem', + name='repair_description', + field=models.TextField( + blank=True, + help_text='Description of the repair work performed', + verbose_name='Repair Description', + ), + ), + ] \ No newline at end of file diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index 1109e65a021a..fa092c0fc7e2 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -3012,6 +3012,22 @@ def company(self): help_text=_('Date order was completed'), ) + technician = models.ForeignKey( + User, + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name='repair_orders', + verbose_name=_('Technician'), + help_text=_('User assigned to perform the repair'), + ) + + is_repair = models.BooleanField( + default=False, + verbose_name=_('Repair Order'), + help_text=_('Mark this order as a repair order'), + ) + # region state changes @property def is_pending(self): @@ -3034,6 +3050,7 @@ def can_hold(self): return self.status in [ ReturnOrderStatus.PENDING.value, ReturnOrderStatus.IN_PROGRESS.value, + ReturnOrderStatus.AWAITING_PARTS.value, ] def _action_hold(self, *args, **kwargs): @@ -3068,7 +3085,11 @@ def _action_cancel(self, *args, **kwargs): def _action_complete(self, *args, **kwargs): """Complete this ReturnOrder (if not already completed).""" - if self.status == ReturnOrderStatus.IN_PROGRESS.value: + if self.status in [ + ReturnOrderStatus.IN_PROGRESS.value, + ReturnOrderStatus.AWAITING_PARTS.value, + ReturnOrderStatus.READY_FOR_PICKUP.value, + ]: self.status = ReturnOrderStatus.COMPLETE.value self.complete_date = InvenTree.helpers.current_date() self.save() @@ -3085,6 +3106,7 @@ def can_issue(self): return self.status in [ ReturnOrderStatus.PENDING.value, ReturnOrderStatus.ON_HOLD.value, + ReturnOrderStatus.AWAITING_PARTS.value, ] def _action_place(self, *args, **kwargs): @@ -3133,6 +3155,42 @@ def cancel_order(self): self.status, ReturnOrderStatus.CANCELLED.value, self, self._action_cancel ) + def _action_await_parts(self, *args, **kwargs): + """Transition this order to AWAITING_PARTS status.""" + if self.status == ReturnOrderStatus.IN_PROGRESS.value: + self.status = ReturnOrderStatus.AWAITING_PARTS.value + self.save() + + trigger_event(ReturnOrderEvents.HOLD, id=self.pk) + + def _action_ready(self, *args, **kwargs): + """Transition this order to READY_FOR_PICKUP status.""" + if self.status == ReturnOrderStatus.IN_PROGRESS.value: + self.status = ReturnOrderStatus.READY_FOR_PICKUP.value + self.save() + + trigger_event(ReturnOrderEvents.COMPLETED, id=self.pk) + + @transaction.atomic + def await_parts_order(self): + """Attempt to transition to AWAITING_PARTS status.""" + return self.handle_transition( + self.status, + ReturnOrderStatus.AWAITING_PARTS.value, + self, + self._action_await_parts, + ) + + @transaction.atomic + def mark_ready(self): + """Attempt to transition to READY_FOR_PICKUP status.""" + return self.handle_transition( + self.status, + ReturnOrderStatus.READY_FOR_PICKUP.value, + self, + self._action_ready, + ) + # endregion @transaction.atomic @@ -3309,6 +3367,18 @@ def received(self): help_text=_('Cost associated with return or repair for this line item'), ) + symptom = models.TextField( + blank=True, + verbose_name=_('Symptom'), + help_text=_('Customer-reported issue or symptom'), + ) + + repair_description = models.TextField( + blank=True, + verbose_name=_('Repair Description'), + help_text=_('Description of the repair work performed'), + ) + class ReturnOrderExtraLine(OrderExtraLine): """Model for a single ExtraLine in a ReturnOrder.""" diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 108a6d51c147..1d92ea505060 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -2153,7 +2153,10 @@ class Meta: 'customer', 'customer_detail', 'customer_reference', + 'is_repair', 'order_currency', + 'technician', + 'technician_detail', 'total_price', 'updated_at', ]) @@ -2199,6 +2202,10 @@ def annotate_queryset(queryset): prefetch_fields=['customer'], ) + technician_detail = UserSerializer( + source='technician', read_only=True, allow_null=True + ) + class ReturnOrderHoldSerializer(OrderAdjustSerializer): """Serializers for holding a ReturnOrder.""" @@ -2232,6 +2239,22 @@ def save(self): self.order.complete_order() +class ReturnOrderAwaitPartsSerializer(OrderAdjustSerializer): + """Serializer for marking a ReturnOrder as awaiting parts.""" + + def save(self): + """Save the serializer to mark the order as awaiting parts.""" + self.order.await_parts_order() + + +class ReturnOrderMarkReadySerializer(OrderAdjustSerializer): + """Serializer for marking a ReturnOrder as ready for pickup.""" + + def save(self): + """Save the serializer to mark the order as ready for pickup.""" + self.order.mark_ready() + + class ReturnOrderLineItemReceiveSerializer(serializers.Serializer): """Serializer for receiving a single line item against a ReturnOrder.""" @@ -2348,6 +2371,8 @@ class Meta: 'outcome', 'price', 'price_currency', + 'repair_description', + 'symptom', # Filterable detail fields 'item_detail', 'part_detail', diff --git a/src/backend/InvenTree/order/status_codes.py b/src/backend/InvenTree/order/status_codes.py index d8893a1fa3c8..152bf4c273d1 100644 --- a/src/backend/InvenTree/order/status_codes.py +++ b/src/backend/InvenTree/order/status_codes.py @@ -80,7 +80,14 @@ class ReturnOrderStatus(StatusCode): ON_HOLD = 25, _('On Hold'), ColorEnum.warning + # Waiting for parts to arrive before repair can continue + AWAITING_PARTS = 27, _('Awaiting Parts'), ColorEnum.warning + COMPLETE = 30, _('Complete'), ColorEnum.success + + # Item has been repaired and is ready to return to customer + READY_FOR_PICKUP = 35, _('Ready for Pickup'), ColorEnum.info + CANCELLED = 40, _('Cancelled'), ColorEnum.danger @@ -91,9 +98,13 @@ class ReturnOrderStatusGroups: ReturnOrderStatus.PENDING.value, ReturnOrderStatus.ON_HOLD.value, ReturnOrderStatus.IN_PROGRESS.value, + ReturnOrderStatus.AWAITING_PARTS.value, ] - COMPLETE = [ReturnOrderStatus.COMPLETE.value] + COMPLETE = [ + ReturnOrderStatus.COMPLETE.value, + ReturnOrderStatus.READY_FOR_PICKUP.value, + ] class ReturnOrderLineStatus(StatusCode):