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:
parent
f27a84a7e5
commit
301347f1d0
@ -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."""
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user