Merge pull request #48729 from frappe/mergify/bp/version-15-hotfix/pr-48382

This commit is contained in:
Smit Vora
2025-07-26 12:13:27 +05:30
committed by GitHub
10 changed files with 186 additions and 78 deletions

View File

@@ -3669,7 +3669,7 @@ class TestPurchaseReceipt(FrappeTestCase):
columns, data = execute( columns, data = execute(
filters=frappe._dict( 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}
) )
) )

View File

@@ -242,19 +242,35 @@ def get_warehouses_based_on_account(account, company=None):
# Will be use for frappe.qb # Will be use for frappe.qb
def apply_warehouse_filter(query, sle, filters): def apply_warehouse_filter(query, sle, filters):
if warehouse := filters.get("warehouse"): if not (warehouses := filters.get("warehouse")):
warehouse_table = frappe.qb.DocType("Warehouse") return query
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) warehouse_table = frappe.qb.DocType("Warehouse")
chilren_subquery = (
frappe.qb.from_(warehouse_table) if isinstance(warehouses, str):
.select(warehouse_table.name) warehouses = [warehouses]
.where(
(warehouse_table.lft >= lft) warehouse_range = frappe.get_all(
& (warehouse_table.rgt <= rgt) "Warehouse",
& (warehouse_table.name == sle.warehouse) filters={
) "name": ("in", warehouses),
) },
query = query.where(ExistsCriterion(chilren_subquery)) 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 return query

View File

@@ -36,38 +36,57 @@ frappe.query_reports["Stock Balance"] = {
}, },
{ {
fieldname: "item_code", fieldname: "item_code",
label: __("Item"), label: __("Items"),
fieldtype: "Link", fieldtype: "MultiSelectList",
width: "80", width: "80",
options: "Item", options: "Item",
get_query: function () { get_data: async function (txt) {
let item_group = frappe.query_report.get_filter_value("item_group"); let item_group = frappe.query_report.get_filter_value("item_group");
return { let filters = {
query: "erpnext.controllers.queries.item_query", ...(item_group && { item_group }),
filters: { is_stock_item: 1,
...(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", fieldname: "warehouse",
label: __("Warehouse"), label: __("Warehouses"),
fieldtype: "Link", fieldtype: "MultiSelectList",
width: "80", width: "80",
options: "Warehouse", options: "Warehouse",
get_query: () => { get_data: (txt) => {
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
let company = frappe.query_report.get_filter_value("company"); let company = frappe.query_report.get_filter_value("company");
return { let filters = {
filters: { ...(warehouse_type && { warehouse_type }),
...(warehouse_type && { warehouse_type }), ...(company && { company }),
...(company && { company }),
},
}; };
return frappe.db.get_link_options("Warehouse", txt, filters);
}, },
}, },
{ {

View File

@@ -24,8 +24,8 @@ class StockBalanceFilter(TypedDict):
from_date: str from_date: str
to_date: str to_date: str
item_group: str | None item_group: str | None
item: str | None item: list[str] | None
warehouse: str | None warehouse: list[str] | None
warehouse_type: str | None warehouse_type: str | None
include_uom: str | None # include extra info in converted UOM include_uom: str | None # include extra info in converted UOM
show_stock_ageing_data: bool show_stock_ageing_data: bool
@@ -283,8 +283,11 @@ class StockBalanceReport:
) )
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]: for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
if self.filters.get(fieldname): if value := self.filters.get(fieldname):
query = query.where(table[fieldname] == self.filters.get(fieldname)) if isinstance(value, list | tuple):
query = query.where(table[fieldname].isin(value))
else:
query = query.where(table[fieldname] == value)
return query.run(as_dict=True) return query.run(as_dict=True)
@@ -347,6 +350,7 @@ class StockBalanceReport:
if self.filters.get("warehouse"): if self.filters.get("warehouse"):
query = apply_warehouse_filter(query, sle, self.filters) query = apply_warehouse_filter(query, sle, self.filters)
elif warehouse_type := self.filters.get("warehouse_type"): elif warehouse_type := self.filters.get("warehouse_type"):
query = ( query = (
query.join(warehouse_table) query.join(warehouse_table)
@@ -361,13 +365,11 @@ class StockBalanceReport:
children = get_descendants_of("Item Group", item_group, ignore_permissions=True) children = get_descendants_of("Item Group", item_group, ignore_permissions=True)
query = query.where(item_table.item_group.isin([*children, item_group])) query = query.where(item_table.item_group.isin([*children, item_group]))
for field in ["item_code", "brand"]: if item_codes := self.filters.get("item_code"):
if not self.filters.get(field): query = query.where(item_table.name.isin(item_codes))
continue
elif field == "item_code": if brand := self.filters.get("brand"):
query = query.where(item_table.name == self.filters.get(field)) query = query.where(item_table.brand == brand)
else:
query = query.where(item_table[field] == self.filters.get(field))
return query return query

View File

@@ -23,7 +23,7 @@ class TestStockBalance(FrappeTestCase):
self.filters = _dict( self.filters = _dict(
{ {
"company": "_Test Company", "company": "_Test Company",
"item_code": self.item.name, "item_code": [self.item.name],
"from_date": "2020-01-01", "from_date": "2020-01-01",
"to_date": str(today()), "to_date": str(today()),
} }
@@ -165,6 +165,6 @@ class TestStockBalance(FrappeTestCase):
variant.save() variant.save()
self.generate_stock_ledger(variant.name, [_dict(qty=5, rate=10)]) 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.assertPartialDictEq(attributes, rows[0])
self.assertInvariants(rows) self.assertInvariants(rows)

View File

@@ -27,25 +27,44 @@ frappe.query_reports["Stock Ledger"] = {
}, },
{ {
fieldname: "warehouse", fieldname: "warehouse",
label: __("Warehouse"), label: __("Warehouses"),
fieldtype: "Link", fieldtype: "MultiSelectList",
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 {
filters: { company: company }, return frappe.db.get_link_options("Warehouse", txt, {
}; company: company,
});
}, },
}, },
{ {
fieldname: "item_code", fieldname: "item_code",
label: __("Item"), label: __("Items"),
fieldtype: "Link", fieldtype: "MultiSelectList",
options: "Item", options: "Item",
get_query: function () { get_data: async function (txt) {
return { let { message: data } = await frappe.call({
query: "erpnext.controllers.queries.item_query", 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 || [];
}, },
}, },
{ {

View File

@@ -456,19 +456,23 @@ def get_items(filters):
query = frappe.qb.from_(item).select(item.name) query = frappe.qb.from_(item).select(item.name)
conditions = [] conditions = []
if item_code := filters.get("item_code"): if item_codes := filters.get("item_code"):
conditions.append(item.name == item_code) conditions.append(item.name.isin(item_codes))
else: else:
if brand := filters.get("brand"): if brand := filters.get("brand"):
conditions.append(item.brand == brand) conditions.append(item.brand == brand)
if item_group := filters.get("item_group"):
if condition := get_item_group_condition(item_group, item): if filters.get("item_group") and (
conditions.append(condition) condition := get_item_group_condition(filters.get("item_group"), item)
):
conditions.append(condition)
items = [] items = []
if conditions: if conditions:
for condition in conditions: for condition in conditions:
query = query.where(condition) query = query.where(condition)
items = [r[0] for r in query.run()] items = [r[0] for r in query.run()]
return items return items
@@ -505,6 +509,7 @@ def get_item_details(items, sl_entries, include_uom):
return item_details return item_details
# TODO: THIS IS NOT USED
def get_sle_conditions(filters): def get_sle_conditions(filters):
conditions = [] conditions = []
if filters.get("warehouse"): if filters.get("warehouse"):
@@ -535,8 +540,8 @@ def get_opening_balance_from_batch(filters, columns, sl_entries):
} }
for fields in ["item_code", "warehouse"]: for fields in ["item_code", "warehouse"]:
if filters.get(fields): if value := filters.get(fields):
query_filters[fields] = filters.get(fields) query_filters[fields] = ("in", value)
opening_data = frappe.get_all( opening_data = frappe.get_all(
"Stock Ledger Entry", "Stock Ledger Entry",
@@ -567,8 +572,16 @@ def get_opening_balance_from_batch(filters, columns, sl_entries):
) )
for field in ["item_code", "warehouse", "company"]: for field in ["item_code", "warehouse", "company"]:
if filters.get(field): value = filters.get(field)
query = query.where(table[field] == 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) bundle_data = query.run(as_dict=True)
@@ -623,13 +636,34 @@ def get_opening_balance(filters, columns, sl_entries):
return row return row
def get_warehouse_condition(warehouse): def get_warehouse_condition(warehouses):
warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) if not warehouses:
if warehouse_details: return ""
return f" exists (select name from `tabWarehouse` wh \
where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)"
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): def get_item_group_condition(item_group, item_table=None):

View File

@@ -17,7 +17,7 @@ class TestStockLedgerReeport(FrappeTestCase):
company="_Test Company", company="_Test Company",
from_date=today(), from_date=today(),
to_date=add_days(today(), 30), to_date=add_days(today(), 30),
item_code="_Test Stock Report Serial Item", item_code=["_Test Stock Report Serial Item"],
) )
def tearDown(self) -> None: def tearDown(self) -> None:

View File

@@ -17,8 +17,15 @@ batch = get_random("Batch")
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
("Stock Ledger", {"_optional": True}), ("Stock Ledger", {"_optional": True}),
("Stock Ledger", {"batch_no": batch}), ("Stock Ledger", {"batch_no": batch}),
("Stock Ledger", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}), ("Stock Ledger", {"item_code": ["_Test Item"], "warehouse": ["_Test Warehouse - _TC"]}),
("Stock Balance", {"_optional": True}), (
"Stock Balance",
{
"item_code": ["_Test Item"],
"warehouse": ["_Test Warehouse - _TC"],
"item_group": "_Test Item Group",
},
),
("Stock Projected Qty", {"_optional": True}), ("Stock Projected Qty", {"_optional": True}),
("Batch-Wise Balance History", {}), ("Batch-Wise Balance History", {}),
("Itemwise Recommended Reorder Level", {"item_group": "All Item Groups"}), ("Itemwise Recommended Reorder Level", {"item_group": "All Item Groups"}),

View File

@@ -1670,8 +1670,20 @@ def get_stock_ledger_entries(
): ):
"""get stock ledger entries filtered by specific posting datetime conditions""" """get stock ledger entries filtered by specific posting datetime conditions"""
conditions = f" and posting_datetime {operator} %(posting_datetime)s" 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"): elif previous_sle.get("warehouse_condition"):
conditions += " and " + previous_sle.get("warehouse_condition") conditions += " and " + previous_sle.get("warehouse_condition")
@@ -1714,8 +1726,7 @@ def get_stock_ledger_entries(
""" """
select *, posting_datetime as "timestamp" select *, posting_datetime as "timestamp"
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where item_code = %(item_code)s where is_cancelled = 0
and is_cancelled = 0
{conditions} {conditions}
order by posting_datetime {order}, creation {order} order by posting_datetime {order}, creation {order}
{limit} {for_update}""".format( {limit} {for_update}""".format(