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..f80126bcb0a --- /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, disabled: 0 }, + }; + }, + }, + ], + 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: { + selected_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..147815be88d --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.json @@ -0,0 +1,28 @@ +{ + "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-11-18 11:35:04.615085", + "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": "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 new file mode 100644 index 00000000000..d88d610d23e --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/stock_qty_vs_batch_qty.py @@ -0,0 +1,120 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import json + +import frappe +from frappe import _ + +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=None): + filters = filters or {} + + item = filters.get("item") + batch_no = filters.get("batch") + + 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: + 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_(batch) + .select(batch.name, batch.item, batch.item_name, batch.batch_qty) + .where(batch.disabled == 0) + ) + + if item: + query = query.where(batch.item == item) + if batch_no: + query = query.where(batch.name == batch_no) + + batch_records = query.run(as_dict=True) or [] + + result = [] + 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": row.get("item"), + "item_name": row.get("item_name"), + "batch": name, + "batch_qty": batch_qty, + "stock_qty": stock_qty, + "difference": difference, + } + ) + + return result + + +@frappe.whitelist() +def update_batch_qty(selected_batches=None): + if not selected_batches: + return + + selected_batches = json.loads(selected_batches) + for row in selected_batches: + batch_name = row.get("batch") + + 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)