Merge branch 'develop' into prod-plan-sub-items-aggregate
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index d640f3f..37d2b9f 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -918,7 +918,7 @@
frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))
@frappe.whitelist()
-def get_children(doctype, parent=None, is_root=False, **filters):
+def get_children(parent=None, is_root=False, **filters):
if not parent or parent=="BOM":
frappe.msgprint(_('Please select a BOM'))
return
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index c33e646..e8759f5 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -232,7 +232,7 @@
});
},
combine_items: function (frm) {
- frm.clear_table('prod_plan_references');
+ frm.clear_table("prod_plan_references");
frappe.call({
method: "get_items",
@@ -247,6 +247,13 @@
});
},
+ combine_sub_items: (frm) => {
+ if (frm.doc.sub_assembly_items.length > 0) {
+ frm.clear_table("sub_assembly_items");
+ frm.trigger("get_sub_assembly_items");
+ }
+ },
+
get_sub_assembly_items: function(frm) {
frm.dirty();
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 56cf2b4..3bfb764 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -36,6 +36,7 @@
"prod_plan_references",
"section_break_24",
"get_sub_assembly_items",
+ "combine_sub_items",
"sub_assembly_items",
"material_request_planning",
"include_non_stock_items",
@@ -340,7 +341,6 @@
{
"fieldname": "prod_plan_references",
"fieldtype": "Table",
- "hidden": 1,
"label": "Production Plan Item Reference",
"options": "Production Plan Item Reference"
},
@@ -370,16 +370,23 @@
"fieldname": "to_delivery_date",
"fieldtype": "Date",
"label": "To Delivery Date"
+ },
+ {
+ "default": "0",
+ "fieldname": "combine_sub_items",
+ "fieldtype": "Check",
+ "label": "Consolidate Sub Assembly Items"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-06 18:35:59.642232",
+ "modified": "2022-02-23 17:16:10.629378",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 80003da..48cd753 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -21,7 +21,8 @@
)
from frappe.utils.csvutils import build_csv_response
-from erpnext.manufacturing.doctype.bom.bom import get_children, validate_bom_no
+from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children
+from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
@@ -570,17 +571,28 @@
@frappe.whitelist()
def get_sub_assembly_items(self, manufacturing_type=None):
+ "Fetch sub assembly items and optionally combine them."
self.sub_assembly_items = []
+ sub_assembly_items_store = [] # temporary store to process all subassembly items
+
for row in self.po_items:
bom_data = []
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
+ sub_assembly_items_store.extend(bom_data)
- self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True)
- for idx, row in enumerate(self.sub_assembly_items, start=1):
- row.idx = idx
+ if self.combine_sub_items:
+ # Combine subassembly items
+ sub_assembly_items_store = self.combine_subassembly_items(sub_assembly_items_store)
+
+ sub_assembly_items_store.sort(key= lambda d: d.bom_level, reverse=True) # sort by bom level
+
+ for idx, row in enumerate(sub_assembly_items_store):
+ row.idx = idx + 1
+ self.append("sub_assembly_items", row)
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
+ "Modify bom_data, set additional details."
for data in bom_data:
data.qty = data.stock_qty
data.production_plan_item = row.name
@@ -589,7 +601,32 @@
data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item
else "In House")
- self.append("sub_assembly_items", data)
+ def combine_subassembly_items(self, sub_assembly_items_store):
+ "Aggregate if same: Item, Warehouse, Inhouse/Outhouse Manu.g, BOM No."
+ key_wise_data = {}
+ for row in sub_assembly_items_store:
+ key = (
+ row.get("production_item"), row.get("fg_warehouse"),
+ row.get("bom_no"), row.get("type_of_manufacturing")
+ )
+ if key not in key_wise_data:
+ # intialise (item, wh, bom no, man.g type) wise dict
+ key_wise_data[key] = row
+ continue
+
+ existing_row = key_wise_data[key]
+ if existing_row:
+ # if row with same (item, wh, bom no, man.g type) key, merge
+ existing_row.qty += flt(row.qty)
+ existing_row.stock_qty += flt(row.stock_qty)
+ existing_row.bom_level = max(existing_row.bom_level, row.bom_level)
+ continue
+ else:
+ # add row with key
+ key_wise_data[key] = row
+
+ sub_assembly_items_store = [key_wise_data[key] for key in key_wise_data] # unpack into single level list
+ return sub_assembly_items_store
def all_items_completed(self):
all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001
@@ -1031,7 +1068,7 @@
}
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
- data = get_children('BOM', parent = bom_no)
+ data = get_bom_children(parent=bom_no)
for d in data:
if d.expandable:
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index b2a41ff..85ecec2 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -38,6 +38,9 @@
if not frappe.db.get_value('BOM', {'item': item}):
make_bom(item = item, raw_materials = raw_materials)
+ def tearDown(self) -> None:
+ frappe.db.rollback()
+
def test_production_plan_mr_creation(self):
"Test if MRs are created for unavailable raw materials."
pln = create_production_plan(item_code='Test Production Item 1')
@@ -258,6 +261,46 @@
pln.reload()
pln.cancel()
+ def test_production_plan_combine_subassembly(self):
+ """
+ Test combining Sub assembly items belonging to the same BOM in Prod Plan.
+ 1) Red-Car -> Wheel (sub assembly) > BOM-WHEEL-001
+ 2) Green-Car -> Wheel (sub assembly) > BOM-WHEEL-001
+ """
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+
+ bom_tree_1 = {
+ "Red-Car": {"Wheel": {"Rubber": {}}}
+ }
+ bom_tree_2 = {
+ "Green-Car": {"Wheel": {"Rubber": {}}}
+ }
+
+ parent_bom_1 = create_nested_bom(bom_tree_1, prefix="")
+ parent_bom_2 = create_nested_bom(bom_tree_2, prefix="")
+
+ # make sure both boms use same subassembly bom
+ subassembly_bom = parent_bom_1.items[0].bom_no
+ frappe.db.set_value("BOM Item", parent_bom_2.items[0].name, "bom_no", subassembly_bom)
+
+ plan = create_production_plan(item_code="Red-Car", use_multi_level_bom=1, do_not_save=True)
+ plan.append("po_items", { # Add Green-Car to Prod Plan
+ 'use_multi_level_bom': 1,
+ 'item_code': "Green-Car",
+ 'bom_no': frappe.db.get_value('Item', "Green-Car", 'default_bom'),
+ 'planned_qty': 1,
+ 'planned_start_date': now_datetime()
+ })
+ plan.get_sub_assembly_items()
+ self.assertTrue(len(plan.sub_assembly_items), 2)
+
+ plan.combine_sub_items = 1
+ plan.get_sub_assembly_items()
+
+ self.assertTrue(len(plan.sub_assembly_items), 1) # check if sub-assembly items merged
+ self.assertEqual(plan.sub_assembly_items[0].qty, 2.0)
+ self.assertEqual(plan.sub_assembly_items[0].stock_qty, 2.0)
+
def test_pp_to_mr_customer_provided(self):
" Test Material Request from Production Plan for Customer Provided Item."
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)