From 94cd27ce5daf62e7c8b6022953f5e397ed5d67d0 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sat, 8 Aug 2026 16:11:20 +0530 Subject: [PATCH] fix: cap a manufacture entry at the job card's pending production Entries from operations without their own BOM carry no For Quantity, so the finished-good reconciliation cannot run for them and a draft created before other entries were submitted could still over-produce. Validate every job-card manufacture entry against the job card directly: finished goods plus process loss must fit in what the job card still has left to produce after earlier submitted entries. --- .../stock/doctype/stock_entry/stock_entry.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 707e7ebc461..7affec10c7f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -319,6 +319,7 @@ class StockEntry(StockController, SubcontractingInwardController): self.validate_batch() self.validate_inspection() self.validate_fg_completed_qty() + self.validate_job_card_pending_production() self.validate_difference_account() self.validate_job_card_item() self.set_purpose_for_stock_entry() @@ -1475,6 +1476,40 @@ class StockEntry(StockController, SubcontractingInwardController): (flt(self.process_loss_qty) / flt(self.fg_completed_qty)) * 100 ) + def validate_job_card_pending_production(self): + """A draft created before other entries were submitted must not book more than the job + card still has left; without this, a stale draft over-produces the finished good.""" + if self.purpose != "Manufacture" or not self.job_card: + return + + job_card = frappe.get_doc("Job Card", self.job_card) + if job_card.is_corrective_job_card or job_card.is_subcontracted: + return + + precision = frappe.get_precision("Stock Entry Detail", "qty") + pending_qty = flt( + flt(job_card.get_qty_to_produce()) + - flt(job_card.manufactured_qty) + - flt(job_card.get_consumed_process_loss()), + precision, + ) + finished_qty = flt(sum(flt(d.transfer_qty) for d in self.items if d.is_finished_item), precision) + entry_qty = flt(finished_qty + flt(self.process_loss_qty), precision) + + if entry_qty > pending_qty: + uom = job_card.stock_uom + frappe.throw( + _( + "The Job Card {0} has only {1} left to produce, but this entry books {2} ({3} finished goods and {4} process loss). Cancel or update its other manufacture entries first." + ).format( + frappe.bold(self.job_card), + frappe.bold(f"{pending_qty} {uom}"), + frappe.bold(f"{entry_qty} {uom}"), + f"{finished_qty} {uom}", + f"{flt(self.process_loss_qty, precision)} {uom}", + ) + ) + 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)."""