From 22fa5205003c757179012893bc027075593a3a07 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 12:25:17 +0530 Subject: [PATCH] fix: reinstate duplicate entry check for manufacture entries The stock_entry.py split (#54466) dropped check_duplicate_entry_for_work_order and DuplicateEntryForWorkOrderError with no replacement. The Work Order still throws StockOverProductionError when submitted entries exceed the planned qty, but nothing blocks saving another Manufacture entry, draft or submitted, once existing entries already cover the full work order qty. Restore the validation in the manufacture purpose handler, gated to work orders without track_semi_finished_goods, matching the pre-split behaviour. --- .../stock_entry/services/manufacturing.py | 45 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 1 + 2 files changed, 46 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/services/manufacturing.py b/erpnext/stock/doctype/stock_entry/services/manufacturing.py index 8bec6d8df3a..7b66623cb02 100644 --- a/erpnext/stock/doctype/stock_entry/services/manufacturing.py +++ b/erpnext/stock/doctype/stock_entry/services/manufacturing.py @@ -21,6 +21,10 @@ from .serial_batch import create_serial_and_batch_bundle from .stock_entry_base import BaseStockEntry +class DuplicateEntryForWorkOrderError(frappe.ValidationError): + pass + + class OperationsNotCompleteError(frappe.ValidationError): pass @@ -279,6 +283,7 @@ class ManufactureStockEntry(BaseManufactureStockEntry): self.validate_warehouse() self.validate_raw_materials_exists() self.check_if_operations_completed() + self.check_duplicate_entry_for_work_order() self.validate_component_and_quantities() self.validate_finished_good_serial_batch_for_work_order() @@ -430,6 +435,46 @@ class ManufactureStockEntry(BaseManufactureStockEntry): OperationsNotCompleteError, ) + def check_duplicate_entry_for_work_order(self): + """Block another manufacture entry once existing entries already cover the full work order qty.""" + if not self.wo_doc or self.wo_doc.track_semi_finished_goods: + return + + other_entries = frappe.get_all( + "Stock Entry", + filters={ + "work_order": self.doc.work_order, + "purpose": self.doc.purpose, + "docstatus": ["!=", 2], + "name": ["!=", self.doc.name], + }, + pluck="name", + ) + if not other_entries: + return + + if self.get_fg_qty_already_entered(other_entries) >= flt(self.wo_doc.qty): + frappe.throw( + _("Stock Entries already created for Work Order {0}: {1}").format( + self.doc.work_order, ", ".join(other_entries) + ), + DuplicateEntryForWorkOrderError, + ) + + def get_fg_qty_already_entered(self, other_entries): + child = frappe.qb.DocType("Stock Entry Detail") + qty = ( + frappe.qb.from_(child) + .select(Sum(child.transfer_qty)) + .where( + child.parent.isin(other_entries) + & (child.item_code == self.wo_doc.production_item) + & (child.s_warehouse.isnull() | (child.s_warehouse == "")) + ) + .run() + )[0][0] + return flt(qty) + def add_items(self): self.add_raw_materials() self.set_process_loss_qty() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 7ce519bf89d..13ae8e66aae 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -38,6 +38,7 @@ from erpnext.stock.utils import get_incoming_rate from .services.disassemble import DisassembleStockEntry from .services.manufacturing import ( + DuplicateEntryForWorkOrderError, ManufactureStockEntry, MaterialConsumptionForManufactureStockEntry, OperationsNotCompleteError,