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 (#38992)

(cherry picked from commit 70abedc)

# Conflicts:
#	erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
#	erpnext/manufacturing/doctype/work_order/test_work_order.py
  • Loading branch information
rohitwaghchaure authored and mergify[bot] committed Dec 29, 2023
1 parent c0b5980 commit 5545d66
Show file tree
Hide file tree
Showing 6 changed files with 211 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 @@ -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 @@ -9,6 +9,36 @@


class ManufacturingSettings(Document):
<<<<<<< HEAD
=======
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from frappe.types import DF

add_corrective_operation_cost_in_finished_good_valuation: DF.Check
allow_overtime: DF.Check
allow_production_on_holidays: DF.Check
backflush_raw_materials_based_on: DF.Literal["BOM", "Material Transferred for Manufacture"]
capacity_planning_for_days: DF.Int
default_fg_warehouse: DF.Link | None
default_scrap_warehouse: DF.Link | None
default_wip_warehouse: DF.Link | None
disable_capacity_planning: DF.Check
job_card_excess_transfer: DF.Check
make_serial_no_batch_from_work_order: DF.Check
material_consumption: DF.Check
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

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


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

<<<<<<< HEAD
def test_make_serial_no_batch_from_work_order_for_serial_no(self):
item_code = "Test Serial No Item For Work Order"
warehouse = "_Test Warehouse - _TC"
Expand Down Expand Up @@ -1705,10 +1706,42 @@ def test_make_serial_no_batch_from_work_order_for_serial_no(self):
item=item_code,
bom_no=bom.name,
qty=5,
=======
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,
>>>>>>> 70abedc57a (fix: work order with multi level, fetch operting cost from sub-assembly (#38992))
skip_transfer=1,
from_wip_warehouse=1,
)

<<<<<<< HEAD
serial_nos = frappe.get_all(
"Serial No",
filters={"item_code": item_code, "work_order": wo_order.name},
Expand All @@ -1725,6 +1758,61 @@ def test_make_serial_no_batch_from_work_order_for_serial_no(self):
self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos)))

frappe.db.set_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order", 0)
=======
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"],
)
>>>>>>> 70abedc57a (fix: work order with multi level, fetch operting cost from sub-assembly (#38992))


def prepare_data_for_workstation_type_check():
Expand Down Expand Up @@ -1955,7 +2043,11 @@ 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"
<<<<<<< HEAD
wo_order.from_wip_warehouse = args.from_wip_warehouse or None
=======
wo_order.from_wip_warehouse = args.from_wip_warehouse or 0
>>>>>>> 70abedc57a (fix: work order with multi level, fetch operting cost from sub-assembly (#38992))

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

0 comments on commit 5545d66

Please sign in to comment.