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.
This commit is contained in:
Sudharsanan11
2026-08-11 16:31:46 +05:30
parent 6c5aca0b37
commit 3c92a9e853
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):
"Get items based on item code, item group or brand."
if item_code := filters.get("item_code"):
return [item_code]
return [item_code] if isinstance(item_code, str) else list(item_code)
else:
item_filters = {"is_stock_item": 1}
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",
label: __("Item"),
fieldtype: "Link",
fieldtype: "MultiSelectList",
width: "80",
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",
label: __("Warehouse"),
fieldtype: "Link",
fieldtype: "MultiSelectList",
width: "80",
options: "Warehouse",
get_query: function () {
get_data: function (txt) {
const company = frappe.query_report.get_filter_value("company");
return {
filters: { company: company },
};
return frappe.db.get_link_options("Warehouse", txt, {
...(company && { company }),
});
},
},
{

View File

@@ -7,6 +7,7 @@
import frappe
from frappe import _
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Count
from frappe.utils import cint, flt, getdate
@@ -115,7 +116,8 @@ def validate_filters(filters):
def get_warehouse_list(filters):
if not filters.get("warehouse"):
warehouses = filters.get("warehouse")
if not warehouses:
return frappe.get_all(
"Warehouse",
filters={"company": filters.get("company"), "is_group": 0},
@@ -123,13 +125,22 @@ def get_warehouse_list(filters):
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")
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 (
frappe.qb.from_(warehouse)
.select("name")
.where((warehouse.lft >= lft) & (warehouse.rgt <= rgt))
.select(warehouse.name)
.where(condition)
.orderby(warehouse.name)
.run(as_dict=True)
)