fix: de-duplicate rows on disassembly with multiple manufacture entries

(cherry picked from commit a091e47bd7)
This commit is contained in:
Smit Vora
2025-12-19 14:07:09 +05:30
committed by Mergify
parent 432b33ac5f
commit 68eeba41c1
2 changed files with 112 additions and 0 deletions

View File

@@ -2467,6 +2467,117 @@ class TestWorkOrder(FrappeTestCase):
f"Work Order disassembled_qty mismatch: expected {disassemble_qty}, got {wo.disassembled_qty}",
)
def test_disassembly_with_multiple_manufacture_entries(self):
"""
Test that disassembly does not create duplicate items when manufacturing
is done in multiple batches (multiple manufacture stock entries).
Scenario:
1. Create Work Order for 10 units
2. Transfer raw materials
3. Manufacture in 2 parts (3 units, then 7 units) - creates 2 stock entries
4. Create Disassembly for 4 units
5. Verify no duplicate items in the disassembly stock entry
"""
# Create RM and FG item
raw_item1 = make_item("Test Raw for Multi Batch Disassembly 1", {"is_stock_item": 1}).name
raw_item2 = make_item("Test Raw for Multi Batch Disassembly 2", {"is_stock_item": 1}).name
fg_item = make_item("Test FG for Multi Batch Disassembly", {"is_stock_item": 1}).name
bom = make_bom(item=fg_item, quantity=1, raw_materials=[raw_item1, raw_item2], rm_qty=2)
# Create WO
wo = make_wo_order_test_record(production_item=fg_item, qty=10, bom_no=bom.name, status="Not Started")
# Ensure enough stock
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
make_stock_entry_test_record(
item_code=raw_item1,
purpose="Material Receipt",
target=wo.wip_warehouse,
qty=50,
basic_rate=100,
)
make_stock_entry_test_record(
item_code=raw_item2,
purpose="Material Receipt",
target=wo.wip_warehouse,
qty=50,
basic_rate=100,
)
# Transfer for manufacture
se_for_material_transfer = frappe.get_doc(
make_stock_entry(wo.name, "Material Transfer for Manufacture", wo.qty)
)
for item in se_for_material_transfer.items:
item.s_warehouse = wo.wip_warehouse
se_for_material_transfer.save()
se_for_material_transfer.submit()
# First Manufacture Entry - 3 units
se_manufacture1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3))
se_manufacture1.submit()
# Second Manufacture Entry - 7 units
se_manufacture2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 7))
se_manufacture2.submit()
wo.reload()
self.assertEqual(wo.produced_qty, 10)
# Count manufacture entries
manufacture_entries = frappe.get_all(
"Stock Entry",
filters={
"work_order": wo.name,
"purpose": "Manufacture",
"docstatus": 1,
},
)
self.assertEqual(len(manufacture_entries), 2, "Expected 2 manufacture entries")
# Disassembly for 4 units
disassemble_qty = 4
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", disassemble_qty))
item_counts = {}
for item in stock_entry.items:
item_code = item.item_code
item_counts[item_code] = item_counts.get(item_code, 0) + 1
# No duplicates
duplicates = {k: v for k, v in item_counts.items() if v > 1}
self.assertEqual(
len(duplicates),
0,
f"Found duplicate items in disassembly stock entry: {duplicates}",
)
expected_items = 3 # FG item + 2 raw materials
self.assertEqual(
len(stock_entry.items),
expected_items,
f"Expected {expected_items} items, found {len(stock_entry.items)}",
)
# FG item qty
fg_item_row = next((i for i in stock_entry.items if i.item_code == fg_item), None)
self.assertEqual(fg_item_row.qty, disassemble_qty)
# RM quantities
for bom_item in bom.items:
expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty
rm_row = next((i for i in stock_entry.items if i.item_code == bom_item.item_code), None)
self.assertAlmostEqual(
rm_row.qty,
expected_qty,
places=3,
msg=f"Raw material {bom_item.item_code} qty mismatch",
)
def test_components_alternate_item_for_bom_based_manufacture_entry(self):
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)

View File

@@ -1970,6 +1970,7 @@ class StockEntry(StockController):
["Stock Entry Detail", "docstatus", "=", 1],
],
order_by="`tabStock Entry Detail`.`idx` desc, `tabStock Entry Detail`.`is_finished_item` desc",
group_by="`tabStock Entry Detail`.`item_code`",
)
@frappe.whitelist()