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
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
|
||||
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:
|
||||
@ -2423,7 +2431,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
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(
|
||||
@ -2452,9 +2460,22 @@ class ReturnOrderLineItem(OrderLineItem):
|
||||
"""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(
|
||||
@ -2473,6 +2494,15 @@ class ReturnOrderLineItem(OrderLineItem):
|
||||
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,
|
||||
|
@ -2040,6 +2040,7 @@ class ReturnOrderLineItemSerializer(
|
||||
'order_detail',
|
||||
'item',
|
||||
'item_detail',
|
||||
'quantity',
|
||||
'received_date',
|
||||
'outcome',
|
||||
'part_detail',
|
||||
@ -2070,9 +2071,15 @@ class ReturnOrderLineItemSerializer(
|
||||
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)
|
||||
|
@ -2395,6 +2395,71 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
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
|
||||
|
@ -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
|
||||
@ -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>
|
||||
@ -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={{
|
||||
@ -209,7 +217,7 @@ export function useReceiveReturnOrderLineItems(
|
||||
/>
|
||||
);
|
||||
},
|
||||
headers: [t`Part`, t`Stock Item`, t`Status`]
|
||||
headers: [t`Part`, t`Quantity`, t`Status`]
|
||||
},
|
||||
location: {
|
||||
filters: {
|
||||
|
@ -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({
|
||||
@ -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: () => {
|
||||
@ -225,7 +236,7 @@ export default function ReturnOrderLineItemTable({
|
||||
})
|
||||
];
|
||||
},
|
||||
[user]
|
||||
[user, inProgress]
|
||||
);
|
||||
|
||||
return (
|
||||
|
Loading…
x
Reference in New Issue
Block a user