On ERPNext v15, a Manufacture Stock Entry with scrap/co-product rows no longer balances to difference = 0.
Setup: multiple source (raw material) rows, and multiple target rows one finished good plus one or more scrap/co-product items, each with its own rate.
What happens: set_basic_rate computes the finished-good rate inline while iterating the items table in order, and subtracts only the scrap rows that were already processed above the FG row. So any scrap row placed below the finished-good row is not subtracted from the FG cost. The finished good ends up absorbing the full source cost, the scrap value is added on top of incoming, and the total difference comes out equal to the scrap total instead of zero.
The exact same data / entry structure balanced fine on our older version and only started showing a non-zero difference after upgrading to v15.115.0.
Problem:
In stock_entry.py, get_basic_rate_for_manufactured_item (line 1559) sums scrap cost from basic_amount of items where is_legacy_scrap_item = 1. But basic_amount is set later in the main loop at line 1488. If scrap rows appear below the FG row in the item table, their basic_amount is still 0 when the FG rate is calculated → scrap not subtracted → difference ≠ 0.
Fix:
In get_basic_rate_for_manufactured_item, before summing scrap cost, ensure scrap items that haven’t had rate set yet get it calculated. Replace line 1559:
Old:
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get(“items”) if d.is_legacy_scrap_item])
New:
scrap_items_cost = 0
for d in self.get(“items”):
if d.is_legacy_scrap_item:
if not d.basic_rate:
d.basic_rate = get_valuation_rate(…) # same args as set_basic_rate fallback
d.basic_amount = flt(d.transfer_qty * d.basic_rate, d.precision(“basic_amount”))
scrap_items_cost += flt(d.basic_amount)
Or the simpler one-line fix just ensure order doesn’t matter by pre-computing scrap cost before the FG loop. But the cleanest patch: move the FG rate calculation to a second pass after all other items have had their rates set:
In set_basic_rate, after the main loop, add:
for d in self.get(“items”):
if d.is_finished_item and not d.set_basic_rate_manually:
d.basic_rate = self.get_basic_rate_for_…(…)
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), …)
This ensures all scrap/co-product items have their basic_amount populated before the FG rate uses them.