From a0b370b2e949548d4e215e80f40e027052711121 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 15:20:54 +0530 Subject: [PATCH] fix: scope manufacture entry process loss to its own job card set_process_loss_qty stamped MAX(process_loss_qty) across every operation of the work order onto each manufacture entry. With semi finished goods tracking, one operation's process loss leaked into the entries of every other operation: validate_fg_completed_qty then rejected the entry when it had a BOM, or the wrong loss was recorded silently when it did not, double-counting the loss across operations. When the entry belongs to a job card, use that job card's loss net of what its earlier entries already booked. The MAX fallback stays for work-order level entries without a job card. Fixes frappe/erpnext#57892 (cherry picked from commit 1b335973b7c58c623693d7eec68aafca5581de32) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.py --- .../stock/doctype/stock_entry/stock_entry.py | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 6f8a4644a56..258963ed9c5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -3192,13 +3192,16 @@ class StockEntry(StockController, SubcontractingInwardController): return precision = self.precision("process_loss_qty") - if self.work_order: - data = frappe.get_all( - "Work Order Operation", - filters={"parent": self.work_order}, - fields=[{"MAX": "process_loss_qty", "as": "process_loss_qty"}], + process_loss_qty = self.get_pending_process_loss_qty() + if process_loss_qty and flt(self.process_loss_qty, precision) != flt(process_loss_qty, precision): + self.process_loss_qty = flt(process_loss_qty, precision) + + frappe.msgprint( + _("The Process Loss Qty has been reset as per the job card's Process Loss Qty"), + alert=True, ) +<<<<<<< HEAD if data and data[0].process_loss_qty: process_loss_qty = data[0].process_loss_qty if flt(self.process_loss_qty, precision) != flt(process_loss_qty, precision): @@ -3208,6 +3211,8 @@ class StockEntry(StockController, SubcontractingInwardController): _("The Process Loss Qty has reset as per job cards Process Loss Qty"), alert=True ) +======= +>>>>>>> 1b335973b7 (fix: scope manufacture entry process loss to its own job card) if not self.process_loss_percentage and not self.process_loss_qty: self.process_loss_percentage = frappe.get_cached_value( "BOM", self.bom_no, "process_loss_percentage" @@ -3222,6 +3227,23 @@ class StockEntry(StockController, SubcontractingInwardController): (flt(self.process_loss_qty) / flt(self.fg_completed_qty)) * 100 ) + def get_pending_process_loss_qty(self): + """Loss this entry should still book: the job card's unbooked loss when the entry + belongs to one, else the largest operation loss on the work order (legacy flow).""" + if self.job_card: + job_card = frappe.get_doc("Job Card", self.job_card) + return max(flt(job_card.process_loss_qty) - flt(job_card.get_consumed_process_loss()), 0) + + if self.work_order: + data = frappe.get_all( + "Work Order Operation", + filters={"parent": self.work_order}, + fields=[{"MAX": "process_loss_qty", "as": "process_loss_qty"}], + ) + return flt(data[0].process_loss_qty) if data else 0 + + return 0 + def set_work_order_details(self): if not getattr(self, "pro_doc", None): self.pro_doc = frappe._dict()