Skip to content

Commit f9fd0ff

Browse files
rohitwaghchauremergify[bot]
authored andcommitted
fix: negative stock for purchae return
(cherry picked from commit d68a04a) # Conflicts: # erpnext/stock/serial_batch_bundle.py
1 parent 69dc9e8 commit f9fd0ff

4 files changed

Lines changed: 155 additions & 14 deletions

File tree

erpnext/stock/deprecated_serial_batch.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ def calculate_avg_rate_from_deprecarated_ledgers(self):
7878
for ledger in entries:
7979
self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value)
8080
self.available_qty[ledger.batch_no] += flt(ledger.batch_qty)
81-
self.total_qty[ledger.batch_no] += flt(ledger.batch_qty)
8281

8382
@deprecated
8483
def get_sle_for_batches(self):
@@ -231,7 +230,6 @@ def set_balance_value_from_sl_entries(self) -> None:
231230
batch_data = query.run(as_dict=True)
232231
for d in batch_data:
233232
self.available_qty[d.batch_no] += flt(d.batch_qty)
234-
self.total_qty[d.batch_no] += flt(d.batch_qty)
235233

236234
for d in batch_data:
237235
if self.available_qty.get(d.batch_no):
@@ -332,7 +330,6 @@ def set_balance_value_from_bundle(self) -> None:
332330
batch_data = query.run(as_dict=True)
333331
for d in batch_data:
334332
self.available_qty[d.batch_no] += flt(d.batch_qty)
335-
self.total_qty[d.batch_no] += flt(d.batch_qty)
336333

337334
if not self.last_sle:
338335
return

erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py

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

46784678
self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]])
46794679

4680+
def test_negative_stock_error_for_purchase_return(self):
4681+
from erpnext.controllers.sales_and_purchase_return import make_return_doc
4682+
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
4683+
4684+
item_code = make_item(
4685+
"Test Negative Stock for Purchase Return Item",
4686+
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TNSFPRI.#####"},
4687+
).name
4688+
4689+
pr = make_purchase_receipt(
4690+
item_code=item_code,
4691+
posting_date=add_days(today(), -3),
4692+
qty=10,
4693+
rate=100,
4694+
warehouse="_Test Warehouse - _TC",
4695+
)
4696+
4697+
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
4698+
4699+
make_purchase_receipt(
4700+
item_code=item_code,
4701+
posting_date=add_days(today(), -4),
4702+
qty=10,
4703+
rate=100,
4704+
warehouse="_Test Warehouse - _TC",
4705+
)
4706+
4707+
make_stock_entry(
4708+
item_code=item_code,
4709+
qty=10,
4710+
source="_Test Warehouse - _TC",
4711+
target="_Test Warehouse 1 - _TC",
4712+
batch_no=batch_no,
4713+
use_serial_batch_fields=1,
4714+
)
4715+
4716+
return_pr = make_return_doc("Purchase Receipt", pr.name)
4717+
self.assertRaises(frappe.ValidationError, return_pr.submit)
4718+
46804719

46814720
def prepare_data_for_internal_transfer():
46824721
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
@@ -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":

erpnext/stock/serial_batch_bundle.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,15 +803,19 @@ def calculate_avg_rate(self):
803803
for ledger in entries:
804804
self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
805805
self.available_qty[ledger.batch_no] += flt(ledger.qty)
806+
<<<<<<< HEAD
806807

807808
entries = self.get_batch_wise_total_available_qty()
808809
for row in entries:
809810
self.total_qty[row.batch_no] += flt(row.total_qty)
811+
=======
812+
>>>>>>> d68a04ad16 (fix: negative stock for purchae return)
810813

811814
self.calculate_avg_rate_from_deprecarated_ledgers()
812815
self.calculate_avg_rate_for_non_batchwise_valuation()
813816
self.set_stock_value_difference()
814817

818+
<<<<<<< HEAD
815819
def get_batch_wise_total_available_qty(self) -> list[dict]:
816820
# Get total qty of each batch no from Serial and Batch Bundle without checking time condition
817821
if not self.batchwise_valuation_batches:
@@ -851,6 +855,9 @@ def get_batch_wise_total_available_qty(self) -> list[dict]:
851855
return query.run(as_dict=True)
852856

853857
def get_batch_no_ledgers(self) -> list[dict]:
858+
=======
859+
def get_batch_stock_before_date(self) -> list[dict]:
860+
>>>>>>> d68a04ad16 (fix: negative stock for purchae return)
854861
# Get batch wise stock value difference from Serial and Batch Bundle considering time condition
855862
if not self.batchwise_valuation_batches:
856863
return []

0 commit comments

Comments
 (0)