Skip to content

Commit fb3fb8c

Browse files
rohitwaghchauremergify[bot]
authored andcommitted
fix: negative stock for purchae return
(cherry picked from commit d68a04a)
1 parent e223234 commit fb3fb8c

4 files changed

Lines changed: 148 additions & 65 deletions

File tree

erpnext/stock/deprecated_serial_batch.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ def calculate_avg_rate_from_deprecarated_ledgers(self):
9797
for ledger in entries:
9898
self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value)
9999
self.available_qty[ledger.batch_no] += flt(ledger.batch_qty)
100-
self.total_qty[ledger.batch_no] += flt(ledger.batch_qty)
101100

102101
@deprecated(
103102
"erpnext.stock.serial_batch_bundle.BatchNoValuation.get_sle_for_batches",
@@ -271,7 +270,6 @@ def set_balance_value_from_sl_entries(self) -> None:
271270
batch_data = query.run(as_dict=True)
272271
for d in batch_data:
273272
self.available_qty[d.batch_no] += flt(d.batch_qty)
274-
self.total_qty[d.batch_no] += flt(d.batch_qty)
275273

276274
for d in batch_data:
277275
if self.available_qty.get(d.batch_no):
@@ -383,7 +381,6 @@ def set_balance_value_from_bundle(self) -> None:
383381
batch_data = query.run(as_dict=True)
384382
for d in batch_data:
385383
self.available_qty[d.batch_no] += flt(d.batch_qty)
386-
self.total_qty[d.batch_no] += flt(d.batch_qty)
387384

388385
if not self.last_sle:
389386
return

erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4997,6 +4997,45 @@ def test_do_not_use_batchwise_valuation_with_fifo(self):
49974997

49984998
self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]])
49994999

5000+
def test_negative_stock_error_for_purchase_return(self):
5001+
from erpnext.controllers.sales_and_purchase_return import make_return_doc
5002+
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
5003+
5004+
item_code = make_item(
5005+
"Test Negative Stock for Purchase Return Item",
5006+
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TNSFPRI.#####"},
5007+
).name
5008+
5009+
pr = make_purchase_receipt(
5010+
item_code=item_code,
5011+
posting_date=add_days(today(), -3),
5012+
qty=10,
5013+
rate=100,
5014+
warehouse="_Test Warehouse - _TC",
5015+
)
5016+
5017+
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
5018+
5019+
make_purchase_receipt(
5020+
item_code=item_code,
5021+
posting_date=add_days(today(), -4),
5022+
qty=10,
5023+
rate=100,
5024+
warehouse="_Test Warehouse - _TC",
5025+
)
5026+
5027+
make_stock_entry(
5028+
item_code=item_code,
5029+
qty=10,
5030+
source="_Test Warehouse - _TC",
5031+
target="_Test Warehouse 1 - _TC",
5032+
batch_no=batch_no,
5033+
use_serial_batch_fields=1,
5034+
)
5035+
5036+
return_pr = make_return_doc("Purchase Receipt", pr.name)
5037+
self.assertRaises(frappe.ValidationError, return_pr.submit)
5038+
50005039

50015040
def prepare_data_for_internal_transfer():
50025041
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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":

erpnext/stock/serial_batch_bundle.py

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -807,62 +807,11 @@ def calculate_avg_rate(self):
807807
for ledger in entries:
808808
self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
809809
self.available_qty[ledger.batch_no] += flt(ledger.qty)
810-
self.total_qty[ledger.batch_no] += flt(ledger.qty)
811-
812-
entries = self.get_batch_stock_after_date()
813-
for row in entries:
814-
self.total_qty[row.batch_no] += flt(row.total_qty)
815810

816811
self.calculate_avg_rate_from_deprecarated_ledgers()
817812
self.calculate_avg_rate_for_non_batchwise_valuation()
818813
self.set_stock_value_difference()
819814

820-
def get_batch_stock_after_date(self) -> list[dict]:
821-
# Get total qty of each batch no from Serial and Batch Bundle without checking time condition
822-
if not self.batchwise_valuation_batches:
823-
return []
824-
825-
child = frappe.qb.DocType("Serial and Batch Entry")
826-
827-
timestamp_condition = ""
828-
if self.sle.posting_datetime:
829-
timestamp_condition = child.posting_datetime > self.sle.posting_datetime
830-
831-
if self.sle.creation:
832-
timestamp_condition |= (child.posting_datetime == self.sle.posting_datetime) & (
833-
child.creation > self.sle.creation
834-
)
835-
836-
query = (
837-
frappe.qb.from_(child)
838-
.select(
839-
child.batch_no,
840-
Sum(child.qty).as_("total_qty"),
841-
)
842-
.where(
843-
(child.item_code == self.sle.item_code)
844-
& (child.warehouse == self.sle.warehouse)
845-
& (child.batch_no.isin(self.batchwise_valuation_batches))
846-
& (child.docstatus == 1)
847-
& (child.type_of_transaction.isin(["Inward", "Outward"]))
848-
)
849-
.for_update()
850-
.groupby(child.batch_no)
851-
)
852-
853-
# Important to exclude the current voucher detail no / voucher no to calculate the correct stock value difference
854-
if self.sle.voucher_detail_no:
855-
query = query.where(child.voucher_detail_no != self.sle.voucher_detail_no)
856-
elif self.sle.voucher_no:
857-
query = query.where(child.voucher_no != self.sle.voucher_no)
858-
859-
query = query.where(child.voucher_type != "Pick List")
860-
861-
if timestamp_condition:
862-
query = query.where(timestamp_condition)
863-
864-
return query.run(as_dict=True)
865-
866815
def get_batch_stock_before_date(self) -> list[dict]:
867816
# Get batch wise stock value difference from Serial and Batch Bundle considering time condition
868817
if not self.batchwise_valuation_batches:

0 commit comments

Comments
 (0)