diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index c96536d049a..cef2f7420e8 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3693,7 +3693,7 @@ class TestPurchaseReceipt(IntegrationTestCase): columns, data = execute( filters=frappe._dict( - {"item_code": item_code, "warehouse": pr.items[0].warehouse, "company": pr.company} + {"item_code": [item_code], "warehouse": [pr.items[0].warehouse], "company": pr.company} ) ) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 7a472b1b816..978f2d4d13c 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -253,19 +253,35 @@ def get_warehouses_based_on_account(account, company=None): # Will be use for frappe.qb def apply_warehouse_filter(query, sle, filters): - if warehouse := filters.get("warehouse"): - warehouse_table = frappe.qb.DocType("Warehouse") + if not (warehouses := filters.get("warehouse")): + return query - lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - chilren_subquery = ( - frappe.qb.from_(warehouse_table) - .select(warehouse_table.name) - .where( - (warehouse_table.lft >= lft) - & (warehouse_table.rgt <= rgt) - & (warehouse_table.name == sle.warehouse) - ) - ) - query = query.where(ExistsCriterion(chilren_subquery)) + warehouse_table = frappe.qb.DocType("Warehouse") + + if isinstance(warehouses, str): + warehouses = [warehouses] + + warehouse_range = frappe.get_all( + "Warehouse", + filters={ + "name": ("in", warehouses), + }, + fields=["lft", "rgt"], + as_list=True, + ) + + child_query = frappe.qb.from_(warehouse_table).select(warehouse_table.name) + + range_conditions = [ + (warehouse_table.lft >= lft) & (warehouse_table.rgt <= rgt) for lft, rgt in warehouse_range + ] + + combined_condition = range_conditions[0] + for condition in range_conditions[1:]: + combined_condition = combined_condition | condition + + child_query = child_query.where(combined_condition).where(warehouse_table.name == sle.warehouse) + + query = query.where(ExistsCriterion(child_query)) return query diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index 0d68caa7e09..c53e80096d3 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -36,38 +36,57 @@ frappe.query_reports["Stock Balance"] = { }, { fieldname: "item_code", - label: __("Item"), - fieldtype: "Link", + label: __("Items"), + fieldtype: "MultiSelectList", width: "80", options: "Item", - get_query: function () { + get_data: async function (txt) { let item_group = frappe.query_report.get_filter_value("item_group"); - return { - query: "erpnext.controllers.queries.item_query", - filters: { - ...(item_group && { item_group }), - is_stock_item: 1, - }, + let filters = { + ...(item_group && { item_group }), + is_stock_item: 1, }; + + 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: filters, + as_dict: 1, + }, + }); + + data = data.map(({ name, description }) => { + return { + value: name, + description: description, + }; + }); + + return data || []; }, }, { fieldname: "warehouse", - label: __("Warehouse"), - fieldtype: "Link", + label: __("Warehouses"), + fieldtype: "MultiSelectList", width: "80", options: "Warehouse", - get_query: () => { + get_data: (txt) => { let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); let company = frappe.query_report.get_filter_value("company"); - return { - filters: { - ...(warehouse_type && { warehouse_type }), - ...(company && { company }), - }, + let filters = { + ...(warehouse_type && { warehouse_type }), + ...(company && { company }), }; + + return frappe.db.get_link_options("Warehouse", txt, filters); }, }, { diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 3177e41642f..026a064c7dc 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -8,7 +8,6 @@ from typing import Any, TypedDict import frappe from frappe import _ -from frappe.query_builder import Order from frappe.query_builder.functions import Coalesce from frappe.utils import add_days, cint, date_diff, flt, getdate from frappe.utils.nestedset import get_descendants_of @@ -26,8 +25,8 @@ class StockBalanceFilter(TypedDict): from_date: str to_date: str item_group: str | None - item: str | None - warehouse: str | None + item: list[str] | None + warehouse: list[str] | None warehouse_type: str | None include_uom: str | None # include extra info in converted UOM show_stock_ageing_data: bool @@ -361,6 +360,7 @@ class StockBalanceReport: if self.filters.get("warehouse"): query = apply_warehouse_filter(query, sle, self.filters) + elif warehouse_type := self.filters.get("warehouse_type"): query = ( query.join(warehouse_table) @@ -375,13 +375,11 @@ class StockBalanceReport: children = get_descendants_of("Item Group", item_group, ignore_permissions=True) query = query.where(item_table.item_group.isin([*children, item_group])) - for field in ["item_code", "brand"]: - if not self.filters.get(field): - continue - elif field == "item_code": - query = query.where(item_table.name == self.filters.get(field)) - else: - query = query.where(item_table[field] == self.filters.get(field)) + if item_codes := self.filters.get("item_code"): + query = query.where(item_table.name.isin(item_codes)) + + if brand := self.filters.get("brand"): + query = query.where(item_table.brand == brand) return query diff --git a/erpnext/stock/report/stock_balance/test_stock_balance.py b/erpnext/stock/report/stock_balance/test_stock_balance.py index 1b856f3ed0d..a0176026c48 100644 --- a/erpnext/stock/report/stock_balance/test_stock_balance.py +++ b/erpnext/stock/report/stock_balance/test_stock_balance.py @@ -23,7 +23,7 @@ class TestStockBalance(IntegrationTestCase): self.filters = _dict( { "company": "_Test Company", - "item_code": self.item.name, + "item_code": [self.item.name], "from_date": "2020-01-01", "to_date": str(today()), } @@ -165,6 +165,6 @@ class TestStockBalance(IntegrationTestCase): variant.save() self.generate_stock_ledger(variant.name, [_dict(qty=5, rate=10)]) - rows = stock_balance(self.filters.update({"show_variant_attributes": 1, "item_code": variant.name})) + rows = stock_balance(self.filters.update({"show_variant_attributes": 1, "item_code": [variant.name]})) self.assertPartialDictEq(attributes, rows[0]) self.assertInvariants(rows) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index d4c11de74e6..db6c0c06281 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -27,25 +27,44 @@ frappe.query_reports["Stock Ledger"] = { }, { fieldname: "warehouse", - label: __("Warehouse"), - fieldtype: "Link", + label: __("Warehouses"), + fieldtype: "MultiSelectList", 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, + }); }, }, { fieldname: "item_code", - label: __("Item"), - fieldtype: "Link", + label: __("Items"), + fieldtype: "MultiSelectList", options: "Item", - get_query: function () { - return { - query: "erpnext.controllers.queries.item_query", - }; + get_data: async function (txt) { + 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: {}, + as_dict: 1, + }, + }); + + data = data.map(({ name, description }) => { + return { + value: name, + description: description, + }; + }); + + return data || []; }, }, { diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 391395503b0..bbc85c5a4ad 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -456,19 +456,23 @@ def get_items(filters): query = frappe.qb.from_(item).select(item.name) conditions = [] - if item_code := filters.get("item_code"): - conditions.append(item.name == item_code) + if item_codes := filters.get("item_code"): + conditions.append(item.name.isin(item_codes)) + else: if brand := filters.get("brand"): conditions.append(item.brand == brand) - if item_group := filters.get("item_group"): - if condition := get_item_group_condition(item_group, item): - conditions.append(condition) + + if filters.get("item_group") and ( + condition := get_item_group_condition(filters.get("item_group"), item) + ): + conditions.append(condition) items = [] if conditions: for condition in conditions: query = query.where(condition) + items = [r[0] for r in query.run()] return items @@ -505,6 +509,7 @@ def get_item_details(items, sl_entries, include_uom): return item_details +# TODO: THIS IS NOT USED def get_sle_conditions(filters): conditions = [] if filters.get("warehouse"): @@ -535,8 +540,8 @@ def get_opening_balance_from_batch(filters, columns, sl_entries): } for fields in ["item_code", "warehouse"]: - if filters.get(fields): - query_filters[fields] = filters.get(fields) + if value := filters.get(fields): + query_filters[fields] = ("in", value) opening_data = frappe.get_all( "Stock Ledger Entry", @@ -567,8 +572,16 @@ def get_opening_balance_from_batch(filters, columns, sl_entries): ) for field in ["item_code", "warehouse", "company"]: - if filters.get(field): - query = query.where(table[field] == filters.get(field)) + value = filters.get(field) + + if not value: + continue + + if isinstance(value, list | tuple): + query = query.where(table[field].isin(value)) + + else: + query = query.where(table[field] == value) bundle_data = query.run(as_dict=True) @@ -623,13 +636,34 @@ def get_opening_balance(filters, columns, sl_entries): return row -def get_warehouse_condition(warehouse): - warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) - if warehouse_details: - return f" exists (select name from `tabWarehouse` wh \ - where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)" +def get_warehouse_condition(warehouses): + if not warehouses: + return "" - return "" + if isinstance(warehouses, str): + warehouses = [warehouses] + + warehouse_range = frappe.get_all( + "Warehouse", + filters={ + "name": ("in", warehouses), + }, + fields=["lft", "rgt"], + as_list=True, + ) + + if not warehouse_range: + return "" + + alias = "wh" + conditions = [] + for lft, rgt in warehouse_range: + conditions.append(f"({alias}.lft >= {lft} and {alias}.rgt <= {rgt})") + + conditions = " or ".join(conditions) + + return f" exists (select name from `tabWarehouse` {alias} \ + where ({conditions}) and warehouse = {alias}.name)" def get_item_group_condition(item_group, item_table=None): diff --git a/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py b/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py index e7bd53c68ab..66e7560caa6 100644 --- a/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py +++ b/erpnext/stock/report/stock_ledger/test_stock_ledger_report.py @@ -17,7 +17,7 @@ class TestStockLedgerReeport(IntegrationTestCase): company="_Test Company", from_date=today(), to_date=add_days(today(), 30), - item_code="_Test Stock Report Serial Item", + item_code=["_Test Stock Report Serial Item"], ) def tearDown(self) -> None: diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py index 36e3e2d29bf..986c1404cbe 100644 --- a/erpnext/stock/report/test_reports.py +++ b/erpnext/stock/report/test_reports.py @@ -18,8 +18,15 @@ batch = get_random("Batch") REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ ("Stock Ledger", {"_optional": True}), ("Stock Ledger", {"batch_no": batch}), - ("Stock Ledger", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}), - ("Stock Balance", {"_optional": True}), + ("Stock Ledger", {"item_code": ["_Test Item"], "warehouse": ["_Test Warehouse - _TC"]}), + ( + "Stock Balance", + { + "item_code": ["_Test Item"], + "warehouse": ["_Test Warehouse - _TC"], + "item_group": "_Test Item Group", + }, + ), ("Stock Projected Qty", {"_optional": True}), ("Batch-Wise Balance History", {}), ("Itemwise Recommended Reorder Level", {"item_group": "All Item Groups"}), diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index fb90e37fa98..fbb74cd8f24 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1683,8 +1683,20 @@ def get_stock_ledger_entries( ): """get stock ledger entries filtered by specific posting datetime conditions""" conditions = f" and posting_datetime {operator} %(posting_datetime)s" - if previous_sle.get("warehouse"): - conditions += " and warehouse = %(warehouse)s" + + if item_code := previous_sle.get("item_code"): + if isinstance(item_code, list | tuple): + conditions += " and item_code in %(item_code)s" + else: + conditions += " and item_code = %(item_code)s" + + if warehouse := previous_sle.get("warehouse"): + if isinstance(warehouse, list | tuple): + conditions += " and warehouse in %(warehouse)s" + + else: + conditions += " and warehouse = %(warehouse)s" + elif previous_sle.get("warehouse_condition"): conditions += " and " + previous_sle.get("warehouse_condition") @@ -1727,8 +1739,7 @@ def get_stock_ledger_entries( """ select *, posting_datetime as "timestamp" from `tabStock Ledger Entry` - where item_code = %(item_code)s - and is_cancelled = 0 + where is_cancelled = 0 {conditions} order by posting_datetime {order}, creation {order} {limit} {for_update}""".format(