Skip to content

Commit 220a528

Browse files
rohitwaghchauremergify[bot]
authored andcommitted
fix: negative stock for purchase return
(cherry picked from commit 7789393)
1 parent e087a8b commit 220a528

2 files changed

Lines changed: 120 additions & 30 deletions

File tree

erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5114,6 +5114,84 @@ def test_internal_purchase_receipt_incoming_rate_with_lcv(self):
51145114
self.assertEqual(stk_ledger.incoming_rate, 120)
51155115
self.assertEqual(stk_ledger.stock_value_difference, 600)
51165116

5117+
def test_negative_stock_error_for_purchase_return_when_stock_exists_in_future_date(self):
5118+
from erpnext.controllers.sales_and_purchase_return import make_return_doc
5119+
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
5120+
from erpnext.stock.stock_ledger import NegativeStockError
5121+
5122+
item_code = make_item(
5123+
"Test Negative Stock for Purchase Return with Future Stock Item",
5124+
{
5125+
"is_stock_item": 1,
5126+
"has_batch_no": 1,
5127+
"create_new_batch": 1,
5128+
"batch_number_series": "TNSPFPRI.#####",
5129+
},
5130+
).name
5131+
5132+
make_purchase_receipt(
5133+
item_code=item_code,
5134+
posting_date=add_days(today(), -4),
5135+
qty=100,
5136+
rate=100,
5137+
warehouse="_Test Warehouse - _TC",
5138+
)
5139+
5140+
pr1 = make_purchase_receipt(
5141+
item_code=item_code,
5142+
posting_date=add_days(today(), -3),
5143+
qty=100,
5144+
rate=100,
5145+
warehouse="_Test Warehouse - _TC",
5146+
)
5147+
5148+
batch1 = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
5149+
5150+
pr2 = make_purchase_receipt(
5151+
item_code=item_code,
5152+
posting_date=add_days(today(), -2),
5153+
qty=100,
5154+
rate=100,
5155+
warehouse="_Test Warehouse - _TC",
5156+
)
5157+
5158+
batch2 = get_batch_from_bundle(pr2.items[0].serial_and_batch_bundle)
5159+
5160+
make_stock_entry(
5161+
item_code=item_code,
5162+
qty=100,
5163+
posting_date=add_days(today(), -1),
5164+
source="_Test Warehouse - _TC",
5165+
target="_Test Warehouse 1 - _TC",
5166+
batch_no=batch1,
5167+
use_serial_batch_fields=1,
5168+
)
5169+
5170+
make_stock_entry(
5171+
item_code=item_code,
5172+
qty=100,
5173+
posting_date=add_days(today(), -1),
5174+
source="_Test Warehouse - _TC",
5175+
target="_Test Warehouse 1 - _TC",
5176+
batch_no=batch2,
5177+
use_serial_batch_fields=1,
5178+
)
5179+
5180+
make_stock_entry(
5181+
item_code=item_code,
5182+
qty=100,
5183+
posting_date=today(),
5184+
source="_Test Warehouse 1 - _TC",
5185+
target="_Test Warehouse - _TC",
5186+
batch_no=batch1,
5187+
use_serial_batch_fields=1,
5188+
)
5189+
5190+
make_purchase_entry = make_return_doc("Purchase Receipt", pr1.name)
5191+
make_purchase_entry.set_posting_time = 1
5192+
make_purchase_entry.posting_date = pr1.posting_date
5193+
self.assertRaises(NegativeStockError, make_purchase_entry.submit)
5194+
51175195

