@@ -576,14 +576,12 @@ def set_incoming_rate_for_outward_transaction(self, row=None, save=False, allow_
576576 d .incoming_rate = abs (flt (sn_obj .batch_avg_rate .get (d .batch_no )))
577577
578578 precision = d .precision ("qty" )
579- for field in ["available_qty" , "total_qty" ]:
580- value = getattr (sn_obj , field )
581- available_qty = flt (value .get (d .batch_no ), precision )
582- if self .docstatus == 1 :
583- available_qty += flt (d .qty , precision )
579+ available_qty = flt (sn_obj .available_qty .get (d .batch_no ), precision )
580+ if self .docstatus == 1 :
581+ available_qty += flt (d .qty , precision )
584582
585- if not allow_negative_stock :
586- self .validate_negative_batch (d .batch_no , available_qty , field )
583+ if not allow_negative_stock :
584+ self .validate_negative_batch (d .batch_no , available_qty )
587585
588586 d .stock_value_difference = flt (d .qty ) * flt (d .incoming_rate )
589587
@@ -596,24 +594,23 @@ def set_incoming_rate_for_outward_transaction(self, row=None, save=False, allow_
596594 }
597595 )
598596
599- def validate_negative_batch (self , batch_no , available_qty , field = None ):
600- if available_qty < 0 and not self .is_stock_reco_for_valuation_adjustment (available_qty , field = field ):
597+ def validate_negative_batch (self , batch_no , available_qty ):
598+ if available_qty < 0 and not self .is_stock_reco_for_valuation_adjustment (available_qty ):
601599 msg = f"""Batch No { bold (batch_no )} of an Item { bold (self .item_code )}
602600 has negative stock
603601 of quantity { bold (available_qty )} in the
604602 warehouse { self .warehouse } """
605603
606604 frappe .throw (_ (msg ), BatchNegativeStockError )
607605
608- def is_stock_reco_for_valuation_adjustment (self , available_qty , field = None ):
606+ def is_stock_reco_for_valuation_adjustment (self , available_qty ):
609607 if (
610608 self .voucher_type == "Stock Reconciliation"
611609 and self .type_of_transaction == "Outward"
612610 and self .voucher_detail_no
613611 and (
614612 abs (frappe .db .get_value ("Stock Reconciliation Item" , self .voucher_detail_no , "qty" ))
615613 == abs (available_qty )
616- or field == "total_qty"
617614 )
618615 ):
619616 return True
@@ -1344,6 +1341,7 @@ def before_submit(self):
13441341 def on_submit (self ):
13451342 self .validate_docstatus ()
13461343 self .validate_serial_nos_inventory ()
1344+ self .validate_batch_quantity ()
13471345
13481346 def validate_docstatus (self ):
13491347 for row in self .entries :
@@ -1437,6 +1435,106 @@ def validate_batch_inventory(self):
14371435
14381436 def on_cancel (self ):
14391437 self .validate_voucher_no_docstatus ()
1438+ self .validate_batch_quantity ()
1439+
1440+ def validate_batch_quantity (self ):
1441+ if not self .has_batch_no :
1442+ return
1443+
1444+ if self .type_of_transaction != "Outward" or (
1445+ self .voucher_type == "Stock Reconciliation" and self .type_of_transaction == "Outward"
1446+ ):
1447+ return
1448+
1449+ batch_wise_available_qty = self .get_batchwise_available_qty ()
1450+ precision = frappe .get_precision ("Serial and Batch Entry" , "qty" )
1451+
1452+ for d in self .entries :
1453+ available_qty = batch_wise_available_qty .get (d .batch_no , 0 )
1454+ if flt (available_qty , precision ) < 0 :
1455+ frappe .throw (
1456+ _ (
1457+ """
1458+ The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry."""
1459+ ).format (
1460+ bold (d .batch_no ),
1461+ bold (self .item_code ),
1462+ bold (self .warehouse ),
1463+ bold (abs (flt (available_qty , precision ))),
1464+ ),
1465+ title = _ ("Negative Stock Error" ),
1466+ )
1467+
1468+ def get_batchwise_available_qty (self ):
1469+ available_qty = self .get_available_qty_from_sabb ()
1470+ available_qty_from_ledger = self .get_available_qty_from_stock_ledger ()
1471+
1472+ if not available_qty_from_ledger :
1473+ return available_qty
1474+
1475+ for batch_no , qty in available_qty_from_ledger .items ():
1476+ if batch_no in available_qty :
1477+ available_qty [batch_no ] += qty
1478+ else :
1479+ available_qty [batch_no ] = qty
1480+
1481+ return available_qty
1482+
1483+ def get_available_qty_from_stock_ledger (self ):
1484+ batches = [d .batch_no for d in self .entries if d .batch_no ]
1485+
1486+ sle = frappe .qb .DocType ("Stock Ledger Entry" )
1487+
1488+ query = (
1489+ frappe .qb .from_ (sle )
1490+ .select (
1491+ sle .batch_no ,
1492+ Sum (sle .actual_qty ).as_ ("available_qty" ),
1493+ )
1494+ .where (
1495+ (sle .item_code == self .item_code )
1496+ & (sle .warehouse == self .warehouse )
1497+ & (sle .is_cancelled == 0 )
1498+ & (sle .batch_no .isin (batches ))
1499+ & (sle .docstatus == 1 )
1500+ & (sle .serial_and_batch_bundle .isnull ())
1501+ & (sle .batch_no .isnotnull ())
1502+ )
1503+ .for_update ()
1504+ .groupby (sle .batch_no )
1505+ )
1506+
1507+ res = query .run (as_list = True )
1508+
1509+ return frappe ._dict (res ) if res else frappe ._dict ()
1510+
1511+ def get_available_qty_from_sabb (self ):
1512+ batches = [d .batch_no for d in self .entries if d .batch_no ]
1513+
1514+ child = frappe .qb .DocType ("Serial and Batch Entry" )
1515+
1516+ query = (
1517+ frappe .qb .from_ (child )
1518+ .select (
1519+ child .batch_no ,
1520+ Sum (child .qty ).as_ ("available_qty" ),
1521+ )
1522+ .where (
1523+ (child .item_code == self .item_code )
1524+ & (child .warehouse == self .warehouse )
1525+ & (child .is_cancelled == 0 )
1526+ & (child .batch_no .isin (batches ))
1527+ & (child .docstatus == 1 )
1528+ & (child .type_of_transaction .isin (["Inward" , "Outward" ]))
1529+ )
1530+ .for_update ()
1531+ .groupby (child .batch_no )
1532+ )
1533+ query = query .where (child .voucher_type != "Pick List" )
1534+
1535+ res = query .run (as_list = True )
1536+
1537+ return frappe ._dict (res ) if res else frappe ._dict ()
14401538
14411539 def validate_voucher_no_docstatus (self ):
14421540 if self .voucher_type == "POS Invoice" :
0 commit comments