feat: update stock ledger report to support multi-select for warehouses and items

(cherry picked from commit f2afd98725)
This commit is contained in:
Karm Soni
2025-07-03 18:04:05 +05:30
committed by Mergify
parent 72e8ce0449
commit ecf9e6e748
3 changed files with 98 additions and 57 deletions

View File

@@ -27,25 +27,24 @@ 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: function (txt) {
return { return frappe.db.get_link_options("Item", txt, {});
query: "erpnext.controllers.queries.item_query",
};
}, },
}, },
{ {

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): 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,31 @@ 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 isinstance(warehouses, str):
if warehouse_details: warehouses = [warehouses]
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 "" warehouse_range = frappe.get_all(
"Warehouse",
filters={
"name": ("in", warehouses),
},
fields=["lft", "rgt"],
as_list=True,
)
if not warehouse_range:
return ""
alias = "wh"
condtions = []
for lft, rgt in warehouse_range:
condtions.append(f"({alias}.lft >= {lft} and {alias}.rgt <= {rgt})")
condtions = " or ".join(condtions)
return f" exists (select name from `tabWarehouse` {alias} \
where ({condtions}) 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

@@ -56,12 +56,12 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
"""Create SL entries from SL entry dicts """Create SL entries from SL entry dicts
args: args:
- allow_negative_stock: disable negative stock valiations if true - allow_negative_stock: disable negative stock valiations if true
- via_landed_cost_voucher: landed cost voucher cancels and reposts - via_landed_cost_voucher: landed cost voucher cancels and reposts
entries of purchase document. This flag is used to identify if entries of purchase document. This flag is used to identify if
cancellation and repost is happening via landed cost voucher, in cancellation and repost is happening via landed cost voucher, in
such cases certain validations need to be ignored (like negative such cases certain validations need to be ignored (like negative
stock) stock)
""" """
from erpnext.controllers.stock_controller import future_sle_exists from erpnext.controllers.stock_controller import future_sle_exists
@@ -526,12 +526,12 @@ class update_entries_after:
:param args: args as dict :param args: args as dict
args = { args = {
"item_code": "ABC", "item_code": "ABC",
"warehouse": "XYZ", "warehouse": "XYZ",
"posting_date": "2012-12-12", "posting_date": "2012-12-12",
"posting_time": "12:00" "posting_time": "12:00"
} }
""" """
def __init__( def __init__(
@@ -599,15 +599,15 @@ class update_entries_after:
:Data Structure: :Data Structure:
self.data = { self.data = {
warehouse1: { warehouse1: {
'previus_sle': {}, 'previus_sle': {},
'qty_after_transaction': 10, 'qty_after_transaction': 10,
'valuation_rate': 100, 'valuation_rate': 100,
'stock_value': 1000, 'stock_value': 1000,
'prev_stock_value': 1000, 'prev_stock_value': 1000,
'stock_queue': '[[10, 100]]', 'stock_queue': '[[10, 100]]',
'stock_value_difference': 1000 'stock_value_difference': 1000
} }
} }
""" """
@@ -1644,11 +1644,11 @@ def get_previous_sle(args, for_update=False, extra_cond=None):
is called from various transaction like stock entry, reco etc is called from various transaction like stock entry, reco etc
args = { args = {
"item_code": "ABC", "item_code": "ABC" or ["ABC", "XYZ"],
"warehouse": "XYZ", "warehouse": "XYZ" or ["XYZ", "ABC"],
"posting_date": "2012-12-12", "posting_date": "2012-12-12",
"posting_time": "12:00", "posting_time": "12:00",
"sle": "name of reference Stock Ledger Entry" "sle": "name of reference Stock Ledger Entry"
} }
""" """
args["name"] = args.get("sle", None) or "" args["name"] = args.get("sle", None) or ""
@@ -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(