Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/backend/InvenTree/order/api.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a full implementation; this does not pass QC checks due to a missing api_version bump amongst other basic things mentioned in the contribution guidelines

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matmair thanks for the review and genuinely sorry for my previous behaviour, I am not going to ask you for assignment, I will complete this pr as you requested changes and then you can decide if I am eligible

Thanks 🙏

Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
),
),
]
72 changes: 71 additions & 1 deletion src/backend/InvenTree/order/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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()
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
25 changes: 25 additions & 0 deletions src/backend/InvenTree/order/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2153,7 +2153,10 @@ class Meta:
'customer',
'customer_detail',
'customer_reference',
'is_repair',
'order_currency',
'technician',
'technician_detail',
'total_price',
'updated_at',
])
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -2348,6 +2371,8 @@ class Meta:
'outcome',
'price',
'price_currency',
'repair_description',
'symptom',
# Filterable detail fields
'item_detail',
'part_detail',
Expand Down
13 changes: 12 additions & 1 deletion src/backend/InvenTree/order/status_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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):
Expand Down