@@ -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
@@ -1346,6 +1343,7 @@ def before_submit(self):
13461343
13471344 def on_submit (self ):
13481345 self .validate_serial_nos_inventory ()
1346+ self .validate_batch_quantity ()
13491347
13501348 def set_purchase_document_no (self ):
13511349 if self .flags .ignore_validate_serial_batch :
@@ -1404,6 +1402,106 @@ def validate_batch_inventory(self):
14041402
14051403 def on_cancel (self ):
14061404 self .validate_voucher_no_docstatus ()
1405+ self .validate_batch_quantity ()
1406+
1407+ def validate_batch_quantity (self ):
1408+ if not self .has_batch_no :
1409+ return
1410+
1411+ if self .type_of_transaction != "Outward" or (
1412+ self .voucher_type == "Stock Reconciliation" and self .type_of_transaction == "Outward"
1413+ ):
1414+ return
1415+
1416+ batch_wise_available_qty = self .get_batchwise_available_qty ()
1417+ precision = frappe .get_precision ("Serial and Batch Entry" , "qty" )
1418+
1419+ for d in self .entries :
1420+ available_qty = batch_wise_available_qty .get (d .batch_no , 0 )
1421+ if flt (available_qty , precision ) < 0 :
1422+ frappe .throw (
1423+ _ (
1424+ """
1425+ 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."""
1426+ ).format (
1427+ bold (d .batch_no ),
1428+ bold (self .item_code ),
1429+ bold (self .warehouse ),
1430+ bold (abs (flt (available_qty , precision ))),
1431+ ),
1432+ title = _ ("Negative Stock Error" ),
1433+ )
1434+
1435+ def get_batchwise_available_qty (self ):
1436+ available_qty = self .get_available_qty_from_sabb ()
1437+ available_qty_from_ledger = self .get_available_qty_from_stock_ledger ()
1438+
1439+ if not available_qty_from_ledger :
1440+ return available_qty
1441+
1442+ for batch_no , qty in available_qty_from_ledger .items ():
1443+ if batch_no in available_qty :
1444+ available_qty [batch_no ] += qty
1445+ else :
1446+ available_qty [batch_no ] = qty
1447+
1448+ return available_qty
1449+
1450+ def get_available_qty_from_stock_ledger (self ):
1451+ batches = [d .batch_no for d in self .entries if d .batch_no ]
1452+
1453+ sle = frappe .qb .DocType ("Stock Ledger Entry" )
1454+
1455+ query = (
1456+ frappe .qb .from_ (sle )
1457+ .select (
1458+ sle .batch_no ,
1459+ Sum (sle .actual_qty ).as_ ("available_qty" ),
1460+ )
1461+ .where (
1462+ (sle .item_code == self .item_code )
1463+ & (sle .warehouse == self .warehouse )
1464+ & (sle .is_cancelled == 0 )
1465+ & (sle .batch_no .isin (batches ))
1466+ & (sle .docstatus == 1 )
1467+ & (sle .serial_and_batch_bundle .isnull ())
1468+ & (sle .batch_no .isnotnull ())
1469+ )
1470+ .for_update ()
1471+ .groupby (sle .batch_no )
1472+ )
1473+
1474+ res = query .run (as_list = True )
1475+
1476+ return frappe ._dict (res ) if res else frappe ._dict ()
1477+
1478+ def get_available_qty_from_sabb (self ):
1479+ batches = [d .batch_no for d in self .entries if d .batch_no ]
1480+
1481+ child = frappe .qb .DocType ("Serial and Batch Entry" )
1482+
1483+ query = (
1484+ frappe .qb .from_ (child )
1485+ .select (
1486+ child .batch_no ,
1487+ Sum (child .qty ).as_ ("available_qty" ),
1488+ )
1489+ .where (
1490+ (child .item_code == self .item_code )
1491+ & (child .warehouse == self .warehouse )
1492+ & (child .is_cancelled == 0 )
1493+ & (child .batch_no .isin (batches ))
1494+ & (child .docstatus == 1 )
1495+ & (child .type_of_transaction .isin (["Inward" , "Outward" ]))
1496+ )
1497+ .for_update ()
1498+ .groupby (child .batch_no )
1499+ )
1500+ query = query .where (child .voucher_type != "Pick List" )
1501+
1502+ res = query .run (as_list = True )
1503+
1504+ return frappe ._dict (res ) if res else frappe ._dict ()
14071505
14081506 def validate_voucher_no_docstatus (self ):
14091507 if self .voucher_type == "POS Invoice" :
0 commit comments