mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-04 20:53:23 +02:00
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:
@@ -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"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user