diff --git a/erpnext/stock/doctype/stock_entry/services/manufacturing.py b/erpnext/stock/doctype/stock_entry/services/manufacturing.py index 3b0eef171fe..8a5555bf391 100644 --- a/erpnext/stock/doctype/stock_entry/services/manufacturing.py +++ b/erpnext/stock/doctype/stock_entry/services/manufacturing.py @@ -25,6 +25,10 @@ class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass +class ManufacturedQtyMandatoryError(frappe.ValidationError): + pass + + class OperationsNotCompleteError(frappe.ValidationError): pass @@ -282,6 +286,7 @@ class ManufactureStockEntry(BaseManufactureStockEntry): def validate(self): self.validate_warehouse() self.validate_raw_materials_exists() + self.validate_manufactured_qty() self.check_if_operations_completed() self.check_duplicate_entry_for_work_order() self.validate_component_and_quantities() @@ -391,6 +396,14 @@ class ManufactureStockEntry(BaseManufactureStockEntry): if not self.doc.work_order: frappe.throw(_("Work Order is mandatory")) + def validate_manufactured_qty(self): + """Without fg_completed_qty, submit never updates or validates the work order's produced qty.""" + if not self.wo_doc or self.wo_doc.track_semi_finished_goods: + return + + if not self.doc.fg_completed_qty: + frappe.throw(_("For Quantity (Manufactured Qty) is mandatory"), ManufacturedQtyMandatoryError) + def check_if_operations_completed(self): """Require operation (job card) completion before manufacture, so operating costs are captured.""" if not self.wo_doc or self.wo_doc.track_semi_finished_goods: @@ -950,6 +963,7 @@ class MaterialConsumptionForManufactureStockEntry(ManufactureStockEntry): def validate(self): self.validate_work_order() + self.validate_manufactured_qty() self.check_if_operations_completed() def add_items(self): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 13ae8e66aae..b26c004237a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -39,6 +39,7 @@ from erpnext.stock.utils import get_incoming_rate from .services.disassemble import DisassembleStockEntry from .services.manufacturing import ( DuplicateEntryForWorkOrderError, + ManufacturedQtyMandatoryError, ManufactureStockEntry, MaterialConsumptionForManufactureStockEntry, OperationsNotCompleteError, diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 56d2a6c0efb..6bfc5f52ee6 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -30,6 +30,7 @@ from erpnext.stock.doctype.serial_no.serial_no import * from erpnext.stock.doctype.stock_entry.stock_entry import ( DuplicateEntryForWorkOrderError, FinishedGoodError, + ManufacturedQtyMandatoryError, get_pending_work_orders, make_stock_in_entry, ) @@ -1303,6 +1304,18 @@ class TestStockEntry(ERPNextTestSuite): within_allowance = frappe.get_doc(make_wo_stock_entry(wo.name, "Manufacture", 1)) within_allowance.insert() + def test_manufacture_blocked_without_manufactured_qty(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, source_warehouse="_Test Warehouse - _TC", skip_transfer=1) + + mfg = frappe.get_doc(make_wo_stock_entry(wo.name, "Manufacture", 1)) + mfg.fg_completed_qty = 0 + self.assertRaises(ManufacturedQtyMandatoryError, mfg.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