From 57c356a1cd4cca079c69bd7561d75789332315e9 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Tue, 7 Oct 2025 15:16:19 +0000 Subject: [PATCH 1/4] feat(report): add batch qty update functionality in report (cherry picked from commit f40c492a050e763153efb39eee254016e41f0b52) --- .../report/stock_qty_vs_batch_qty/__init__.py | 0 .../stock_qty_vs_batch_qty.js | 71 +++++++++++++ .../stock_qty_vs_batch_qty.json | 31 ++++++ .../stock_qty_vs_batch_qty.py | 99 +++++++++++++++++++ 4 files changed, 201 insertions(+) create mode 100644 erpnext/stock/report/stock_qty_vs_batch_qty/__init__.py create mode 100644 erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js create mode 100644 erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.json create mode 100644 erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/__init__.py b/erpnext/stock/report/stock_qty_vs_batch_qty/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js new file mode 100644 index 00000000000..785a18b0c8c --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js @@ -0,0 +1,71 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Stock Qty vs Batch Qty"] = { + filters: [ + { + fieldname: "item", + label: __("Item"), + fieldtype: "Link", + options: "Item", + get_query: function () { + return { + filters: { has_batch_no: true }, + }; + }, + }, + { + fieldname: "batch", + label: __("Batch"), + fieldtype: "Link", + options: "Batch", + get_query: function () { + const item_code = frappe.query_report.get_filter_value("item"); + return { + filters: { item: item_code }, + }; + }, + }, + ], + onload: function (report) { + report.page.add_inner_button(__("Update Batch Qty"), function () { + let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); + let selected_rows = indexes + .map((i) => frappe.query_report.data[i]) + .filter((row) => row.difference != 0); + + if (selected_rows.length) { + frappe.call({ + method: "erpnext.stock.report.stock_qty_vs_batch_qty.stock_qty_vs_batch_qty.update_batch_qty", + args: { + batches: selected_rows, + }, + callback: function (r) { + if (!r.exc) { + report.refresh(); + } + }, + }); + } else { + frappe.msgprint(__("Please select at least one row with difference value")); + } + }); + }, + + formatter: function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (column.fieldname == "difference" && data) { + if (data.difference > 0) { + value = "" + value + ""; + } else if (data.difference < 0) { + value = "" + value + ""; + } + } + return value; + }, + get_datatable_options(options) { + return Object.assign(options, { + checkboxColumn: true, + }); + }, +}; diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.json b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.json new file mode 100644 index 00000000000..b1885bb07f4 --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2025-10-07 20:03:45.952352", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-10-07 20:03:45.952352", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Qty vs Batch Qty", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Item", + "report_name": "Stock Qty vs Batch Qty", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock Manager" + }, + { + "role": "Stock User" + } + ], + "timeout": 0 +} diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py new file mode 100644 index 00000000000..56038111b93 --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py @@ -0,0 +1,99 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import json + +import frappe +from frappe import _ +from frappe.query_builder import DocType + +from erpnext.stock.doctype.batch.batch import get_batch_qty + + +def execute(filters=None): + if not filters: + filters = {} + + columns = get_columns() + data = get_data(filters) + + return columns, data + + +def get_columns() -> list[dict]: + columns = [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 200, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 200}, + {"label": _("Batch"), "fieldname": "batch", "fieldtype": "Link", "options": "Batch", "width": 200}, + {"label": _("Batch Qty"), "fieldname": "batch_qty", "fieldtype": "Float", "width": 150}, + {"label": _("Stock Qty"), "fieldname": "stock_qty", "fieldtype": "Float", "width": 150}, + {"label": _("Difference"), "fieldname": "difference", "fieldtype": "Float", "width": 150}, + ] + + return columns + + +def get_data(filters): + item_filter = filters.get("item") + batch_filter = filters.get("batch") + + Batch = DocType("Batch") + + query = ( + frappe.qb.from_(Batch) + .select(Batch.item.as_("item_code"), Batch.item_name, Batch.batch_qty, Batch.name.as_("batch_no")) + .where(Batch.disabled == 0) + ) + + if item_filter: + query = query.where(Batch.item == item_filter) + + if batch_filter: + query = query.where(Batch.name == batch_filter) + + batch_list = query.run(as_dict=True) + data = [] + for batch in batch_list: + batches = get_batch_qty(batch_no=batch.batch_no) + + if not batches: + continue + + batch_qty = batch.get("batch_qty", 0) + actual_qty = sum(b.get("qty", 0) for b in batches) + + difference = batch_qty - actual_qty + + row = { + "item_code": batch.item_code, + "item_name": batch.item_name, + "batch": batch.batch_no, + "batch_qty": batch_qty, + "stock_qty": actual_qty, + "difference": difference, + } + + data.append(row) + + return data + + +@frappe.whitelist() +def update_batch_qty(batches=None): + if not batches: + return + + batches = json.loads(batches) + for batch in batches: + batch_name = batch.get("batch") + stock_qty = batch.get("stock_qty") + + frappe.db.set_value("Batch", batch_name, "batch_qty", stock_qty) + + frappe.msgprint(_("Batch Qty updated successfully"), alert=True) From e7fcacbe69a1bf77301325d87b86010ebb8f348d Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Wed, 8 Oct 2025 14:16:26 +0000 Subject: [PATCH 2/4] refactor: fetch batch qty difference in a single db query (cherry picked from commit 9cc77934a6ce1f822928d9587242c6944f289c80) --- .../stock_qty_vs_batch_qty.js | 2 +- .../stock_qty_vs_batch_qty.py | 56 ++++++++----------- 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js index 785a18b0c8c..bd906665398 100644 --- a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js @@ -22,7 +22,7 @@ frappe.query_reports["Stock Qty vs Batch Qty"] = { get_query: function () { const item_code = frappe.query_report.get_filter_value("item"); return { - filters: { item: item_code }, + filters: { item: item_code, disabled: 0 }, }; }, }, diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py index 56038111b93..bcd04fb4c45 100644 --- a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py @@ -5,9 +5,7 @@ import json import frappe from frappe import _ -from frappe.query_builder import DocType - -from erpnext.stock.doctype.batch.batch import get_batch_qty +from frappe.query_builder.functions import Sum def execute(filters=None): @@ -43,43 +41,37 @@ def get_data(filters): item_filter = filters.get("item") batch_filter = filters.get("batch") - Batch = DocType("Batch") + stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") + batch_ledger = frappe.qb.DocType("Serial and Batch Entry") + batch_table = frappe.qb.DocType("Batch") query = ( - frappe.qb.from_(Batch) - .select(Batch.item.as_("item_code"), Batch.item_name, Batch.batch_qty, Batch.name.as_("batch_no")) - .where(Batch.disabled == 0) + frappe.qb.from_(stock_ledger_entry) + .inner_join(batch_ledger) + .on(stock_ledger_entry.serial_and_batch_bundle == batch_ledger.parent) + .inner_join(batch_table) + .on(batch_ledger.batch_no == batch_table.name) + .select( + batch_table.item.as_("item_code"), + batch_table.item_name.as_("item_name"), + batch_table.name.as_("batch"), + batch_table.batch_qty.as_("batch_qty"), + Sum(batch_ledger.qty).as_("stock_qty"), + (Sum(batch_ledger.qty) - batch_table.batch_qty).as_("difference"), + ) + .where(batch_table.disabled == 0) + .where(stock_ledger_entry.is_cancelled == 0) + .groupby(batch_table.name) + .having((Sum(batch_ledger.qty) - batch_table.batch_qty) != 0) ) if item_filter: - query = query.where(Batch.item == item_filter) + query = query.where(batch_table.item == item_filter) if batch_filter: - query = query.where(Batch.name == batch_filter) + query = query.where(batch_table.name == batch_filter) - batch_list = query.run(as_dict=True) - data = [] - for batch in batch_list: - batches = get_batch_qty(batch_no=batch.batch_no) - - if not batches: - continue - - batch_qty = batch.get("batch_qty", 0) - actual_qty = sum(b.get("qty", 0) for b in batches) - - difference = batch_qty - actual_qty - - row = { - "item_code": batch.item_code, - "item_name": batch.item_name, - "batch": batch.batch_no, - "batch_qty": batch_qty, - "stock_qty": actual_qty, - "difference": difference, - } - - data.append(row) + data = query.run(as_dict=True) return data From 10b0da8bc81fda31aa9fc2047478928c78fb2e2c Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Mon, 13 Oct 2025 15:53:19 +0000 Subject: [PATCH 3/4] fix: use get_batch_qty to fetch batch data (cherry picked from commit cf03d0303356dc92c0903b1893850e6f8a7f53e1) --- .../stock_qty_vs_batch_qty.py | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py index bcd04fb4c45..4818f36610c 100644 --- a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py @@ -5,7 +5,8 @@ import json import frappe from frappe import _ -from frappe.query_builder.functions import Sum + +from erpnext.stock.doctype.batch.batch import get_batch_qty def execute(filters=None): @@ -37,43 +38,56 @@ def get_columns() -> list[dict]: return columns -def get_data(filters): - item_filter = filters.get("item") - batch_filter = filters.get("batch") +def get_data(filters=None): + filters = filters or {} - stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") - batch_ledger = frappe.qb.DocType("Serial and Batch Entry") - batch_table = frappe.qb.DocType("Batch") + item = filters.get("item") + batch_no = filters.get("batch") + + batch_sle_data = get_batch_qty(item_code=item, batch_no=batch_no) or [] + + stock_qty_map = {} + for row in batch_sle_data: + batch = row.get("batch_no") + if not batch: + continue + stock_qty_map[batch] = stock_qty_map.get(batch, 0) + (row.get("qty") or 0) + + batch = frappe.qb.DocType("Batch") query = ( - frappe.qb.from_(stock_ledger_entry) - .inner_join(batch_ledger) - .on(stock_ledger_entry.serial_and_batch_bundle == batch_ledger.parent) - .inner_join(batch_table) - .on(batch_ledger.batch_no == batch_table.name) - .select( - batch_table.item.as_("item_code"), - batch_table.item_name.as_("item_name"), - batch_table.name.as_("batch"), - batch_table.batch_qty.as_("batch_qty"), - Sum(batch_ledger.qty).as_("stock_qty"), - (Sum(batch_ledger.qty) - batch_table.batch_qty).as_("difference"), - ) - .where(batch_table.disabled == 0) - .where(stock_ledger_entry.is_cancelled == 0) - .groupby(batch_table.name) - .having((Sum(batch_ledger.qty) - batch_table.batch_qty) != 0) + frappe.qb.from_(batch) + .select(batch.name, batch.item, batch.item_name, batch.batch_qty) + .where(batch.disabled == 0) ) - if item_filter: - query = query.where(batch_table.item == item_filter) + if item: + query = query.where(batch.item == item) + if batch_no: + query = query.where(batch.name == batch_no) - if batch_filter: - query = query.where(batch_table.name == batch_filter) + batch_records = query.run(as_dict=True) or [] - data = query.run(as_dict=True) + result = [] + for batch_doc in batch_records: + name = batch_doc.get("name") + batch_qty = batch_doc.get("batch_qty") or 0 + stock_qty = stock_qty_map.get(name, 0) + difference = stock_qty - batch_qty - return data + if difference != 0: + result.append( + { + "item_code": batch_doc.get("item"), + "item_name": batch_doc.get("item_name"), + "batch": name, + "batch_qty": batch_qty, + "stock_qty": stock_qty, + "difference": difference, + } + ) + + return result @frappe.whitelist() From ca835c831b7bfd8ecec40251aab487a9c2137fcb Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Fri, 19 Dec 2025 14:05:22 +0000 Subject: [PATCH 4/4] fix: update batch_qty using get_batch_qty (cherry picked from commit 15d9d8b7199f74bf1f51d18d7cab24a3e884a713) --- .../stock_qty_vs_batch_qty.js | 2 +- .../stock_qty_vs_batch_qty.json | 7 +--- .../stock_qty_vs_batch_qty.py | 41 +++++++++++++------ 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js index bd906665398..f80126bcb0a 100644 --- a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.js @@ -38,7 +38,7 @@ frappe.query_reports["Stock Qty vs Batch Qty"] = { frappe.call({ method: "erpnext.stock.report.stock_qty_vs_batch_qty.stock_qty_vs_batch_qty.update_batch_qty", args: { - batches: selected_rows, + selected_batches: selected_rows, }, callback: function (r) { if (!r.exc) { diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.json b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.json index b1885bb07f4..147815be88d 100644 --- a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.json +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.json @@ -10,7 +10,7 @@ "idx": 0, "is_standard": "Yes", "letterhead": null, - "modified": "2025-10-07 20:03:45.952352", + "modified": "2025-11-18 11:35:04.615085", "modified_by": "Administrator", "module": "Stock", "name": "Stock Qty vs Batch Qty", @@ -21,10 +21,7 @@ "report_type": "Script Report", "roles": [ { - "role": "Stock Manager" - }, - { - "role": "Stock User" + "role": "Item Manager" } ], "timeout": 0 diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py index 4818f36610c..d88d610d23e 100644 --- a/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py @@ -44,7 +44,12 @@ def get_data(filters=None): item = filters.get("item") batch_no = filters.get("batch") - batch_sle_data = get_batch_qty(item_code=item, batch_no=batch_no) or [] + batch_sle_data = ( + get_batch_qty( + item_code=item, batch_no=batch_no, for_stock_levels=True, consider_negative_batches=True + ) + or [] + ) stock_qty_map = {} for row in batch_sle_data: @@ -69,17 +74,17 @@ def get_data(filters=None): batch_records = query.run(as_dict=True) or [] result = [] - for batch_doc in batch_records: - name = batch_doc.get("name") - batch_qty = batch_doc.get("batch_qty") or 0 + for row in batch_records: + name = row.get("name") + batch_qty = row.get("batch_qty") or 0 stock_qty = stock_qty_map.get(name, 0) difference = stock_qty - batch_qty if difference != 0: result.append( { - "item_code": batch_doc.get("item"), - "item_name": batch_doc.get("item_name"), + "item_code": row.get("item"), + "item_name": row.get("item_name"), "batch": name, "batch_qty": batch_qty, "stock_qty": stock_qty, @@ -91,15 +96,25 @@ def get_data(filters=None): @frappe.whitelist() -def update_batch_qty(batches=None): - if not batches: +def update_batch_qty(selected_batches=None): + if not selected_batches: return - batches = json.loads(batches) - for batch in batches: - batch_name = batch.get("batch") - stock_qty = batch.get("stock_qty") + selected_batches = json.loads(selected_batches) + for row in selected_batches: + batch_name = row.get("batch") - frappe.db.set_value("Batch", batch_name, "batch_qty", stock_qty) + batches = get_batch_qty( + batch_no=batch_name, + item_code=row.get("item_code"), + for_stock_levels=True, + consider_negative_batches=True, + ) + batch_qty = 0.0 + if batches: + for batch in batches: + batch_qty += batch.get("qty") + + frappe.db.set_value("Batch", batch_name, "batch_qty", batch_qty) frappe.msgprint(_("Batch Qty updated successfully"), alert=True)