66
77import frappe
88from frappe import _
9- from frappe .utils import cint , date_diff , flt , get_datetime
9+ from frappe .query_builder import Order
10+ from frappe .utils import add_days , cint , date_diff , flt , get_date_str , get_datetime , getdate
1011
1112from erpnext .stock .doctype .serial_no .serial_no import get_serial_nos
1213
@@ -49,7 +50,13 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
4950 latest_age = date_diff (to_date , fifo_queue [- 1 ][1 ])
5051 range1 , range2 , range3 , above_range3 = get_range_age (filters , fifo_queue , to_date , item_dict )
5152
52- row = [details .name , details .item_name , details .description , details .item_group , details .brand ]
53+ row = [
54+ details .name or details .item_code ,
55+ details .item_name ,
56+ details .description ,
57+ details .item_group ,
58+ details .brand ,
59+ ]
5360
5461 if filters .get ("show_warehouse_wise_stock" ):
5562 row .append (details .warehouse )
@@ -217,6 +224,67 @@ def __init__(self, filters: dict | None = None, sle: list | None = None):
217224 self .filters = filters
218225 self .sle = sle
219226
227+ def get_closing_balance (self ):
228+ if self .filters .get ("ignore_closing_balance" ):
229+ return []
230+
231+ if (
232+ self .filters .get ("item_code" )
233+ or self .filters .get ("warehouse" )
234+ or self .filters .get ("warehouse_type" )
235+ ):
236+ return
237+
238+ if self .sle :
239+ return
240+
241+ table = frappe .qb .DocType ("Closing Stock Balance" )
242+
243+ query = (
244+ frappe .qb .from_ (table )
245+ .select (table .name , table .to_date )
246+ .where (
247+ (table .docstatus == 1 )
248+ & (table .company == self .filters .company )
249+ & (table .to_date < self .filters .get ("to_date" ))
250+ & (table .status == "Completed" )
251+ )
252+ .orderby (table .to_date , order = Order .desc )
253+ .limit (1 )
254+ )
255+
256+ for fieldname in ["warehouse" , "item_code" , "item_group" , "warehouse_type" ]:
257+ if self .filters .get (fieldname ):
258+ query = query .where (table [fieldname ] == self .filters .get (fieldname ))
259+
260+ return query .run (as_dict = True )
261+
262+ def prepare_stock_ageing_from_stock_closing_balance (self ):
263+ closing_balance = self .get_closing_balance ()
264+ if not closing_balance :
265+ return
266+
267+ self .start_from = add_days (closing_balance [0 ].to_date , 1 )
268+ closing_data = frappe .get_doc ("Closing Stock Balance" , closing_balance [0 ].name ).get_prepared_data ()
269+ stock_ledger_entries = closing_data .get ("data" )
270+
271+ for d in stock_ledger_entries :
272+ if isinstance (d , dict ):
273+ d = frappe ._dict (d )
274+
275+ d .actual_qty = d .bal_qty
276+ key , fifo_queue , transferred_item_key = self .__init_key_stores (d )
277+ serial_nos = d .serial_no if d .serial_no else []
278+ if fifo_queue and isinstance (fifo_queue [0 ][0 ], str ):
279+ d .has_serial_no = 1
280+
281+ if d .actual_qty > 0 :
282+ self .__compute_incoming_stock (d , fifo_queue , transferred_item_key , serial_nos )
283+ else :
284+ self .__compute_outgoing_stock (d , fifo_queue , transferred_item_key , serial_nos )
285+
286+ self .__update_balances (d , key )
287+
220288 def generate (self ) -> dict :
221289 """
222290 Returns dict of the foll.g structure:
@@ -227,6 +295,9 @@ def generate(self) -> dict:
227295 consumed/updated and maintained via FIFO. **
228296 }
229297 """
298+ self .start_from = None
299+ self .prepare_stock_ageing_from_stock_closing_balance ()
300+
230301 stock_ledger_entries = self .sle
231302
232303 _system_settings = frappe .get_cached_doc ("System Settings" )
@@ -259,15 +330,32 @@ def generate(self) -> dict:
259330
260331 return self .item_details
261332
333+ def format_fifo_queue (self , fifo_queue : list ) -> list :
334+ if not fifo_queue :
335+ return []
336+
337+ fifo_queue = [[x [0 ], getdate (x [1 ])] for x in fifo_queue ]
338+ return fifo_queue
339+
262340 def __init_key_stores (self , row : dict ) -> tuple :
263341 "Initialise keys and FIFO Queue."
264342
265- key = (row .name , row .warehouse )
266- self .item_details .setdefault (key , {"details" : row , "fifo_queue" : []})
343+ if not row .name :
344+ key = (row .item_code , row .warehouse )
345+ else :
346+ key = (row .name , row .warehouse )
347+
348+ if key not in self .item_details :
349+ row .fifo_queue = self .format_fifo_queue (row .fifo_queue )
350+
351+ self .item_details .setdefault (key , {"details" : row , "fifo_queue" : row .fifo_queue or []})
352+
267353 fifo_queue = self .item_details [key ]["fifo_queue" ]
354+ transferred_item_key = None
268355
269- transferred_item_key = (row .voucher_no , row .name , row .warehouse )
270- self .transferred_item_details .setdefault (transferred_item_key , [])
356+ if row .voucher_no :
357+ transferred_item_key = (row .voucher_no , row .name , row .warehouse )
358+ self .transferred_item_details .setdefault (transferred_item_key , [])
271359
272360 return key , fifo_queue , transferred_item_key
273361
@@ -351,10 +439,10 @@ def add_to_fifo_queue(slot):
351439 transfer_qty_to_pop = 0
352440
353441 def __update_balances (self , row : dict , key : tuple | str ):
354- self .item_details [key ]["qty_after_transaction" ] = row .qty_after_transaction
442+ self .item_details [key ]["qty_after_transaction" ] = row .qty_after_transaction or row . bal_qty
355443
356444 if "total_qty" not in self .item_details [key ]:
357- self .item_details [key ]["total_qty" ] = row .actual_qty
445+ self .item_details [key ]["total_qty" ] = row .actual_qty or row . bal_qty
358446 else :
359447 self .item_details [key ]["total_qty" ] += row .actual_qty
360448
@@ -417,6 +505,10 @@ def __get_stock_ledger_entries(self) -> list[dict]:
417505 )
418506 )
419507
508+ if self .start_from :
509+ from_date = get_datetime (get_date_str (self .start_from ) + " 00:00:00" )
510+ sle_query = sle_query .where (sle .posting_datetime >= from_date )
511+
420512 if self .filters .get ("warehouse" ):
421513 sle_query = self .__get_warehouse_conditions (sle , sle_query )
422514 elif self .filters .get ("warehouse_type" ):
0 commit comments