perf: stock ageing report generation

(cherry picked from commit 7a74dac2c2)
This commit is contained in:
Rohit Waghchaure
2025-04-10 09:56:08 +05:30
committed by Mergify
parent 24681bd64f
commit eaa297475f
2 changed files with 107 additions and 9 deletions

View File

@@ -81,5 +81,11 @@ frappe.query_reports["Stock Ageing"] = {
fieldtype: "Check",
default: 0,
},
{
fieldname: "ignore_closing_balance",
label: __("Ignore Closing Balance"),
fieldtype: "Check",
default: 0,
},
],
};

View File

@@ -6,7 +6,8 @@ from operator import itemgetter
import frappe
from frappe import _
from frappe.utils import cint, date_diff, flt, get_datetime
from frappe.query_builder import Order
from frappe.utils import add_days, cint, date_diff, flt, get_date_str, get_datetime, getdate
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -49,7 +50,13 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
latest_age = date_diff(to_date, fifo_queue[-1][1])
range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict)
row = [details.name, details.item_name, details.description, details.item_group, details.brand]
row = [
details.name or details.item_code,
details.item_name,
details.description,
details.item_group,
details.brand,
]
if filters.get("show_warehouse_wise_stock"):
row.append(details.warehouse)
@@ -217,6 +224,67 @@ class FIFOSlots:
self.filters = filters
self.sle = sle
def get_closing_balance(self):
if self.filters.get("ignore_closing_balance"):
return []
if (
self.filters.get("item_code")
or self.filters.get("warehouse")
or self.filters.get("warehouse_type")
):
return
if self.sle:
return
table = frappe.qb.DocType("Closing Stock Balance")
query = (
frappe.qb.from_(table)
.select(table.name, table.to_date)
.where(
(table.docstatus == 1)
& (table.company == self.filters.company)
& (table.to_date < self.filters.get("to_date"))
& (table.status == "Completed")
)
.orderby(table.to_date, order=Order.desc)
.limit(1)
)
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
if self.filters.get(fieldname):
query = query.where(table[fieldname] == self.filters.get(fieldname))
return query.run(as_dict=True)
def prepare_stock_ageing_from_stock_closing_balance(self):
closing_balance = self.get_closing_balance()
if not closing_balance:
return
self.start_from = add_days(closing_balance[0].to_date, 1)
closing_data = frappe.get_doc("Closing Stock Balance", closing_balance[0].name).get_prepared_data()
stock_ledger_entries = closing_data.get("data")
for d in stock_ledger_entries:
if isinstance(d, dict):
d = frappe._dict(d)
d.actual_qty = d.bal_qty
key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
serial_nos = d.serial_no if d.serial_no else []
if fifo_queue and isinstance(fifo_queue[0][0], str):
d.has_serial_no = 1
if d.actual_qty > 0:
self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
else:
self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
self.__update_balances(d, key)
def generate(self) -> dict:
"""
Returns dict of the foll.g structure:
@@ -227,6 +295,9 @@ class FIFOSlots:
consumed/updated and maintained via FIFO. **
}
"""
self.start_from = None
self.prepare_stock_ageing_from_stock_closing_balance()
stock_ledger_entries = self.sle
_system_settings = frappe.get_cached_doc("System Settings")
@@ -259,15 +330,32 @@ class FIFOSlots:
return self.item_details
def format_fifo_queue(self, fifo_queue: list) -> list:
if not fifo_queue:
return []
fifo_queue = [[x[0], getdate(x[1])] for x in fifo_queue]
return fifo_queue
def __init_key_stores(self, row: dict) -> tuple:
"Initialise keys and FIFO Queue."
key = (row.name, row.warehouse)
self.item_details.setdefault(key, {"details": row, "fifo_queue": []})
fifo_queue = self.item_details[key]["fifo_queue"]
if not row.name:
key = (row.item_code, row.warehouse)
else:
key = (row.name, row.warehouse)
transferred_item_key = (row.voucher_no, row.name, row.warehouse)
self.transferred_item_details.setdefault(transferred_item_key, [])
if key not in self.item_details:
row.fifo_queue = self.format_fifo_queue(row.fifo_queue)
self.item_details.setdefault(key, {"details": row, "fifo_queue": row.fifo_queue or []})
fifo_queue = self.item_details[key]["fifo_queue"]
transferred_item_key = None
if row.voucher_no:
transferred_item_key = (row.voucher_no, row.name, row.warehouse)
self.transferred_item_details.setdefault(transferred_item_key, [])
return key, fifo_queue, transferred_item_key
@@ -351,10 +439,10 @@ class FIFOSlots:
transfer_qty_to_pop = 0
def __update_balances(self, row: dict, key: tuple | str):
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction or row.bal_qty
if "total_qty" not in self.item_details[key]:
self.item_details[key]["total_qty"] = row.actual_qty
self.item_details[key]["total_qty"] = row.actual_qty or row.bal_qty
else:
self.item_details[key]["total_qty"] += row.actual_qty
@@ -417,6 +505,10 @@ class FIFOSlots:
)
)
if self.start_from:
from_date = get_datetime(get_date_str(self.start_from) + " 00:00:00")
sle_query = sle_query.where(sle.posting_datetime >= from_date)
if self.filters.get("warehouse"):
sle_query = self.__get_warehouse_conditions(sle, sle_query)
elif self.filters.get("warehouse_type"):