From 22fa5205003c757179012893bc027075593a3a07 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 12:25:17 +0530 Subject: [PATCH 1/4] 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, From 1665873fc9f984ce99b96ab6ad75f0774d5123b4 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 12:27:06 +0530 Subject: [PATCH 2/4] test: duplicate manufacture entry against a work order A second Manufacture stock entry saved while existing entries already cover the full work order qty must raise DuplicateEntryForWorkOrderError. --- .../doctype/stock_entry/test_stock_entry.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 5a29b5c37cb..c4f3469e1c4 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -28,6 +28,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle ) from erpnext.stock.doctype.serial_no.serial_no import * from erpnext.stock.doctype.stock_entry.stock_entry import ( + DuplicateEntryForWorkOrderError, FinishedGoodError, get_pending_work_orders, make_stock_in_entry, @@ -1272,6 +1273,30 @@ class TestStockEntry(ERPNextTestSuite): se_ok.submit() self.assertEqual(se_ok.docstatus, 1) + def test_duplicate_entry_for_work_order(self): + from erpnext.manufacturing.doctype.work_order.mapper import ( + make_stock_entry as make_wo_stock_entry, + ) + from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record + + wo = make_wo_order_test_record(qty=1) + make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100) + make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=100 + ) + + transfer = frappe.get_doc(make_wo_stock_entry(wo.name, "Material Transfer for Manufacture", 1)) + for d in transfer.get("items"): + d.s_warehouse = "Stores - _TC" + transfer.insert() + transfer.submit() + + mfg = frappe.get_doc(make_wo_stock_entry(wo.name, "Manufacture", 1)) + mfg.insert() + + duplicate = frappe.get_doc(make_wo_stock_entry(wo.name, "Manufacture", 1)) + self.assertRaises(DuplicateEntryForWorkOrderError, duplicate.insert) + @ERPNextTestSuite.change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"}) def test_quality_inspection_required_for_manufacture(self): from erpnext.exceptions import QualityInspectionRejectedError, QualityInspectionRequiredError From 492ee05727420cb5a080d64057f62224498faa37 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 12:33:59 +0530 Subject: [PATCH 3/4] fix: honor overproduction allowance in duplicate entry check Compare already-entered finished good qty against the work order qty plus the configured overproduction percentage, mirroring the submit-time guard in work_order/services/status.py, so a save is never rejected that the submission contract would accept. --- .../stock/doctype/stock_entry/services/manufacturing.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/services/manufacturing.py b/erpnext/stock/doctype/stock_entry/services/manufacturing.py index 7b66623cb02..3b0eef171fe 100644 --- a/erpnext/stock/doctype/stock_entry/services/manufacturing.py +++ b/erpnext/stock/doctype/stock_entry/services/manufacturing.py @@ -436,7 +436,7 @@ class ManufactureStockEntry(BaseManufactureStockEntry): ) def check_duplicate_entry_for_work_order(self): - """Block another manufacture entry once existing entries already cover the full work order qty.""" + """Block another manufacture entry once existing entries already cover the work order qty plus allowance.""" if not self.wo_doc or self.wo_doc.track_semi_finished_goods: return @@ -453,7 +453,11 @@ class ManufactureStockEntry(BaseManufactureStockEntry): if not other_entries: return - if self.get_fg_qty_already_entered(other_entries) >= flt(self.wo_doc.qty): + allowance_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) + allowed_qty = flt(self.wo_doc.qty) + (allowance_percentage / 100 * flt(self.wo_doc.qty)) + if self.get_fg_qty_already_entered(other_entries) >= allowed_qty: frappe.throw( _("Stock Entries already created for Work Order {0}: {1}").format( self.doc.work_order, ", ".join(other_entries) From e5344cc07e1dc110e8a58563ec4368b8a0ee947c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 11 Aug 2026 12:34:00 +0530 Subject: [PATCH 4/4] test: overproduction allowance permits a further manufacture entry --- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index c4f3469e1c4..56d2a6c0efb 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1297,6 +1297,12 @@ class TestStockEntry(ERPNextTestSuite): duplicate = frappe.get_doc(make_wo_stock_entry(wo.name, "Manufacture", 1)) self.assertRaises(DuplicateEntryForWorkOrderError, duplicate.insert) + with self.change_settings( + "Manufacturing Settings", {"overproduction_percentage_for_work_order": 100} + ): + within_allowance = frappe.get_doc(make_wo_stock_entry(wo.name, "Manufacture", 1)) + within_allowance.insert() + @ERPNextTestSuite.change_settings("Stock Settings", {"action_if_quality_inspection_is_rejected": "Stop"}) def test_quality_inspection_required_for_manufacture(self): from erpnext.exceptions import QualityInspectionRejectedError, QualityInspectionRequiredError