feat(stock): multi select item and warehouse filters in warehouse wise item balance

the item and warehouse filters took one value at a time, so comparing a
few warehouses meant re-running the report for each one.

both are multiselectlist now, matching the stock balance report. the
warehouse column list unions the subtree of every selected warehouse,
and get_items passes a list through instead of wrapping it. plain string
values still work, so saved filters and existing callers are unaffected.

(cherry picked from commit 3c92a9e853)
This commit is contained in:
Sudharsanan11
2026-08-11 16:31:46 +05:30
parent d4bacb7354
commit bf33bc17c0
3 changed files with 50 additions and 11 deletions

View File

@@ -270,7 +270,7 @@ def get_chart_data(period_columns):
def get_items(filters): def get_items(filters):
"Get items based on item code, item group or brand." "Get items based on item code, item group or brand."
if item_code := filters.get("item_code"): if item_code := filters.get("item_code"):
return [item_code] return [item_code] if isinstance(item_code, str) else list(item_code)
else: else:
item_filters = {"is_stock_item": 1} item_filters = {"is_stock_item": 1}
if item_group := filters.get("item_group"): if item_group := filters.get("item_group"):

View File

@@ -38,21 +38,49 @@ frappe.query_reports["Warehouse wise Item Balance Age and Value"] = {
{ {
fieldname: "item_code", fieldname: "item_code",
label: __("Item"), label: __("Item"),
fieldtype: "Link", fieldtype: "MultiSelectList",
width: "80", width: "80",
options: "Item", options: "Item",
get_data: async function (txt) {
const item_group = frappe.query_report.get_filter_value("item_group");
let { message: data } = await frappe.call({
method: "erpnext.controllers.queries.item_query",
args: {
doctype: "Item",
txt: txt,
searchfield: "name",
start: 0,
page_len: 10,
filters: {
...(item_group && { item_group }),
is_stock_item: 1,
},
as_dict: 1,
},
});
data = data.map(({ name, ...rest }) => {
return {
value: name,
description: Object.values(rest),
};
});
return data || [];
},
}, },
{ {
fieldname: "warehouse", fieldname: "warehouse",
label: __("Warehouse"), label: __("Warehouse"),
fieldtype: "Link", fieldtype: "MultiSelectList",
width: "80", width: "80",
options: "Warehouse", options: "Warehouse",
get_query: function () { get_data: function (txt) {
const company = frappe.query_report.get_filter_value("company"); const company = frappe.query_report.get_filter_value("company");
return { return frappe.db.get_link_options("Warehouse", txt, {
filters: { company: company }, ...(company && { company }),
}; });
}, },
}, },
{ {

View File

@@ -7,6 +7,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Count from frappe.query_builder.functions import Count
from frappe.utils import cint, flt, getdate from frappe.utils import cint, flt, getdate
@@ -115,7 +116,8 @@ def validate_filters(filters):
def get_warehouse_list(filters): def get_warehouse_list(filters):
if not filters.get("warehouse"): warehouses = filters.get("warehouse")
if not warehouses:
return frappe.get_all( return frappe.get_all(
"Warehouse", "Warehouse",
filters={"company": filters.get("company"), "is_group": 0}, filters={"company": filters.get("company"), "is_group": 0},
@@ -123,13 +125,22 @@ def get_warehouse_list(filters):
order_by="name", order_by="name",
) )
if isinstance(warehouses, str):
warehouses = [warehouses]
# columns cover every selected warehouse along with its descendants
subtrees = frappe.get_all("Warehouse", filters={"name": ("in", warehouses)}, fields=["lft", "rgt"])
if not subtrees:
return []
warehouse = frappe.qb.DocType("Warehouse") warehouse = frappe.qb.DocType("Warehouse")
lft, rgt = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"]) condition = Criterion.any([(warehouse.lft >= row.lft) & (warehouse.rgt <= row.rgt) for row in subtrees])
return ( return (
frappe.qb.from_(warehouse) frappe.qb.from_(warehouse)
.select("name") .select(warehouse.name)
.where((warehouse.lft >= lft) & (warehouse.rgt <= rgt)) .where(condition)
.orderby(warehouse.name)
.run(as_dict=True) .run(as_dict=True)
) )