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

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

(cherry picked from commit 70abedc57ad2147719c23537ab9aaf348d6e9182)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
This commit is contained in:
mergify[bot] 2023-12-29 20:54:26 +05:30 committed by GitHub
parent f3254c2010
commit 3a7506ecbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 178 additions and 7 deletions

View File

@ -1486,3 +1486,47 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
)
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

View File

@ -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"
],
@ -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",
@ -216,5 +224,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -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

View File

@ -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(

View File

@ -1731,6 +1731,93 @@ class TestWorkOrder(FrappeTestCase):
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
@ -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"):

View File

@ -25,7 +25,12 @@ from frappe.utils import (
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
@ -1898,11 +1903,22 @@ class StockEntry(StockController):
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 = ""
@ -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