diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index ef40ccc3cdc..54aaae61615 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2965,56 +2965,45 @@ class TestWorkOrder(IntegrationTestCase): fg_warehouse="_Test Warehouse 2 - _TC", ) - # Initial check - self.assertEqual(wo.operations[0].operation, "Test Operation A") - self.assertEqual(wo.operations[1].operation, "Test Operation B") - self.assertEqual(wo.operations[2].operation, "Test Operation C") - self.assertEqual(wo.operations[3].operation, "Test Operation D") - wo = frappe.copy_doc(wo) - wo.operations[3].sequence_id = 2 + wo.operations[3].sequence_id = None + + # Test 1 : If any one operation does not have sequence ID then error will be thrown + self.assertRaises(frappe.ValidationError, wo.submit) + + for op in wo.operations: + op.sequence_id = None wo.submit() - # Test 2 : Sort line items in child table based on sequence ID - self.assertEqual(wo.operations[0].operation, "Test Operation A") - self.assertEqual(wo.operations[1].operation, "Test Operation B") - self.assertEqual(wo.operations[2].operation, "Test Operation D") - self.assertEqual(wo.operations[3].operation, "Test Operation C") + # Test 2 : If none of the operations have sequence ID then they will be sequenced as per their idx + for op in wo.operations: + self.assertEqual(op.sequence_id, op.idx) wo = frappe.copy_doc(wo) - wo.operations[3].sequence_id = 1 - wo.submit() + wo.operations[0].sequence_id = 2 - self.assertEqual(wo.operations[0].operation, "Test Operation A") - self.assertEqual(wo.operations[1].operation, "Test Operation C") - self.assertEqual(wo.operations[2].operation, "Test Operation B") - self.assertEqual(wo.operations[3].operation, "Test Operation D") + # Test 3 : Sequence IDs should not miss the correct sequence of numbers + self.assertRaises(frappe.ValidationError, wo.submit) - wo = frappe.copy_doc(wo) - wo.operations[0].sequence_id = 3 - wo.submit() + wo.operations[1].sequence_id = 1 - self.assertEqual(wo.operations[0].operation, "Test Operation C") - self.assertEqual(wo.operations[1].operation, "Test Operation B") - self.assertEqual(wo.operations[2].operation, "Test Operation D") - self.assertEqual(wo.operations[3].operation, "Test Operation A") - - wo = frappe.copy_doc(wo) - wo.operations[1].sequence_id = 0 - - # Test 3 - Error should be thrown if any one operation does not have sequence id but others do + # Test 4 : Sequence IDs should be in the correct ascending order self.assertRaises(frappe.ValidationError, wo.submit) workstation = frappe.get_doc("Workstation", "Test Workstation A") workstation.production_capacity = 4 workstation.save() - wo = frappe.copy_doc(wo) + wo.operations[0].sequence_id = 1 wo.operations[1].sequence_id = 2 + wo.operations[2].sequence_id = 2 + wo.operations[3].sequence_id = 3 wo.submit() - # Test 4 - If Sequence ID is same then planned start time for both operations should be same - self.assertEqual(wo.operations[1].planned_start_time, wo.operations[2].planned_start_time) + # Test 5 : If two operations have the same sequence ID then the next operation will start 10 mins after the longest previous operation ends + self.assertEqual( + wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10) + ) def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 1eca56b5a15..18b89080cba 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -199,6 +199,7 @@ class WorkOrder(Document): self.set_required_items(reset_only_qty=len(self.get("required_items"))) self.enable_auto_reserve_stock() + self.validate_operations_sequence() def validate_dates(self): if self.actual_start_date and self.actual_end_date: @@ -229,6 +230,30 @@ class WorkOrder(Document): if self.is_new() and frappe.db.get_single_value("Stock Settings", "auto_reserve_stock"): self.reserve_stock = 1 + def validate_operations_sequence(self): + if all([not op.sequence_id for op in self.operations]): + for op in self.operations: + op.sequence_id = op.idx + else: + sequence_id = 1 + for op in self.operations: + if op.idx == 1 and op.sequence_id != 1: + frappe.throw( + _("Row #1: Sequence ID must be 1 for Operation {0}.").format( + frappe.bold(op.operation) + ) + ) + elif op.sequence_id != sequence_id and op.sequence_id != sequence_id + 1: + frappe.throw( + _("Row #{0}: Sequence ID must be {1} or {2} for Operation {3}.").format( + op.idx, + frappe.bold(sequence_id), + frappe.bold(sequence_id + 1), + frappe.bold(op.operation), + ) + ) + sequence_id = op.sequence_id + def set_warehouses(self): for row in self.required_items: if not row.source_warehouse: @@ -726,17 +751,6 @@ class WorkOrder(Document): enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 - if all([op.sequence_id for op in self.operations]): - self.operations = sorted(self.operations, key=lambda op: op.sequence_id) - for idx, op in enumerate(self.operations): - op.idx = idx + 1 - elif any([op.sequence_id for op in self.operations]): - frappe.throw( - _( - "Row #{0}: Incorrect Sequence ID. If any single operation has a Sequence ID then all other operations must have one too." - ).format(next((op.idx for op in self.operations if not op.sequence_id), None)) - ) - for idx, row in enumerate(self.operations): qty = self.qty while qty > 0: diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index e13940ef36b..6cbcc855d01 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -202,6 +202,7 @@ "fieldname": "sequence_id", "fieldtype": "Int", "label": "Sequence ID", + "non_negative": 1, "print_hide": 1 }, { @@ -299,7 +300,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-04-09 16:21:47.110564", + "modified": "2025-05-15 15:10:06.885440", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation",