fix: allow disassemble stock entry without work order (backport #51761) (#51836)

fix: allow disassemble stock entry without work order (#51761)

* fix: allow disassemble stock entry without work order

* fix: use existing functionality to load fg item

* chore: better dict update

(cherry picked from commit 83919119f8)

Co-authored-by: Smit Vora <smitvora203@gmail.com>
This commit is contained in:
mergify[bot]
2026-01-19 12:54:06 +05:30
committed by GitHub
parent e2b95da24d
commit c830bf6fc7
2 changed files with 90 additions and 4 deletions

View File

@@ -724,7 +724,7 @@ class StockEntry(StockController, SubcontractingInwardController):
"Subcontracting Return",
]
validate_for_manufacture = any([d.bom_no for d in self.get("items")])
has_bom = any([d.bom_no for d in self.get("items")])
if self.purpose in source_mandatory and self.purpose not in target_mandatory:
self.to_warehouse = None
@@ -753,7 +753,7 @@ class StockEntry(StockController, SubcontractingInwardController):
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
if self.purpose == "Manufacture":
if validate_for_manufacture:
if has_bom:
if d.is_finished_item or d.is_scrap_item:
d.s_warehouse = None
if not d.t_warehouse:
@@ -763,6 +763,17 @@ class StockEntry(StockController, SubcontractingInwardController):
if not d.s_warehouse:
frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx))
if self.purpose == "Disassemble":
if has_bom:
if d.is_finished_item:
d.t_warehouse = None
if not d.s_warehouse:
frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx))
else:
d.s_warehouse = None
if not d.t_warehouse:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
if cstr(d.s_warehouse) == cstr(d.t_warehouse) and self.purpose not in [
"Material Transfer for Manufacture",
"Material Transfer",
@@ -2162,9 +2173,12 @@ class StockEntry(StockController, SubcontractingInwardController):
def get_items_for_disassembly(self):
"""Get items for Disassembly Order"""
if not self.work_order:
frappe.throw(_("The Work Order is mandatory for Disassembly Order"))
if self.work_order:
return self._add_items_for_disassembly_from_work_order()
return self._add_items_for_disassembly_from_bom()
def _add_items_for_disassembly_from_work_order(self):
items = self.get_items_from_manufacture_entry()
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
@@ -2196,6 +2210,23 @@ class StockEntry(StockController, SubcontractingInwardController):
child_row.t_warehouse = row.s_warehouse
child_row.is_finished_item = 0 if row.is_finished_item else 1
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"))
# Raw Materials
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
for item_row in item_dict.values():
item_row["to_warehouse"] = self.to_warehouse
item_row["from_warehouse"] = ""
item_row["is_finished_item"] = 0
self.add_to_stock_entry_detail(item_dict)
# Finished goods
self.load_items_from_bom()
def get_items_from_manufacture_entry(self):
return frappe.get_all(
"Stock Entry",
@@ -2560,6 +2591,7 @@ class StockEntry(StockController, SubcontractingInwardController):
expense_account = item.get("expense_account")
if not expense_account:
expense_account = frappe.get_cached_value("Company", self.company, "stock_adjustment_account")
args = {
"to_warehouse": to_warehouse,
"from_warehouse": "",
@@ -2573,6 +2605,15 @@ class StockEntry(StockController, SubcontractingInwardController):
"sample_quantity": item.get("sample_quantity"),
}
if self.purpose == "Disassemble":
args.update(
{
"from_warehouse": self.from_warehouse,
"to_warehouse": "",
"qty": flt(self.fg_completed_qty),
}
)
if (
self.work_order
and self.pro_doc.has_batch_no

View File

@@ -2277,6 +2277,51 @@ class TestStockEntry(IntegrationTestCase):
se.save()
se.submit()
def test_disassemble_entry_without_wo(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
fg_item = make_item("_Disassemble Mobile", properties={"is_stock_item": 1}).name
rm_item1 = make_item("_Disassemble Temper Glass", properties={"is_stock_item": 1}).name
rm_item2 = make_item("_Disassemble Battery", properties={"is_stock_item": 1}).name
warehouse = "_Test Warehouse - _TC"
# Stock up the FG item (what we'll disassemble)
make_stock_entry(item_code=fg_item, target=warehouse, qty=5, purpose="Material Receipt")
bom_no = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]).name
se = make_stock_entry(item_code=fg_item, qty=1, purpose="Disassemble", do_not_save=True)
se.from_bom = 1
se.use_multi_level_bom = 1
se.bom_no = bom_no
se.fg_completed_qty = 1
se.from_warehouse = warehouse
se.to_warehouse = warehouse
se.get_items()
# Verify FG as source (being consumed)
fg_items = [d for d in se.items if d.is_finished_item]
self.assertEqual(len(fg_items), 1)
self.assertEqual(fg_items[0].item_code, fg_item)
self.assertEqual(fg_items[0].qty, 1)
self.assertEqual(fg_items[0].s_warehouse, warehouse)
self.assertFalse(fg_items[0].t_warehouse)
# Verify RM as target (being received)
rm_items = {d.item_code: d for d in se.items if not d.is_finished_item}
self.assertEqual(len(rm_items), 2)
self.assertIn(rm_item1, rm_items)
self.assertIn(rm_item2, rm_items)
self.assertEqual(rm_items[rm_item1].qty, 1)
self.assertEqual(rm_items[rm_item2].qty, 1)
self.assertEqual(rm_items[rm_item1].t_warehouse, warehouse)
self.assertFalse(rm_items[rm_item1].s_warehouse)
se.calculate_rate_and_amount()
se.save()
se.submit()
@IntegrationTestCase.change_settings(
"Stock Settings", {"sample_retention_warehouse": "_Test Warehouse 1 - _TC"}
)