Skip to content

Commit

Permalink
Return Order - Improvements (#8590)
Browse files Browse the repository at this point in the history
* Increase query limit

* Add "quantity" field to ReturnOrderLineItem model

* Add 'quantity' to serializer

* Optionally split stock when returning from customer

* Update the line item when splitting

* PUI updates

* Bump API version

* Add unit test
  • Loading branch information
SchrodingersGat authored Nov 29, 2024
1 parent dd9a6a8 commit 20d862e
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 13 deletions.
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 289
INVENTREE_API_VERSION = 290

"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""


INVENTREE_API_TEXT = """
v290 - 2024-11-29 : https://github.com/inventree/InvenTree/pull/8590
- Adds "quantity" field to ReturnOrderLineItem model and API
v289 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8570
- Enable status change when transferring stock items
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.16 on 2024-11-29 00:37

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('order', '0103_alter_salesorderallocation_shipment'),
]

operations = [
migrations.AlterField(
model_name='returnorderlineitem',
name='quantity',
field=models.DecimalField(decimal_places=5, default=1, help_text='Quantity to return', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity'),
),
]
36 changes: 33 additions & 3 deletions src/backend/InvenTree/order/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2391,6 +2391,14 @@ def receive_line_item(self, line, location, user, **kwargs):

stock_item = line.item

if not stock_item.serialized and line.quantity < stock_item.quantity:
# Split the stock item if we are returning less than the full quantity
stock_item = stock_item.splitStock(line.quantity, user=user)

# Update the line item to point to the *new* stock item
line.item = stock_item
line.save()

status = kwargs.get('status')

if status is None:
Expand Down Expand Up @@ -2423,7 +2431,7 @@ def receive_line_item(self, line, location, user, **kwargs):
line.received_date = InvenTree.helpers.current_date()
line.save()

trigger_event(ReturnOrderEvents.RECEIVED, id=self.pk)
trigger_event(ReturnOrderEvents.RECEIVED, id=self.pk, line_item_id=line.pk)

# Notify responsible users
notify_responsible(
Expand Down Expand Up @@ -2452,9 +2460,22 @@ def clean(self):
"""Perform extra validation steps for the ReturnOrderLineItem model."""
super().clean()

if self.item and not self.item.serialized:
if not self.item:
raise ValidationError({'item': _('Stock item must be specified')})

if self.quantity > self.item.quantity:
raise ValidationError({
'item': _('Only serialized items can be assigned to a Return Order')
'quantity': _('Return quantity exceeds stock quantity')
})

if self.quantity <= 0:
raise ValidationError({
'quantity': _('Return quantity must be greater than zero')
})

if self.item.serialized and self.quantity != 1:
raise ValidationError({
'quantity': _('Invalid quantity for serialized stock item')
})

order = models.ForeignKey(
Expand All @@ -2473,6 +2494,15 @@ def clean(self):
help_text=_('Select item to return from customer'),
)

quantity = models.DecimalField(
verbose_name=('Quantity'),
help_text=('Quantity to return'),
max_digits=15,
decimal_places=5,
validators=[MinValueValidator(0)],
default=1,
)

received_date = models.DateField(
null=True,
blank=True,
Expand Down
7 changes: 7 additions & 0 deletions src/backend/InvenTree/order/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2040,6 +2040,7 @@ class Meta:
'order_detail',
'item',
'item_detail',
'quantity',
'received_date',
'outcome',
'part_detail',
Expand Down Expand Up @@ -2070,9 +2071,15 @@ def __init__(self, *args, **kwargs):
self.fields.pop('part_detail', None)

order_detail = ReturnOrderSerializer(source='order', many=False, read_only=True)

quantity = serializers.FloatField(
label=_('Quantity'), help_text=_('Quantity to return')
)

item_detail = stock.serializers.StockItemSerializer(
source='item', many=False, read_only=True
)

part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True)

price = InvenTreeMoneySerializer(allow_null=True)
Expand Down
65 changes: 65 additions & 0 deletions src/backend/InvenTree/order/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2395,6 +2395,71 @@ def receive(items, location=None, expected_code=400):
self.assertEqual(deltas['location'], 1)
self.assertEqual(deltas['returnorder'], rma.pk)

def test_receive_untracked(self):
"""Test that we can receive untracked items against a ReturnOrder.
Ref: https://github.com/inventree/InvenTree/pull/8590
"""
self.assignRole('return_order.add')
company = Company.objects.get(pk=4)

# Create a new ReturnOrder
rma = models.ReturnOrder.objects.create(
customer=company, description='A return order'
)

rma.issue_order()

# Create some new line items
part = Part.objects.get(pk=25)

n_items = part.stock_entries().count()

for idx in range(2):
stock_item = StockItem.objects.create(
part=part, customer=company, quantity=10
)

models.ReturnOrderLineItem.objects.create(
order=rma, item=stock_item, quantity=(idx + 1) * 5
)

self.assertEqual(part.stock_entries().count(), n_items + 2)

line_items = rma.lines.all()

# Receive items against the order
url = reverse('api-return-order-receive', kwargs={'pk': rma.pk})

LOCATION_ID = 1

self.post(
url,
{
'items': [
{'item': line.pk, 'status': StockStatus.DAMAGED.value}
for line in line_items
],
'location': LOCATION_ID,
},
expected_code=201,
)

# Due to the quantities received, we should have created 1 new stock item
self.assertEqual(part.stock_entries().count(), n_items + 3)

rma.refresh_from_db()

for line in rma.lines.all():
self.assertTrue(line.received)
self.assertIsNotNone(line.received_date)

# Check that the associated StockItem has been updated correctly
self.assertEqual(line.item.status, StockStatus.DAMAGED)
self.assertIsNone(line.item.customer)
self.assertIsNone(line.item.sales_order)
self.assertEqual(line.item.location.pk, LOCATION_ID)

def test_ro_calendar(self):
"""Test the calendar export endpoint."""
# Full test is in test_po_calendar. Since these use the same backend, test only
Expand Down
16 changes: 12 additions & 4 deletions src/frontend/src/forms/ReturnOrderForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ export function useReturnOrderLineItemFields({
item: {
filters: {
customer: customerId,
part_detail: true,
serialized: true
part_detail: true
}
},
quantity: {},
reference: {},
outcome: {
hidden: create == true
Expand Down Expand Up @@ -147,6 +147,14 @@ function ReturnOrderLineItemFormRow({
);
}, []);

const quantityDisplay = useMemo(() => {
if (record.item_detail?.serial && record.quantity == 1) {
return `# ${record.item_detail.serial}`;
} else {
return record.quantity;
}
}, [record.quantity, record.item_detail]);

return (
<>
<Table.Tr>
Expand All @@ -160,7 +168,7 @@ function ReturnOrderLineItemFormRow({
<div>{record.part_detail.name}</div>
</Flex>
</Table.Td>
<Table.Td># {record.item_detail.serial}</Table.Td>
<Table.Td>{quantityDisplay}</Table.Td>
<Table.Td>
<StandaloneField
fieldDefinition={{
Expand Down Expand Up @@ -209,7 +217,7 @@ export function useReceiveReturnOrderLineItems(
/>
);
},
headers: [t`Part`, t`Stock Item`, t`Status`]
headers: [t`Part`, t`Quantity`, t`Status`]
},
location: {
filters: {
Expand Down
21 changes: 16 additions & 5 deletions src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,21 @@ export default function ReturnOrderLineItemTable({
},
{
accessor: 'item_detail.serial',
title: t`Serial Number`,
switchable: false
title: t`Quantity`,
switchable: false,
render: (record: any) => {
if (record.item_detail.serial && record.quantity == 1) {
return `# ${record.item_detail.serial}`;
} else {
return record.quantity;
}
}
},
StatusColumn({
model: ModelType.stockitem,
sortable: false,
accessor: 'item_detail.status'
accessor: 'item_detail.status',
title: t`Status`
}),
ReferenceColumn({}),
StatusColumn({
Expand Down Expand Up @@ -201,7 +209,10 @@ export default function ReturnOrderLineItemTable({

return [
{
hidden: received || !user.hasChangeRole(UserRoles.return_order),
hidden:
received ||
!inProgress ||
!user.hasChangeRole(UserRoles.return_order),
title: t`Receive Item`,
icon: <IconSquareArrowRight />,
onClick: () => {
Expand All @@ -225,7 +236,7 @@ export default function ReturnOrderLineItemTable({
})
];
},
[user]
[user, inProgress]
);

return (
Expand Down

0 comments on commit 20d862e

Please sign in to comment.