feat: reserve qty against production plan raw materials in BIN
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 8c61d54..09bf1d8 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 62715e6..ab7aa52 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -336,10 +336,6 @@
},
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 @@
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 0cc0f80..df50cbf 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.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 @@
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 @@
"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 @@
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 2bf14c2..91864d0 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 @@
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 66b871c..7584522 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -558,12 +558,19 @@
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 @@
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 @@
| (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 d822f4a..a115727 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 72654e6..5abea9e 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -24,8 +24,30 @@
- 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 @@
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 @@
"planned_qty",
"reserved_qty_for_production",
"reserved_qty_for_sub_contract",
+ "reserved_qty_for_production_plan",
],
as_dict=1,
)
@@ -188,6 +213,7 @@
- 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 f477d8f..31c756d 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 @@
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,
@@ -174,6 +175,13 @@
"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",
"fieldtype": "Float",
@@ -232,6 +240,7 @@
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)