diff --git a/erpnext/stock/ledger_snapshot.py b/erpnext/stock/ledger_snapshot.py index 977ede74ed0..a50d245670d 100644 --- a/erpnext/stock/ledger_snapshot.py +++ b/erpnext/stock/ledger_snapshot.py @@ -114,6 +114,7 @@ class SLSnapshot: sle.valuation_rate, sle.stock_value, sle.stock_value_difference, + sle.serial_and_batch_bundle, sle.posting_date, ) .where( @@ -136,6 +137,9 @@ class SLSnapshot: "valuation_rate": flt(row.valuation_rate, RATE_PRECISION), "stock_value": flt(row.stock_value, RATE_PRECISION), "stock_value_difference": flt(row.stock_value_difference, RATE_PRECISION), + # Linkage presence, not the volatile bundle docname — catches a dropped + # serial/batch bundle link without coupling the golden to generated names. + "has_serial_and_batch_bundle": bool(row.serial_and_batch_bundle), "posting_date": str(row.posting_date), } diff --git a/erpnext/stock/ledger_snapshots/dn_basic.json b/erpnext/stock/ledger_snapshots/dn_basic.json index ec5d8c1c1ee..0c405a4772d 100644 --- a/erpnext/stock/ledger_snapshots/dn_basic.json +++ b/erpnext/stock/ledger_snapshots/dn_basic.json @@ -32,12 +32,13 @@ "sle": [ { "actual_qty": -5.0, + "has_serial_and_batch_bundle": false, "incoming_rate": 0.0, "item_code": "_Test Item", "posting_date": "2024-01-15", - "qty_after_transaction": -5.0, + "qty_after_transaction": 5.0, "stock_uom": "_Test UOM", - "stock_value": -500.0, + "stock_value": 500.0, "stock_value_difference": -500.0, "valuation_rate": 100.0, "warehouse": "Stores - TCP1" diff --git a/erpnext/stock/ledger_snapshots/dn_return.json b/erpnext/stock/ledger_snapshots/dn_return.json index e5dbe2be296..0144a698310 100644 --- a/erpnext/stock/ledger_snapshots/dn_return.json +++ b/erpnext/stock/ledger_snapshots/dn_return.json @@ -32,12 +32,13 @@ "sle": [ { "actual_qty": 5.0, + "has_serial_and_batch_bundle": false, "incoming_rate": 100.0, "item_code": "_Test Item", "posting_date": "2024-01-15", - "qty_after_transaction": -5.0, + "qty_after_transaction": 10.0, "stock_uom": "_Test UOM", - "stock_value": -500.0, + "stock_value": 1000.0, "stock_value_difference": 500.0, "valuation_rate": 100.0, "warehouse": "Stores - TCP1" diff --git a/erpnext/stock/ledger_snapshots/pr_basic.json b/erpnext/stock/ledger_snapshots/pr_basic.json index 8cbc6763694..62e422fb272 100644 --- a/erpnext/stock/ledger_snapshots/pr_basic.json +++ b/erpnext/stock/ledger_snapshots/pr_basic.json @@ -32,12 +32,13 @@ "sle": [ { "actual_qty": 5.0, + "has_serial_and_batch_bundle": false, "incoming_rate": 100.0, "item_code": "_Test Item", "posting_date": "2024-01-15", - "qty_after_transaction": 0.0, + "qty_after_transaction": 5.0, "stock_uom": "_Test UOM", - "stock_value": 0.0, + "stock_value": 500.0, "stock_value_difference": 500.0, "valuation_rate": 100.0, "warehouse": "Stores - TCP1" diff --git a/erpnext/stock/ledger_snapshots/pr_batch_item.json b/erpnext/stock/ledger_snapshots/pr_batch_item.json new file mode 100644 index 00000000000..76c4bf81517 --- /dev/null +++ b/erpnext/stock/ledger_snapshots/pr_batch_item.json @@ -0,0 +1,47 @@ +{ + "gl": [ + { + "account": "Stock In Hand - TCP1", + "account_currency": "INR", + "against": "Stock Received But Not Billed - TCP1", + "cost_center": "Main - TCP1", + "credit": 0.0, + "credit_in_account_currency": 0.0, + "debit": 1000.0, + "debit_in_account_currency": 1000.0, + "is_opening": "No", + "party": null, + "party_type": null, + "posting_date": "2024-01-15" + }, + { + "account": "Stock Received But Not Billed - TCP1", + "account_currency": "INR", + "against": "Stock In Hand - TCP1", + "cost_center": "Main - TCP1", + "credit": 1000.0, + "credit_in_account_currency": 1000.0, + "debit": 0.0, + "debit_in_account_currency": 0.0, + "is_opening": "No", + "party": null, + "party_type": null, + "posting_date": "2024-01-15" + } + ], + "sle": [ + { + "actual_qty": 10.0, + "has_serial_and_batch_bundle": true, + "incoming_rate": 100.0, + "item_code": "_Test Characterization Batch Item", + "posting_date": "2024-01-15", + "qty_after_transaction": 10.0, + "stock_uom": "Nos", + "stock_value": 1000.0, + "stock_value_difference": 1000.0, + "valuation_rate": 100.0, + "warehouse": "Stores - TCP1" + } + ] +} diff --git a/erpnext/stock/ledger_snapshots/pr_return.json b/erpnext/stock/ledger_snapshots/pr_return.json index 830593473f1..9dc339da1a3 100644 --- a/erpnext/stock/ledger_snapshots/pr_return.json +++ b/erpnext/stock/ledger_snapshots/pr_return.json @@ -32,6 +32,7 @@ "sle": [ { "actual_qty": -5.0, + "has_serial_and_batch_bundle": false, "incoming_rate": 0.0, "item_code": "_Test Item", "posting_date": "2024-01-15", diff --git a/erpnext/stock/ledger_snapshots/pr_serial_item.json b/erpnext/stock/ledger_snapshots/pr_serial_item.json new file mode 100644 index 00000000000..b581bff8d00 --- /dev/null +++ b/erpnext/stock/ledger_snapshots/pr_serial_item.json @@ -0,0 +1,47 @@ +{ + "gl": [ + { + "account": "Stock In Hand - TCP1", + "account_currency": "INR", + "against": "Stock Received But Not Billed - TCP1", + "cost_center": "Main - TCP1", + "credit": 0.0, + "credit_in_account_currency": 0.0, + "debit": 500.0, + "debit_in_account_currency": 500.0, + "is_opening": "No", + "party": null, + "party_type": null, + "posting_date": "2024-01-15" + }, + { + "account": "Stock Received But Not Billed - TCP1", + "account_currency": "INR", + "against": "Stock In Hand - TCP1", + "cost_center": "Main - TCP1", + "credit": 500.0, + "credit_in_account_currency": 500.0, + "debit": 0.0, + "debit_in_account_currency": 0.0, + "is_opening": "No", + "party": null, + "party_type": null, + "posting_date": "2024-01-15" + } + ], + "sle": [ + { + "actual_qty": 5.0, + "has_serial_and_batch_bundle": true, + "incoming_rate": 100.0, + "item_code": "_Test Characterization Serial Item", + "posting_date": "2024-01-15", + "qty_after_transaction": 5.0, + "stock_uom": "Nos", + "stock_value": 500.0, + "stock_value_difference": 500.0, + "valuation_rate": 100.0, + "warehouse": "Stores - TCP1" + } + ] +} diff --git a/erpnext/stock/ledger_snapshots/pr_with_taxes.json b/erpnext/stock/ledger_snapshots/pr_with_taxes.json index 71e51a92f09..dc6a71b6ed7 100644 --- a/erpnext/stock/ledger_snapshots/pr_with_taxes.json +++ b/erpnext/stock/ledger_snapshots/pr_with_taxes.json @@ -60,6 +60,7 @@ "sle": [ { "actual_qty": 5.0, + "has_serial_and_batch_bundle": false, "incoming_rate": 150.0, "item_code": "_Test Item", "posting_date": "2024-01-15", diff --git a/erpnext/stock/ledger_snapshots/se_material_issue.json b/erpnext/stock/ledger_snapshots/se_material_issue.json index 0f8298b4c91..87f1be7efff 100644 --- a/erpnext/stock/ledger_snapshots/se_material_issue.json +++ b/erpnext/stock/ledger_snapshots/se_material_issue.json @@ -7,8 +7,8 @@ "cost_center": "Main - TCP1", "credit": 0.0, "credit_in_account_currency": 0.0, - "debit": 750.0, - "debit_in_account_currency": 750.0, + "debit": 500.0, + "debit_in_account_currency": 500.0, "is_opening": "No", "party": null, "party_type": null, @@ -19,8 +19,8 @@ "account_currency": "INR", "against": "Stock Adjustment - TCP1", "cost_center": "Main - TCP1", - "credit": 750.0, - "credit_in_account_currency": 750.0, + "credit": 500.0, + "credit_in_account_currency": 500.0, "debit": 0.0, "debit_in_account_currency": 0.0, "is_opening": "No", @@ -32,14 +32,15 @@ "sle": [ { "actual_qty": -5.0, + "has_serial_and_batch_bundle": false, "incoming_rate": 0.0, "item_code": "_Test Item", "posting_date": "2024-01-15", - "qty_after_transaction": 0.0, + "qty_after_transaction": 5.0, "stock_uom": "_Test UOM", - "stock_value": 0.0, - "stock_value_difference": -750.0, - "valuation_rate": 150.0, + "stock_value": 500.0, + "stock_value_difference": -500.0, + "valuation_rate": 100.0, "warehouse": "Stores - TCP1" } ] diff --git a/erpnext/stock/ledger_snapshots/se_material_receipt.json b/erpnext/stock/ledger_snapshots/se_material_receipt.json index 7697282b7f9..47c8aa744ce 100644 --- a/erpnext/stock/ledger_snapshots/se_material_receipt.json +++ b/erpnext/stock/ledger_snapshots/se_material_receipt.json @@ -32,6 +32,7 @@ "sle": [ { "actual_qty": 5.0, + "has_serial_and_batch_bundle": false, "incoming_rate": 100.0, "item_code": "_Test Item", "posting_date": "2024-01-15", diff --git a/erpnext/stock/ledger_snapshots/se_material_transfer.json b/erpnext/stock/ledger_snapshots/se_material_transfer.json index 08b5429f3d5..d9d76458d3f 100644 --- a/erpnext/stock/ledger_snapshots/se_material_transfer.json +++ b/erpnext/stock/ledger_snapshots/se_material_transfer.json @@ -3,18 +3,20 @@ "sle": [ { "actual_qty": -5.0, + "has_serial_and_batch_bundle": false, "incoming_rate": 0.0, "item_code": "_Test Item", "posting_date": "2024-01-15", - "qty_after_transaction": 0.0, + "qty_after_transaction": 5.0, "stock_uom": "_Test UOM", - "stock_value": 0.0, + "stock_value": 500.0, "stock_value_difference": -500.0, "valuation_rate": 100.0, "warehouse": "Stores - TCP1" }, { "actual_qty": 5.0, + "has_serial_and_batch_bundle": false, "incoming_rate": 100.0, "item_code": "_Test Item", "posting_date": "2024-01-15", diff --git a/erpnext/stock/ledger_snapshots/sr_basic.json b/erpnext/stock/ledger_snapshots/sr_basic.json index f8a92672ebf..1203530416d 100644 --- a/erpnext/stock/ledger_snapshots/sr_basic.json +++ b/erpnext/stock/ledger_snapshots/sr_basic.json @@ -32,6 +32,7 @@ "sle": [ { "actual_qty": 0.0, + "has_serial_and_batch_bundle": false, "incoming_rate": 0.0, "item_code": "_Test Item", "posting_date": "2024-01-15", diff --git a/erpnext/stock/test_ledger_characterization.py b/erpnext/stock/test_ledger_characterization.py index d9fdd0fe7f0..9a3cbd69b20 100644 --- a/erpnext/stock/test_ledger_characterization.py +++ b/erpnext/stock/test_ledger_characterization.py @@ -6,6 +6,13 @@ a stored snapshot (see ``erpnext/stock/ledger_snapshots``). They assert nothing about *correct* accounting or valuation — only that ledger output stays byte-identical as ``stock_controller`` is split into services. +Determinism: each test is wrapped in a savepoint that is rolled back in tearDown, +so the cumulative Stock Ledger fields (qty_after_transaction, stock_value, +valuation_rate) do not depend on test execution order or on state left by other +tests. Prerequisite stock is posted on PREREQUISITE_DATE (before POSTING_DATE) so +balances are positive and independent of the wall-clock date. Run the module in +isolation (``--module ...``) as below. + Regenerate goldens after an intentional change:: REGEN_LEDGER_SNAPSHOTS=1 bench run-tests --site test-erpnext-v17 \\ @@ -15,26 +22,38 @@ Regenerate goldens after an intentional change:: import frappe from frappe.tests import IntegrationTestCase +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.ledger_snapshot import assert_ledger_snapshot POSTING_DATE = "2024-01-15" +PREREQUISITE_DATE = "2024-01-10" CUSTOMER = "_Test Customer" COMPANY = "_Test Company with perpetual inventory" WAREHOUSE = "Stores - TCP1" class TestLedgerCharacterization(IntegrationTestCase): + def setUp(self): + frappe.db.savepoint("ledger_characterization") + + def tearDown(self): + frappe.db.rollback(save_point="ledger_characterization") + def test_dn_basic(self): - make_stock_entry(item_code="_Test Item", target=WAREHOUSE, qty=10, basic_rate=100) + make_stock_entry( + item_code="_Test Item", target=WAREHOUSE, qty=10, basic_rate=100, posting_date=PREREQUISITE_DATE + ) dn = _make_dated_delivery_note(qty=5, rate=150) dn.insert() dn.submit() assert_ledger_snapshot(self, "dn_basic", "Delivery Note", dn.name) def test_dn_return(self): - make_stock_entry(item_code="_Test Item", target=WAREHOUSE, qty=10, basic_rate=100) + make_stock_entry( + item_code="_Test Item", target=WAREHOUSE, qty=10, basic_rate=100, posting_date=PREREQUISITE_DATE + ) original = _make_dated_delivery_note(qty=5, rate=150) original.insert() original.submit() @@ -64,7 +83,14 @@ class TestLedgerCharacterization(IntegrationTestCase): assert_ledger_snapshot(self, "se_material_receipt", "Stock Entry", se.name) def test_se_material_issue(self): - make_stock_entry(item_code="_Test Item", target=WAREHOUSE, qty=10, basic_rate=100, company=COMPANY) + make_stock_entry( + item_code="_Test Item", + target=WAREHOUSE, + qty=10, + basic_rate=100, + company=COMPANY, + posting_date=PREREQUISITE_DATE, + ) se = make_stock_entry( item_code="_Test Item", source=WAREHOUSE, @@ -77,7 +103,14 @@ class TestLedgerCharacterization(IntegrationTestCase): assert_ledger_snapshot(self, "se_material_issue", "Stock Entry", se.name) def test_se_material_transfer(self): - make_stock_entry(item_code="_Test Item", target=WAREHOUSE, qty=10, basic_rate=100, company=COMPANY) + make_stock_entry( + item_code="_Test Item", + target=WAREHOUSE, + qty=10, + basic_rate=100, + company=COMPANY, + posting_date=PREREQUISITE_DATE, + ) se = make_stock_entry( item_code="_Test Item", source=WAREHOUSE, @@ -126,6 +159,43 @@ class TestLedgerCharacterization(IntegrationTestCase): ret.submit() assert_ledger_snapshot(self, "pr_return", "Purchase Receipt", ret.name) + def test_pr_batch_item(self): + """Exercises SerialBatchBundleService bundle creation + SLE bundle linkage.""" + item_code = make_item( + "_Test Characterization Batch Item", + { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "CHAR-BATCH-.#####", + "is_stock_item": 1, + }, + ).name + pr = make_purchase_receipt( + item_code=item_code, + company=COMPANY, + warehouse=WAREHOUSE, + posting_date=POSTING_DATE, + qty=10, + rate=100, + ) + assert_ledger_snapshot(self, "pr_batch_item", "Purchase Receipt", pr.name) + + def test_pr_serial_item(self): + """Exercises SerialBatchBundleService for serialized items + SLE bundle linkage.""" + item_code = make_item( + "_Test Characterization Serial Item", + {"has_serial_no": 1, "serial_no_series": "CHAR-SER-.#####", "is_stock_item": 1}, + ).name + pr = make_purchase_receipt( + item_code=item_code, + company=COMPANY, + warehouse=WAREHOUSE, + posting_date=POSTING_DATE, + qty=5, + rate=100, + ) + assert_ledger_snapshot(self, "pr_serial_item", "Purchase Receipt", pr.name) + def _make_dated_delivery_note(**args) -> frappe.Document: """Minimal Delivery Note on a fixed posting date using the perpetual-inventory