mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-01 04:56:45 +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
|
REQUIRE_RESPONSIBLE_SETTING = None
|
||||||
|
LOCK_SETTING = None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options. Abstract ensures no database table is created."""
|
"""Metaclass options. Abstract ensures no database table is created."""
|
||||||
@ -219,14 +220,51 @@ class Order(
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Custom save method for the order models.
|
"""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)
|
self.reference_int = self.rebuild_reference_field(self.reference)
|
||||||
if not self.creation_date:
|
if not self.creation_date:
|
||||||
self.creation_date = InvenTree.helpers.current_date()
|
self.creation_date = InvenTree.helpers.current_date()
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
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):
|
def clean(self):
|
||||||
"""Custom clean method for the generic order class."""
|
"""Custom clean method for the generic order class."""
|
||||||
super().clean()
|
super().clean()
|
||||||
@ -397,6 +435,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
REFERENCE_PATTERN_SETTING = 'PURCHASEORDER_REFERENCE_PATTERN'
|
REFERENCE_PATTERN_SETTING = 'PURCHASEORDER_REFERENCE_PATTERN'
|
||||||
REQUIRE_RESPONSIBLE_SETTING = 'PURCHASEORDER_REQUIRE_RESPONSIBLE'
|
REQUIRE_RESPONSIBLE_SETTING = 'PURCHASEORDER_REQUIRE_RESPONSIBLE'
|
||||||
STATUS_CLASS = PurchaseOrderStatus
|
STATUS_CLASS = PurchaseOrderStatus
|
||||||
|
LOCK_SETTING = 'PURCHASEORDER_EDIT_COMPLETED_ORDERS'
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Model meta options."""
|
"""Model meta options."""
|
||||||
@ -970,6 +1009,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
REFERENCE_PATTERN_SETTING = 'SALESORDER_REFERENCE_PATTERN'
|
REFERENCE_PATTERN_SETTING = 'SALESORDER_REFERENCE_PATTERN'
|
||||||
REQUIRE_RESPONSIBLE_SETTING = 'SALESORDER_REQUIRE_RESPONSIBLE'
|
REQUIRE_RESPONSIBLE_SETTING = 'SALESORDER_REQUIRE_RESPONSIBLE'
|
||||||
STATUS_CLASS = SalesOrderStatus
|
STATUS_CLASS = SalesOrderStatus
|
||||||
|
LOCK_SETTING = 'SALESORDER_EDIT_COMPLETED_ORDERS'
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Model meta options."""
|
"""Model meta options."""
|
||||||
@ -1449,6 +1489,11 @@ class OrderLineItem(InvenTree.models.InvenTreeMetadataModel):
|
|||||||
|
|
||||||
Calls save method on the linked order
|
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)
|
super().save(*args, **kwargs)
|
||||||
self.order.save()
|
self.order.save()
|
||||||
|
|
||||||
@ -1457,6 +1502,11 @@ class OrderLineItem(InvenTree.models.InvenTreeMetadataModel):
|
|||||||
|
|
||||||
Calls save method on the linked order
|
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)
|
super().delete(*args, **kwargs)
|
||||||
self.order.save()
|
self.order.save()
|
||||||
|
|
||||||
@ -2215,6 +2265,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
REFERENCE_PATTERN_SETTING = 'RETURNORDER_REFERENCE_PATTERN'
|
REFERENCE_PATTERN_SETTING = 'RETURNORDER_REFERENCE_PATTERN'
|
||||||
REQUIRE_RESPONSIBLE_SETTING = 'RETURNORDER_REQUIRE_RESPONSIBLE'
|
REQUIRE_RESPONSIBLE_SETTING = 'RETURNORDER_REQUIRE_RESPONSIBLE'
|
||||||
STATUS_CLASS = ReturnOrderStatus
|
STATUS_CLASS = ReturnOrderStatus
|
||||||
|
LOCK_SETTING = 'RETURNORDER_EDIT_COMPLETED_ORDERS'
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Model meta options."""
|
"""Model meta options."""
|
||||||
|
@ -61,6 +61,72 @@ class OrderTest(TestCase, ExchangeRateMixin):
|
|||||||
order.save()
|
order.save()
|
||||||
self.assertEqual(order.reference_int, 12345)
|
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):
|
def test_overdue(self):
|
||||||
"""Test overdue status functionality."""
|
"""Test overdue status functionality."""
|
||||||
today = datetime.now().date()
|
today = datetime.now().date()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user