51185196
def prepare_data_for_internal_transfer():
51195197
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: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
cint,
1818
cstr,
1919
flt,
20+
get_datetime,
2021
get_link_to_form,
2122
getdate,
2223
now,
@@ -1452,31 +1453,44 @@ def validate_batch_quantity(self):
14521453
for d in self.entries:
14531454
available_qty = batch_wise_available_qty.get(d.batch_no, 0)
14541455
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-
)
1456+
self.throw_negative_batch(d.batch_no, available_qty, precision)
1457+
1458+
def throw_negative_batch(self, batch_no, available_qty, precision):
1459+
from erpnext.stock.stock_ledger import NegativeStockError
1460+
1461+
frappe.throw(
1462+
_(
1463+
"""
1464+
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."""
1465+
).format(
1466+
bold(batch_no),
1467+
bold(self.item_code),
1468+
bold(self.warehouse),
1469+
bold(abs(flt(available_qty, precision))),
1470+
),
1471+
title=_("Negative Stock Error"),
1472+
exc=NegativeStockError,
1473+
)
14671474

14681475
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()
1476+
batchwise_entries = self.get_available_qty_from_sabb()
1477+
batchwise_entries.extend(self.get_available_qty_from_stock_ledger())
14711478

1472-
if not available_qty_from_ledger:
1473-
return available_qty
1479+
available_qty = frappe._dict({})
1480+
batchwise_entries = sorted(
1481+
batchwise_entries,
1482+
key=lambda x: (get_datetime(x.get("posting_datetime")), get_datetime(x.get("creation"))),
1483+
)
14741484

1475-
for batch_no, qty in available_qty_from_ledger.items():
1476-
if batch_no in available_qty:
1477-
available_qty[batch_no] += qty
1485+
precision = frappe.get_precision("Serial and Batch Entry", "qty")
1486+
for row in batchwise_entries:
1487+
if row.batch_no in available_qty:
1488+
available_qty[row.batch_no] += flt(row.qty)
14781489
else:
1479-
available_qty[batch_no] = qty
1490+
available_qty[row.batch_no] = flt(row.qty)
1491+
1492+
if flt(available_qty[row.batch_no], precision) < 0:
1493+
self.throw_negative_batch(row.batch_no, available_qty[row.batch_no], precision)
14801494

14811495
return available_qty
14821496

@@ -1489,7 +1503,9 @@ def get_available_qty_from_stock_ledger(self):
14891503
frappe.qb.from_(sle)
14901504
.select(
14911505
sle.batch_no,
1492-
Sum(sle.actual_qty).as_("available_qty"),
1506+
sle.actual_qty.as_("qty"),
1507+
sle.posting_datetime,
1508+
sle.creation,
14931509
)
14941510
.where(
14951511
(sle.item_code == self.item_code)
@@ -1501,12 +1517,9 @@ def get_available_qty_from_stock_ledger(self):
15011517
& (sle.batch_no.isnotnull())
15021518
)
15031519
.for_update()
1504-
.groupby(sle.batch_no)
15051520
)
15061521

1507-
res = query.run(as_list=True)
1508-
1509-
return frappe._dict(res) if res else frappe._dict()
1522+
return query.run(as_dict=True)
15101523

15111524
def get_available_qty_from_sabb(self):
15121525
batches = [d.batch_no for d in self.entries if d.batch_no]
@@ -1517,7 +1530,9 @@ def get_available_qty_from_sabb(self):
15171530
frappe.qb.from_(child)
15181531
.select(
15191532
child.batch_no,
1520-
Sum(child.qty).as_("available_qty"),
1533+
child.qty,
1534+
child.posting_datetime,
1535+
child.creation,
15211536
)
15221537
.where(
15231538
(child.item_code == self.item_code)
@@ -1528,13 +1543,10 @@ def get_available_qty_from_sabb(self):
15281543
& (child.type_of_transaction.isin(["Inward", "Outward"]))
15291544
)
15301545
.for_update()
1531-
.groupby(child.batch_no)
15321546
)
15331547
query = query.where(child.voucher_type != "Pick List")
15341548

1535-
res = query.run(as_list=True)
1536-
1537-
return frappe._dict(res) if res else frappe._dict()
1549+
return query.run(as_dict=True)
15381550

15391551
def validate_voucher_no_docstatus(self):
15401552
if self.voucher_type == "POS Invoice":

0 commit comments

Comments
 (0)