mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-11 08:53:03 +00:00
fix: handle multi-select stock ageing filters (#55775)
This commit is contained in:
@@ -11,6 +11,7 @@ from frappe.query_builder.functions import Abs, Count
|
||||
from frappe.utils import cint, date_diff, flt, get_datetime
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
|
||||
from erpnext.stock.valuation import round_off_if_near_zero
|
||||
|
||||
Filters = frappe._dict
|
||||
@@ -72,12 +73,17 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
|
||||
return data
|
||||
|
||||
|
||||
def normalize_fifo_queue(fifo_queue: list) -> list:
|
||||
"""Convert batch valuation slots to the standard [qty, posting_date, value] shape."""
|
||||
return [get_batch_report_slot(slot) if is_batch_slot(slot) else slot for slot in fifo_queue]
|
||||
|
||||
|
||||
def get_report_fifo_queue(fifo_queue: list, has_batch_no: bool) -> list:
|
||||
get_posting_date = itemgetter(FIFO_POSTING_DATE_INDEX)
|
||||
fifo_queue = sorted([slot for slot in fifo_queue if get_posting_date(slot)], key=get_posting_date)
|
||||
|
||||
if has_batch_no:
|
||||
return [get_batch_report_slot(slot) for slot in fifo_queue]
|
||||
return normalize_fifo_queue(fifo_queue)
|
||||
|
||||
return fifo_queue
|
||||
|
||||
@@ -113,7 +119,7 @@ def get_report_row(filters: Filters, item_dict: dict, fifo_queue: list, to_date:
|
||||
|
||||
def get_average_age(fifo_queue: list, to_date: str) -> float:
|
||||
age_qty = total_qty = 0.0
|
||||
for slot in fifo_queue:
|
||||
for slot in normalize_fifo_queue(fifo_queue):
|
||||
qty = get_slot_qty(slot)
|
||||
age_qty += date_diff(to_date, slot[FIFO_DATE_INDEX]) * qty
|
||||
total_qty += qty
|
||||
@@ -925,9 +931,7 @@ class FIFOSlots:
|
||||
)
|
||||
)
|
||||
|
||||
for field in ["item_code"]:
|
||||
if self.filters.get(field):
|
||||
query = query.where(bundle[field] == self.filters.get(field))
|
||||
query = self._apply_filter(query, bundle, "item_code")
|
||||
|
||||
if self.filters.get("warehouse"):
|
||||
query = self._get_warehouse_conditions(bundle, query)
|
||||
@@ -965,9 +969,7 @@ class FIFOSlots:
|
||||
)
|
||||
)
|
||||
|
||||
for field in ["item_code"]:
|
||||
if self.filters.get(field):
|
||||
query = query.where(bundle[field] == self.filters.get(field))
|
||||
query = self._apply_filter(query, bundle, "item_code")
|
||||
|
||||
if self.filters.get("warehouse"):
|
||||
query = self._get_warehouse_conditions(bundle, query)
|
||||
@@ -997,27 +999,25 @@ class FIFOSlots:
|
||||
"has_batch_no",
|
||||
)
|
||||
|
||||
if self.filters.get("item_code"):
|
||||
item = item.where(item_table.item_code == self.filters.get("item_code"))
|
||||
item = self._apply_filter(item, item_table, "item_code")
|
||||
|
||||
if self.filters.get("brand"):
|
||||
item = item.where(item_table.brand == self.filters.get("brand"))
|
||||
|
||||
return item
|
||||
|
||||
def _apply_filter(self, query, table, fieldname: str):
|
||||
filter_value = self.filters.get(fieldname)
|
||||
if not filter_value:
|
||||
return query
|
||||
|
||||
if isinstance(filter_value, list | tuple | set):
|
||||
return query.where(table[fieldname].isin(filter_value))
|
||||
|
||||
return query.where(table[fieldname] == filter_value)
|
||||
|
||||
def _get_warehouse_conditions(self, sle, sle_query) -> str:
|
||||
warehouse = frappe.qb.DocType("Warehouse")
|
||||
lft, rgt = frappe.db.get_value("Warehouse", self.filters.get("warehouse"), ["lft", "rgt"])
|
||||
|
||||
warehouse_results = (
|
||||
frappe.qb.from_(warehouse)
|
||||
.select("name")
|
||||
.where((warehouse.lft >= lft) & (warehouse.rgt <= rgt))
|
||||
.run()
|
||||
)
|
||||
warehouse_results = [x[0] for x in warehouse_results]
|
||||
|
||||
return sle_query.where(sle.warehouse.isin(warehouse_results))
|
||||
return apply_warehouse_filter(sle_query, sle, self.filters)
|
||||
|
||||
def prepare_stock_reco_voucher_wise_count(self):
|
||||
self.stock_reco_voucher_wise_count = frappe._dict()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data
|
||||
from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data, get_average_age
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -125,6 +125,18 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
self.assertEqual(queue[0][0], 10.0)
|
||||
self.assertEqual(queue[1][0], 10.0)
|
||||
|
||||
def test_item_filter_supports_multi_select_values(self):
|
||||
bundle = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
query = frappe.qb.from_(bundle).select(bundle.name)
|
||||
|
||||
filtered_query = FIFOSlots(frappe._dict(item_code=["Item A"]), [])._apply_filter(
|
||||
query, bundle, "item_code"
|
||||
)
|
||||
|
||||
sql = filtered_query.get_sql()
|
||||
self.assertIn(" IN ", sql)
|
||||
self.assertNotIn("=[", sql)
|
||||
|
||||
def test_basic_stock_reconciliation(self):
|
||||
"""
|
||||
Ledger (same wh): [+30, reco reset >> 50, -10]
|
||||
@@ -893,6 +905,11 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
|
||||
self.assertEqual(report_data[0][7:15], [8.0, 80.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
|
||||
|
||||
def test_average_age_accepts_batchwise_valuation_slots(self):
|
||||
fifo_queue = [["SA-BATCH-SLOT", 1, 5.0, "2021-12-01", 50.0]]
|
||||
|
||||
self.assertEqual(get_average_age(fifo_queue, "2021-12-10"), 9.0)
|
||||
|
||||
def test_serial_transfer_replay_preserves_serial_slots(self):
|
||||
fifo_slots = FIFOSlots(self.filters, [])
|
||||
transfer_key = ("001", "Serial Item", "WH 1")
|
||||
|
||||
@@ -16,7 +16,11 @@ import erpnext
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||
from erpnext.stock.doctype.stock_closing_entry.stock_closing_entry import StockClosing
|
||||
from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
|
||||
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,
|
||||
normalize_fifo_queue,
|
||||
)
|
||||
from erpnext.stock.utils import add_additional_uom_columns
|
||||
|
||||
|
||||
@@ -280,8 +284,6 @@ class StockBalanceReport:
|
||||
self.filters["show_warehouse_wise_stock"] = True
|
||||
item_wise_fifo_queue = FIFOSlots(self.filters).generate()
|
||||
|
||||
_func = itemgetter(1)
|
||||
|
||||
del self.sle_entries
|
||||
|
||||
sre_details = self.get_sre_reserved_qty_details()
|
||||
@@ -307,15 +309,7 @@ class StockBalanceReport:
|
||||
stock_ageing_data = {"average_age": 0, "earliest_age": 0, "latest_age": 0}
|
||||
|
||||
if opening_fifo_queue:
|
||||
fifo_queue = sorted(filter(_func, opening_fifo_queue), key=_func)
|
||||
if not fifo_queue:
|
||||
continue
|
||||
|
||||
to_date = self.to_date
|
||||
stock_ageing_data["average_age"] = get_average_age(fifo_queue, to_date)
|
||||
stock_ageing_data["earliest_age"] = date_diff(to_date, fifo_queue[0][1])
|
||||
stock_ageing_data["latest_age"] = date_diff(to_date, fifo_queue[-1][1])
|
||||
stock_ageing_data["fifo_queue"] = fifo_queue
|
||||
stock_ageing_data.update(get_stock_ageing_data(opening_fifo_queue, self.to_date))
|
||||
|
||||
report_data.update(stock_ageing_data)
|
||||
|
||||
@@ -698,6 +692,21 @@ class StockBalanceReport:
|
||||
return opening_fifo_queue
|
||||
|
||||
|
||||
def get_stock_ageing_data(fifo_queue: list, to_date: str) -> dict:
|
||||
stock_ageing_data = {"average_age": 0, "earliest_age": 0, "latest_age": 0}
|
||||
fifo_queue = sorted(filter(itemgetter(1), normalize_fifo_queue(fifo_queue)), key=itemgetter(1))
|
||||
|
||||
if not fifo_queue:
|
||||
return stock_ageing_data
|
||||
|
||||
stock_ageing_data["average_age"] = get_average_age(fifo_queue, to_date)
|
||||
stock_ageing_data["earliest_age"] = date_diff(to_date, fifo_queue[0][1])
|
||||
stock_ageing_data["latest_age"] = date_diff(to_date, fifo_queue[-1][1])
|
||||
stock_ageing_data["fifo_queue"] = fifo_queue
|
||||
|
||||
return stock_ageing_data
|
||||
|
||||
|
||||
def filter_items_with_no_transactions(
|
||||
iwb_map, float_precision: float, inventory_dimensions: list | None = None
|
||||
):
|
||||
|
||||
@@ -6,7 +6,7 @@ from frappe.utils import today
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.report.stock_balance.stock_balance import execute
|
||||
from erpnext.stock.report.stock_balance.stock_balance import execute, get_stock_ageing_data
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -166,3 +166,19 @@ class TestStockBalance(ERPNextTestSuite):
|
||||
rows = stock_balance(self.filters.update({"show_variant_attributes": 1, "item_code": [variant.name]}))
|
||||
self.assertPartialDictEq(attributes, rows[0])
|
||||
self.assertInvariants(rows)
|
||||
|
||||
def test_stock_ageing_data_accepts_batchwise_valuation_slots(self):
|
||||
fifo_queue = [
|
||||
["SA-BATCH-NEWER", 1, 2.0, "2021-12-05", 20.0],
|
||||
["SA-BATCH-OLDER", 1, 3.0, "2021-12-01", 30.0],
|
||||
]
|
||||
|
||||
stock_ageing_data = get_stock_ageing_data(fifo_queue, "2021-12-10")
|
||||
|
||||
self.assertEqual(stock_ageing_data["average_age"], 7.4)
|
||||
self.assertEqual(stock_ageing_data["earliest_age"], 9)
|
||||
self.assertEqual(stock_ageing_data["latest_age"], 5)
|
||||
self.assertEqual(
|
||||
stock_ageing_data["fifo_queue"],
|
||||
[[3.0, "2021-12-01", 30.0], [2.0, "2021-12-05", 20.0]],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user