From 06e91e758f18ea463351b5c6b6e5ab5204a4705c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 3 May 2023 18:06:47 +0530 Subject: [PATCH] feat: reserve qty against production plan raw materials in BIN --- .../material_request_plan_item.json | 9 +++- .../production_plan/production_plan.js | 10 ++--- .../production_plan/production_plan.py | 44 +++++++++++++++++++ .../production_plan/test_production_plan.py | 21 +++++++++ .../doctype/work_order/work_order.py | 30 +++++++++---- erpnext/stock/doctype/bin/bin.json | 9 +++- erpnext/stock/doctype/bin/bin.py | 30 ++++++++++++- .../stock_projected_qty.py | 9 ++++ 8 files changed, 143 insertions(+), 19 deletions(-) diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index 8c61d545b8..09bf1d8a73 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -16,6 +16,7 @@ "column_break_4", "quantity", "uom", + "conversion_factor", "projected_qty", "reserved_qty_for_production", "safety_stock", @@ -169,11 +170,17 @@ "label": "Qty As Per BOM", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2022-11-26 14:59:25.879631", + "modified": "2023-05-03 12:43:29.895754", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 62715e6565..ab7aa52bb7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -336,10 +336,6 @@ frappe.ui.form.on('Production Plan', { }, get_items_for_material_requests(frm, warehouses) { - let set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'from_warehouse', - 'min_order_qty', 'required_bom_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'ordered_qty', - 'reserved_qty_for_production', 'material_request_type']; - frappe.call({ method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests", freeze: true, @@ -352,11 +348,11 @@ frappe.ui.form.on('Production Plan', { frm.set_value('mr_items', []); r.message.forEach(row => { let d = frm.add_child('mr_items'); - set_fields.forEach(field => { - if (row[field]) { + for (let field in row) { + if (field !== 'name') { d[field] = row[field]; } - }); + } }); } refresh_field('mr_items'); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 0cc0f80cf1..df50cbf269 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -28,6 +28,7 @@ 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 from erpnext.stock.get_item_details import get_conversion_factor +from erpnext.stock.utils import get_or_make_bin from erpnext.utilities.transaction_base import validate_uom_is_integer @@ -398,9 +399,20 @@ class ProductionPlan(Document): self.set_status() self.db_set("status", self.status) + def on_submit(self): + self.update_bin_qty() + def on_cancel(self): self.db_set("status", "Cancelled") self.delete_draft_work_order() + self.update_bin_qty() + + def update_bin_qty(self): + for d in self.mr_items: + if d.warehouse: + bin_name = get_or_make_bin(d.item_code, d.warehouse) + bin = frappe.get_doc("Bin", bin_name, for_update=True) + bin.update_reserved_qty_for_production_plan() def delete_draft_work_order(self): for d in frappe.get_all( @@ -1068,6 +1080,7 @@ def get_material_request_items( "item_code": row.item_code, "item_name": row.item_name, "quantity": required_qty / conversion_factor, + "conversion_factor": conversion_factor, "required_bom_qty": total_qty, "stock_uom": row.get("stock_uom"), "warehouse": warehouse @@ -1474,3 +1487,34 @@ def set_default_warehouses(row, default_warehouses): for field in ["wip_warehouse", "fg_warehouse"]: if not row.get(field): row[field] = default_warehouses.get(field) + + +def get_reserved_qty_for_production_plan(item_code, warehouse): + from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production + + table = frappe.qb.DocType("Production Plan") + child = frappe.qb.DocType("Material Request Plan Item") + + query = ( + frappe.qb.from_(table) + .inner_join(child) + .on(table.name == child.parent) + .select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0))) + .where( + (table.docstatus == 1) + & (child.item_code == item_code) + & (child.warehouse == warehouse) + & (table.status.notin(["Completed", "Closed"])) + ) + ).run() + + if not query: + return 0.0 + + reserved_qty_for_production_plan = flt(query[0][0]) + + reserved_qty_for_production = flt( + get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True) + ) + + return reserved_qty_for_production_plan - reserved_qty_for_production diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2bf14c24cf..91864d09db 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -868,6 +868,27 @@ class TestProductionPlan(FrappeTestCase): for item_code in mr_items: self.assertTrue(item_code in validate_mr_items) + def test_resered_qty_for_production_plan_for_material_requests(self): + from erpnext.stock.utils import get_or_make_bin + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + pln = create_production_plan(item_code="Test Production Item 1") + + 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")) + + 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")) + + self.assertEqual(after_qty, before_qty) + def create_production_plan(**args): """ diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 66b871c746..75845226a6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -558,12 +558,19 @@ class WorkOrder(Document): and self.production_plan_item and not self.production_plan_sub_assembly_item ): - qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0 + table = frappe.qb.DocType("Work Order") - if self.docstatus == 1: - qty += self.qty - elif self.docstatus == 2: - qty -= self.qty + query = ( + frappe.qb.from_(table) + .select(Sum(table.qty)) + .where( + (table.production_plan == self.production_plan) + & (table.production_plan_item == self.production_plan_item) + & (table.docstatus == 1) + ) + ).run() + + qty = flt(query[0][0]) if query else 0 frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty) @@ -1476,12 +1483,14 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): return doc -def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float: +def get_reserved_qty_for_production( + item_code: str, warehouse: str, check_production_plan: bool = False +) -> float: """Get total reserved quantity for any item in specified warehouse""" wo = frappe.qb.DocType("Work Order") wo_item = frappe.qb.DocType("Work Order Item") - return ( + query = ( frappe.qb.from_(wo) .from_(wo_item) .select( @@ -1502,7 +1511,12 @@ def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float: | (wo_item.required_qty > wo_item.consumed_qty) ) ) - ).run()[0][0] or 0.0 + ) + + if check_production_plan: + query = query.where(wo.production_plan.isnotnull()) + + return query.run()[0][0] or 0.0 @frappe.whitelist() diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index d822f4a609..a11572776a 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -15,6 +15,7 @@ "projected_qty", "reserved_qty_for_production", "reserved_qty_for_sub_contract", + "reserved_qty_for_production_plan", "ma_rate", "stock_uom", "fcfs_rate", @@ -165,13 +166,19 @@ "oldfieldname": "stock_value", "oldfieldtype": "Currency", "read_only": 1 + }, + { + "fieldname": "reserved_qty_for_production_plan", + "fieldtype": "Float", + "label": "Reserved Qty for Production Plan", + "read_only": 1 } ], "hide_toolbar": 1, "idx": 1, "in_create": 1, "links": [], - "modified": "2022-03-30 07:22:23.868602", + "modified": "2023-05-02 23:26:21.806965", "modified_by": "Administrator", "module": "Stock", "name": "Bin", diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 72654e6f81..5abea9e69f 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -24,8 +24,30 @@ class Bin(Document): - flt(self.reserved_qty) - flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract) + - flt(self.reserved_qty_for_production_plan) ) + def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False): + """Update qty reserved for production from Production Plan tables + in open production plan""" + from erpnext.manufacturing.doctype.production_plan.production_plan import ( + get_reserved_qty_for_production_plan, + ) + + self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan( + self.item_code, self.warehouse + ) + + self.db_set( + "reserved_qty_for_production_plan", + flt(self.reserved_qty_for_production_plan), + update_modified=True, + ) + + if not skip_project_qty_update: + self.set_projected_qty() + self.db_set("projected_qty", self.projected_qty, update_modified=True) + def update_reserved_qty_for_production(self): """Update qty reserved for production from Production Item tables in open work orders""" @@ -35,11 +57,13 @@ class Bin(Document): self.item_code, self.warehouse ) - self.set_projected_qty() - self.db_set( "reserved_qty_for_production", flt(self.reserved_qty_for_production), update_modified=True ) + + self.update_reserved_qty_for_production_plan(skip_project_qty_update=True) + + self.set_projected_qty() self.db_set("projected_qty", self.projected_qty, update_modified=True) def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"): @@ -141,6 +165,7 @@ def get_bin_details(bin_name): "planned_qty", "reserved_qty_for_production", "reserved_qty_for_sub_contract", + "reserved_qty_for_production_plan", ], as_dict=1, ) @@ -188,6 +213,7 @@ def update_qty(bin_name, args): - flt(reserved_qty) - flt(bin_details.reserved_qty_for_production) - flt(bin_details.reserved_qty_for_sub_contract) + - flt(bin_details.reserved_qty_for_production_plan) ) frappe.db.set_value( diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index f477d8f08f..31c756da82 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -76,6 +76,7 @@ def execute(filters=None): bin.ordered_qty, bin.reserved_qty, bin.reserved_qty_for_production, + bin.reserved_qty_for_production_plan, bin.reserved_qty_for_sub_contract, reserved_qty_for_pos, bin.projected_qty, @@ -173,6 +174,13 @@ def get_columns(): "width": 100, "convertible": "qty", }, + { + "label": _("Reserved for Production Plan"), + "fieldname": "reserved_qty_for_production_plan", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, { "label": _("Reserved for Sub Contracting"), "fieldname": "reserved_qty_for_sub_contract", @@ -232,6 +240,7 @@ def get_bin_list(filters): bin.reserved_qty, bin.reserved_qty_for_production, bin.reserved_qty_for_sub_contract, + bin.reserved_qty_for_production_plan, bin.projected_qty, ) .orderby(bin.item_code, bin.warehouse)