fix(stock): preserve serial selection and use internal IDs in tests

This commit is contained in:
Mihir Kandoi
2026-09-08 12:49:02 +05:30
parent 48818c963a
commit 6f2cf3bf91
15 changed files with 142 additions and 266 deletions

View File

@@ -818,7 +818,7 @@ class TestPOSInvoice(POSInvoiceTestMixin):
)
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
create_batch_item_with_batch("_BATCH ITEM", "TestBatch 01")
batch_no = create_batch_item_with_batch("_BATCH ITEM", "TestBatch 01")
item = frappe.get_doc("Item", "_BATCH ITEM")
se = make_stock_entry(
@@ -826,12 +826,10 @@ class TestPOSInvoice(POSInvoiceTestMixin):
item_code="_BATCH ITEM",
qty=2,
basic_rate=100,
batch_no="TestBatch 01",
batch_no=batch_no,
)
pos_inv1 = create_pos_invoice(
item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01"
)
pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1, batch_no=batch_no)
pos_inv1.append(
"payments",
{"mode_of_payment": "Cash", "amount": 300},
@@ -849,7 +847,7 @@ class TestPOSInvoice(POSInvoiceTestMixin):
"voucher_no": pos_inv2.name,
"qty": 2,
"avg_rate": 300,
"batches": frappe._dict({"TestBatch 01": 2}),
"batches": frappe._dict({batch_no: 2}),
"type_of_transaction": "Outward",
"company": pos_inv2.company,
}
@@ -925,6 +923,7 @@ class TestPOSInvoice(POSInvoiceTestMixin):
self.assertRaises(frappe.ValidationError, pos_inv.submit)
@ERPNextTestSuite.change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_bundle_stock_availability_validation(self):
from erpnext.accounts.doctype.pos_invoice.pos_invoice import ProductBundleStockValidationError
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle

View File

@@ -36,6 +36,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle
make_serial_batch_bundle,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
from erpnext.stock.tests.test_utils import StockTestMixin
from erpnext.tests.utils import ERPNextTestSuite
@@ -2643,25 +2644,8 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
batch_no = "BATCH-PI-BNU-TPRBI-0001"
serial_nos = ["SNU-PI-TPRSI-0001", "SNU-PI-TPRSI-0002", "SNU-PI-TPRSI-0003"]
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": batch_item,
}
).insert()
for serial_no in serial_nos:
if not frappe.db.exists("Serial No", serial_no):
frappe.get_doc(
{
"doctype": "Serial No",
"item_code": serial_item,
"serial_no": serial_no,
"company": "_Test Company",
}
).insert()
batch_no = SerialBatchIdentity("Batch").resolve(batch_item, [batch_no], create=True)[0]
serial_nos = SerialBatchIdentity("Serial No").resolve(serial_item, serial_nos, create=True)
pi = make_purchase_invoice(
item_code=batch_item,

View File

@@ -21,6 +21,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor
)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation, get_serial_nos_from_bundle
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
from erpnext.stock.utils import get_incoming_rate
@@ -433,9 +434,10 @@ class SubcontractingController(StockController):
consumed_bundles = voucher_bundle_data.get(bundle_key, frappe._dict())
if consumed_bundles.serial_nos:
self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(consumed_bundles.serial_nos)
)
consumed_serials = set(consumed_bundles.serial_nos)
self.available_materials[key]["serial_no"] = [
sn for sn in self.available_materials[key]["serial_no"] if sn not in consumed_serials
]
if consumed_bundles.batch_nos:
for batch_no, qty in consumed_bundles.batch_nos.items():
@@ -449,9 +451,10 @@ class SubcontractingController(StockController):
from erpnext.deprecation_dumpster import deprecation_warning
deprecation_warning("unknown", "v16", "No instructions.")
self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
)
consumed_serials = set(get_serial_nos(row.serial_no))
self.available_materials[key]["serial_no"] = [
sn for sn in self.available_materials[key]["serial_no"] if sn not in consumed_serials
]
# Will be deprecated in v16
if row.batch_no and not consumed_bundles.batch_nos:
@@ -531,6 +534,12 @@ class SubcontractingController(StockController):
self.__set_alternative_item_details(row)
serial_numbers = SerialBatchIdentity("Serial No").labels(
[sn for details in self.available_materials.values() for sn in details.serial_no]
)
for details in self.available_materials.values():
details.serial_no.sort(key=lambda sn: serial_numbers.get(sn) or sn)
self.__transferred_items = copy.deepcopy(self.available_materials)
self.__update_consumed_materials("Subcontracting Receipt")
@@ -682,7 +691,7 @@ class SubcontractingController(StockController):
return available_batches
def __get_serial_nos_for_bundle(self, qty, key):
available_sns = sorted(self.available_materials[key]["serial_no"])[0 : cint(qty)]
available_sns = self.available_materials[key]["serial_no"][0 : cint(qty)]
serial_nos = []
for serial_no in available_sns:

