77
88import frappe
99from frappe import _
10+ from frappe .query_builder .functions import Count
1011from frappe .utils import cint , date_diff , flt , get_datetime
1112
1213from erpnext .stock .doctype .serial_no .serial_no import get_serial_nos
@@ -240,9 +241,9 @@ def generate(self) -> dict:
240241 Returns dict of the foll.g structure:
241242 Key = Item A / (Item A, Warehouse A)
242243 Key: {
243- 'details' -> Dict: ** item details **,
244- 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock,
245- consumed/updated and maintained via FIFO. **
244+ 'details' -> Dict: ** item details **,
245+ 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock,
246+ consumed/updated and maintained via FIFO. **
246247 }
247248 """
248249 from erpnext .stock .serial_batch_bundle import get_serial_nos_from_bundle
@@ -253,16 +254,33 @@ def generate(self) -> dict:
253254 if stock_ledger_entries is None :
254255 bundle_wise_serial_nos = self .__get_bundle_wise_serial_nos ()
255256
257+ # prepare single sle voucher detail lookup
258+ self .prepare_stock_reco_voucher_wise_count ()
259+
256260 with frappe .db .unbuffered_cursor ():
257261 if stock_ledger_entries is None :
258262 stock_ledger_entries = self .__get_stock_ledger_entries ()
259263
260264 for d in stock_ledger_entries :
261265 key , fifo_queue , transferred_item_key = self .__init_key_stores (d )
266+ prev_balance_qty = self .item_details [key ].get ("qty_after_transaction" , 0 )
267+
268+ if d .voucher_type == "Stock Reconciliation" and (
269+ not d .batch_no or d .serial_no or d .serial_and_batch_bundle
270+ ):
271+ if d .voucher_detail_no in self .stock_reco_voucher_wise_count :
272+ # for legacy recon with single sle has qty_after_transaction and stock_value_difference without outward entry
273+ # for exisitng handle emptying the existing queue and details.
274+ d .stock_value_difference = flt (d .qty_after_transaction * d .valuation_rate )
275+ d .actual_qty = d .qty_after_transaction
276+ self .item_details [key ]["qty_after_transaction" ] = 0
277+ self .item_details [key ]["total_qty" ] = 0
278+ fifo_queue .clear ()
279+ else :
280+ d .actual_qty = flt (d .qty_after_transaction ) - flt (prev_balance_qty )
262281
263- if d .voucher_type == "Stock Reconciliation" :
282+ elif d .voucher_type == "Stock Reconciliation" :
264283 # get difference in qty shift as actual qty
265- prev_balance_qty = self .item_details [key ].get ("qty_after_transaction" , 0 )
266284 d .actual_qty = flt (d .qty_after_transaction ) - flt (prev_balance_qty )
267285
268286 serial_nos = get_serial_nos (d .serial_no ) if d .serial_no else []
@@ -280,6 +298,14 @@ def generate(self) -> dict:
280298
281299 self .__update_balances (d , key )
282300
301+ # handle serial nos misconsumption
302+ if d .has_serial_no :
303+ qty_after = cint (self .item_details [key ]["qty_after_transaction" ])
304+ if qty_after <= 0 :
305+ fifo_queue .clear ()
306+ elif len (fifo_queue ) > qty_after :
307+ fifo_queue [:] = fifo_queue [:qty_after ]
308+
283309 # Note that stock_ledger_entries is an iterator, you can not reuse it like a list
284310 del stock_ledger_entries
285311
@@ -406,7 +432,6 @@ def add_to_fifo_queue(slot):
406432
407433 def __update_balances (self , row : dict , key : tuple | str ):
408434 self .item_details [key ]["qty_after_transaction" ] = row .qty_after_transaction
409-
410435 if "total_qty" not in self .item_details [key ]:
411436 self .item_details [key ]["total_qty" ] = row .actual_qty
412437 else :
@@ -462,6 +487,7 @@ def __get_stock_ledger_entries(self) -> Iterator[dict]:
462487 sle .posting_date ,
463488 sle .voucher_type ,
464489 sle .voucher_no ,
490+ sle .voucher_detail_no ,
465491 sle .serial_no ,
466492 sle .batch_no ,
467493 sle .qty_after_transaction ,
@@ -558,3 +584,36 @@ def __get_warehouse_conditions(self, sle, sle_query) -> str:
558584 warehouse_results = [x [0 ] for x in warehouse_results ]
559585
560586 return sle_query .where (sle .warehouse .isin (warehouse_results ))
587+
588+ def prepare_stock_reco_voucher_wise_count (self ):
589+ self .stock_reco_voucher_wise_count = frappe ._dict ()
590+
591+ doctype = frappe .qb .DocType ("Stock Ledger Entry" )
592+ item = frappe .qb .DocType ("Item" )
593+
594+ query = (
595+ frappe .qb .from_ (doctype )
596+ .inner_join (item )
597+ .on (doctype .item_code == item .name )
598+ .select (doctype .voucher_detail_no , Count (doctype .name ).as_ ("count" ))
599+ .where (
600+ (doctype .voucher_type == "Stock Reconciliation" )
601+ & (doctype .docstatus < 2 )
602+ & (doctype .is_cancelled == 0 )
603+ )
604+ .groupby (doctype .voucher_detail_no )
605+ )
606+
607+ data = query .run (as_dict = True )
608+ if not data :
609+ return
610+
611+ for row in data :
612+ if row .count != 1 :
613+ continue
614+
615+ sr_item = frappe .db .get_value (
616+ "Stock Reconciliation Item" , row .voucher_detail_no , ["current_qty" , "qty" ], as_dict = True
617+ )
618+ if sr_item .qty and sr_item .current_qty :
619+ self .stock_reco_voucher_wise_count [row .voucher_detail_no ] = sr_item .current_qty
0 commit comments