feat: reserved production plan sub assembly items (#37884)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 4a00416..49386c4 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",
"combine_sub_items",
+ "sub_assembly_warehouse",
"section_break_ucc4",
"skip_available_sub_assembly_item",
"column_break_igxl",
@@ -416,13 +417,19 @@
{
"fieldname": "column_break_igxl",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "sub_assembly_warehouse",
+ "fieldtype": "Link",
+ "label": "Sub Assembly Warehouse",
+ "options": "Warehouse"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-09-29 11:41:03.246059",
+ "modified": "2023-11-03 14:08:11.928027",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 1850d1e..6b12a29 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -490,6 +490,12 @@
bin = frappe.get_doc("Bin", bin_name, for_update=True)
bin.update_reserved_qty_for_production_plan()
+ for d in self.sub_assembly_items:
+ if d.fg_warehouse and d.type_of_manufacturing == "In House":
+ bin_name = get_or_make_bin(d.production_item, d.fg_warehouse)
+ bin = frappe.get_doc("Bin", bin_name, for_update=True)
+ bin.update_reserved_qty_for_for_sub_assembly()
+
def delete_draft_work_order(self):
for d in frappe.get_all(
"Work Order", fields=["name"], filters={"docstatus": 0, "production_plan": ("=", self.name)}
@@ -809,7 +815,11 @@
bom_data = []
- warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
+ warehouse = (
+ (self.sub_assembly_warehouse or row.warehouse)
+ if self.skip_available_sub_assembly_item
+ else None
+ )
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
sub_assembly_items_store.extend(bom_data)
@@ -831,7 +841,7 @@
for data in bom_data:
data.qty = data.stock_qty
data.production_plan_item = row.name
- data.fg_warehouse = row.warehouse
+ data.fg_warehouse = self.sub_assembly_warehouse or row.warehouse
data.schedule_date = row.planned_start_date
data.type_of_manufacturing = manufacturing_type or (
"Subcontract" if data.is_sub_contracted_item else "In House"
@@ -1637,8 +1647,8 @@
query = query.run()
- if not query:
- return 0.0
+ if not query or query[0][0] is None:
+ return None
reserved_qty_for_production_plan = flt(query[0][0])
@@ -1780,3 +1790,29 @@
query = query.offset(start)
return query.run()
+
+
+def get_reserved_qty_for_sub_assembly(item_code, warehouse):
+ table = frappe.qb.DocType("Production Plan")
+ child = frappe.qb.DocType("Production Plan Sub Assembly Item")
+
+ query = (
+ frappe.qb.from_(table)
+ .inner_join(child)
+ .on(table.name == child.parent)
+ .select(Sum(child.qty - IfNull(child.wo_produced_qty, 0)))
+ .where(
+ (table.docstatus == 1)
+ & (child.production_item == item_code)
+ & (child.fg_warehouse == warehouse)
+ & (table.status.notin(["Completed", "Closed"]))
+ )
+ )
+
+ query = query.run()
+
+ if not query or query[0][0] is None:
+ return None
+
+ qty = flt(query[0][0])
+ return qty if qty > 0 else 0.0
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index d414988..e9c6ee3 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1042,13 +1042,14 @@
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty - before_qty, 1)
-
pln = frappe.get_doc("Production Plan", pln.name)
pln.cancel()
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
+ pln.reload()
+ self.assertEqual(pln.docstatus, 2)
self.assertEqual(after_qty, before_qty)
def test_resered_qty_for_production_plan_for_work_order(self):
@@ -1359,6 +1360,93 @@
if row.item_code == "ChildPart2 For SUB Test":
self.assertEqual(row.quantity, 2)
+ def test_reserve_sub_assembly_items(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+ bom_tree = {
+ "Fininshed Goods Bicycle": {
+ "Frame Assembly": {"Frame": {}},
+ "Chain Assembly": {"Chain": {}},
+ }
+ }
+ parent_bom = create_nested_bom(bom_tree, prefix="")
+
+ warehouse = "_Test Warehouse - _TC"
+ company = "_Test Company"
+
+ sub_assembly_warehouse = create_warehouse("SUB ASSEMBLY WH", company=company)
+
+ for item_code in ["Frame", "Chain"]:
+ make_stock_entry(item_code=item_code, target=warehouse, qty=2, basic_rate=100)
+
+ before_qty = flt(
+ frappe.db.get_value(
+ "Bin",
+ {"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
+ "reserved_qty_for_production_plan",
+ )
+ )
+
+ plan = create_production_plan(
+ item_code=parent_bom.item,
+ planned_qty=2,
+ ignore_existing_ordered_qty=1,
+ do_not_submit=1,
+ skip_available_sub_assembly_item=1,
+ warehouse=warehouse,
+ sub_assembly_warehouse=sub_assembly_warehouse,
+ )
+
+ plan.get_sub_assembly_items()
+ plan.submit()
+
+ after_qty = flt(
+ frappe.db.get_value(
+ "Bin",
+ {"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
+ "reserved_qty_for_production_plan",
+ )
+ )
+
+ self.assertEqual(after_qty, before_qty + 2)
+
+ plan.make_work_order()
+ work_orders = frappe.get_all(
+ "Work Order",
+ fields=["name", "production_item"],
+ filters={"production_plan": plan.name},
+ order_by="creation desc",
+ )
+
+ for d in work_orders:
+ wo_doc = frappe.get_doc("Work Order", d.name)
+ wo_doc.skip_transfer = 1
+ wo_doc.from_wip_warehouse = 1
+
+ wo_doc.wip_warehouse = (
+ warehouse
+ if d.production_item in ["Frame Assembly", "Chain Assembly"]
+ else sub_assembly_warehouse
+ )
+
+ wo_doc.submit()
+
+ if d.production_item == "Frame Assembly":
+ self.assertEqual(wo_doc.fg_warehouse, sub_assembly_warehouse)
+ se_doc = frappe.get_doc(make_se_from_wo(wo_doc.name, "Manufacture", 2))
+ se_doc.submit()
+
+ after_qty = flt(
+ frappe.db.get_value(
+ "Bin",
+ {"item_code": "Frame Assembly", "warehouse": sub_assembly_warehouse},
+ "reserved_qty_for_production_plan",
+ )
+ )
+
+ self.assertEqual(after_qty, before_qty)
+
def create_production_plan(**args):
"""
@@ -1379,6 +1467,7 @@
"ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0,
"get_items_from": "Sales Order",
"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
+ "sub_assembly_warehouse": args.sub_assembly_warehouse,
}
)
diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
index fde0404..aff740b 100644
--- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
+++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
@@ -17,11 +17,10 @@
"type_of_manufacturing",
"supplier",
"work_order_details_section",
- "work_order",
+ "wo_produced_qty",
"purchase_order",
"production_plan_item",
"column_break_7",
- "produced_qty",
"received_qty",
"indent",
"section_break_19",
@@ -53,13 +52,6 @@
"label": "Reference"
},
{
- "fieldname": "work_order",
- "fieldtype": "Link",
- "label": "Work Order",
- "options": "Work Order",
- "read_only": 1
- },
- {
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
@@ -81,7 +73,8 @@
{
"fieldname": "received_qty",
"fieldtype": "Float",
- "label": "Received Qty"
+ "label": "Received Qty",
+ "read_only": 1
},
{
"fieldname": "bom_no",
@@ -162,12 +155,6 @@
"options": "Warehouse"
},
{
- "fieldname": "produced_qty",
- "fieldtype": "Data",
- "label": "Produced Quantity",
- "read_only": 1
- },
- {
"default": "In House",
"fieldname": "type_of_manufacturing",
"fieldtype": "Select",
@@ -209,12 +196,18 @@
"label": "Projected Qty",
"no_copy": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "wo_produced_qty",
+ "fieldtype": "Float",
+ "label": "Produced Qty",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-05-22 17:52:34.708879",
+ "modified": "2023-11-03 13:33:42.959387",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index f9fddcb..36a0cae 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -293,6 +293,7 @@
update_produced_qty_in_so_item(self.sales_order, self.sales_order_item)
if self.production_plan:
+ self.set_produced_qty_for_sub_assembly_item()
self.update_production_plan_status()
def get_transferred_or_manufactured_qty(self, purpose):
@@ -569,16 +570,49 @@
)
def update_planned_qty(self):
+ from erpnext.manufacturing.doctype.production_plan.production_plan import (
+ get_reserved_qty_for_sub_assembly,
+ )
+
+ qty_dict = {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)}
+
+ if self.production_plan_sub_assembly_item and self.production_plan:
+ qty_dict["reserved_qty_for_production_plan"] = get_reserved_qty_for_sub_assembly(
+ self.production_item, self.fg_warehouse
+ )
+
update_bin_qty(
self.production_item,
self.fg_warehouse,
- {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)},
+ qty_dict,
)
if self.material_request:
mr_obj = frappe.get_doc("Material Request", self.material_request)
mr_obj.update_requested_qty([self.material_request_item])
+ def set_produced_qty_for_sub_assembly_item(self):
+ table = frappe.qb.DocType("Work Order")
+
+ query = (
+ frappe.qb.from_(table)
+ .select(Sum(table.produced_qty))
+ .where(
+ (table.production_plan == self.production_plan)
+ & (table.production_plan_sub_assembly_item == self.production_plan_sub_assembly_item)
+ & (table.docstatus == 1)
+ )
+ ).run()
+
+ produced_qty = flt(query[0][0]) if query else 0
+
+ frappe.db.set_value(
+ "Production Plan Sub Assembly Item",
+ self.production_plan_sub_assembly_item,
+ "wo_produced_qty",
+ produced_qty,
+ )
+
def update_ordered_qty(self):
if (
self.production_plan
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index df466ed..8b2e5cf 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -34,10 +34,15 @@
get_reserved_qty_for_production_plan,
)
- self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
+ reserved_qty_for_production_plan = get_reserved_qty_for_production_plan(
self.item_code, self.warehouse
)
+ if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
+ return
+
+ self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
+
self.db_set(
"reserved_qty_for_production_plan",
flt(self.reserved_qty_for_production_plan),
@@ -48,6 +53,29 @@
self.set_projected_qty()
self.db_set("projected_qty", self.projected_qty, update_modified=True)
+ def update_reserved_qty_for_for_sub_assembly(self):
+ from erpnext.manufacturing.doctype.production_plan.production_plan import (
+ get_reserved_qty_for_sub_assembly,
+ )
+
+ reserved_qty_for_production_plan = get_reserved_qty_for_sub_assembly(
+ self.item_code, self.warehouse
+ )
+
+ if reserved_qty_for_production_plan is None and not self.reserved_qty_for_production_plan:
+ return
+
+ self.reserved_qty_for_production_plan = flt(reserved_qty_for_production_plan)
+ self.set_projected_qty()
+
+ self.db_set(
+ {
+ "projected_qty": self.projected_qty,
+ "reserved_qty_for_production_plan": flt(self.reserved_qty_for_production_plan),
+ },
+ update_modified=True,
+ )
+
def update_reserved_qty_for_production(self):
"""Update qty reserved for production from Production Item tables
in open work orders"""