fix(manufacturing): scope secondary items to job card (#58260)

This commit is contained in:
Mihir Kandoi
2026-08-19 20:53:53 +05:30
committed by GitHub
parent 696f0df811
commit 8320c77694
2 changed files with 32 additions and 13 deletions

View File

@@ -1507,15 +1507,14 @@ class TestJobCard(ERPNextTestSuite):
return bom
rm1 = create_item("RM 1")
scrap1 = create_item("Scrap 1")
shared_scrap = create_item("Shared Scrap")
sfg = create_item("SFG 1")
sfg_bom = create_bom(rm1.name, sfg.name, scrap1.name)
sfg_bom = create_bom(rm1.name, sfg.name, shared_scrap.name)
rm2 = create_item("RM 2")
fg1 = create_item("FG 1")
scrap2 = create_item("Scrap 2")
scrap_extra = create_item("Scrap Extra")
fg_bom = create_bom(rm2.name, fg1.name, scrap2.name, submit=False)
fg_bom = create_bom(rm2.name, fg1.name, shared_scrap.name, submit=False)
fg_bom.with_operations = 1
fg_bom.track_semi_finished_goods = 1
@@ -1596,7 +1595,7 @@ class TestJobCard(ERPNextTestSuite):
manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item())
manufacturing_entry.submit()
self.assertEqual(manufacturing_entry.items[2].item_code, scrap1.name)
self.assertEqual(manufacturing_entry.items[2].item_code, shared_scrap.name)
self.assertEqual(manufacturing_entry.items[2].qty, 9)
self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.556)
self.assertEqual(manufacturing_entry.items[3].item_code, scrap_extra.name)
@@ -1637,7 +1636,7 @@ class TestJobCard(ERPNextTestSuite):
sfg_row = next(row for row in manufacturing_entry.items if row.item_code == sfg.name)
self.assertEqual(flt(sfg_row.basic_rate, 3), 95.0)
self.assertEqual(manufacturing_entry.items[2].item_code, scrap2.name)
self.assertEqual(manufacturing_entry.items[2].item_code, shared_scrap.name)
self.assertEqual(manufacturing_entry.items[2].qty, 9)
self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.278)
@@ -2777,6 +2776,18 @@ class TestJobCard(ERPNextTestSuite):
self.assertEqual(s.items[3].item_code, "_Test Item")
self.assertEqual(s.items[3].transfer_qty, 2)
frappe.db.set_value(
"Stock Entry Detail",
s.items[3].name,
{"secondary_item_type": None, "is_legacy_scrap_item": 1},
)
from erpnext.stock.doctype.stock_entry.services.manufacturing import ManufactureStockEntry
stock_entry = frappe.get_doc({"doctype": "Stock Entry", "work_order": self.work_order.name})
used_secondary_items = ManufactureStockEntry(stock_entry).get_used_secondary_items()
self.assertEqual(used_secondary_items[("_Test Item", "Scrap")], 2)
@ERPNextTestSuite.change_settings(
"Manufacturing Settings", {"overproduction_percentage_for_work_order": 100}
)

View File

@@ -862,6 +862,9 @@ class ManufactureStockEntry(BaseManufactureStockEntry):
secondary_items = self.get_secondary_items_from_job_card()
for row in secondary_items:
if row.stock_qty <= 0:
continue
row.uom = row.uom or row.stock_uom
row.qty = ceil_qty_if_uom_has_whole_number(row.stock_qty, row.stock_uom)
row.transfer_qty = row.qty
@@ -888,33 +891,38 @@ class ManufactureStockEntry(BaseManufactureStockEntry):
def _adjust_secondary_item_qtys(self, secondary_items, used_secondary_items, pending_qty):
for row in secondary_items:
row.stock_qty -= flt(used_secondary_items.get(row.item_code))
key = (row.item_code, row.secondary_item_type or "")
row.stock_qty -= flt(used_secondary_items.get(key))
row.stock_qty = row.stock_qty * flt(self.doc.fg_completed_qty) / flt(pending_qty)
if used_secondary_items.get(row.item_code):
used_secondary_items[row.item_code] -= row.stock_qty
def get_used_secondary_items(self):
data = self._query_used_secondary_items()
used_secondary_items = defaultdict(float)
for row in data:
used_secondary_items[row.item_code] += row.qty
secondary_item_type = row.secondary_item_type or ("Scrap" if row.is_legacy_scrap_item else "")
key = (row.item_code, secondary_item_type)
used_secondary_items[key] += row.qty
return used_secondary_items
def _query_used_secondary_items(self):
se = frappe.qb.DocType("Stock Entry")
sed = frappe.qb.DocType("Stock Entry Detail")
return (
query = (
frappe.qb.from_(se)
.inner_join(sed)
.on(sed.parent == se.name)
.select(sed.item_code, sed.qty)
.select(sed.item_code, sed.secondary_item_type, sed.is_legacy_scrap_item, sed.qty)
.where(
(se.work_order == self.doc.work_order)
& ((sed.secondary_item_type.isnotnull()) | (sed.is_legacy_scrap_item == 1))
& (se.docstatus == 1)
& (se.purpose.isin(["Repack", "Manufacture"]))
)
).run(as_dict=1)
)
if self.doc.job_card:
query = query.where(se.job_card == self.doc.job_card)
return query.run(as_dict=1)
def get_completed_job_card_qty(self):
return flt(min([d.completed_qty for d in self.wo_doc.operations]))