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.assertTrue(scrap_row.s_warehouse)
self.assertFalse(scrap_row.t_warehouse) self.assertFalse(scrap_row.t_warehouse)
self.assertEqual(scrap_row.s_warehouse, wo.scrap_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 # RM quantities
for bom_item in bom.items: for bom_item in bom.items:

View File

@@ -2328,7 +2328,7 @@ class StockEntry(StockController, SubcontractingInwardController):
Priority: Priority:
1. From a specific Manufacture Stock Entry (exact reversal) 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) 3. From BOM (standalone disassembly)
""" """
@@ -2362,29 +2362,51 @@ class StockEntry(StockController, SubcontractingInwardController):
_("Source Stock Entry {0} has no finished goods quantity").format(self.source_stock_entry) _("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)
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)
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))
disassemble_qty = flt(self.fg_completed_qty)
if disassemble_qty <= 0:
frappe.throw(_("Disassemble Qty cannot be less than or equal to 0."))
scale_factor = disassemble_qty / wo_produced_qty
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): for source_row in self.get_items_from_manufacture_stock_entry(self.source_stock_entry):
if source_row.is_finished_item: if source_row.is_finished_item:
qty = flt(self.fg_completed_qty) qty = disassemble_qty
s_warehouse = self.from_warehouse or source_row.t_warehouse s_warehouse = self.from_warehouse or source_row.t_warehouse
t_warehouse = "" t_warehouse = ""
elif source_row.s_warehouse: elif source_row.s_warehouse:
# RM: was consumed FROM s_warehouse return TO s_warehouse # RM: was consumed FROM s_warehouse -> return TO s_warehouse
qty = flt(source_row.qty * scale_factor) qty = flt(source_row.qty * scale_factor)
s_warehouse = "" s_warehouse = ""
t_warehouse = self.to_warehouse or source_row.s_warehouse t_warehouse = self.to_warehouse or source_row.s_warehouse
else: else:
# Scrap/secondary: was produced TO t_warehouse take FROM t_warehouse # Scrap/secondary: was produced TO t_warehouse -> take FROM t_warehouse
qty = flt(source_row.qty * scale_factor) qty = flt(source_row.qty * scale_factor)
s_warehouse = source_row.t_warehouse s_warehouse = source_row.t_warehouse
t_warehouse = "" t_warehouse = ""
use_serial_batch_fields = 1 if (source_row.batch_no or source_row.serial_no) else 0 item = {
self.append(
"items",
{
"item_code": source_row.item_code, "item_code": source_row.item_code,
"item_name": source_row.item_name, "item_name": source_row.item_name,
"description": source_row.description, "description": source_row.description,
@@ -2399,66 +2421,19 @@ class StockEntry(StockController, SubcontractingInwardController):
"type": source_row.type, "type": source_row.type,
"is_legacy_scrap_item": source_row.is_legacy_scrap_item, "is_legacy_scrap_item": source_row.is_legacy_scrap_item,
"bom_secondary_item": source_row.bom_secondary_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 # batch and serial bundles built on submit
"use_serial_batch_fields": use_serial_batch_fields, "use_serial_batch_fields": 1 if (source_row.batch_no or source_row.serial_no) else 0,
}, }
)
def _add_items_for_disassembly_from_work_order(self): if source_stock_entry:
wo = frappe.get_doc("Work Order", self.work_order) item.update(
if not wo.required_items:
return self._add_items_for_disassembly_from_bom()
scale_factor = flt(self.fg_completed_qty) / flt(wo.qty) if flt(wo.qty) else 0
# RMs
for ri in wo.required_items:
self.append(
"items",
{ {
"item_code": ri.item_code, "against_stock_entry": source_stock_entry,
"item_name": ri.item_name, "ste_detail": source_row.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,
},
) )
# Secondary/Scrap items self.append("items", item)
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,
},
)
def _add_items_for_disassembly_from_bom(self): def _add_items_for_disassembly_from_bom(self):
if not self.bom_no or not self.fg_completed_qty: if not self.bom_no or not self.fg_completed_qty: