Merge pull request #53994 from aerele/fix/update-sabe-stock-queue

fix(stock): update stock queue in SABE for return entries
This commit is contained in:
rohitwaghchaure
2026-04-04 12:51:34 +05:30
committed by GitHub
2 changed files with 200 additions and 0 deletions

View File

@@ -410,6 +410,25 @@ class SerialandBatchBundle(Document):
def set_valuation_rate_for_return_entry(self, return_against, row, save=False, prev_sle=None):
if valuation_details := self.get_valuation_rate_for_return_entry(return_against):
from erpnext.stock.utils import get_valuation_method
valuation_method = get_valuation_method(self.item_code, self.company)
stock_queue = []
non_batchwise_batches = []
if not self.has_serial_no and valuation_method == "FIFO":
non_batchwise_batches = frappe.get_all(
"Batch",
filters={
"name": ("in", [d.batch_no for d in self.entries if d.batch_no]),
"use_batchwise_valuation": 0,
},
pluck="name",
)
if non_batchwise_batches and prev_sle and prev_sle.stock_queue:
stock_queue = parse_json(prev_sle.stock_queue)
for row in self.entries:
if valuation_details:
self.validate_returned_serial_batch_no(return_against, row, valuation_details)
@@ -431,11 +450,25 @@ class SerialandBatchBundle(Document):
row.incoming_rate = flt(valuation_rate)
row.stock_value_difference = flt(row.qty) * flt(row.incoming_rate)
if (
non_batchwise_batches
and row.batch_no in non_batchwise_batches
and row.incoming_rate is not None
):
if flt(row.qty) > 0:
stock_queue.append([row.qty, row.incoming_rate])
elif flt(row.qty) < 0:
stock_queue = FIFOValuation(stock_queue)
stock_queue.remove_stock(qty=abs(row.qty))
stock_queue = stock_queue.state
row.stock_queue = json.dumps(stock_queue)
if save:
row.db_set(
{
"incoming_rate": row.incoming_rate,
"stock_value_difference": row.stock_value_difference,
"stock_queue": row.get("stock_queue"),
}
)

View File

@@ -1109,6 +1109,173 @@ class TestSerialandBatchBundle(ERPNextTestSuite):
self.assertEqual(frappe.get_value("Serial No", serial_no, "reference_name"), se1.name)
def test_stock_queue_for_return_entry_with_non_batchwise_valuation(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
batch_item_code = "Old Batch Return Queue Test"
make_item(
batch_item_code,
{
"has_batch_no": 1,
"batch_number_series": "TEST-RET-Q-.#####",
"create_new_batch": 1,
"is_stock_item": 1,
"valuation_method": "FIFO",
},
)
batch_id = "Old Batch Return Queue 1"
if not frappe.db.exists("Batch", batch_id):
batch_doc = frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_id,
"item": batch_item_code,
"use_batchwise_valuation": 0,
}
).insert(ignore_permissions=True)
batch_doc.db_set(
{
"use_batchwise_valuation": 0,
"batch_qty": 0,
}
)
# Create initial stock with FIFO queue: [[10, 100], [20, 200]]
make_stock_entry(
item_code=batch_item_code,
target="_Test Warehouse - _TC",
qty=10,
rate=100,
batch_no=batch_id,
use_serial_batch_fields=True,
)
make_stock_entry(
item_code=batch_item_code,
target="_Test Warehouse - _TC",
qty=20,
rate=200,
batch_no=batch_id,
use_serial_batch_fields=True,
)
# Purchase Receipt: inward 5 @ 300
pr = make_purchase_receipt(
item_code=batch_item_code,
warehouse="_Test Warehouse - _TC",
qty=5,
rate=300,
batch_no=batch_id,
use_serial_batch_fields=True,
)
sle = frappe.db.get_value(
"Stock Ledger Entry",
{"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": pr.name},
["stock_queue"],
as_dict=True,
)
# Stock queue should now be [[10, 100], [20, 200], [5, 300]]
self.assertEqual(json.loads(sle.stock_queue), [[10, 100], [20, 200], [5, 300]])
# Purchase Return: return 5 against the PR
return_pr = make_return_doc("Purchase Receipt", pr.name)
return_pr.submit()
return_sle = frappe.db.get_value(
"Stock Ledger Entry",
{"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": return_pr.name},
["stock_queue"],
as_dict=True,
)
# Stock queue should have 5 removed via FIFO from [[10, 100], [20, 200], [5, 300]]
# FIFO removes from front: [10, 100] -> [5, 100], rest unchanged
self.assertEqual(json.loads(return_sle.stock_queue), [[5, 100], [20, 200], [5, 300]])
def test_stock_queue_for_return_entry_with_empty_fifo_queue(self):
"""Credit note (sales return) against empty FIFO queue should still rebuild stock_queue."""
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
batch_item_code = "Old Batch Empty Queue Test"
make_item(
batch_item_code,
{
"has_batch_no": 1,
"batch_number_series": "TEST-EQ-.#####",
"create_new_batch": 1,
"is_stock_item": 1,
"valuation_method": "FIFO",
},
)
batch_id = "Old Batch Empty Queue 1"
if not frappe.db.exists("Batch", batch_id):
batch_doc = frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_id,
"item": batch_item_code,
"use_batchwise_valuation": 0,
}
).insert(ignore_permissions=True)
batch_doc.db_set(
{
"use_batchwise_valuation": 0,
"batch_qty": 0,
}
)
# Inward 10 @ 100, then outward all 10 to empty the queue
make_stock_entry(
item_code=batch_item_code,
target="_Test Warehouse - _TC",
qty=10,
rate=100,
batch_no=batch_id,
use_serial_batch_fields=True,
)
dn = create_delivery_note(
item_code=batch_item_code,
warehouse="_Test Warehouse - _TC",
qty=10,
rate=150,
batch_no=batch_id,
use_serial_batch_fields=True,
)
# Verify queue is empty after full outward
sle = frappe.db.get_value(
"Stock Ledger Entry",
{"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": dn.name},
["stock_queue"],
as_dict=True,
)
self.assertFalse(json.loads(sle.stock_queue or "[]"))
# Sales return (credit note): 5 items come back at original rate 100
return_dn = make_return_doc("Delivery Note", dn.name)
for row in return_dn.items:
row.qty = -5
return_dn.save().submit()
return_sle = frappe.db.get_value(
"Stock Ledger Entry",
{"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": return_dn.name},
["stock_queue"],
as_dict=True,
)
# Stock queue should have the returned stock: [[5, 100]]
self.assertEqual(json.loads(return_sle.stock_queue), [[5, 100]])
def get_batch_from_bundle(bundle):
from erpnext.stock.serial_batch_bundle import get_batch_nos