refactor: rewrite stock balance query to QB

This commit is contained in:
Ankush Menat 2022-04-11 15:34:53 +05:30
parent 8a499e95d3
commit febc74a21b
2 changed files with 88 additions and 65 deletions

View File

@ -7,11 +7,13 @@ from typing import Optional, TypedDict
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import CombineDatetime
from frappe.utils import cint, date_diff, flt, getdate from frappe.utils import cint, date_diff, flt, getdate
from frappe.utils.nestedset import get_descendants_of
from pypika.terms import ExistsCriterion
import erpnext import erpnext
from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_valuation_in_progress from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_valuation_in_progress
@ -33,8 +35,6 @@ def execute(filters: Optional[StockBalanceFilter] = None):
if not filters: if not filters:
filters = {} filters = {}
to_date = filters.get("to_date")
if filters.get("company"): if filters.get("company"):
company_currency = erpnext.get_company_currency(filters.get("company")) company_currency = erpnext.get_company_currency(filters.get("company"))
else: else:
@ -62,6 +62,7 @@ def execute(filters: Optional[StockBalanceFilter] = None):
_func = itemgetter(1) _func = itemgetter(1)
to_date = filters.get("to_date")
for (company, item, warehouse) in sorted(iwb_map): for (company, item, warehouse) in sorted(iwb_map):
if item_map.get(item): if item_map.get(item):
qty_dict = iwb_map[(company, item, warehouse)] qty_dict = iwb_map[(company, item, warehouse)]
@ -229,64 +230,75 @@ def get_columns(filters: StockBalanceFilter):
return columns return columns
def get_conditions(filters: StockBalanceFilter): def apply_conditions(query, filters):
conditions = "" sle = frappe.qb.DocType("Stock Ledger Entry")
warehouse_table = frappe.qb.DocType("Warehouse")
if not filters.get("from_date"): if not filters.get("from_date"):
frappe.throw(_("'From Date' is required")) frappe.throw(_("'From Date' is required"))
if filters.get("to_date"): if to_date := filters.get("to_date"):
conditions += " and sle.posting_date <= %s" % frappe.db.escape(filters.get("to_date")) query = query.where(sle.posting_date <= to_date)
else: else:
frappe.throw(_("'To Date' is required")) frappe.throw(_("'To Date' is required"))
if filters.get("company"): if company := filters.get("company"):
conditions += " and sle.company = %s" % frappe.db.escape(filters.get("company")) query = query.where(sle.company == company)
if filters.get("warehouse"): if warehouse := filters.get("warehouse"):
warehouse_details = frappe.db.get_value( lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 chilren_subquery = (
) frappe.qb.from_(warehouse_table)
if warehouse_details: .select(warehouse_table.name)
conditions += ( .where(
" and exists (select name from `tabWarehouse` wh \ (warehouse_table.lft >= lft)
where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" & (warehouse_table.rgt <= rgt)
% (warehouse_details.lft, warehouse_details.rgt) & (warehouse_table.name == sle.warehouse)
) )
)
if filters.get("warehouse_type") and not filters.get("warehouse"): query = query.where(ExistsCriterion(chilren_subquery))
conditions += ( elif warehouse_type := filters.get("warehouse_type"):
" and exists (select name from `tabWarehouse` wh \ query = (
where wh.warehouse_type = '%s' and sle.warehouse = wh.name)" query.join(warehouse_table)
% (filters.get("warehouse_type")) .on(warehouse_table.name == sle.warehouse)
.where(warehouse_table.warehouse_type == warehouse_type)
) )
return conditions return query
def get_stock_ledger_entries(filters: StockBalanceFilter, items): def get_stock_ledger_entries(filters: StockBalanceFilter, items):
item_conditions_sql = "" sle = frappe.qb.DocType("Stock Ledger Entry")
if items:
item_conditions_sql = " and sle.item_code in ({})".format( query = (
", ".join(frappe.db.escape(i, percent=False) for i in items) frappe.qb.from_(sle)
.select(
sle.item_code,
sle.warehouse,
sle.posting_date,
sle.actual_qty,
sle.valuation_rate,
sle.company,
sle.voucher_type,
sle.qty_after_transaction,
sle.stock_value_difference,
sle.item_code.as_("name"),
sle.voucher_no,
sle.stock_value,
sle.batch_no,
) )
.where((sle.docstatus < 2) & (sle.is_cancelled == 0))
conditions = get_conditions(filters) .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
.orderby(sle.creation)
return frappe.db.sql( .orderby(sle.actual_qty)
"""
select
sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate,
sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference,
sle.item_code as name, sle.voucher_no, sle.stock_value, sle.batch_no
from
`tabStock Ledger Entry` sle
where sle.docstatus < 2 %s %s
and is_cancelled = 0
order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty"""
% (item_conditions_sql, conditions), # nosec
as_dict=1,
) )
if items:
query = query.where(sle.item_code.isin(items))
query = apply_conditions(query, filters)
return query.run(as_dict=True)
def get_item_warehouse_map(filters: StockBalanceFilter, sle): def get_item_warehouse_map(filters: StockBalanceFilter, sle):
iwb_map = {} iwb_map = {}
@ -365,21 +377,17 @@ def filter_items_with_no_transactions(iwb_map, float_precision):
def get_items(filters: StockBalanceFilter): def get_items(filters: StockBalanceFilter):
"Get items based on item code, item group or brand." "Get items based on item code, item group or brand."
conditions = [] if item_code := filters.get("item_code"):
if filters.get("item_code"): return [item_code]
conditions.append("item.name=%(item_code)s")
else: else:
if filters.get("item_group"): item_filters = {}
conditions.append(get_item_group_condition(filters.get("item_group"))) if item_group := filters.get("item_group"):
if filters.get("brand"): # used in stock analytics report children = get_descendants_of("Item Group", item_group, ignore_permissions=True)
conditions.append("item.brand=%(brand)s") item_filters["item_group"] = ("in", children + [item_group])
if brand := filters.get("brand"):
item_filters["brand"] = brand
items = [] return frappe.get_all("Item", filters=item_filters, pluck="name", order_by=None, debug=1)
if conditions:
items = frappe.db.sql_list(
"""select name from `tabItem` item where {}""".format(" and ".join(conditions)), filters
)
return items
def get_item_details(items, sle, filters: StockBalanceFilter): def get_item_details(items, sle, filters: StockBalanceFilter):
@ -416,7 +424,7 @@ def get_item_details(items, sle, filters: StockBalanceFilter):
for item in res: for item in res:
item_details.setdefault(item.name, item) item_details.setdefault(item.name, item)
if filters.get("show_variant_attributes", 0) == 1: if filters.get("show_variant_attributes"):
variant_values = get_variant_values_for(list(item_details)) variant_values = get_variant_values_for(list(item_details))
item_details = {k: v.update(variant_values.get(k, {})) for k, v in item_details.items()} item_details = {k: v.update(variant_values.get(k, {})) for k, v in item_details.items()}
@ -443,7 +451,7 @@ def get_item_reorder_details(items):
def get_variants_attributes(): def get_variants_attributes():
"""Return all item variant attributes.""" """Return all item variant attributes."""
return [i.name for i in frappe.get_all("Item Attribute")] return frappe.get_all("Item Attribute", pluck="name")
def get_variant_values_for(items): def get_variant_values_for(items):

