From 8b6430c886aa1f8a328c22437037b7b670f06044 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 21 Sep 2023 14:01:52 +0530 Subject: [PATCH 1/2] refactor(minor): `Stock Reservation Entry` --- .../stock_reconciliation.py | 2 +- .../stock_reservation_entry.js | 58 ++++++++--------- .../stock_reservation_entry.py | 65 ++++++++++++------- .../stock_reservation_entry_list.js | 15 +++-- .../report/stock_balance/stock_balance.py | 2 +- 5 files changed, 80 insertions(+), 62 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 26ca012d2c..e36d5769bd 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -346,7 +346,7 @@ class StockReconciliation(StockController): """Raises an exception if there is any reserved stock for the items in the Stock Reconciliation.""" from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - get_sre_reserved_qty_for_item_and_warehouse as get_sre_reserved_qty_details, + get_sre_reserved_qty_for_items_and_warehouses as get_sre_reserved_qty_details, ) item_code_list, warehouse_list = [], [] diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js index 4d9663602d..c5df319e22 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.js @@ -1,42 +1,42 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on("Stock Reservation Entry", { +frappe.ui.form.on('Stock Reservation Entry', { refresh(frm) { - frm.trigger("set_queries"); - frm.trigger("toggle_read_only_fields"); - frm.trigger("hide_rate_related_fields"); - frm.trigger("hide_primary_action_button"); - frm.trigger("make_sb_entries_warehouse_read_only"); + frm.trigger('set_queries'); + frm.trigger('toggle_read_only_fields'); + frm.trigger('hide_rate_related_fields'); + frm.trigger('hide_primary_action_button'); + frm.trigger('make_sb_entries_warehouse_read_only'); }, has_serial_no(frm) { - frm.trigger("toggle_read_only_fields"); + frm.trigger('toggle_read_only_fields'); }, has_batch_no(frm) { - frm.trigger("toggle_read_only_fields"); + frm.trigger('toggle_read_only_fields'); }, warehouse(frm) { if (frm.doc.warehouse) { frm.doc.sb_entries.forEach((row) => { - frappe.model.set_value(row.doctype, row.name, "warehouse", frm.doc.warehouse); + frappe.model.set_value(row.doctype, row.name, 'warehouse', frm.doc.warehouse); }); } }, set_queries(frm) { - frm.set_query("warehouse", () => { + frm.set_query('warehouse', () => { return { filters: { - "is_group": 0, - "company": frm.doc.company, + 'is_group': 0, + 'company': frm.doc.company, } }; }); - frm.set_query("serial_no", "sb_entries", function(doc, cdt, cdn) { + frm.set_query('serial_no', 'sb_entries', function(doc, cdt, cdn) { var selected_serial_nos = doc.sb_entries.map(row => { return row.serial_no; }); @@ -45,16 +45,16 @@ frappe.ui.form.on("Stock Reservation Entry", { filters: { item_code: doc.item_code, warehouse: row.warehouse, - status: "Active", - name: ["not in", selected_serial_nos], + status: 'Active', + name: ['not in', selected_serial_nos], } } }); - frm.set_query("batch_no", "sb_entries", function(doc, cdt, cdn) { + frm.set_query('batch_no', 'sb_entries', function(doc, cdt, cdn) { let filters = { item: doc.item_code, - batch_qty: [">", 0], + batch_qty: ['>', 0], disabled: 0, } @@ -63,7 +63,7 @@ frappe.ui.form.on("Stock Reservation Entry", { return row.batch_no; }); - filters.name = ["not in", selected_batch_nos]; + filters.name = ['not in', selected_batch_nos]; } return { filters: filters } @@ -74,37 +74,37 @@ frappe.ui.form.on("Stock Reservation Entry", { if (frm.doc.has_serial_no) { frm.doc.sb_entries.forEach(row => { if (row.qty !== 1) { - frappe.model.set_value(row.doctype, row.name, "qty", 1); + frappe.model.set_value(row.doctype, row.name, 'qty', 1); } }) } frm.fields_dict.sb_entries.grid.update_docfield_property( - "serial_no", "read_only", !frm.doc.has_serial_no + 'serial_no', 'read_only', !frm.doc.has_serial_no ); frm.fields_dict.sb_entries.grid.update_docfield_property( - "batch_no", "read_only", !frm.doc.has_batch_no + 'batch_no', 'read_only', !frm.doc.has_batch_no ); // Qty will always be 1 for Serial No. frm.fields_dict.sb_entries.grid.update_docfield_property( - "qty", "read_only", frm.doc.has_serial_no + 'qty', 'read_only', frm.doc.has_serial_no ); - frm.set_df_property("sb_entries", "allow_on_submit", frm.doc.against_pick_list ? 0 : 1); + frm.set_df_property('sb_entries', 'allow_on_submit', frm.doc.against_pick_list ? 0 : 1); }, hide_rate_related_fields(frm) { - ["incoming_rate", "outgoing_rate", "stock_value_difference", "is_outward", "stock_queue"].forEach(field => { + ['incoming_rate', 'outgoing_rate', 'stock_value_difference', 'is_outward', 'stock_queue'].forEach(field => { frm.fields_dict.sb_entries.grid.update_docfield_property( - field, "hidden", 1 + field, 'hidden', 1 ); }); }, hide_primary_action_button(frm) { - // Hide "Amend" button on cancelled document + // Hide 'Amend' button on cancelled document if (frm.doc.docstatus == 2) { frm.page.btn_primary.hide() } @@ -112,15 +112,15 @@ frappe.ui.form.on("Stock Reservation Entry", { make_sb_entries_warehouse_read_only(frm) { frm.fields_dict.sb_entries.grid.update_docfield_property( - "warehouse", "read_only", 1 + 'warehouse', 'read_only', 1 ); }, }); -frappe.ui.form.on("Serial and Batch Entry", { +frappe.ui.form.on('Serial and Batch Entry', { sb_entries_add(frm, cdt, cdn) { if (frm.doc.warehouse) { - frappe.model.set_value(cdt, cdn, "warehouse", frm.doc.warehouse); + frappe.model.set_value(cdt, cdn, 'warehouse', frm.doc.warehouse); } }, }); \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index bd7bb66836..936be3f73b 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -14,7 +14,7 @@ class StockReservationEntry(Document): self.validate_amended_doc() self.validate_mandatory() - self.validate_for_group_warehouse() + self.validate_group_warehouse() validate_disabled_warehouse(self.warehouse) validate_warehouse_company(self.warehouse, self.company) self.validate_uom_is_integer() @@ -74,7 +74,7 @@ class StockReservationEntry(Document): msg = _("{0} is required").format(self.meta.get_label(d)) frappe.throw(msg) - def validate_for_group_warehouse(self) -> None: + def validate_group_warehouse(self) -> None: """Raises an exception if `Warehouse` is a Group Warehouse.""" if frappe.get_cached_value("Warehouse", self.warehouse, "is_group"): @@ -544,10 +544,36 @@ def get_available_serial_nos_to_reserve( return available_serial_nos_list -def get_sre_reserved_qty_for_item_and_warehouse( - item_code: str | list, warehouse: str | list = None -) -> float | dict: - """Returns `Reserved Qty` for Item and Warehouse combination OR a dict like {("item_code", "warehouse"): "reserved_qty", ... }.""" +def get_sre_reserved_qty_for_item_and_warehouse(item_code: str, warehouse: str = None) -> float: + """Returns current `Reserved Qty` for Item and Warehouse combination.""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select(Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty")) + .where( + (sre.docstatus == 1) + & (sre.item_code == item_code) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + .groupby(sre.item_code, sre.warehouse) + ) + + if warehouse: + query = query.where(sre.warehouse == warehouse) + + reserved_qty = query.run(as_list=True) + + return flt(reserved_qty[0][0]) if reserved_qty else 0.0 + + +def get_sre_reserved_qty_for_items_and_warehouses( + item_code_list: list, warehouse_list: list = None +) -> dict: + """Returns a dict like {("item_code", "warehouse"): "reserved_qty", ... }.""" + + if not item_code_list: + return {} sre = frappe.qb.DocType("Stock Reservation Entry") query = ( @@ -557,29 +583,20 @@ def get_sre_reserved_qty_for_item_and_warehouse( sre.warehouse, Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"), ) - .where((sre.docstatus == 1) & (sre.status.notin(["Delivered", "Cancelled"]))) + .where( + (sre.docstatus == 1) + & sre.item_code.isin(item_code_list) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) .groupby(sre.item_code, sre.warehouse) ) - query = ( - query.where(sre.item_code.isin(item_code)) - if isinstance(item_code, list) - else query.where(sre.item_code == item_code) - ) - - if warehouse: - query = ( - query.where(sre.warehouse.isin(warehouse)) - if isinstance(warehouse, list) - else query.where(sre.warehouse == warehouse) - ) + if warehouse_list: + query = query.where(sre.warehouse.isin(warehouse_list)) data = query.run(as_dict=True) - if isinstance(item_code, str) and isinstance(warehouse, str): - return data[0]["reserved_qty"] if data else 0.0 - else: - return {(d["item_code"], d["warehouse"]): d["reserved_qty"] for d in data} if data else {} + return {(d["item_code"], d["warehouse"]): d["reserved_qty"] for d in data} if data else {} def get_sre_reserved_qty_details_for_voucher(voucher_type: str, voucher_no: str) -> dict: @@ -711,7 +728,7 @@ def get_serial_batch_entries_for_voucher(sre_name: str) -> list[dict]: ).run(as_dict=True) -def get_ssb_bundle_for_voucher(sre: dict) -> object | None: +def get_ssb_bundle_for_voucher(sre: dict) -> object: """Returns a new `Serial and Batch Bundle` against the provided SRE.""" sb_entries = get_serial_batch_entries_for_voucher(sre["name"]) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry_list.js b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry_list.js index 442ac39f13..5b390f7f1c 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry_list.js +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry_list.js @@ -4,13 +4,14 @@ frappe.listview_settings['Stock Reservation Entry'] = { get_indicator: function (doc) { const status_colors = { - 'Draft': 'red', - 'Partially Reserved': 'orange', - 'Reserved': 'blue', - 'Partially Delivered': 'purple', - 'Delivered': 'green', - 'Cancelled': 'red', + 'Draft': 'red', + 'Partially Reserved': 'orange', + 'Reserved': 'blue', + 'Partially Delivered': 'purple', + 'Delivered': 'green', + 'Cancelled': 'red', }; - return [__(doc.status), status_colors[doc.status], 'status,=,' + doc.status]; + + return [__(doc.status), status_colors[doc.status], 'status,=,' + doc.status]; }, }; \ No newline at end of file diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 337b0ea3a5..a59f9de42e 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -165,7 +165,7 @@ class StockBalanceReport(object): def get_sre_reserved_qty_details(self) -> dict: from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - get_sre_reserved_qty_for_item_and_warehouse as get_reserved_qty_details, + get_sre_reserved_qty_for_items_and_warehouses as get_reserved_qty_details, ) item_code_list, warehouse_list = [], [] From b6437e387ff5d50779c1804e4048c5c37e89fd04 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 21 Sep 2023 14:03:00 +0530 Subject: [PATCH 2/2] fix: `Item Dashboard` --- erpnext/stock/dashboard/item_dashboard.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py index 5a8d84a2c5..e638268866 100644 --- a/erpnext/stock/dashboard/item_dashboard.py +++ b/erpnext/stock/dashboard/item_dashboard.py @@ -3,7 +3,7 @@ from frappe.model.db_query import DatabaseQuery from frappe.utils import cint, flt from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock, + get_sre_reserved_qty_for_items_and_warehouses as get_reserved_stock_details, ) @@ -61,7 +61,10 @@ def get_data( limit_page_length=21, ) - sre_reserved_stock_details = get_reserved_stock(item_code, warehouse) + item_code_list = [item_code] if item_code else [i.item_code for i in items] + warehouse_list = [warehouse] if warehouse else [i.warehouse for i in items] + + sre_reserved_stock_details = get_reserved_stock_details(item_code_list, warehouse_list) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) for item in items: @@ -75,7 +78,8 @@ def get_data( "reserved_qty_for_production": flt(item.reserved_qty_for_production, precision), "reserved_qty_for_sub_contract": flt(item.reserved_qty_for_sub_contract, precision), "actual_qty": flt(item.actual_qty, precision), - "reserved_stock": sre_reserved_stock_details, + "reserved_stock": flt(sre_reserved_stock_details.get((item.item_code, item.warehouse))), } ) + return items