fix: avg stock entries for disassembly from WO

(cherry picked from commit 71fd18bdf9)
This commit is contained in:
vorasmit
2026-04-01 23:56:44 +05:30
committed by Mergify
parent 31ac46ae4c
commit 0ceb084104
2 changed files with 67 additions and 90 deletions

View File

@@ -2642,7 +2642,9 @@ class TestWorkOrder(ERPNextTestSuite):
self.assertTrue(scrap_row.s_warehouse)
self.assertFalse(scrap_row.t_warehouse)
self.assertEqual(scrap_row.s_warehouse, wo.scrap_warehouse)
self.assertEqual(scrap_row.qty, 40)
# BOM has scrap_qty=10/FG but also process_loss_per=10%, so actual scrap per FG = 9
# Total produced = 9*3 + 9*7 = 90, disassemble 4/10 → 36
self.assertEqual(scrap_row.qty, 36)
# RM quantities
for bom_item in bom.items:

View File

@@ -2328,7 +2328,7 @@ class StockEntry(StockController, SubcontractingInwardController):
Priority:
1. From a specific Manufacture Stock Entry (exact reversal)
2. From Work Order required_items (reflects WO changes)
2. From Work Order Manufacture Stock Entries (averaged reversal)
3. From BOM (standalone disassembly)
"""
@@ -2362,104 +2362,79 @@ class StockEntry(StockController, SubcontractingInwardController):
_("Source Stock Entry {0} has no finished goods quantity").format(self.source_stock_entry)
)
scale_factor = flt(self.fg_completed_qty) / flt(source_fg_qty)
disassemble_qty = flt(self.fg_completed_qty)
scale_factor = disassemble_qty / flt(source_fg_qty)
for source_row in self.get_items_from_manufacture_stock_entry(self.source_stock_entry):
if source_row.is_finished_item:
qty = flt(self.fg_completed_qty)
s_warehouse = self.from_warehouse or source_row.t_warehouse
t_warehouse = ""
elif source_row.s_warehouse:
# RM: was consumed FROM s_warehouse → return TO s_warehouse
qty = flt(source_row.qty * scale_factor)
s_warehouse = ""
t_warehouse = self.to_warehouse or source_row.s_warehouse
else:
# Scrap/secondary: was produced TO t_warehouse → take FROM t_warehouse
qty = flt(source_row.qty * scale_factor)
s_warehouse = source_row.t_warehouse
t_warehouse = ""
use_serial_batch_fields = 1 if (source_row.batch_no or source_row.serial_no) else 0
self.append(
"items",
{
"item_code": source_row.item_code,
"item_name": source_row.item_name,
"description": source_row.description,
"stock_uom": source_row.stock_uom,
"uom": source_row.uom,
"conversion_factor": source_row.conversion_factor,
"basic_rate": source_row.basic_rate,
"qty": qty,
"s_warehouse": s_warehouse,
"t_warehouse": t_warehouse,
"is_finished_item": source_row.is_finished_item,
"type": source_row.type,
"is_legacy_scrap_item": source_row.is_legacy_scrap_item,
"bom_secondary_item": source_row.bom_secondary_item,
"against_stock_entry": self.source_stock_entry,
"ste_detail": source_row.name,
# batch and serial bundles built on submit
"use_serial_batch_fields": use_serial_batch_fields,
},
)
self._append_disassembly_row_from_source(
disassemble_qty=disassemble_qty,
scale_factor=scale_factor,
source_stock_entry=self.source_stock_entry,
)
def _add_items_for_disassembly_from_work_order(self):
wo = frappe.get_doc("Work Order", self.work_order)
if not wo.required_items:
return self._add_items_for_disassembly_from_bom()
wo_produced_qty = flt(wo.produced_qty)
if wo_produced_qty <= 0:
frappe.throw(_("Work Order {0} has no produced qty").format(self.work_order))
scale_factor = flt(self.fg_completed_qty) / flt(wo.qty) if flt(wo.qty) else 0
disassemble_qty = flt(self.fg_completed_qty)
if disassemble_qty <= 0:
frappe.throw(_("Disassemble Qty cannot be less than or equal to 0."))
# RMs
for ri in wo.required_items:
self.append(
"items",
{
"item_code": ri.item_code,
"item_name": ri.item_name,
"description": ri.description,
"qty": flt(ri.required_qty * scale_factor),
"stock_uom": ri.stock_uom,
"uom": ri.stock_uom,
"conversion_factor": 1,
# manufacture transfers RMs from WIP (not source warehouse)
"t_warehouse": self.to_warehouse or wo.wip_warehouse,
"s_warehouse": "",
"is_finished_item": 0,
},
)
scale_factor = disassemble_qty / wo_produced_qty
# Secondary/Scrap items
secondary_items = self.get_secondary_items(self.fg_completed_qty)
if secondary_items:
scrap_warehouse = wo.scrap_warehouse or self.from_warehouse or wo.fg_warehouse
for item in secondary_items.values():
item["from_warehouse"] = scrap_warehouse
item["to_warehouse"] = ""
item["is_finished_item"] = 0
self.add_to_stock_entry_detail(secondary_items, bom_no=self.bom_no)
# FG
self.append(
"items",
{
"item_code": wo.production_item,
"item_name": wo.item_name,
"description": wo.description,
"qty": flt(self.fg_completed_qty),
"stock_uom": wo.stock_uom,
"uom": wo.stock_uom,
"conversion_factor": 1,
"s_warehouse": self.from_warehouse or wo.fg_warehouse,
"t_warehouse": "",
"is_finished_item": 1,
},
self._append_disassembly_row_from_source(
disassemble_qty=disassemble_qty,
scale_factor=scale_factor,
)
def _append_disassembly_row_from_source(self, disassemble_qty, scale_factor, source_stock_entry=None):
for source_row in self.get_items_from_manufacture_stock_entry(self.source_stock_entry):
if source_row.is_finished_item:
qty = disassemble_qty
s_warehouse = self.from_warehouse or source_row.t_warehouse
t_warehouse = ""
elif source_row.s_warehouse:
# RM: was consumed FROM s_warehouse -> return TO s_warehouse
qty = flt(source_row.qty * scale_factor)
s_warehouse = ""
t_warehouse = self.to_warehouse or source_row.s_warehouse
else:
# Scrap/secondary: was produced TO t_warehouse -> take FROM t_warehouse
qty = flt(source_row.qty * scale_factor)
s_warehouse = source_row.t_warehouse
t_warehouse = ""
item = {
"item_code": source_row.item_code,
"item_name": source_row.item_name,
"description": source_row.description,
"stock_uom": source_row.stock_uom,
"uom": source_row.uom,
"conversion_factor": source_row.conversion_factor,
"basic_rate": source_row.basic_rate,
"qty": qty,
"s_warehouse": s_warehouse,
"t_warehouse": t_warehouse,
"is_finished_item": source_row.is_finished_item,
"type": source_row.type,
"is_legacy_scrap_item": source_row.is_legacy_scrap_item,
"bom_secondary_item": source_row.bom_secondary_item,
# batch and serial bundles built on submit
"use_serial_batch_fields": 1 if (source_row.batch_no or source_row.serial_no) else 0,
}
if source_stock_entry:
item.update(
{
"against_stock_entry": source_stock_entry,
"ste_detail": source_row.name,
}
)
self.append("items", item)
def _add_items_for_disassembly_from_bom(self):
if not self.bom_no or not self.fg_completed_qty:
frappe.throw(_("BOM and Finished Good Quantity is mandatory for Disassembly"))