View File

@@ -995,9 +995,9 @@ class TestSubcontractingController(ERPNextTestSuite):
if value.get(field):
data = value.get(field)
if field == "serial_no":
data = sorted(data)
self.assertEqual(data, transferred_detais.get(field))
self.assertCountEqual(data, transferred_detais.get(field))
else:
self.assertEqual(data, transferred_detais.get(field))
scr2 = make_subcontracting_receipt(sco.name)
scr2.save()
@@ -1010,9 +1010,9 @@ class TestSubcontractingController(ERPNextTestSuite):
if value.get(field):
data = value.get(field)
if field == "serial_no":
data = sorted(data)
self.assertEqual(data, transferred_detais.get(field))
self.assertCountEqual(data, transferred_detais.get(field))
else:
self.assertEqual(data, transferred_detais.get(field))
def test_subcontracting_with_same_components_different_fg_with_serial_batch_fields(self):
"""
@@ -1338,7 +1338,7 @@ def make_stock_transfer_entry(**args):
batches = defaultdict(float)
if item_details and item_details.serial_no:
serial_nos = item_details.serial_no[0 : cint(row.qty)]
item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos))
item_details.serial_no = item_details.serial_no[cint(row.qty) :]
if item_details and item_details.batch_no:
for batch_no, batch_qty in item_details.batch_no.items():

View File

@@ -1860,7 +1860,7 @@ class TestJobCard(ERPNextTestSuite):
self.assertEqual(len(entries), 5)
for entry in entries:
self.assertEqual(flt(entry.qty), 10.0)
self.assertTrue(entry.batch_no.startswith("BS-ROD-PC-"))
self.assertTrue(frappe.db.get_value("Batch", entry.batch_no, "batch_id").startswith("BS-ROD-PC-"))
self.assertEqual(frappe.db.get_value("Batch", entry.batch_no, "parent_batch"), parent_batch)
manufacture_entry.reload()

View File

@@ -36,6 +36,7 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.doctype.stock_entry.stock_entry import OperationsNotCompleteError
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
from erpnext.stock.utils import get_bin
from erpnext.tests.utils import ERPNextTestSuite
@@ -1971,6 +1972,7 @@ class TestWorkOrder(ERPNextTestSuite):
self.assertAlmostEqual(rows["Stores - _TC"], flt(first.required_qty) * 4 / 10, places=6)
self.assertAlmostEqual(rows["_Test Warehouse 1 - _TC"], 5 * 4 / 10, places=6)
@ERPNextTestSuite.change_settings("Buying Settings", {"allow_multiple_items": 0})
def test_allocation_collapses_groups_when_multiple_items_disallowed(self):
work_order = make_wo_order_test_record(
planned_start_date=now(), qty=10, source_warehouse="Stores - _TC"
@@ -2163,6 +2165,8 @@ class TestWorkOrder(ERPNextTestSuite):
)
transferred_ste_doc.items[0].serial_no = "\n".join(serial_nos_list)
transferred_ste_doc.items[0].serial_and_batch_bundle = None
transferred_ste_doc.items[0].use_serial_batch_fields = 1
transferred_ste_doc.submit()
# First Manufacture stock entry
@@ -3770,8 +3774,12 @@ class TestWorkOrder(ERPNextTestSuite):
# Pre-generate two sets of FG serial numbers
series = frappe.db.get_value("Item", fg_item, "serial_no_series")
fg_serials_1 = [make_autoname(series) for _ in range(3)]
fg_serials_2 = [make_autoname(series) for _ in range(3)]
fg_serials_1 = SerialBatchIdentity("Serial No").resolve(
fg_item, [make_autoname(series) for _ in range(3)], create=True
)
fg_serials_2 = SerialBatchIdentity("Serial No").resolve(
fg_item, [make_autoname(series) for _ in range(3)], create=True
)
# Manufacture entry 1 — consumes rm_serials_1, produces fg_serials_1
se_manufacture_1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3))

View File

@@ -3453,8 +3453,8 @@ class TestSalesOrder(ERPNextTestSuite):
serial_nos_in_bundle = get_serial_nos(dn.packed_items[1].serial_and_batch_bundle)
batches_in_bundle = list(get_batches_from_bundle(dn.packed_items[1].serial_and_batch_bundle).keys())
self.assertEqual(sre_serial_nos, serial_nos_in_bundle)
self.assertEqual(sre_batch_nos, batches_in_bundle)
self.assertCountEqual(sre_serial_nos, serial_nos_in_bundle)
self.assertCountEqual(sre_batch_nos, batches_in_bundle)
dn.items[0].qty = 5
dn.save()
@@ -3495,8 +3495,8 @@ class TestSalesOrder(ERPNextTestSuite):
serial_nos_in_bundle = get_serial_nos(si.packed_items[1].serial_and_batch_bundle)
batches_in_bundle = list(get_batches_from_bundle(si.packed_items[1].serial_and_batch_bundle).keys())
self.assertEqual(sre_serial_nos, serial_nos_in_bundle)
self.assertEqual(sre_batch_nos, batches_in_bundle)
self.assertCountEqual(sre_serial_nos, serial_nos_in_bundle)
self.assertCountEqual(sre_batch_nos, batches_in_bundle)
si.items[0].qty = 5
si.save()

View File

@@ -20,6 +20,7 @@ from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import (
SerialNoInventoryDimensionError,
)
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
from erpnext.tests.utils import ERPNextTestSuite
@@ -503,6 +504,7 @@ class TestInventoryDimension(ERPNextTestSuite):
{"has_serial_no": 1, "is_stock_item": 1},
)
serial_no = "Test Serialized Inventory Dimension Serial No"
serial_no = SerialBatchIdentity("Serial No").resolve(item.name, [serial_no], create=True)[0]
warehouse = create_warehouse("Serialized Inventory Dimension Warehouse")
create_inventory_dimension(
@@ -563,6 +565,7 @@ class TestInventoryDimension(ERPNextTestSuite):
{"has_serial_no": 1, "is_stock_item": 1},
)
serial_no = "Test Serialized Empty Inventory Dimension Serial No"
serial_no = SerialBatchIdentity("Serial No").resolve(item.name, [serial_no], create=True)[0]
warehouse = create_warehouse("Serialized Empty Inventory Dimension Warehouse")
create_inventory_dimension(
@@ -599,6 +602,7 @@ class TestInventoryDimension(ERPNextTestSuite):
{"has_serial_no": 1, "is_stock_item": 1},
)
serial_no = "Test Serialized Required Inventory Dimension Serial No"
serial_no = SerialBatchIdentity("Serial No").resolve(item.name, [serial_no], create=True)[0]
warehouse = create_warehouse("Serialized Required Inventory Dimension Warehouse")
create_inventory_dimension(
@@ -644,6 +648,7 @@ class TestInventoryDimension(ERPNextTestSuite):
{"has_serial_no": 1, "is_stock_item": 1},
)
serial_no = "Test Serialized Legacy Inventory Dimension Serial No"
serial_no = SerialBatchIdentity("Serial No").resolve(item.name, [serial_no], create=True)[0]
warehouse = create_warehouse("Serialized Legacy Inventory Dimension Warehouse")
create_inventory_dimension(

View File

@@ -21,6 +21,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle
get_serial_nos_from_bundle,
)
from erpnext.stock.serial_batch_bundle import SerialNoValuation
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
from erpnext.tests.utils import ERPNextTestSuite
@@ -433,15 +434,7 @@ class TestLandedCostVoucher(ERPNextTestSuite):
item_code = "_Test Serialized Item"
warehouse = "Stores - TCP1"
if not frappe.db.exists("Serial No", serial_no):
frappe.get_doc(
{
"doctype": "Serial No",
"item_code": item_code,
"serial_no": serial_no,
"company": "_Test Company",
}
).insert()
serial_no = SerialBatchIdentity("Serial No").resolve(item_code, [serial_no], create=True)[0]
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
@@ -751,27 +744,12 @@ class TestLandedCostVoucher(ERPNextTestSuite):
"SN-TLCVSNO-0005",
]
for sn in serial_nos:
if not frappe.db.exists("Serial No", sn):
sn_doc = frappe.get_doc(
{
"doctype": "Serial No",
"item_code": sn_item,
"serial_no": sn,
"company": "_Test Company",
}
)
sn_doc.insert()
serial_nos = SerialBatchIdentity("Serial No").resolve(
sn_item, serial_nos, create=True, defaults={"company": "_Test Company"}
)
if not frappe.db.exists("Batch", "BATCH-TLCVSNO-0001"):
batch_doc = frappe.get_doc(
{
"doctype": "Batch",
"item": batch_item,
"batch_id": "BATCH-TLCVSNO-0001",
}
)
batch_doc.insert()
batch_no = SerialBatchIdentity("Batch").resolve(batch_item, ["BATCH-TLCVSNO-0001"], create=True)[0]
batch_doc = frappe.get_doc("Batch", batch_no)
warehouse = "_Test Warehouse - _TC"
company = frappe.db.get_value("Warehouse", warehouse, "company")
@@ -813,7 +791,7 @@ class TestLandedCostVoucher(ERPNextTestSuite):
if row.item_code == sn_item:
row.db_set("serial_no", ", ".join(serial_nos))
else:
row.db_set("batch_no", "BATCH-TLCVSNO-0001")
row.db_set("batch_no", batch_no)
for sn in serial_nos:
sn_doc = frappe.get_doc("Serial No", sn)
@@ -902,27 +880,12 @@ class TestLandedCostVoucher(ERPNextTestSuite):
"SN-TDVLCVSNO-0005",
]
for sn in serial_nos:
if not frappe.db.exists("Serial No", sn):
sn_doc = frappe.get_doc(
{
"doctype": "Serial No",
"item_code": sn_item,
"serial_no": sn,
"company": "_Test Company",
}
)
sn_doc.insert()
serial_nos = SerialBatchIdentity("Serial No").resolve(
sn_item, serial_nos, create=True, defaults={"company": "_Test Company"}
)
if not frappe.db.exists("Batch", "BATCH-TDVLCVSNO-0001"):
batch_doc = frappe.get_doc(
{
"doctype": "Batch",
"item": batch_item,
"batch_id": "BATCH-TDVLCVSNO-0001",
}
)
batch_doc.insert()
batch_no = SerialBatchIdentity("Batch").resolve(batch_item, ["BATCH-TDVLCVSNO-0001"], create=True)[0]
batch_doc = frappe.get_doc("Batch", batch_no)
warehouse = "_Test Warehouse - _TC"
company = frappe.db.get_value("Warehouse", warehouse, "company")
@@ -974,7 +937,7 @@ class TestLandedCostVoucher(ERPNextTestSuite):
if row.item_code == sn_item:
row.db_set("serial_no", ", ".join(serial_nos))
else:
row.db_set("batch_no", "BATCH-TDVLCVSNO-0001")
row.db_set("batch_no", batch_no)
stock_ledger_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": pr.name})
for sle in stock_ledger_entries:
@@ -982,7 +945,7 @@ class TestLandedCostVoucher(ERPNextTestSuite):
if doc.item_code == sn_item:
doc.db_set("serial_no", ", ".join(serial_nos))
else:
doc.db_set("batch_no", "BATCH-TDVLCVSNO-0001")
doc.db_set("batch_no", batch_no)
dn = create_delivery_note(
company=company,
@@ -1017,14 +980,14 @@ class TestLandedCostVoucher(ERPNextTestSuite):
if doc.item_code == sn_item:
doc.db_set("serial_no", ", ".join(serial_nos))
else:
doc.db_set("batch_no", "BATCH-TDVLCVSNO-0001")
doc.db_set("batch_no", batch_no)
available_batches = get_auto_batch_nos(
frappe._dict(
{
"item_code": batch_item,
"warehouse": warehouse,
"batch_no": ["BATCH-TDVLCVSNO-0001"],
"batch_no": [batch_no],
"consider_negative_batches": True,
}
)
@@ -1092,17 +1055,9 @@ class TestLandedCostVoucher(ERPNextTestSuite):
"SN-ALCVTDVLCVSNO-0005",
]
for sn in serial_nos:
if not frappe.db.exists("Serial No", sn):
sn_doc = frappe.get_doc(
{
"doctype": "Serial No",
"item_code": sn_item,
"serial_no": sn,
"company": "_Test Company",
}
)
sn_doc.insert()
serial_nos = SerialBatchIdentity("Serial No").resolve(
sn_item, serial_nos, create=True, defaults={"company": "_Test Company"}
)
warehouse = "_Test Warehouse - _TC"
company = frappe.db.get_value("Warehouse", warehouse, "company")

View File

@@ -24,6 +24,7 @@ from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import BackDate
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.stock.tests.test_utils import StockTestMixin
from erpnext.tests.utils import ERPNextTestSuite
@@ -59,11 +60,9 @@ class TestStockLedgerEntry(ERPNextTestSuite, StockTestMixin):
item = "_Test Serialized Item"
serial = "_Test SN Tie 9"
company_a, company_b = "_Test Company", "_Test Company 1"
if frappe.db.exists("Serial No", serial):
frappe.delete_doc("Serial No", serial, force=1)
frappe.get_doc(
{"doctype": "Serial No", "serial_no": serial, "item_code": item, "company": company_b}
).insert(ignore_permissions=True)
serial = SerialBatchIdentity("Serial No").resolve(
item, [serial], create=True, defaults={"company": company_b}
)[0]
def mk_sle(name, rate):
if frappe.db.exists("Stock Ledger Entry", name):
@@ -1691,7 +1690,8 @@ def setup_item_valuation_test(
batches = [f"IV - Test Batch {i} {valuation_method} {suffix}" for i in batches_list]
for i, batch_id in enumerate(batches):
if not frappe.db.exists("Batch", batch_id):
batches[i] = frappe.db.get_value("Batch", {"item": item.item_code, "batch_id": batch_id})
if not batches[i]:
ubw = use_batchwise_valuation
if isinstance(use_batchwise_valuation, list | tuple):
ubw = use_batchwise_valuation[i]
@@ -1702,6 +1702,7 @@ def setup_item_valuation_test(
).insert()
batch.use_batchwise_valuation = ubw
batch.db_update()
batches[i] = batch.name
return item.item_code, warehouses, batches

View File

@@ -549,7 +549,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
def test_valid_batch(self):
create_batch_item_with_batch("Testing Batch Item 1", "001")
create_batch_item_with_batch("Testing Batch Item 2", "002")
batch_no = create_batch_item_with_batch("Testing Batch Item 2", "002")
doc = frappe.get_doc(
{
@@ -559,7 +559,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
"voucher_type": "Stock Reconciliation",
"entries": [
{
"batch_no": "002",
"batch_no": batch_no,
"qty": 1,
"incoming_rate": 100,
}
@@ -567,7 +567,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
}
)
self.assertRaises(frappe.ValidationError, doc.save)
self.assertRaisesRegex(frappe.ValidationError, "does not belong to Item", doc.save)
def test_serial_no_cancellation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -2219,17 +2219,15 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
def create_batch_item_with_batch(item_name, batch_id):
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
batch_item_doc = create_item(item_name, is_stock_item=1)
if not batch_item_doc.has_batch_no:
batch_item_doc.has_batch_no = 1
batch_item_doc.create_new_batch = 1
batch_item_doc.save(ignore_permissions=True)
if not frappe.db.exists("Batch", batch_id):
b = frappe.new_doc("Batch")
b.item = item_name
b.batch_id = batch_id
b.save()
return SerialBatchIdentity("Batch").resolve(item_name, [batch_id], create=True)[0]
def insert_existing_sle(warehouse, item_code="_Test Item"):

View File

@@ -487,7 +487,7 @@ class FIFOSlots:
or []
)
return self.uppercase_serial_nos(serial_nos), batch_nos
return serial_nos, batch_nos
def _get_row_batch_nos(self, row: dict) -> list:
if not row.batch_no:
@@ -495,7 +495,7 @@ class FIFOSlots:
return [
[
row.batch_no.upper(),
row.batch_no,
self._get_batchwise_valuation(row.batch_no),
abs(row.actual_qty),
abs(row.stock_value_difference),
@@ -512,10 +512,6 @@ class FIFOSlots:
elif len(fifo_queue) > qty_after:
fifo_queue[:] = fifo_queue[:qty_after]
def uppercase_serial_nos(self, serial_nos):
"Convert serial nos to uppercase for uniformity."
return [sn.upper() for sn in serial_nos]
def _get_batchwise_valuation(self, batch_no: str):
if batch_no not in self.batchwise_valuation_by_batch:
# only reachable when stock ledger entries are passed in directly;
@@ -1152,7 +1148,7 @@ class FIFOSlots:
bundle_wise_batch_nos = frappe._dict({})
for bundle_name, batch_no, use_batchwise_valuation, qty, stock_value_difference in query.run():
bundle_wise_batch_nos.setdefault(bundle_name, []).append(
[batch_no.upper(), use_batchwise_valuation, qty, stock_value_difference]
[batch_no, use_batchwise_valuation, qty, stock_value_difference]
)
return bundle_wise_batch_nos

View File

@@ -12,6 +12,7 @@ from erpnext.stock.report.stock_ageing.stock_ageing import (
format_report_data,
get_average_age,
)
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
from erpnext.tests.utils import ERPNextTestSuite
@@ -19,6 +20,27 @@ class TestStockAgeing(ERPNextTestSuite):
def setUp(self) -> None:
self.filters = frappe._dict(company="_Test Company", to_date="2021-12-10", ranges=["30", "60", "90"])
def test_serial_ids_keep_their_case_in_fifo_slots(self):
rows = [
frappe._dict(
name="Serialized Item",
actual_qty=qty,
qty_after_transaction=balance,
stock_value_difference=qty * 10,
warehouse="WH 1",
posting_date=date,
voucher_type="Stock Entry",
voucher_no=str(index),
has_serial_no=True,
serial_no=serials,
)
for index, (qty, balance, date, serials) in enumerate(
[(2, 2, "2021-12-01", "id-aB\nid-Cd"), (-1, 1, "2021-12-02", "id-aB")]
)
]
slots = FIFOSlots(self.filters, rows).generate()
self.assertEqual(slots["Serialized Item"]["fifo_queue"], [["id-Cd", "2021-12-01", 10.0]])
def test_normal_inward_outward_queue(self):
"Reference: Case 1 in stock_ageing_fifo_logic.md (same wh)"
sle = [
@@ -530,12 +552,7 @@ class TestStockAgeing(ERPNextTestSuite):
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
).name
batch_no = "SA-RECO-REVALUE-BATCH"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
ignore_permissions=True
)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
batch_no = make_batch(item_code, "SA-RECO-REVALUE-BATCH")
def make_sle(posting_date, voucher_type, voucher_no, actual_qty, qty_after, stock_value_difference):
return frappe._dict(
@@ -583,12 +600,7 @@ class TestStockAgeing(ERPNextTestSuite):
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
).name
batch_no = "SA-PARTIAL-RECO-BATCH"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
ignore_permissions=True
)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
batch_no = make_batch(item_code, "SA-PARTIAL-RECO-BATCH")
def make_sle(posting_date, voucher_type, voucher_no, actual_qty, qty_after, stock_value_difference):
return frappe._dict(
@@ -636,12 +648,7 @@ class TestStockAgeing(ERPNextTestSuite):
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
).name
batch_no = "SA-POOL-SPLIT-BATCH"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
ignore_permissions=True
)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
batch_no = make_batch(item_code, "SA-POOL-SPLIT-BATCH")
def make_sle(posting_date, voucher_no, actual_qty, qty_after, stock_value_difference):
return frappe._dict(
@@ -687,12 +694,7 @@ class TestStockAgeing(ERPNextTestSuite):
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
).name
batch_no = "SA-POOL-RESIDUAL-BATCH"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
ignore_permissions=True
)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
batch_no = make_batch(item_code, "SA-POOL-RESIDUAL-BATCH")
def make_sle(posting_date, voucher_no, actual_qty, qty_after, stock_value_difference):
return frappe._dict(
@@ -734,12 +736,7 @@ class TestStockAgeing(ERPNextTestSuite):
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
).name
batch_no = "SA-POOL-REBALANCE-BATCH"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
ignore_permissions=True
)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
batch_no = make_batch(item_code, "SA-POOL-REBALANCE-BATCH")
def make_sle(posting_date, voucher_no, actual_qty, qty_after, stock_value_difference):
return frappe._dict(
@@ -1533,38 +1530,14 @@ class TestStockAgeing(ERPNextTestSuite):
},
).name
def make_batch(batch_id, use_batchwise_valuation):
if not frappe.db.exists("Batch", batch_id):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_id,
"item": item_code,
}
).insert(ignore_permissions=True)
frappe.db.set_value("Batch", batch_id, "use_batchwise_valuation", use_batchwise_valuation)
batchwise_above_90 = "SA-BATCHWISE-ABOVE-90"
non_batchwise_above_90 = "SA-NON-BATCHWISE-ABOVE-90"
batchwise_61_90 = "SA-BATCHWISE-61-90"
non_batchwise_61_90 = "SA-NON-BATCHWISE-61-90"
batchwise_31_60 = "SA-BATCHWISE-31-60"
non_batchwise_31_60 = "SA-NON-BATCHWISE-31-60"
batchwise_0_30 = "SA-BATCHWISE-0-30"
non_batchwise_0_30 = "SA-NON-BATCHWISE-0-30"
for batch_id, use_batchwise_valuation in {
batchwise_above_90: 1,
non_batchwise_above_90: 0,
batchwise_61_90: 1,
non_batchwise_61_90: 0,
batchwise_31_60: 1,
non_batchwise_31_60: 0,
batchwise_0_30: 1,
non_batchwise_0_30: 0,
}.items():
make_batch(batch_id, use_batchwise_valuation)
batchwise_above_90 = make_batch(item_code, "SA-BATCHWISE-ABOVE-90", 1)
non_batchwise_above_90 = make_batch(item_code, "SA-NON-BATCHWISE-ABOVE-90", 0)
batchwise_61_90 = make_batch(item_code, "SA-BATCHWISE-61-90", 1)
non_batchwise_61_90 = make_batch(item_code, "SA-NON-BATCHWISE-61-90", 0)
batchwise_31_60 = make_batch(item_code, "SA-BATCHWISE-31-60", 1)
non_batchwise_31_60 = make_batch(item_code, "SA-NON-BATCHWISE-31-60", 0)
batchwise_0_30 = make_batch(item_code, "SA-BATCHWISE-0-30", 1)
non_batchwise_0_30 = make_batch(item_code, "SA-NON-BATCHWISE-0-30", 0)
qty_after_transaction = 0
@@ -1641,22 +1614,8 @@ class TestStockAgeing(ERPNextTestSuite):
},
).name
def make_batch(batch_id):
if not frappe.db.exists("Batch", batch_id):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_id,
"item": item_code,
}
).insert(ignore_permissions=True)
frappe.db.set_value("Batch", batch_id, "use_batchwise_valuation", 1)
source_batch = "SA-BATCHWISE-TRANSFER-SOURCE"
target_batch = "SA-BATCHWISE-TRANSFER-TARGET"
make_batch(source_batch)
make_batch(target_batch)
source_batch = make_batch(item_code, "SA-BATCHWISE-TRANSFER-SOURCE")
target_batch = make_batch(item_code, "SA-BATCHWISE-TRANSFER-TARGET")
sle = [
frappe._dict(
@@ -1735,17 +1694,7 @@ class TestStockAgeing(ERPNextTestSuite):
},
).name
batch_no = "SA-BATCHWISE-NEGATIVE-STOCK"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": item_code,
}
).insert(ignore_permissions=True)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
batch_no = make_batch(item_code, "SA-BATCHWISE-NEGATIVE-STOCK")
sle = [
frappe._dict(
@@ -1814,19 +1763,8 @@ class TestStockAgeing(ERPNextTestSuite):
},
).name
buffer_batch = "SA-BATCHWISE-NEGATIVE-BUFFER"
negative_batch = "SA-BATCHWISE-NEGATIVE-NON-HEAD"
for batch_no in [buffer_batch, negative_batch]:
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": item_code,
}
).insert(ignore_permissions=True)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
buffer_batch = make_batch(item_code, "SA-BATCHWISE-NEGATIVE-BUFFER")
negative_batch = make_batch(item_code, "SA-BATCHWISE-NEGATIVE-NON-HEAD")
sle = [
frappe._dict(
@@ -1905,17 +1843,7 @@ class TestStockAgeing(ERPNextTestSuite):
},
).name
batch_no = "SA-BATCHWISE-NEGATIVE-LATER-VOUCHER"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": item_code,
}
).insert(ignore_permissions=True)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
batch_no = make_batch(item_code, "SA-BATCHWISE-NEGATIVE-LATER-VOUCHER")
sle = [
frappe._dict(
@@ -2056,7 +1984,7 @@ class TestStockAgeing(ERPNextTestSuite):
self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"])
self.assertEqual(item_result["total_qty"], 5.0)
self.assertEqual(
item_result["fifo_queue"], [[batch_no.upper(), 1, 5.0, getdate(add_days(base_date, -2)), 50.0]]
item_result["fifo_queue"], [[batch_no, 1, 5.0, getdate(add_days(base_date, -2)), 50.0]]
)
def test_legacy_batch_no_sle_with_streaming_cursor(self):
@@ -2143,3 +2071,9 @@ def generate_item_and_item_wh_wise_slots(filters, sle):
filters.show_warehouse_wise_stock = False
return item_wise_slots, item_wh_wise_slots
def make_batch(item_code, batch_id, use_batchwise_valuation=1):
batch_no = SerialBatchIdentity("Batch").resolve(item_code, [batch_id], create=True)[0]
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", use_batchwise_valuation)
return batch_no

View File

@@ -77,7 +77,7 @@ class TestGetItemDetail(ERPNextTestSuite):
).insert()
# create batch
frappe.get_doc(
batch = frappe.get_doc(
{
"doctype": "Batch",
"batch_id": "BATCH01",
@@ -92,7 +92,7 @@ class TestGetItemDetail(ERPNextTestSuite):
"price_list": "Standard Selling",
"item_code": item.item_code,
"price_list_rate": 50,
"batch_no": "BATCH01",
"batch_no": batch.name,
}
).insert()
@@ -104,7 +104,7 @@ class TestGetItemDetail(ERPNextTestSuite):
warehouse="_Test Warehouse - _TC",
qty=100,
rate=100,
batch_no="BATCH01",
batch_no=batch.name,
)
# creating sales order just to create delivery note from it
@@ -122,7 +122,7 @@ class TestGetItemDetail(ERPNextTestSuite):
# Test 2 : On saving the DN, item's batch will be fetched and rate will be updated from Item Price
dn.save()
self.assertEqual(dn.items[0].batch_no, "BATCH01")
self.assertEqual(dn.items[0].batch_no, batch.name)
self.assertEqual(dn.items[0].rate, 50)
def test_maintain_same_rate_keeps_source_rate_on_refetch(self):

View File

@@ -37,6 +37,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
make_subcontracting_receipt,
)
@@ -1753,14 +1754,7 @@ class TestSubcontractingReceipt(ERPNextTestSuite):
)
batch_no = "BATCH-BNGS-0001"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": fg_item,
}
).insert()
batch_no = SerialBatchIdentity("Batch").resolve(fg_item, [batch_no], create=True)[0]
scr = make_subcontracting_receipt(sco.name)
self.assertFalse(scr.items[0].serial_and_batch_bundle)
@@ -1830,14 +1824,7 @@ class TestSubcontractingReceipt(ERPNextTestSuite):
)
batch_no = "BATCH-REJ-BNGS-0001"
if not frappe.db.exists("Batch", batch_no):
frappe.get_doc(
{
"doctype": "Batch",
"batch_id": batch_no,
"item": fg_item,
}
).insert()
batch_no = SerialBatchIdentity("Batch").resolve(fg_item, [batch_no], create=True)[0]
rej_warehouse = create_warehouse("_Test Subcontract Warehouse For Rejected Qty")