diff --git a/erpnext/manufacturing/doctype/work_order/services/status.py b/erpnext/manufacturing/doctype/work_order/services/status.py index f80e48c7f6e..7ae2b4170f6 100644 --- a/erpnext/manufacturing/doctype/work_order/services/status.py +++ b/erpnext/manufacturing/doctype/work_order/services/status.py @@ -203,8 +203,8 @@ class StatusService: return if fieldname == "material_transferred_for_manufacturing": - # Owned by the net-coverage recomputation; the claimed fg_completed_qty is not - # validated here because item rows, not the claim, decide the stored value. + # Owned by the net-coverage recomputation; the per-entry allowance guard runs on + # stock entry submit, where the submitting entry's claim is known. self.doc.refresh_material_transferred_for_manufacturing() self.set_process_loss_qty() self._update_produced_qty_in_so() diff --git a/erpnext/stock/doctype/stock_entry/services/material_transfer.py b/erpnext/stock/doctype/stock_entry/services/material_transfer.py index 11933cf4c9d..71a96861a42 100644 --- a/erpnext/stock/doctype/stock_entry/services/material_transfer.py +++ b/erpnext/stock/doctype/stock_entry/services/material_transfer.py @@ -382,6 +382,7 @@ class MaterialTransferForManufactureStockEntry(BaseMaterialTransferStockEntry): if self.doc.fg_completed_qty: if self.doc.docstatus == 1: self.wo_doc.add_additional_items(self.doc) + self._validate_transfer_within_allowance() else: self.wo_doc.remove_additional_items(self.doc) @@ -391,6 +392,34 @@ class MaterialTransferForManufactureStockEntry(BaseMaterialTransferStockEntry): if not self.wo_doc.operations: self.wo_doc.set_actual_dates() + def _validate_transfer_within_allowance(self): + """Reject a transfer whose For Quantity, on top of the effective qty already + transferred, exceeds the planned qty plus the transfer allowance.""" + from erpnext.manufacturing.doctype.work_order.services.status import StatusService + from erpnext.manufacturing.doctype.work_order.work_order import StockOverProductionError + + if self.doc.is_return or self.doc.is_additional_transfer_entry: + return + if self.wo_doc.track_semi_finished_goods: + return + if self.wo_doc.operations and self.wo_doc.transfer_material_against == "Job Card": + return + + allowance = StatusService(self.wo_doc).get_qty_allowance("Material Transfer for Manufacture") + allowed_qty = flt(self.wo_doc.qty) * (1.0 + allowance / 100.0) + transferred_qty = flt(self.wo_doc.material_transferred_for_manufacturing) + projected_qty = transferred_qty + flt(self.doc.fg_completed_qty) + precision = self.wo_doc.precision("material_transferred_for_manufacturing") + if flt(projected_qty, precision) <= flt(allowed_qty, precision): + return + + frappe.throw( + _( + "For Quantity ({0}) with the already transferred quantity ({1}) cannot be greater than allowed quantity ({2}) in Work Order {3}" + ).format(flt(self.doc.fg_completed_qty), transferred_qty, allowed_qty, self.wo_doc.name), + StockOverProductionError, + ) + class MaterialRequestStockEntry(BaseMaterialTransferStockEntry): def before_validate(self):