mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-01 21:16:46 +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:
parent
dd9a6a8a2d
commit
20d862e350
@ -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
|
||||||
|
|
||||||
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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: {
|
||||||
|
@ -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 (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user