2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-30 20:46:47 +00:00

[UI] Settings to control editing of "completed" orders (#9070)

* [UI] Settings to contrl editing of "completed" orders
Fixes #8976

* only check for completness if locking is enabled

* also lock lines

* allow editing of status to get an order "free" again
This commit is contained in:
Matthias Mair 2025-02-13 23:30:19 +01:00 committed by GitHub
parent f27a84a7e5
commit 301347f1d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 118 additions and 1 deletions

View File

@ -210,6 +210,7 @@ class Order(
"""
REQUIRE_RESPONSIBLE_SETTING = None
LOCK_SETTING = None
class Meta:
"""Metaclass options. Abstract ensures no database table is created."""
@ -219,14 +220,51 @@ class Order(
def save(self, *args, **kwargs):
"""Custom save method for the order models.
Ensures that the reference field is rebuilt whenever the instance is saved.
Enforces various business logics:
- Ensures the object is not locked
- Ensures that the reference field is rebuilt whenever the instance is saved.
"""
# check if we are updating the model, not adding it
update = self.pk is not None
# Locking
if update and self.check_locked(True):
# Ensure that order status can be changed still
if self.get_db_instance().status != self.status:
pass
else:
raise ValidationError({
'reference': _('This order is locked and cannot be modified')
})
# Reference calculations
self.reference_int = self.rebuild_reference_field(self.reference)
if not self.creation_date:
self.creation_date = InvenTree.helpers.current_date()
super().save(*args, **kwargs)
def check_locked(self, db: bool = False) -> bool:
"""Check if this order is 'locked'.
Args:
db: If True, check with the database. If False, check the instance (default False).
"""
return (
self.LOCK_SETTING
and get_global_setting(self.LOCK_SETTING)
and self.check_complete(db)
)
def check_complete(self, db: bool = False) -> bool:
"""Check if this order is 'complete'.
Args:
db: If True, check with the database. If False, check the instance (default False).
"""
status = self.get_db_instance().status if db else self.status
return status in self.get_status_class().COMPLETE
def clean(self):
"""Custom clean method for the generic order class."""
super().clean()
@ -397,6 +435,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
REFERENCE_PATTERN_SETTING = 'PURCHASEORDER_REFERENCE_PATTERN'
REQUIRE_RESPONSIBLE_SETTING = 'PURCHASEORDER_REQUIRE_RESPONSIBLE'
STATUS_CLASS = PurchaseOrderStatus
LOCK_SETTING = 'PURCHASEORDER_EDIT_COMPLETED_ORDERS'
class Meta:
"""Model meta options."""
@ -970,6 +1009,7 @@ class SalesOrder(TotalPriceMixin, Order):
REFERENCE_PATTERN_SETTING = 'SALESORDER_REFERENCE_PATTERN'
REQUIRE_RESPONSIBLE_SETTING = 'SALESORDER_REQUIRE_RESPONSIBLE'
STATUS_CLASS = SalesOrderStatus
LOCK_SETTING = 'SALESORDER_EDIT_COMPLETED_ORDERS'
class Meta:
"""Model meta options."""
@ -1449,6 +1489,11 @@ class OrderLineItem(InvenTree.models.InvenTreeMetadataModel):
Calls save method on the linked order
"""
if self.order and self.order.check_locked():
raise ValidationError({
'reference': _('The order is locked and cannot be modified')
})
super().save(*args, **kwargs)
self.order.save()
@ -1457,6 +1502,11 @@ class OrderLineItem(InvenTree.models.InvenTreeMetadataModel):
Calls save method on the linked order
"""
if self.order and self.order.check_locked():
raise ValidationError({
'reference': _('The order is locked and cannot be modified')
})
super().delete(*args, **kwargs)
self.order.save()
@ -2215,6 +2265,7 @@ class ReturnOrder(TotalPriceMixin, Order):
REFERENCE_PATTERN_SETTING = 'RETURNORDER_REFERENCE_PATTERN'
REQUIRE_RESPONSIBLE_SETTING = 'RETURNORDER_REQUIRE_RESPONSIBLE'
STATUS_CLASS = ReturnOrderStatus
LOCK_SETTING = 'RETURNORDER_EDIT_COMPLETED_ORDERS'
class Meta:
"""Model meta options."""

View File

@ -61,6 +61,72 @@ class OrderTest(TestCase, ExchangeRateMixin):
order.save()
self.assertEqual(order.reference_int, 12345)
def test_locking(self):
"""Test the (auto)locking functionality of the (Purchase)Order model."""
order = PurchaseOrder.objects.get(pk=1)
order.status = PurchaseOrderStatus.PENDING
order.save()
self.assertFalse(order.check_locked())
order.status = PurchaseOrderStatus.COMPLETE
order.save()
self.assertFalse(order.check_locked())
order.add_line_item(SupplierPart.objects.get(pk=100), 100)
last_line = order.lines.last()
self.assertEqual(last_line.quantity, 100)
# Reset
order.status = PurchaseOrderStatus.PENDING
order.save()
# Turn on auto-locking
set_global_setting(PurchaseOrder.LOCK_SETTING, True)
# still not locked
self.assertFalse(order.check_locked())
order.status = PurchaseOrderStatus.COMPLETE
# the instance is locked, the db instance is not
self.assertFalse(order.check_locked(True))
self.assertTrue(order.check_locked())
order.save()
# now everything is locked
self.assertTrue(order.check_locked(True))
self.assertTrue(order.check_locked())
# No editing allowed
with self.assertRaises(django_exceptions.ValidationError):
order.description = 'test1'
order.save()
order.refresh_from_db()
self.assertEqual(order.description, 'Ordering some screws')
# Also no adding of line items
with self.assertRaises(django_exceptions.ValidationError):
order.add_line_item(SupplierPart.objects.get(pk=100), 100)
# or deleting
with self.assertRaises(django_exceptions.ValidationError):
last_line.delete()
# Still can create a completed item
PurchaseOrder.objects.create(
supplier=Company.objects.get(pk=1),
reference='PO-99999',
status=PurchaseOrderStatus.COMPLETE,
)
# No editing (except status ;-) ) allowed
order.status = PurchaseOrderStatus.PENDING
order.save()
# Now it is a free for all again
order.description = 'test2'
order.save()
order.refresh_from_db()
self.assertEqual(order.description, 'test2')
def test_overdue(self):
"""Test overdue status functionality."""
today = datetime.now().date()