From 73683b275457279402d060fb9e246662b7de2b7b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 8 Apr 2025 15:02:12 +0530 Subject: [PATCH 1/6] fix: group sub assemblies in production plan (cherry picked from commit f58abed935f17f7d4c6b79c1c097954ce5a0643b) --- .../production_plan/production_plan.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index cfd3f789e78..5330001c617 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -939,6 +939,7 @@ class ProductionPlan(Document): bom_data = [] get_sub_assembly_items( + [item.production_item for item in sub_assembly_items_store], row.bom_no, bom_data, row.planned_qty, @@ -1528,10 +1529,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d so_item_details = frappe._dict() - sub_assembly_items = {} + sub_assembly_items = defaultdict(int) if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"): for d in doc.get("sub_assembly_items"): - sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty")) + sub_assembly_items[(d.get("production_item"), d.get("bom_no"))] += d.get("qty") for data in po_items: if not data.get("include_exploded_items") and doc.get("sub_assembly_items"): @@ -1560,6 +1561,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d item_details = {} if doc.get("sub_assembly_items"): item_details = get_raw_materials_of_sub_assembly_items( + so_item_details[doc.get("sales_order")].keys() if so_item_details else [], item_details, company, bom_no, @@ -1737,6 +1739,7 @@ def get_item_data(item_code): def get_sub_assembly_items( + sub_assembly_items, bom_no, bom_data, to_produce_qty, @@ -1752,7 +1755,7 @@ def get_sub_assembly_items( stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) bin_details = frappe._dict() - if skip_available_sub_assembly_item: + if skip_available_sub_assembly_item and d.item_code not in sub_assembly_items: bin_details = get_bin_details(d, company, for_warehouse=warehouse) for _bin_dict in bin_details: @@ -1787,6 +1790,7 @@ def get_sub_assembly_items( if d.value: get_sub_assembly_items( + sub_assembly_items, d.value, bom_data, stock_qty, @@ -1866,7 +1870,13 @@ def get_non_completed_production_plans(): def get_raw_materials_of_sub_assembly_items( - item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1 + existing_sub_assembly_items, + item_details, + company, + bom_no, + include_non_stock_items, + sub_assembly_items, + planned_qty=1, ): bei = frappe.qb.DocType("BOM Item") bom = frappe.qb.DocType("BOM") @@ -1910,12 +1920,13 @@ def get_raw_materials_of_sub_assembly_items( for item in items: key = (item.item_code, item.bom_no) - if item.bom_no and key not in sub_assembly_items: + if (item.bom_no and key not in sub_assembly_items) or (item.item_code in existing_sub_assembly_items): continue if item.bom_no: planned_qty = flt(sub_assembly_items[key]) get_raw_materials_of_sub_assembly_items( + existing_sub_assembly_items, item_details, company, item.bom_no, From b3e852adfcfdb522152aad235e38c08dae2d0053 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 11 Apr 2025 16:22:28 +0530 Subject: [PATCH 2/6] fix: logic and added test case (cherry picked from commit f071255340bfb6fa2dd1c2f58d819c73c36ee6ae) --- .../production_plan/production_plan.py | 18 ++++-- .../production_plan/test_production_plan.py | 58 +++++++++++++++++++ 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 5330001c617..2d5dfbf32fe 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -925,6 +925,7 @@ class ProductionPlan(Document): "Fetch sub assembly items and optionally combine them." self.sub_assembly_items = [] sub_assembly_items_store = [] # temporary store to process all subassembly items + bin_details = frappe._dict() for row in self.po_items: if self.skip_available_sub_assembly_item and not self.sub_assembly_warehouse: @@ -940,6 +941,7 @@ class ProductionPlan(Document): get_sub_assembly_items( [item.production_item for item in sub_assembly_items_store], + bin_details, row.bom_no, bom_data, row.planned_qty, @@ -1740,6 +1742,7 @@ def get_item_data(item_code): def get_sub_assembly_items( sub_assembly_items, + bin_details, bom_no, bom_data, to_produce_qty, @@ -1754,25 +1757,27 @@ def get_sub_assembly_items( parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) - bin_details = frappe._dict() if skip_available_sub_assembly_item and d.item_code not in sub_assembly_items: - bin_details = get_bin_details(d, company, for_warehouse=warehouse) + bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse)) - for _bin_dict in bin_details: + for _bin_dict in bin_details[d.item_code]: if _bin_dict.projected_qty > 0: - if _bin_dict.projected_qty > stock_qty: + if _bin_dict.projected_qty >= stock_qty: + _bin_dict.projected_qty -= stock_qty stock_qty = 0 continue else: stock_qty = stock_qty - _bin_dict.projected_qty elif warehouse: - bin_details = get_bin_details(d, company, for_warehouse=warehouse) + bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse)) if stock_qty > 0: bom_data.append( frappe._dict( { - "actual_qty": bin_details[0].get("actual_qty", 0) if bin_details else 0, + "actual_qty": bin_details[d.item_code][0].get("actual_qty", 0) + if bin_details + else 0, "parent_item_code": parent_item_code, "description": d.description, "production_item": d.item_code, @@ -1791,6 +1796,7 @@ def get_sub_assembly_items( if d.value: get_sub_assembly_items( sub_assembly_items, + bin_details, d.value, bom_data, stock_qty, diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index c7228823bed..e5b60c7a4e6 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1635,6 +1635,64 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(row.production_item, sf_item) self.assertEqual(row.qty, 5.0) + def test_calculation_of_sub_assembly_items(self): + make_item("Sub Assembly Item ", properties={"is_stock_item": 1}) + make_item("RM Item 1", properties={"is_stock_item": 1}) + make_item("RM Item 2", properties={"is_stock_item": 1}) + make_bom(item="Sub Assembly Item", raw_materials=["RM Item 1", "RM Item 2"]) + make_bom(item="_Test FG Item", raw_materials=["Sub Assembly Item", "RM Item 1"]) + make_bom(item="_Test FG Item 2", raw_materials=["Sub Assembly Item"]) + + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + make_stock_entry( + item_code="Sub Assembly Item", + qty=80, + purpose="Material Receipt", + to_warehouse="_Test Warehouse - _TC", + ) + make_stock_entry( + item_code="RM Item 1", qty=90, purpose="Material Receipt", to_warehouse="_Test Warehouse - _TC" + ) + + plan = create_production_plan( + skip_available_sub_assembly_item=1, + sub_assembly_warehouse="_Test Warehouse - _TC", + warehouse="_Test Warehouse - _TC", + item_code="_Test FG Item", + skip_getting_mr_items=1, + planned_qty=100, + do_not_save=1, + ) + plan.get_items_from = "" + plan.append( + "po_items", + { + "use_multi_level_bom": 1, + "item_code": "_Test FG Item 2", + "bom_no": frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"), + "planned_qty": 50, + "planned_start_date": now_datetime(), + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + ) + plan.save() + + plan.get_sub_assembly_items() + + self.assertEqual(plan.sub_assembly_items[0].qty, 20) + self.assertEqual(plan.sub_assembly_items[1].qty, 50) + + from erpnext.manufacturing.doctype.production_plan.production_plan import ( + get_items_for_material_requests, + ) + + mr_items = get_items_for_material_requests(plan.as_dict()) + + self.assertEqual(mr_items[0].get("quantity"), 80) + self.assertEqual(mr_items[1].get("quantity"), 70) + def create_production_plan(**args): """ From dedb19e3e9f0df09a48b56ec2ed0973293f103ee Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 11 Apr 2025 19:30:35 +0530 Subject: [PATCH 3/6] fix: test cases (cherry picked from commit a7394329cab73a8203ab3444fd7d44f4056e896c) --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 2d5dfbf32fe..c285e34c7e6 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1776,7 +1776,7 @@ def get_sub_assembly_items( frappe._dict( { "actual_qty": bin_details[d.item_code][0].get("actual_qty", 0) - if bin_details + if bin_details[d.item_code] else 0, "parent_item_code": parent_item_code, "description": d.description, From 13d3b27a1fe91e8b017968e064d2ab123ef62ba5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 14 Apr 2025 11:09:45 +0530 Subject: [PATCH 4/6] fix: test cases error (cherry picked from commit 8df18762a9ded000fd82bef20789622ed60cb9d4) --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index c285e34c7e6..1e27e8d59de 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1776,7 +1776,7 @@ def get_sub_assembly_items( frappe._dict( { "actual_qty": bin_details[d.item_code][0].get("actual_qty", 0) - if bin_details[d.item_code] + if bin_details.get(d.item_code) else 0, "parent_item_code": parent_item_code, "description": d.description, From 924e9b94b641f8fb816f3a45868bf8381570a595 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 14 Apr 2025 13:28:06 +0530 Subject: [PATCH 5/6] fix: import error --- erpnext/manufacturing/doctype/production_plan/production_plan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 1e27e8d59de..937af0fac54 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -22,6 +22,7 @@ from frappe.utils import ( ) from frappe.utils.csvutils import build_csv_response from pypika.terms import ExistsCriterion +from collections import defaultdict from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children from erpnext.manufacturing.doctype.bom.bom import validate_bom_no From c58800a92949975e817d38630bdfee32cd78c5e6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 14 Apr 2025 13:33:48 +0530 Subject: [PATCH 6/6] fix: linter --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 937af0fac54..2e89a191f66 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -4,6 +4,7 @@ import copy import json +from collections import defaultdict import frappe from frappe import _, msgprint @@ -22,7 +23,6 @@ from frappe.utils import ( ) from frappe.utils.csvutils import build_csv_response from pypika.terms import ExistsCriterion -from collections import defaultdict from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children from erpnext.manufacturing.doctype.bom.bom import validate_bom_no