2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-02 13:28:49 +00:00

Return Order - Improvements (#8590)

* 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
This commit is contained in:
Oliver 2024-11-29 17:06:35 +11:00 committed by GitHub
parent dd9a6a8a2d
commit 20d862e350
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 156 additions and 13 deletions

View File

@ -1,13 +1,16 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # 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.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ 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 v289 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8570
- Enable status change when transferring stock items - Enable status change when transferring stock items

View File

@ -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'),
),
]

View File

@ -2391,6 +2391,14 @@ class ReturnOrder(TotalPriceMixin, Order):
stock_item = line.item 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') status = kwargs.get('status')
if status is None: if status is None:
@ -2423,7 +2431,7 @@ class ReturnOrder(TotalPriceMixin, Order):
line.received_date = InvenTree.helpers.current_date() line.received_date = InvenTree.helpers.current_date()
line.save() 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 users
notify_responsible( notify_responsible(
@ -2452,9 +2460,22 @@ class ReturnOrderLineItem(OrderLineItem):
"""Perform extra validation steps for the ReturnOrderLineItem model.""" """Perform extra validation steps for the ReturnOrderLineItem model."""
super().clean() 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({ 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( order = models.ForeignKey(
@ -2473,6 +2494,15 @@ class ReturnOrderLineItem(OrderLineItem):
help_text=_('Select item to return from customer'), 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( received_date = models.DateField(
null=True, null=True,
blank=True, blank=True,

View File

@ -2040,6 +2040,7 @@ class ReturnOrderLineItemSerializer(
'order_detail', 'order_detail',
'item', 'item',
'item_detail', 'item_detail',
'quantity',
'received_date', 'received_date',
'outcome', 'outcome',
'part_detail', 'part_detail',
@ -2070,9 +2071,15 @@ class ReturnOrderLineItemSerializer(
self.fields.pop('part_detail', None) self.fields.pop('part_detail', None)
order_detail = ReturnOrderSerializer(source='order', many=False, read_only=True) 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( item_detail = stock.serializers.StockItemSerializer(
source='item', many=False, read_only=True source='item', many=False, read_only=True
) )
part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True) part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True)
price = InvenTreeMoneySerializer(allow_null=True) price = InvenTreeMoneySerializer(allow_null=True)

View File

@ -2395,6 +2395,71 @@ class ReturnOrderTests(InvenTreeAPITestCase):
self.assertEqual(deltas['location'], 1) self.assertEqual(deltas['location'], 1)
self.assertEqual(deltas['returnorder'], rma.pk) 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): def test_ro_calendar(self):
"""Test the calendar export endpoint.""" """Test the calendar export endpoint."""
# Full test is in test_po_calendar. Since these use the same backend, test only # Full test is in test_po_calendar. Since these use the same backend, test only

View File

@ -106,10 +106,10 @@ export function useReturnOrderLineItemFields({
item: { item: {
filters: { filters: {
customer: customerId, customer: customerId,
part_detail: true, part_detail: true
serialized: true
} }
}, },
quantity: {},
reference: {}, reference: {},
outcome: { outcome: {
hidden: create == true hidden: create == true
@ -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 ( return (
<> <>
<Table.Tr> <Table.Tr>
@ -160,7 +168,7 @@ function ReturnOrderLineItemFormRow({
<div>{record.part_detail.name}</div> <div>{record.part_detail.name}</div>
</Flex> </Flex>
</Table.Td> </Table.Td>
<Table.Td># {record.item_detail.serial}</Table.Td> <Table.Td>{quantityDisplay}</Table.Td>
<Table.Td> <Table.Td>
<StandaloneField <StandaloneField
fieldDefinition={{ fieldDefinition={{
@ -209,7 +217,7 @@ export function useReceiveReturnOrderLineItems(
/> />
); );
}, },
headers: [t`Part`, t`Stock Item`, t`Status`] headers: [t`Part`, t`Quantity`, t`Status`]
}, },
location: { location: {
filters: { filters: {

View File

@ -111,13 +111,21 @@ export default function ReturnOrderLineItemTable({
}, },
{ {
accessor: 'item_detail.serial', accessor: 'item_detail.serial',
title: t`Serial Number`, title: t`Quantity`,
switchable: false switchable: false,
render: (record: any) => {
if (record.item_detail.serial && record.quantity == 1) {
return `# ${record.item_detail.serial}`;
} else {
return record.quantity;
}
}
}, },
StatusColumn({ StatusColumn({
model: ModelType.stockitem, model: ModelType.stockitem,
sortable: false, sortable: false,
accessor: 'item_detail.status' accessor: 'item_detail.status',
title: t`Status`
}), }),
ReferenceColumn({}), ReferenceColumn({}),
StatusColumn({ StatusColumn({
@ -201,7 +209,10 @@ export default function ReturnOrderLineItemTable({
return [ return [
{ {
hidden: received || !user.hasChangeRole(UserRoles.return_order), hidden:
received ||
!inProgress ||
!user.hasChangeRole(UserRoles.return_order),
title: t`Receive Item`, title: t`Receive Item`,
icon: <IconSquareArrowRight />, icon: <IconSquareArrowRight />,
onClick: () => { onClick: () => {
@ -225,7 +236,7 @@ export default function ReturnOrderLineItemTable({
}) })
]; ];
}, },
[user] [user, inProgress]
); );
return ( return (