mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-16 17:28:11 +00:00
Fix for shipping virtual parts (#10853)
* Additional checks for virtual parts in sales order process * Prevent allocation against virtual parts * Fix order of operations * Adjust part form fields based on selections * Prevent order locking * Updated playwright tests * Add unit test
This commit is contained in:
@@ -1388,8 +1388,13 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
return any(line.is_overallocated() for line in self.lines.all())
|
||||
|
||||
def is_completed(self) -> bool:
|
||||
"""Check if this order is "shipped" (all line items delivered)."""
|
||||
return all(line.is_completed() for line in self.lines.all())
|
||||
"""Check if this order is "shipped" (all line items delivered).
|
||||
|
||||
Note: Any "virtual" parts are ignored in this calculation.
|
||||
"""
|
||||
lines = self.lines.all().filter(part__virtual=False)
|
||||
|
||||
return all(line.is_completed() for line in lines)
|
||||
|
||||
def can_complete(
|
||||
self, raise_error: bool = False, allow_incomplete_lines: bool = False
|
||||
@@ -1424,10 +1429,15 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
_('Order cannot be completed as there are incomplete allocations')
|
||||
)
|
||||
|
||||
if not allow_incomplete_lines and self.pending_line_count > 0:
|
||||
raise ValidationError(
|
||||
_('Order cannot be completed as there are incomplete line items')
|
||||
)
|
||||
if not allow_incomplete_lines:
|
||||
pending_lines = self.pending_line_items().exclude(part__virtual=True)
|
||||
|
||||
if pending_lines.count() > 0:
|
||||
raise ValidationError(
|
||||
_(
|
||||
'Order cannot be completed as there are incomplete line items'
|
||||
)
|
||||
)
|
||||
|
||||
except ValidationError as e:
|
||||
if raise_error:
|
||||
@@ -1484,6 +1494,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
|
||||
trigger_event(SalesOrderEvents.HOLD, id=self.pk)
|
||||
|
||||
@transaction.atomic
|
||||
def _action_complete(self, *args, **kwargs):
|
||||
"""Mark this order as "complete."""
|
||||
user = kwargs.pop('user', None)
|
||||
@@ -1495,6 +1506,16 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
get_global_setting('SALESORDER_SHIP_COMPLETE')
|
||||
)
|
||||
|
||||
# Update line items
|
||||
for line in self.lines.all():
|
||||
# Mark any "virtual" parts as shipped at this point
|
||||
if line.part and line.part.virtual and line.shipped != line.quantity:
|
||||
line.shipped = line.quantity
|
||||
line.save()
|
||||
|
||||
if line.part:
|
||||
line.part.schedule_pricing_update(create=True)
|
||||
|
||||
if bypass_shipped or self.status == SalesOrderStatus.SHIPPED:
|
||||
self.status = SalesOrderStatus.COMPLETE.value
|
||||
else:
|
||||
@@ -1506,11 +1527,6 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
|
||||
self.save()
|
||||
|
||||
# Schedule pricing update for any referenced parts
|
||||
for line in self.lines.all():
|
||||
if line.part:
|
||||
line.part.schedule_pricing_update(create=True)
|
||||
|
||||
trigger_event(SalesOrderEvents.COMPLETED, id=self.pk)
|
||||
|
||||
return True
|
||||
@@ -1574,7 +1590,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
"""Attempt to transition to COMPLETED status."""
|
||||
return self.handle_transition(
|
||||
self.status,
|
||||
SalesOrderStatus.COMPLETED.value,
|
||||
SalesOrderStatus.COMPLETE.value,
|
||||
self,
|
||||
self._action_complete,
|
||||
user=user,
|
||||
|
||||
@@ -488,3 +488,45 @@ class SalesOrderTest(InvenTreeTestCase):
|
||||
p.set_metadata(k, k)
|
||||
|
||||
self.assertEqual(len(p.metadata.keys()), 4)
|
||||
|
||||
def test_virtual_parts(self):
|
||||
"""Test shipment of virtual parts against an order."""
|
||||
vp = Part.objects.create(
|
||||
name='Virtual Part',
|
||||
salable=True,
|
||||
virtual=True,
|
||||
description='A virtual part that I sell',
|
||||
)
|
||||
|
||||
so = SalesOrder.objects.create(
|
||||
customer=self.customer,
|
||||
reference='SO-VIRTUAL-1',
|
||||
customer_reference='VIRT-001',
|
||||
)
|
||||
|
||||
for qty in [5, 10, 15]:
|
||||
SalesOrderLineItem.objects.create(order=so, part=vp, quantity=qty)
|
||||
|
||||
# Delete pending shipments (if any)
|
||||
so.shipments.all().delete()
|
||||
|
||||
for line in so.lines.all():
|
||||
self.assertEqual(line.part.virtual, True)
|
||||
self.assertEqual(line.shipped, 0)
|
||||
self.assertGreater(line.quantity, 0)
|
||||
self.assertTrue(line.is_fully_allocated())
|
||||
self.assertTrue(line.is_completed())
|
||||
|
||||
# Complete the order
|
||||
so.ship_order(None)
|
||||
|
||||
so.refresh_from_db()
|
||||
self.assertEqual(so.status, status.SalesOrderStatus.SHIPPED)
|
||||
|
||||
so.complete_order(None)
|
||||
so.refresh_from_db()
|
||||
self.assertEqual(so.status, status.SalesOrderStatus.COMPLETE)
|
||||
|
||||
# Ensure that virtual line item quantity values have been updated
|
||||
for line in so.lines.all():
|
||||
self.assertEqual(line.shipped, line.quantity)
|
||||
|
||||
Reference in New Issue
Block a user