Skip to content

Commit

Permalink
fix: work order with multi level, fetch operting cost from sub-assemb…
Browse files Browse the repository at this point in the history
…ly (backport #38992) (#39028)

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

(cherry picked from commit 70abedc)

Co-authored-by: rohitwaghchaure <[email protected]>
  • Loading branch information
mergify[bot] and rohitwaghchaure authored Dec 29, 2023
1 parent f3254c2 commit 3a7506e
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 7 deletions.
44 changes: 44 additions & 0 deletions erpnext/manufacturing/doctype/bom/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1486,3 +1486,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 @@ -32,6 +32,7 @@ class ManufacturingSettings(Document):
mins_between_operations: DF.Int
overproduction_percentage_for_sales_order: DF.Percent
overproduction_percentage_for_work_order: DF.Percent
set_op_cost_and_scrape_from_sub_assemblies: DF.Check
update_bom_costs_automatically: DF.Check
# end: auto-generated types

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,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
88 changes: 88 additions & 0 deletions erpnext/manufacturing/doctype/work_order/test_work_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -1731,6 +1731,93 @@ def test_job_card_extra_qty(self):
job_card2.time_logs = []
job_card2.save()

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 @@ -1977,6 +2064,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 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 @@ -25,7 +25,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_qty
Expand Down Expand Up @@ -1898,11 +1903,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 @@ -2653,6 +2669,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

0 comments on commit 3a7506e

Please sign in to comment.