Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: work order with multi level, fetch operting cost from sub-assembly (backport #38992) #39027

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions erpnext/manufacturing/doctype/bom/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1395,3 +1395,47 @@ def postprocess(source, doc):
)

return doc


def get_op_cost_from_sub_assemblies(bom_no, op_cost=0):
# Get operating cost from sub-assemblies

bom_items = frappe.get_all(
"BOM Item", filters={"parent": bom_no, "docstatus": 1}, fields=["bom_no"], order_by="idx asc"
)

for row in bom_items:
if not row.bom_no:
continue

if cost := frappe.get_cached_value("BOM", row.bom_no, "operating_cost_per_bom_quantity"):
op_cost += flt(cost)
get_op_cost_from_sub_assemblies(row.bom_no, op_cost)

return op_cost


def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None):
if not scrap_items:
scrap_items = {}

bom_items = frappe.get_all(
"BOM Item",
filters={"parent": bom_no, "docstatus": 1},
fields=["bom_no", "qty"],
order_by="idx asc",
)

for row in bom_items:
if not row.bom_no:
continue

qty = flt(row.qty) * flt(qty)
items = get_bom_items_as_dict(
row.bom_no, company, qty=qty, fetch_exploded=0, fetch_scrap_items=1
)
scrap_items.update(items)

get_scrap_items_from_sub_assemblies(row.bom_no, company, qty, scrap_items)

return scrap_items
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"job_card_excess_transfer",
"other_settings_section",
"update_bom_costs_automatically",
"set_op_cost_and_scrape_from_sub_assemblies",
"column_break_23",
"make_serial_no_batch_from_work_order"
],
Expand Down Expand Up @@ -194,13 +195,20 @@
"fieldname": "job_card_excess_transfer",
"fieldtype": "Check",
"label": "Allow Excess Material Transfer"
},
{
"default": "0",
"description": "In the case of 'Use Multi-Level BOM' in a work order, if the user wishes to add sub-assembly costs to Finished Goods items without using a job card as well the scrap items, then this option needs to be enable.",
"fieldname": "set_op_cost_and_scrape_from_sub_assemblies",
"fieldtype": "Check",
"label": "Set Operating Cost / Scrape Items From Sub-assemblies"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-09-13 22:09:09.401559",
"modified": "2023-12-28 16:37:44.874096",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
Expand All @@ -216,5 +224,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
Original file line number Diff line number Diff line change
Expand Up @@ -1591,6 +1591,10 @@ def make_bom(**args):
}
)

if args.operating_cost_per_bom_quantity:
bom.fg_based_operating_cost = 1
bom.operating_cost_per_bom_quantity = args.operating_cost_per_bom_quantity

for item in args.raw_materials:
item_doc = frappe.get_doc("Item", item)
bom.append(
Expand Down
89 changes: 88 additions & 1 deletion erpnext/manufacturing/doctype/work_order/test_work_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,93 @@ def test_make_serial_no_batch_from_work_order_for_serial_no(self):

frappe.db.set_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order", 0)

def test_op_cost_and_scrap_based_on_sub_assemblies(self):
# Make Sub Assembly BOM 1

frappe.db.set_single_value(
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 1
)

items = {
"Test Final FG Item": 0,
"Test Final SF Item 1": 0,
"Test Final SF Item 2": 0,
"Test Final RM Item 1": 100,
"Test Final RM Item 2": 200,
"Test Final Scrap Item 1": 50,
"Test Final Scrap Item 2": 60,
}

for item in items:
if not frappe.db.exists("Item", item):
item_properties = {"is_stock_item": 1, "valuation_rate": items[item]}

make_item(item_code=item, properties=item_properties),

prepare_boms_for_sub_assembly_test()

wo_order = make_wo_order_test_record(
production_item="Test Final FG Item",
qty=10,
use_multi_level_bom=1,
skip_transfer=1,
from_wip_warehouse=1,
)

se_doc = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
se_doc.save()

self.assertTrue(se_doc.additional_costs)
scrap_items = []
for item in se_doc.items:
if item.is_scrap_item:
scrap_items.append(item.item_code)

self.assertEqual(
sorted(scrap_items), sorted(["Test Final Scrap Item 1", "Test Final Scrap Item 2"])
)
for row in se_doc.additional_costs:
self.assertEqual(row.amount, 3000)

frappe.db.set_single_value(
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 0
)


def prepare_boms_for_sub_assembly_test():
if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}):
bom = make_bom(
item="Test Final SF Item 1",
source_warehouse="Stores - _TC",
raw_materials=["Test Final RM Item 1"],
operating_cost_per_bom_quantity=100,
do_not_submit=True,
)

bom.append("scrap_items", {"item_code": "Test Final Scrap Item 1", "qty": 1})

bom.submit()

if not frappe.db.exists("BOM", {"item": "Test Final SF Item 2"}):
bom = make_bom(
item="Test Final SF Item 2",
source_warehouse="Stores - _TC",
raw_materials=["Test Final RM Item 2"],
operating_cost_per_bom_quantity=200,
do_not_submit=True,
)

bom.append("scrap_items", {"item_code": "Test Final Scrap Item 2", "qty": 1})

bom.submit()

if not frappe.db.exists("BOM", {"item": "Test Final FG Item"}):
bom = make_bom(
item="Test Final FG Item",
source_warehouse="Stores - _TC",
raw_materials=["Test Final SF Item 1", "Test Final SF Item 2"],
)


def prepare_data_for_workstation_type_check():
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
Expand Down Expand Up @@ -1955,7 +2042,7 @@ def make_wo_order_test_record(**args):
wo_order.sales_order = args.sales_order or None
wo_order.planned_start_date = args.planned_start_date or now()
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
wo_order.from_wip_warehouse = args.from_wip_warehouse or None
wo_order.from_wip_warehouse = args.from_wip_warehouse or 0

if args.source_warehouse:
for item in wo_order.get("required_items"):
Expand Down
37 changes: 31 additions & 6 deletions erpnext/stock/doctype/stock_entry/stock_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
from erpnext.manufacturing.doctype.bom.bom import add_additional_cost, validate_bom_no
from erpnext.manufacturing.doctype.bom.bom import (
add_additional_cost,
get_op_cost_from_sub_assemblies,
get_scrap_items_from_sub_assemblies,
validate_bom_no,
)
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.batch.batch import get_batch_no, get_batch_qty, set_batch_nos
Expand Down Expand Up @@ -1767,11 +1772,22 @@ def get_bom_raw_materials(self, qty):
def get_bom_scrap_material(self, qty):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict

# item dict = { item_code: {qty, description, stock_uom} }
item_dict = (
get_bom_items_as_dict(self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1)
or {}
)
if (
frappe.db.get_single_value(
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies"
)
and self.work_order
and frappe.get_cached_value("Work Order", self.work_order, "use_multi_level_bom")
):
item_dict = get_scrap_items_from_sub_assemblies(self.bom_no, self.company, qty)
else:
# item dict = { item_code: {qty, description, stock_uom} }
item_dict = (
get_bom_items_as_dict(
self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1
)
or {}
)

for item in item_dict.values():
item.from_warehouse = ""
Expand Down Expand Up @@ -2527,6 +2543,15 @@ def get_work_order_details(work_order, company):
def get_operating_cost_per_unit(work_order=None, bom_no=None):
operating_cost_per_unit = 0
if work_order:
if (
bom_no
and frappe.db.get_single_value(
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies"
)
and frappe.get_cached_value("Work Order", work_order, "use_multi_level_bom")
):
return get_op_cost_from_sub_assemblies(bom_no)

if not bom_no:
bom_no = work_order.bom_no

Expand Down