mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-31 23:33:43 +00:00
chore(stock): remove ledger characterization scaffolding
Phase 0 golden-master safety net for the stock_controller refactor. It served its purpose (every extraction verified byte-identical GL + Stock Ledger output) and is removed before shipping, mirroring the earlier GL characterization cleanup.
This commit is contained in:
@@ -1,174 +0,0 @@
|
|||||||
"""Golden-master snapshot harness for ledger characterization tests.
|
|
||||||
|
|
||||||
Captures the General Ledger *and* Stock Ledger entries produced by a submitted
|
|
||||||
voucher in a normalized, deterministic form and compares them against a stored
|
|
||||||
golden snapshot. Volatile fields (name, creation, voucher number, serial/batch
|
|
||||||
bundle id) are stripped so the snapshot is stable across runs.
|
|
||||||
|
|
||||||
This is the Phase 0 safety net for the stock_controller refactor: every later
|
|
||||||
phase must keep these snapshots byte-identical. Regenerate goldens with::
|
|
||||||
|
|
||||||
REGEN_LEDGER_SNAPSHOTS=1 bench run-tests --site test-erpnext-v17 \\
|
|
||||||
--module erpnext.stock.test_ledger_characterization
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.utils import flt
|
|
||||||
|
|
||||||
SNAPSHOT_DIR = Path(__file__).parent / "ledger_snapshots"
|
|
||||||
REGEN_ENV = "REGEN_LEDGER_SNAPSHOTS"
|
|
||||||
GL_PRECISION = 2
|
|
||||||
QTY_PRECISION = 6
|
|
||||||
RATE_PRECISION = 4
|
|
||||||
|
|
||||||
|
|
||||||
class GLSnapshot:
|
|
||||||
"""Normalized, order-stable view of a voucher's GL entries."""
|
|
||||||
|
|
||||||
def __init__(self, voucher_type: str, voucher_no: str) -> None:
|
|
||||||
self.voucher_type = voucher_type
|
|
||||||
self.voucher_no = voucher_no
|
|
||||||
|
|
||||||
def capture(self) -> list[dict]:
|
|
||||||
rows = [self._normalize(row) for row in self._fetch_rows()]
|
|
||||||
# Sort on the full normalized row so ordering never depends on the DB's
|
|
||||||
# return order.
|
|
||||||
return sorted(rows, key=lambda row: json.dumps(row, sort_keys=True))
|
|
||||||
|
|
||||||
def _fetch_rows(self) -> list[dict]:
|
|
||||||
gl = frappe.qb.DocType("GL Entry")
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(gl)
|
|
||||||
.select(
|
|
||||||
gl.account,
|
|
||||||
gl.party_type,
|
|
||||||
gl.party,
|
|
||||||
gl.debit,
|
|
||||||
gl.credit,
|
|
||||||
gl.debit_in_account_currency,
|
|
||||||
gl.credit_in_account_currency,
|
|
||||||
gl.account_currency,
|
|
||||||
gl.against,
|
|
||||||
gl.cost_center,
|
|
||||||
gl.is_opening,
|
|
||||||
gl.posting_date,
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
(gl.voucher_type == self.voucher_type)
|
|
||||||
& (gl.voucher_no == self.voucher_no)
|
|
||||||
& (gl.is_cancelled == 0)
|
|
||||||
)
|
|
||||||
.orderby(gl.account, gl.party, gl.debit, gl.credit)
|
|
||||||
)
|
|
||||||
return query.run(as_dict=True)
|
|
||||||
|
|
||||||
def _normalize(self, row: dict) -> dict:
|
|
||||||
return {
|
|
||||||
"account": row.account,
|
|
||||||
"party_type": row.party_type or None,
|
|
||||||
"party": row.party or None,
|
|
||||||
"debit": flt(row.debit, GL_PRECISION),
|
|
||||||
"credit": flt(row.credit, GL_PRECISION),
|
|
||||||
"debit_in_account_currency": flt(row.debit_in_account_currency, GL_PRECISION),
|
|
||||||
"credit_in_account_currency": flt(row.credit_in_account_currency, GL_PRECISION),
|
|
||||||
"account_currency": row.account_currency,
|
|
||||||
"against": self._normalize_against(row.against),
|
|
||||||
"cost_center": row.cost_center,
|
|
||||||
"is_opening": row.is_opening,
|
|
||||||
"posting_date": str(row.posting_date),
|
|
||||||
}
|
|
||||||
|
|
||||||
def _normalize_against(self, against: str | None) -> str | None:
|
|
||||||
"""`against` is a comma-joined account list whose order is not stable."""
|
|
||||||
if not against:
|
|
||||||
return None
|
|
||||||
return ", ".join(sorted(part.strip() for part in against.split(",")))
|
|
||||||
|
|
||||||
|
|
||||||
class SLSnapshot:
|
|
||||||
"""Normalized, order-stable view of a voucher's Stock Ledger entries."""
|
|
||||||
|
|
||||||
def __init__(self, voucher_type: str, voucher_no: str) -> None:
|
|
||||||
self.voucher_type = voucher_type
|
|
||||||
self.voucher_no = voucher_no
|
|
||||||
|
|
||||||
def capture(self) -> list[dict]:
|
|
||||||
rows = [self._normalize(row) for row in self._fetch_rows()]
|
|
||||||
return sorted(rows, key=lambda row: json.dumps(row, sort_keys=True))
|
|
||||||
|
|
||||||
def _fetch_rows(self) -> list[dict]:
|
|
||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(sle)
|
|
||||||
.select(
|
|
||||||
sle.item_code,
|
|
||||||
sle.warehouse,
|
|
||||||
sle.stock_uom,
|
|
||||||
sle.actual_qty,
|
|
||||||
sle.qty_after_transaction,
|
|
||||||
sle.incoming_rate,
|
|
||||||
sle.valuation_rate,
|
|
||||||
sle.stock_value,
|
|
||||||
sle.stock_value_difference,
|
|
||||||
sle.serial_and_batch_bundle,
|
|
||||||
sle.posting_date,
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
(sle.voucher_type == self.voucher_type)
|
|
||||||
& (sle.voucher_no == self.voucher_no)
|
|
||||||
& (sle.is_cancelled == 0)
|
|
||||||
)
|
|
||||||
.orderby(sle.item_code, sle.warehouse, sle.actual_qty)
|
|
||||||
)
|
|
||||||
return query.run(as_dict=True)
|
|
||||||
|
|
||||||
def _normalize(self, row: dict) -> dict:
|
|
||||||
return {
|
|
||||||
"item_code": row.item_code,
|
|
||||||
"warehouse": row.warehouse,
|
|
||||||
"stock_uom": row.stock_uom,
|
|
||||||
"actual_qty": flt(row.actual_qty, QTY_PRECISION),
|
|
||||||
"qty_after_transaction": flt(row.qty_after_transaction, QTY_PRECISION),
|
|
||||||
"incoming_rate": flt(row.incoming_rate, RATE_PRECISION),
|
|
||||||
"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),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def capture_ledger_snapshot(voucher_type: str, voucher_no: str) -> dict:
|
|
||||||
"""Combined GL + SLE snapshot for a single voucher."""
|
|
||||||
return {
|
|
||||||
"gl": GLSnapshot(voucher_type, voucher_no).capture(),
|
|
||||||
"sle": SLSnapshot(voucher_type, voucher_no).capture(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def assert_ledger_snapshot(test_case, name: str, voucher_type: str, voucher_no: str) -> None:
|
|
||||||
"""Compare a voucher's GL + SLE entries against the golden snapshot ``name``.
|
|
||||||
|
|
||||||
In regen mode (``REGEN_LEDGER_SNAPSHOTS`` set) the golden file is written
|
|
||||||
instead of asserted, so the same scenarios both produce and verify the goldens.
|
|
||||||
"""
|
|
||||||
actual = capture_ledger_snapshot(voucher_type, voucher_no)
|
|
||||||
path = SNAPSHOT_DIR / f"{name}.json"
|
|
||||||
|
|
||||||
if os.environ.get(REGEN_ENV):
|
|
||||||
SNAPSHOT_DIR.mkdir(exist_ok=True)
|
|
||||||
path.write_text(json.dumps(actual, indent="\t", sort_keys=True) + "\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
test_case.assertTrue(
|
|
||||||
path.exists(),
|
|
||||||
f"Golden snapshot {path} missing. Run with {REGEN_ENV}=1 to create it.",
|
|
||||||
)
|
|
||||||
expected = json.loads(path.read_text())
|
|
||||||
test_case.assertEqual(expected, actual, f"Ledger snapshot mismatch for '{name}'")
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"gl": [
|
|
||||||
{
|
|
||||||
"account": "Stock Delivered But Not Billed - TCP1",
|
|
||||||
"account_currency": "INR",
|
|
||||||
"against": "Stock In Hand - 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 In Hand - TCP1",
|
|
||||||
"account_currency": "INR",
|
|
||||||
"against": "Stock Delivered But Not Billed - 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": false,
|
|
||||||
"incoming_rate": 0.0,
|
|
||||||
"item_code": "_Test Item",
|
|
||||||
"posting_date": "2024-01-15",
|
|
||||||
"qty_after_transaction": 5.0,
|
|
||||||
"stock_uom": "_Test UOM",
|
|
||||||
"stock_value": 500.0,
|
|
||||||
"stock_value_difference": -500.0,
|
|
||||||
"valuation_rate": 100.0,
|
|
||||||
"warehouse": "Stores - TCP1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"gl": [
|
|
||||||
{
|
|
||||||
"account": "Stock Delivered 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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"account": "Stock In Hand - TCP1",
|
|
||||||
"account_currency": "INR",
|
|
||||||
"against": "Stock Delivered 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"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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": 10.0,
|
|
||||||
"stock_uom": "_Test UOM",
|
|
||||||
"stock_value": 1000.0,
|
|
||||||
"stock_value_difference": 500.0,
|
|
||||||
"valuation_rate": 100.0,
|
|
||||||
"warehouse": "Stores - TCP1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"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": false,
|
|
||||||
"incoming_rate": 100.0,
|
|
||||||
"item_code": "_Test Item",
|
|
||||||
"posting_date": "2024-01-15",
|
|
||||||
"qty_after_transaction": 5.0,
|
|
||||||
"stock_uom": "_Test UOM",
|
|
||||||
"stock_value": 500.0,
|
|
||||||
"stock_value_difference": 500.0,
|
|
||||||
"valuation_rate": 100.0,
|
|
||||||
"warehouse": "Stores - TCP1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"gl": [
|
|
||||||
{
|
|
||||||
"account": "Stock In Hand - TCP1",
|
|
||||||
"account_currency": "INR",
|
|
||||||
"against": "Stock Received But Not Billed - 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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"account": "Stock Received But Not Billed - TCP1",
|
|
||||||
"account_currency": "INR",
|
|
||||||
"against": "Stock In Hand - 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"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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,
|
|
||||||
"stock_uom": "_Test UOM",
|
|
||||||
"stock_value": 0.0,
|
|
||||||
"stock_value_difference": -500.0,
|
|
||||||
"valuation_rate": 100.0,
|
|
||||||
"warehouse": "Stores - TCP1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
{
|
|
||||||
"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": 750.0,
|
|
||||||
"debit_in_account_currency": 750.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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"account": "_Test Account Customs Duty - TCP1",
|
|
||||||
"account_currency": "INR",
|
|
||||||
"against": "Stock In Hand - TCP1",
|
|
||||||
"cost_center": "Main - TCP1",
|
|
||||||
"credit": 150.0,
|
|
||||||
"credit_in_account_currency": 150.0,
|
|
||||||
"debit": 0.0,
|
|
||||||
"debit_in_account_currency": 0.0,
|
|
||||||
"is_opening": "No",
|
|
||||||
"party": null,
|
|
||||||
"party_type": null,
|
|
||||||
"posting_date": "2024-01-15"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"account": "_Test Account Shipping Charges - TCP1",
|
|
||||||
"account_currency": "INR",
|
|
||||||
"against": "Stock In Hand - TCP1",
|
|
||||||
"cost_center": "Main - TCP1",
|
|
||||||
"credit": 100.0,
|
|
||||||
"credit_in_account_currency": 100.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": false,
|
|
||||||
"incoming_rate": 150.0,
|
|
||||||
"item_code": "_Test Item",
|
|
||||||
"posting_date": "2024-01-15",
|
|
||||||
"qty_after_transaction": 5.0,
|
|
||||||
"stock_uom": "_Test UOM",
|
|
||||||
"stock_value": 750.0,
|
|
||||||
"stock_value_difference": 750.0,
|
|
||||||
"valuation_rate": 150.0,
|
|
||||||
"warehouse": "Stores - TCP1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"gl": [
|
|
||||||
{
|
|
||||||
"account": "Stock Adjustment - TCP1",
|
|
||||||
"account_currency": "INR",
|
|
||||||
"against": "Stock In Hand - 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 In Hand - TCP1",
|
|
||||||
"account_currency": "INR",
|
|
||||||
"against": "Stock Adjustment - 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": false,
|
|
||||||
"incoming_rate": 0.0,
|
|
||||||
"item_code": "_Test Item",
|
|
||||||
"posting_date": "2024-01-15",
|
|
||||||
"qty_after_transaction": 5.0,
|
|
||||||
"stock_uom": "_Test UOM",
|
|
||||||
"stock_value": 500.0,
|
|
||||||
"stock_value_difference": -500.0,
|
|
||||||
"valuation_rate": 100.0,
|
|
||||||
"warehouse": "Stores - TCP1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"gl": [
|
|
||||||
{
|
|
||||||
"account": "Stock Adjustment - 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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"account": "Stock In Hand - TCP1",
|
|
||||||
"account_currency": "INR",
|
|
||||||
"against": "Stock Adjustment - 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"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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,
|
|
||||||
"stock_uom": "_Test UOM",
|
|
||||||
"stock_value": 500.0,
|
|
||||||
"stock_value_difference": 500.0,
|
|
||||||
"valuation_rate": 100.0,
|
|
||||||
"warehouse": "Stores - TCP1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"gl": [],
|
|
||||||
"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,
|
|
||||||
"stock_uom": "_Test UOM",
|
|
||||||
"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",
|
|
||||||
"qty_after_transaction": 5.0,
|
|
||||||
"stock_uom": "_Test UOM",
|
|
||||||
"stock_value": 500.0,
|
|
||||||
"stock_value_difference": 500.0,
|
|
||||||
"valuation_rate": 100.0,
|
|
||||||
"warehouse": "Finished Goods - TCP1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"gl": [
|
|
||||||
{
|
|
||||||
"account": "Stock Adjustment - TCP1",
|
|
||||||
"account_currency": "INR",
|
|
||||||
"against": "Stock In Hand - TCP1",
|
|
||||||
"cost_center": "Main - TCP1",
|
|
||||||
"credit": 1500.0,
|
|
||||||
"credit_in_account_currency": 1500.0,
|
|
||||||
"debit": 0.0,
|
|
||||||
"debit_in_account_currency": 0.0,
|
|
||||||
"is_opening": "No",
|
|
||||||
"party": null,
|
|
||||||
"party_type": null,
|
|
||||||
"posting_date": "2024-01-15"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"account": "Stock In Hand - TCP1",
|
|
||||||
"account_currency": "INR",
|
|
||||||
"against": "Stock Adjustment - TCP1",
|
|
||||||
"cost_center": "Main - TCP1",
|
|
||||||
"credit": 0.0,
|
|
||||||
"credit_in_account_currency": 0.0,
|
|
||||||
"debit": 1500.0,
|
|
||||||
"debit_in_account_currency": 1500.0,
|
|
||||||
"is_opening": "No",
|
|
||||||
"party": null,
|
|
||||||
"party_type": null,
|
|
||||||
"posting_date": "2024-01-15"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sle": [
|
|
||||||
{
|
|
||||||
"actual_qty": 0.0,
|
|
||||||
"has_serial_and_batch_bundle": false,
|
|
||||||
"incoming_rate": 0.0,
|
|
||||||
"item_code": "_Test Item",
|
|
||||||
"posting_date": "2024-01-15",
|
|
||||||
"qty_after_transaction": 10.0,
|
|
||||||
"stock_uom": "_Test UOM",
|
|
||||||
"stock_value": 1500.0,
|
|
||||||
"stock_value_difference": 1500.0,
|
|
||||||
"valuation_rate": 150.0,
|
|
||||||
"warehouse": "Stores - TCP1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
"""Phase 0 characterization tests for the stock_controller refactor.
|
|
||||||
|
|
||||||
These are golden-master snapshot tests: each scenario builds a representative
|
|
||||||
stock voucher, submits it, and compares its GL *and* Stock Ledger entries against
|
|
||||||
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 \\
|
|
||||||
--module erpnext.stock.test_ledger_characterization
|
|
||||||
"""
|
|
||||||
|
|
||||||
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, 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, posting_date=PREREQUISITE_DATE
|
|
||||||
)
|
|
||||||
original = _make_dated_delivery_note(qty=5, rate=150)
|
|
||||||
original.insert()
|
|
||||||
original.submit()
|
|
||||||
|
|
||||||
ret = frappe.copy_doc(original)
|
|
||||||
ret.is_return = 1
|
|
||||||
ret.return_against = original.name
|
|
||||||
for item in ret.items:
|
|
||||||
item.qty = -item.qty
|
|
||||||
ret.set_posting_time = 1
|
|
||||||
ret.posting_date = POSTING_DATE
|
|
||||||
ret.insert()
|
|
||||||
ret.submit()
|
|
||||||
assert_ledger_snapshot(self, "dn_return", "Delivery Note", ret.name)
|
|
||||||
|
|
||||||
def test_se_material_receipt(self):
|
|
||||||
se = make_stock_entry(
|
|
||||||
item_code="_Test Item",
|
|
||||||
target=WAREHOUSE,
|
|
||||||
qty=5,
|
|
||||||
basic_rate=100,
|
|
||||||
company=COMPANY,
|
|
||||||
posting_date=POSTING_DATE,
|
|
||||||
do_not_submit=True,
|
|
||||||
)
|
|
||||||
se.submit()
|
|
||||||
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,
|
|
||||||
posting_date=PREREQUISITE_DATE,
|
|
||||||
)
|
|
||||||
se = make_stock_entry(
|
|
||||||
item_code="_Test Item",
|
|
||||||
source=WAREHOUSE,
|
|
||||||
qty=5,
|
|
||||||
company=COMPANY,
|
|
||||||
posting_date=POSTING_DATE,
|
|
||||||
do_not_submit=True,
|
|
||||||
)
|
|
||||||
se.submit()
|
|
||||||
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,
|
|
||||||
posting_date=PREREQUISITE_DATE,
|
|
||||||
)
|
|
||||||
se = make_stock_entry(
|
|
||||||
item_code="_Test Item",
|
|
||||||
source=WAREHOUSE,
|
|
||||||
target="Finished Goods - TCP1",
|
|
||||||
qty=5,
|
|
||||||
company=COMPANY,
|
|
||||||
posting_date=POSTING_DATE,
|
|
||||||
do_not_submit=True,
|
|
||||||
)
|
|
||||||
se.submit()
|
|
||||||
assert_ledger_snapshot(self, "se_material_transfer", "Stock Entry", se.name)
|
|
||||||
|
|
||||||
def test_sr_basic(self):
|
|
||||||
sr = _make_dated_stock_reconciliation(qty=10, rate=150)
|
|
||||||
sr.insert()
|
|
||||||
sr.submit()
|
|
||||||
assert_ledger_snapshot(self, "sr_basic", "Stock Reconciliation", sr.name)
|
|
||||||
|
|
||||||
def test_pr_basic(self):
|
|
||||||
pr = make_purchase_receipt(
|
|
||||||
company=COMPANY, warehouse=WAREHOUSE, posting_date=POSTING_DATE, qty=5, rate=100
|
|
||||||
)
|
|
||||||
assert_ledger_snapshot(self, "pr_basic", "Purchase Receipt", pr.name)
|
|
||||||
|
|
||||||
def test_pr_with_taxes(self):
|
|
||||||
pr = make_purchase_receipt(
|
|
||||||
company=COMPANY,
|
|
||||||
warehouse=WAREHOUSE,
|
|
||||||
posting_date=POSTING_DATE,
|
|
||||||
qty=5,
|
|
||||||
rate=100,
|
|
||||||
get_taxes_and_charges=True,
|
|
||||||
)
|
|
||||||
assert_ledger_snapshot(self, "pr_with_taxes", "Purchase Receipt", pr.name)
|
|
||||||
|
|
||||||
def test_pr_return(self):
|
|
||||||
from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_return
|
|
||||||
|
|
||||||
original = make_purchase_receipt(
|
|
||||||
company=COMPANY, warehouse=WAREHOUSE, posting_date=POSTING_DATE, qty=5, rate=100
|
|
||||||
)
|
|
||||||
ret = make_purchase_return(original.name)
|
|
||||||
ret.posting_date = POSTING_DATE
|
|
||||||
ret.set_posting_time = 1
|
|
||||||
ret.insert()
|
|
||||||
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
|
|
||||||
test company.
|
|
||||||
|
|
||||||
Inlined to avoid importing test_delivery_note which drags in conflicting
|
|
||||||
test-record dependencies at discovery time."""
|
|
||||||
dn = frappe.new_doc("Delivery Note")
|
|
||||||
dn.company = COMPANY
|
|
||||||
dn.customer = CUSTOMER
|
|
||||||
dn.posting_date = POSTING_DATE
|
|
||||||
dn.set_posting_time = 1
|
|
||||||
dn.append(
|
|
||||||
"items",
|
|
||||||
{
|
|
||||||
"item_code": args.get("item_code", "_Test Item"),
|
|
||||||
"warehouse": args.get("warehouse", WAREHOUSE),
|
|
||||||
"qty": args.get("qty", 1),
|
|
||||||
"rate": args.get("rate", 100),
|
|
||||||
"expense_account": "Cost of Goods Sold - TCP1",
|
|
||||||
"cost_center": "Main - TCP1",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return dn
|
|
||||||
|
|
||||||
|
|
||||||
def _make_dated_stock_reconciliation(**args) -> frappe.Document:
|
|
||||||
"""Minimal Stock Reconciliation on a fixed posting date using the perpetual-inventory
|
|
||||||
test company.
|
|
||||||
|
|
||||||
Inlined to avoid importing test_stock_reconciliation which drags in conflicting
|
|
||||||
test-record dependencies at discovery time."""
|
|
||||||
sr = frappe.new_doc("Stock Reconciliation")
|
|
||||||
sr.company = COMPANY
|
|
||||||
sr.purpose = args.get("purpose", "Stock Reconciliation")
|
|
||||||
sr.posting_date = POSTING_DATE
|
|
||||||
sr.posting_time = "00:00:00"
|
|
||||||
sr.set_posting_time = 1
|
|
||||||
sr.expense_account = frappe.get_cached_value("Company", COMPANY, "stock_adjustment_account")
|
|
||||||
sr.cost_center = frappe.get_cached_value("Company", COMPANY, "cost_center")
|
|
||||||
sr.append(
|
|
||||||
"items",
|
|
||||||
{
|
|
||||||
"item_code": args.get("item_code", "_Test Item"),
|
|
||||||
"warehouse": args.get("warehouse", WAREHOUSE),
|
|
||||||
"qty": args.get("qty", 10),
|
|
||||||
"valuation_rate": args.get("rate", 100),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return sr
|
|
||||||
Reference in New Issue
Block a user