mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-18 08:58:43 +00:00
test(stock): harden ledger characterization harness
Address code-review findings on the Phase-0 safety net: - Per-test savepoint/rollback isolation so cumulative SLE fields (qty_after_transaction, stock_value, valuation_rate) are deterministic regardless of test order or leftover state (were order-coupled before). - Backdate prerequisite stock to PREREQUISITE_DATE so balances are positive and independent of the wall-clock date. - Capture has_serial_and_batch_bundle (boolean linkage, not the volatile docname) so a dropped serial/batch bundle link is caught. - Add pr_batch_item and pr_serial_item scenarios to exercise SerialBatchBundleService (the largest extraction, previously uncovered). Goldens regenerated. Verified deterministic across repeated assert runs.
This commit is contained in:
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
47
erpnext/stock/ledger_snapshots/pr_batch_item.json
Normal file
47
erpnext/stock/ledger_snapshots/pr_batch_item.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
47
erpnext/stock/ledger_snapshots/pr_serial_item.json
Normal file
47
erpnext/stock/ledger_snapshots/pr_serial_item.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user