View File

@ -38,11 +38,9 @@ class TestStockBalance(FrappeTestCase):
def generate_stock_ledger(self, item_code: str, movements): def generate_stock_ledger(self, item_code: str, movements):
for movement in map(_dict, movements): for movement in map(_dict, movements):
make_stock_entry( if "to_warehouse" not in movement:
item_code=item_code, movement.to_warehouse = "_Test Warehouse - _TC"
**movement, make_stock_entry(item_code=item_code, **movement)
to_warehouse=movement.to_warehouse or "_Test Warehouse - _TC",
)
def assertInvariants(self, rows): def assertInvariants(self, rows):
last_balance = frappe.db.sql( last_balance = frappe.db.sql(
@ -135,3 +133,20 @@ class TestStockBalance(FrappeTestCase):
rows = stock_balance(self.filters.update({"include_uom": "Box"})) rows = stock_balance(self.filters.update({"include_uom": "Box"}))
self.assertEqual(rows[0].bal_qty_alt, 1) self.assertEqual(rows[0].bal_qty_alt, 1)
def test_item_group(self):
self.filters.pop("item_code", None)
rows = stock_balance(self.filters.update({"item_group": self.item.item_group}))
self.assertTrue(all(r.item_group == self.item.item_group for r in rows))
def test_child_warehouse_balances(self):
# This is default
self.generate_stock_ledger(self.item.name, [_dict(qty=5, rate=10, to_warehouse="Stores - _TC")])
self.filters.pop("item_code", None)
rows = stock_balance(self.filters.update({"warehouse": "All Warehouses - _TC"}))
self.assertTrue(
any(r.item_code == self.item.name and r.warehouse == "Stores - _TC" for r in rows),
msg=f"Expected child warehouse balances \n{rows}",
)