diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index e6bd02692cf..5da500a61e0 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -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 diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 78e270ebb70..f59a49467fa 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -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, diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index a78b45f4d86..cd42f00d818 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -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: diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 1b5f94b42cc..215c1a59b1e 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -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(): diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 414f8e67b25..c6bc9a09dd1 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -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() diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index ba6f5ee165a..72c1aac4705 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -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)) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 9d9160cb8b4..4f26c2a007e 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -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() diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 90bdf593cd7..23af3908438 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -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( diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 2d7d18eb364..b06817b40a9 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -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") diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 5dc26d8cd08..98b453ed2f6 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -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 diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index d97256ddb3a..6058c5e0d09 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -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"): diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 463d195d38f..78de7a9c08f 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -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 diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 39c046fb689..283d7181ea7 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -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 diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py index 467bdb7c8f6..cec12db06a0 100644 --- a/erpnext/stock/tests/test_get_item_details.py +++ b/erpnext/stock/tests/test_get_item_details.py @@ -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): diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index a16b23f2343..c80a80d9351 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -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")