chore(accounts): remove GL characterization scaffolding

The golden-master snapshots, capture harness, characterization test, and
refactor spec existed to prove the accounts refactor preserved GL output.
All 29 characterization tests pass against the merged code, so the
scaffolding has served its purpose and is removed before merge.

Removes:
- erpnext/accounts/gl_snapshot.py
- erpnext/accounts/gl_snapshots/ (29 snapshots)
- erpnext/accounts/test_gl_characterization.py
- specs/accounts_refactor_spec.md
This commit is contained in:
Nabin Hait
2026-06-03 16:43:33 +05:30
parent 4c6f33000b
commit c271826130
32 changed files with 0 additions and 1796 deletions

View File

@@ -1,110 +0,0 @@
"""Golden-master snapshot harness for GL Entry characterization tests.
Captures the General 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) are stripped so the
snapshot is stable across runs.
This is the Phase 0 safety net for the accounts/controller refactor: every
later phase must keep these snapshots byte-identical. Regenerate goldens with::
REGEN_GL_SNAPSHOTS=1 bench run-tests --site test-site-ai \\
--module erpnext.accounts.test_gl_characterization
"""
import json
import os
from pathlib import Path
import frappe
from frappe.utils import flt
SNAPSHOT_DIR = Path(__file__).parent / "gl_snapshots"
REGEN_ENV = "REGEN_GL_SNAPSHOTS"
PRECISION = 2
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 — e.g. two POS payment legs that tie on account/party/amount
# but differ only in `against`.
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, PRECISION),
"credit": flt(row.credit, PRECISION),
"debit_in_account_currency": flt(row.debit_in_account_currency, PRECISION),
"credit_in_account_currency": flt(row.credit_in_account_currency, 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(",")))
def assert_gl_snapshot(test_case, name: str, voucher_type: str, voucher_no: str) -> None:
"""Compare a voucher's GL entries against the golden snapshot ``name``.
In regen mode (``REGEN_GL_SNAPSHOTS`` set) the golden file is written instead
of asserted, so the same scenarios both produce and verify the goldens.
"""
actual = GLSnapshot(voucher_type, voucher_no).capture()
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"GL snapshot mismatch for '{name}'")

View File

@@ -1,30 +0,0 @@
[
{
"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"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"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"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "Write Off - _TC",
"cost_center": "_Test Cost Center - _TC",
"credit": 1000.0,
"credit_in_account_currency": 1000.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Write Off - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"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"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "_Test Bank - _TC",
"account_currency": "INR",
"against": "_Test Cash - _TC",
"cost_center": "_Test Cost Center - _TC",
"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"
},
{
"account": "_Test Cash - _TC",
"account_currency": "INR",
"against": "_Test Bank - _TC",
"cost_center": "_Test Cost Center - _TC",
"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"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "_Test Bank - _TC",
"account_currency": "INR",
"against": "_Test Bank USD - _TC",
"cost_center": "_Test Cost Center - _TC",
"credit": 7500.0,
"credit_in_account_currency": 7500.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 Bank USD - _TC",
"account_currency": "USD",
"against": "_Test Bank - _TC",
"cost_center": "_Test Cost Center - _TC",
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 7500.0,
"debit_in_account_currency": 100.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "_Test Bank - _TC",
"account_currency": "INR",
"against": "_Test Supplier USD",
"cost_center": null,
"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"
},
{
"account": "_Test Payable USD - _TC",
"account_currency": "USD",
"against": "_Test Bank - _TC",
"cost_center": null,
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 1000.0,
"debit_in_account_currency": 12.5,
"is_opening": "No",
"party": "_Test Supplier USD",
"party_type": "Supplier",
"posting_date": "2024-01-15"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "Creditors - _TC",
"account_currency": "INR",
"against": "_Test Bank - _TC",
"cost_center": null,
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 250.0,
"debit_in_account_currency": 250.0,
"is_opening": "No",
"party": "_Test Supplier",
"party_type": "Supplier",
"posting_date": "2024-01-15"
},
{
"account": "_Test Bank - _TC",
"account_currency": "INR",
"against": "_Test Supplier",
"cost_center": null,
"credit": 250.0,
"credit_in_account_currency": 250.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "_Test Cash - _TC",
"cost_center": null,
"credit": 1000.0,
"credit_in_account_currency": 1000.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "_Test Cash - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": null,
"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"
}
]

View File

@@ -1,58 +0,0 @@
[
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "_Test Cash - _TC",
"cost_center": null,
"credit": 1000.0,
"credit_in_account_currency": 1000.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "_Test Cash - _TC",
"cost_center": null,
"credit": 50.0,
"credit_in_account_currency": 50.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Write Off - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 50.0,
"debit_in_account_currency": 50.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
},
{
"account": "_Test Cash - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": null,
"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"
}
]

View File

@@ -1,44 +0,0 @@
[
{
"account": "Creditors - _TC",
"account_currency": "INR",
"against": "_Test Bank - _TC",
"cost_center": null,
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 1000.0,
"debit_in_account_currency": 1000.0,
"is_opening": "No",
"party": "_Test Supplier",
"party_type": "Supplier",
"posting_date": "2024-01-15"
},
{
"account": "_Test Account Service Tax - _TC",
"account_currency": "INR",
"against": "_Test Supplier",
"cost_center": "_Test Cost Center - _TC",
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 100.0,
"debit_in_account_currency": 100.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
},
{
"account": "_Test Bank - _TC",
"account_currency": "INR",
"against": "_Test Supplier",
"cost_center": null,
"credit": 1100.0,
"credit_in_account_currency": 1100.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "Creditors - _TC",
"account_currency": "INR",
"against": "_Test Account Cost for Goods Sold - _TC",
"cost_center": null,
"credit": 250.0,
"credit_in_account_currency": 250.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": "_Test Supplier",
"party_type": "Supplier",
"posting_date": "2024-01-15"
},
{
"account": "_Test Account Cost for Goods Sold - _TC",
"account_currency": "INR",
"against": "_Test Supplier",
"cost_center": "_Test Cost Center - _TC",
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 250.0,
"debit_in_account_currency": 250.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "Creditors - _TC",
"account_currency": "INR",
"against": "_Test Account Cost for Goods Sold - _TC",
"cost_center": null,
"credit": 18750.0,
"credit_in_account_currency": 18750.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": "_Test Supplier",
"party_type": "Supplier",
"posting_date": "2024-01-15"
},
{
"account": "_Test Account Cost for Goods Sold - _TC",
"account_currency": "INR",
"against": "_Test Supplier",
"cost_center": "_Test Cost Center - _TC",
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 18750.0,
"debit_in_account_currency": 18750.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "Creditors - _TC",
"account_currency": "INR",
"against": "_Test Account Cost for Goods Sold - _TC",
"cost_center": null,
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 250.0,
"debit_in_account_currency": 250.0,
"is_opening": "No",
"party": "_Test Supplier",
"party_type": "Supplier",
"posting_date": "2024-01-15"
},
{
"account": "_Test Account Cost for Goods Sold - _TC",
"account_currency": "INR",
"against": "_Test Supplier",
"cost_center": "_Test Cost Center - _TC",
"credit": 250.0,
"credit_in_account_currency": 250.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
}
]

View File

@@ -1,58 +0,0 @@
[
{
"account": "Creditors - _TC",
"account_currency": "INR",
"against": "_Test Account Cost for Goods Sold - _TC",
"cost_center": null,
"credit": 288.0,
"credit_in_account_currency": 288.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": "_Test Supplier",
"party_type": "Supplier",
"posting_date": "2024-01-15"
},
{
"account": "Round Off - _TC",
"account_currency": "INR",
"against": "_Test Supplier",
"cost_center": "Main - _TC",
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 0.5,
"debit_in_account_currency": 0.5,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
},
{
"account": "_Test Account Cost for Goods Sold - _TC",
"account_currency": "INR",
"against": "_Test Supplier",
"cost_center": "_Test Cost Center - _TC",
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 250.0,
"debit_in_account_currency": 250.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
},
{
"account": "_Test Account VAT - _TC",
"account_currency": "INR",
"against": "_Test Supplier",
"cost_center": "_Test Cost Center - _TC",
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 37.5,
"debit_in_account_currency": 37.5,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"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"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"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"
}
]

View File

@@ -1,58 +0,0 @@
[
{
"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"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"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": 750.0,
"debit_in_account_currency": 750.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": 750.0,
"credit_in_account_currency": 750.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"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"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "Sales - _TC",
"cost_center": null,
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 1000.0,
"debit_in_account_currency": 1000.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Sales - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"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"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "Sales - _TC",
"cost_center": null,
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 75000.0,
"debit_in_account_currency": 75000.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Sales - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"credit": 75000.0,
"credit_in_account_currency": 75000.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
}
]

View File

@@ -1,86 +0,0 @@
[
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "Sales - _TC",
"cost_center": null,
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 1000.0,
"debit_in_account_currency": 1000.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "_Test Bank - _TC",
"cost_center": null,
"credit": 500.0,
"credit_in_account_currency": 500.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "_Test Cash - _TC",
"cost_center": null,
"credit": 500.0,
"credit_in_account_currency": 500.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Sales - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"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"
},
{
"account": "_Test Bank - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": null,
"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": "_Test Cash - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": null,
"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"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "Sales - _TC",
"cost_center": null,
"credit": 1000.0,
"credit_in_account_currency": 1000.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Sales - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"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"
}
]

View File

@@ -1,58 +0,0 @@
[
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "Sales - _TC",
"cost_center": null,
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 106.0,
"debit_in_account_currency": 106.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Round Off - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "Main - _TC",
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 0.5,
"debit_in_account_currency": 0.5,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
},
{
"account": "Sales - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"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"
},
{
"account": "_Test Account Service Tax - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"credit": 6.5,
"credit_in_account_currency": 6.5,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "Sales - _TC",
"cost_center": null,
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 1000.0,
"debit_in_account_currency": 1000.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Sales - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"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"
}
]

View File

@@ -1,44 +0,0 @@
[
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "Sales - _TC",
"cost_center": null,
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 90.0,
"debit_in_account_currency": 90.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Discount Account - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 10.0,
"debit_in_account_currency": 10.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
},
{
"account": "Sales - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"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"
}
]

View File

@@ -1,44 +0,0 @@
[
{
"account": "Debtors - _TC",
"account_currency": "INR",
"against": "Sales - _TC",
"cost_center": null,
"credit": 0.0,
"credit_in_account_currency": 0.0,
"debit": 1140.0,
"debit_in_account_currency": 1140.0,
"is_opening": "No",
"party": "_Test Customer",
"party_type": "Customer",
"posting_date": "2024-01-15"
},
{
"account": "Sales - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"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"
},
{
"account": "_Test Account Service Tax - _TC",
"account_currency": "INR",
"against": "_Test Customer",
"cost_center": "_Test Cost Center - _TC",
"credit": 140.0,
"credit_in_account_currency": 140.0,
"debit": 0.0,
"debit_in_account_currency": 0.0,
"is_opening": "No",
"party": null,
"party_type": null,
"posting_date": "2024-01-15"
}
]

View File

@@ -1,30 +0,0 @@
[
{
"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"
}
]

View File

@@ -1,532 +0,0 @@
"""Phase 0 characterization tests for the accounts/controller refactor.
These are golden-master snapshot tests: each scenario builds a representative
voucher, submits it, and compares its GL entries against a stored snapshot
(see ``erpnext/accounts/gl_snapshots``). They assert nothing about *correct*
accounting — only that GL output stays byte-identical as the GL pipeline is
refactored into composer / validator / sink services.
Regenerate goldens after an intentional change::
REGEN_GL_SNAPSHOTS=1 bench run-tests --site test-erpnext-v17 \\
--module erpnext.accounts.test_gl_characterization
"""
import frappe
from frappe.tests import IntegrationTestCase
from frappe.tests.classes.context_managers import change_settings
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
set_default_account_for_mode_of_payment,
)
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.mapper import make_debit_note
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.mapper import make_sales_return
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.gl_snapshot import assert_gl_snapshot
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
POSTING_DATE = "2024-01-15"
COMPANY = "_Test Company"
CUSTOMER = "_Test Customer"
WAREHOUSE = "_Test Warehouse - _TC"
DN_COMPANY = "_Test Company with perpetual inventory"
DN_WAREHOUSE = "Stores - TCP1"
def make_dated_purchase_invoice(**args):
"""make_purchase_invoice ignores posting_date unless set_posting_time is on,
which would make snapshots depend on the run date. Force the backdated time."""
pi = make_purchase_invoice(do_not_save=True, **args)
pi.set_posting_time = 1
pi.posting_date = POSTING_DATE
return pi
def make_dated_payment_entry(**args):
"""Standalone Payment Entry (no invoice reference) on a fixed posting date.
Mirrors test_payment_entry.create_payment_entry without importing that test
module, whose import drags in test-record dependencies that conflict during
discovery."""
pe = frappe.new_doc("Payment Entry")
pe.company = COMPANY
pe.payment_type = args.get("payment_type") or "Pay"
pe.party_type = args.get("party_type") or "Supplier"
pe.party = args.get("party") or "_Test Supplier"
pe.paid_from = args.get("paid_from") or "_Test Bank - _TC"
pe.paid_to = args.get("paid_to") or "Creditors - _TC"
pe.paid_amount = args.get("paid_amount") or 1000
pe.setup_party_account_field()
pe.set_missing_values()
pe.set_exchange_rate()
pe.received_amount = pe.paid_amount / pe.target_exchange_rate
pe.reference_no = "Test001"
pe.posting_date = POSTING_DATE
pe.reference_date = POSTING_DATE
return pe
def make_dated_journal_entry(accounts, multi_currency=0):
"""Journal Entry on a fixed posting date built from explicit account rows.
Inlined rather than importing test_journal_entry.make_journal_entry, whose
import drags in test-record dependencies that conflict during discovery."""
jv = frappe.new_doc("Journal Entry")
jv.posting_date = POSTING_DATE
jv.company = COMPANY
jv.remark = "test"
jv.multi_currency = multi_currency
jv.set("accounts", accounts)
return jv
class TestGLCharacterization(IntegrationTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
for mode, account in (("Cash", "_Test Cash - _TC"), ("Bank Draft", "_Test Bank - _TC")):
set_default_account_for_mode_of_payment(frappe.get_doc("Mode of Payment", mode), COMPANY, account)
def test_si_basic(self):
si = create_sales_invoice(posting_date=POSTING_DATE, qty=10, rate=100)
assert_gl_snapshot(self, "si_basic", "Sales Invoice", si.name)
def test_si_with_taxes(self):
si = create_sales_invoice(posting_date=POSTING_DATE, qty=10, rate=100, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 14,
},
)
si.insert()
si.submit()
assert_gl_snapshot(self, "si_with_taxes", "Sales Invoice", si.name)
def test_si_multi_currency(self):
si = create_sales_invoice(
posting_date=POSTING_DATE, qty=10, rate=100, currency="USD", conversion_rate=75
)
assert_gl_snapshot(self, "si_multi_currency", "Sales Invoice", si.name)
def test_si_return(self):
original = create_sales_invoice(posting_date=POSTING_DATE, qty=10, rate=100)
credit_note = make_sales_return(original.name)
credit_note.set_posting_time = 1
credit_note.posting_date = POSTING_DATE
credit_note.insert()
credit_note.submit()
assert_gl_snapshot(self, "si_return", "Sales Invoice", credit_note.name)
def test_si_round_off(self):
si = create_sales_invoice(posting_date=POSTING_DATE, qty=1, rate=100, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 6.5,
},
)
si.insert()
si.submit()
assert_gl_snapshot(self, "si_round_off", "Sales Invoice", si.name)
def test_si_with_discount_accounting(self):
with change_settings("Selling Settings", {"enable_discount_accounting": 1}):
discount_account = create_account(
account_name="Discount Account",
parent_account="Indirect Expenses - _TC",
company=COMPANY,
)
si = create_sales_invoice(
posting_date=POSTING_DATE, qty=1, rate=90, discount_account=discount_account
)
assert_gl_snapshot(self, "si_with_discount", "Sales Invoice", si.name)
def test_si_with_advance(self):
advance = frappe.get_doc(
{
"doctype": "Payment Entry",
"payment_type": "Receive",
"party_type": "Customer",
"party": CUSTOMER,
"company": COMPANY,
"posting_date": POSTING_DATE,
"paid_from": "Debtors - _TC",
"paid_to": "_Test Cash - _TC",
"paid_from_account_currency": "INR",
"paid_to_account_currency": "INR",
"source_exchange_rate": 1,
"target_exchange_rate": 1,
"reference_no": "ADV-1",
"reference_date": POSTING_DATE,
"paid_amount": 500,
"received_amount": 500,
}
)
advance.insert()
advance.submit()
si = create_sales_invoice(posting_date=POSTING_DATE, qty=10, rate=100, do_not_save=True)
si.allocate_advances_automatically = 1
si.insert()
si.submit()
assert_gl_snapshot(self, "si_with_advance", "Sales Invoice", si.name)
def test_si_pos(self):
si = create_sales_invoice(posting_date=POSTING_DATE, qty=10, rate=100, do_not_save=True)
si.is_pos = 1
si.append("payments", {"mode_of_payment": "Cash", "amount": 500})
si.append("payments", {"mode_of_payment": "Bank Draft", "amount": 500})
si.insert()
si.submit()
assert_gl_snapshot(self, "si_pos", "Sales Invoice", si.name)
def test_pi_basic(self):
pi = make_dated_purchase_invoice(qty=5, rate=50)
pi.insert()
pi.submit()
assert_gl_snapshot(self, "pi_basic", "Purchase Invoice", pi.name)
def test_pi_with_taxes(self):
pi = make_dated_purchase_invoice(qty=5, rate=50)
pi.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"rate": 15,
},
)
pi.insert()
pi.submit()
assert_gl_snapshot(self, "pi_with_taxes", "Purchase Invoice", pi.name)
def test_pi_multi_currency(self):
pi = make_dated_purchase_invoice(qty=5, rate=50, currency="USD", conversion_rate=75)
pi.insert()
pi.submit()
assert_gl_snapshot(self, "pi_multi_currency", "Purchase Invoice", pi.name)
def test_pi_return(self):
original = make_dated_purchase_invoice(qty=5, rate=50)
original.insert()
original.submit()
debit_note = make_debit_note(original.name)
debit_note.set_posting_time = 1
debit_note.posting_date = POSTING_DATE
debit_note.insert()
debit_note.submit()
assert_gl_snapshot(self, "pi_return", "Purchase Invoice", debit_note.name)
def test_pe_receive_against_si(self):
si = create_sales_invoice(posting_date=POSTING_DATE, qty=10, rate=100)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
pe.posting_date = POSTING_DATE
pe.reference_no = "PE-REC-1"
pe.reference_date = POSTING_DATE
pe.insert()
pe.submit()
assert_gl_snapshot(self, "pe_receive_against_si", "Payment Entry", pe.name)
def test_pe_pay_against_pi(self):
pi = make_dated_purchase_invoice(qty=5, rate=50)
pi.insert()
pi.submit()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.posting_date = POSTING_DATE
pe.reference_no = "PE-PAY-1"
pe.reference_date = POSTING_DATE
pe.insert()
pe.submit()
assert_gl_snapshot(self, "pe_pay_against_pi", "Payment Entry", pe.name)
def test_pe_with_deductions(self):
si = create_sales_invoice(posting_date=POSTING_DATE, qty=10, rate=100)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
pe.posting_date = POSTING_DATE
pe.reference_no = "PE-DED-1"
pe.reference_date = POSTING_DATE
pe.received_amount = pe.received_amount - 50
pe.append(
"deductions",
{
"account": "Write Off - _TC",
"cost_center": "_Test Cost Center - _TC",
"amount": 50,
},
)
pe.insert()
pe.submit()
assert_gl_snapshot(self, "pe_with_deductions", "Payment Entry", pe.name)
def test_pe_with_taxes(self):
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 1)
pe = make_dated_payment_entry(party="_Test Supplier", paid_to="Creditors - _TC")
pe.append(
"taxes",
{
"account_head": "_Test Account Service Tax - _TC",
"charge_type": "Actual",
"tax_amount": 100,
"add_deduct_tax": "Add",
"description": "Service Tax",
"cost_center": "_Test Cost Center - _TC",
},
)
pe.save()
pe.submit()
assert_gl_snapshot(self, "pe_with_taxes", "Payment Entry", pe.name)
def test_pe_multi_currency(self):
pe = make_dated_payment_entry(party="_Test Supplier USD", paid_to="_Test Payable USD - _TC")
pe.target_exchange_rate = 80
pe.received_amount = pe.paid_amount / pe.target_exchange_rate
pe.save()
pe.submit()
assert_gl_snapshot(self, "pe_multi_currency", "Payment Entry", pe.name)
def test_je_basic(self):
jv = make_dated_journal_entry(
[
{
"account": "_Test Cash - _TC",
"cost_center": "_Test Cost Center - _TC",
"debit_in_account_currency": 1000,
"exchange_rate": 1,
},
{
"account": "_Test Bank - _TC",
"cost_center": "_Test Cost Center - _TC",
"credit_in_account_currency": 1000,
"exchange_rate": 1,
},
]
)
jv.insert()
jv.submit()
assert_gl_snapshot(self, "je_basic", "Journal Entry", jv.name)
def test_je_multi_currency(self):
jv = make_dated_journal_entry(
[
{
"account": "_Test Bank USD - _TC",
"cost_center": "_Test Cost Center - _TC",
"debit_in_account_currency": 100,
"exchange_rate": 75,
},
{
"account": "_Test Bank - _TC",
"cost_center": "_Test Cost Center - _TC",
"credit_in_account_currency": 7500,
"exchange_rate": 1,
},
],
multi_currency=1,
)
jv.insert()
jv.submit()
assert_gl_snapshot(self, "je_multi_currency", "Journal Entry", jv.name)
def test_je_against_si(self):
si = create_sales_invoice(posting_date=POSTING_DATE, qty=10, rate=100)
jv = make_dated_journal_entry(
[
{
"account": "Write Off - _TC",
"cost_center": "_Test Cost Center - _TC",
"debit_in_account_currency": 1000,
"exchange_rate": 1,
},
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": CUSTOMER,
"cost_center": "_Test Cost Center - _TC",
"credit_in_account_currency": 1000,
"exchange_rate": 1,
"reference_type": "Sales Invoice",
"reference_name": si.name,
},
]
)
jv.insert()
jv.submit()
assert_gl_snapshot(self, "je_against_si", "Journal Entry", jv.name)
def test_dn_basic(self):
make_stock_entry(item_code="_Test Item", target=DN_WAREHOUSE, qty=10, basic_rate=100)
dn = _make_dated_delivery_note(qty=5, rate=150)
dn.insert()
dn.submit()
assert_gl_snapshot(self, "dn_basic", "Delivery Note", dn.name)
def test_dn_return(self):
make_stock_entry(item_code="_Test Item", target=DN_WAREHOUSE, qty=10, basic_rate=100)
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_gl_snapshot(self, "dn_return", "Delivery Note", ret.name)
def test_se_material_receipt(self):
se = make_stock_entry(
item_code="_Test Item",
target=DN_WAREHOUSE,
qty=5,
basic_rate=100,
company=DN_COMPANY,
posting_date=POSTING_DATE,
do_not_submit=True,
)
se.submit()
assert_gl_snapshot(self, "se_material_receipt", "Stock Entry", se.name)
def test_se_material_issue(self):
make_stock_entry(
item_code="_Test Item", target=DN_WAREHOUSE, qty=10, basic_rate=100, company=DN_COMPANY
)
se = make_stock_entry(
item_code="_Test Item",
source=DN_WAREHOUSE,
qty=5,
company=DN_COMPANY,
posting_date=POSTING_DATE,
do_not_submit=True,
)
se.submit()
assert_gl_snapshot(self, "se_material_issue", "Stock Entry", se.name)
def test_se_material_transfer(self):
make_stock_entry(
item_code="_Test Item", target=DN_WAREHOUSE, qty=10, basic_rate=100, company=DN_COMPANY
)
se = make_stock_entry(
item_code="_Test Item",
source=DN_WAREHOUSE,
target="Finished Goods - TCP1",
qty=5,
company=DN_COMPANY,
posting_date=POSTING_DATE,
do_not_submit=True,
)
se.submit()
assert_gl_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_gl_snapshot(self, "sr_basic", "Stock Reconciliation", sr.name)
def test_pr_basic(self):
pr = make_purchase_receipt(
company=DN_COMPANY,
warehouse=DN_WAREHOUSE,
posting_date=POSTING_DATE,
qty=5,
rate=100,
)
assert_gl_snapshot(self, "pr_basic", "Purchase Receipt", pr.name)
def test_pr_with_taxes(self):
pr = make_purchase_receipt(
company=DN_COMPANY,
warehouse=DN_WAREHOUSE,
posting_date=POSTING_DATE,
qty=5,
rate=100,
get_taxes_and_charges=True,
)
assert_gl_snapshot(self, "pr_with_taxes", "Purchase Receipt", pr.name)
def test_pr_return(self):
original = make_purchase_receipt(
company=DN_COMPANY,
warehouse=DN_WAREHOUSE,
posting_date=POSTING_DATE,
qty=5,
rate=100,
)
from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_return
ret = make_purchase_return(original.name)
ret.posting_date = POSTING_DATE
ret.set_posting_time = 1
ret.insert()
ret.submit()
assert_gl_snapshot(self, "pr_return", "Purchase Receipt", ret.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 = DN_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", DN_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 = DN_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", DN_COMPANY, "stock_adjustment_account")
sr.cost_center = frappe.get_cached_value("Company", DN_COMPANY, "cost_center")
sr.append(
"items",
{
"item_code": args.get("item_code", "_Test Item"),
"warehouse": args.get("warehouse", DN_WAREHOUSE),
"qty": args.get("qty", 10),
"valuation_rate": args.get("rate", 100),
},
)
return sr

View File

@@ -1,103 +0,0 @@
# Accounts / Controller Refactor — Spec
## Motivation
Move ERPNext away from the deep `AccountsController → SellingController/BuyingController → SalesInvoice`
inheritance chain and the monolithic `sales_invoice.py` / god-object `accounts_controller.py`
toward **composition**: per-doctype `services/` plus shared module-level `accounts/services/`.
Goal is testability, readability, and factoring shared domain logic out so Sales/Purchase
voucher logic is not duplicated.
## Target structure
```
erpnext
├── controllers
│ └── transaction_controller.py # thin lifecycle base, delegates to services
├── accounts
│ ├── general_ledger.py # the SINK (unchanged): post / merge / round-off / reverse
│ ├── services
│ │ ├── base_gl_composer.py # BaseGLComposer — shared GL helpers
│ │ ├── gl_validator.py # list-level validation (functions, stateless)
│ │ ├── advances.py
│ │ ├── taxes.py
│ │ └── budget.py
│ └── doctype
│ └── sales_invoice
│ ├── sales_invoice.py # thin: delegates to services
│ ├── services
│ │ ├── gl_composer.py # SalesInvoiceGLComposer(BaseGLComposer)
│ │ ├── pos.py
│ │ ├── loyalty.py
│ │ ├── status.py
│ │ ├── inter_company.py
│ │ ├── fixed_assets.py
│ │ └── timesheet_billing.py
│ ├── mapper.py
│ └── api.py
```
## GL layer — frozen design
Pipeline:
```
SalesInvoiceGLComposer.compose() → gl_entries → gl_validator.validate(gl_entries) → general_ledger.make_gl_entries()
```
| Role | Location | Form | Responsibility |
|---|---|---|---|
| **Composer (base)** | `accounts/services/base_gl_composer.py``BaseGLComposer` | class (stateful, holds `self.doc`) | shared row factory + common entries |
| **Composer (doctype)** | `sales_invoice/services/gl_composer.py``SalesInvoiceGLComposer(BaseGLComposer)` | class | voucher-specific rows via `.compose()` |
| **Validator** | `accounts/services/gl_validator.py` | module functions (stateless) | assert the finished `gl_entries` list is legal to post |
| **Sink** | `accounts/general_ledger.py` (unchanged) | module functions | merge / round-off / post / reverse |
### Naming decisions (frozen)
- Chose **`compose`** over `make`/`build` — the sink already owns the verb `make` (`make_gl_entries`); `compose` avoids a two-makers collision.
- `base_` prefix on the shared/abstract file; the concrete subclass carries the specific name, no prefix.
- Rejected: `gl_map` (it's a list, not a map — but it's an entrenched public param; rename to `gl_entries` later as its own deprecation pass), `gl_processor` (redundant with `general_ledger.py`), `gl_entries.py` (collides with the `gl_entry` doctype + the ubiquitous local var), `ledger_builder` (clashes with stock/payment ledger), `builder`/`maker` (generic; "maker" collides with `make_gl_entries`).
## Bucketing `accounts_controller.py`
- **Base composer (`BaseGLComposer`):** `get_gl_dict`, `get_value_in_transaction_currency`, `make_discount_gl_entries` (+ `get_amount_and_base_amount`, `get_tax_amounts`), `make_precision_loss_gl_entry`, `make_exchange_gain_loss_journal` (+ `gain_loss_journal_already_booked`), `set_transaction_currency_and_rate_in_gl_map`. Regional hooks `update_gl_dict_with_regional_fields` / `..._app_based_fields` stay free functions called inside `get_gl_dict`.
- **Advances service:** `set_advances`, `get_advance_entries`, `clear_unallocated_advances`, `validate_advance_entries`, `set_advance_gain_or_loss`, `calculate_total_advance_from_ledger`, `set_total_advance_paid`, `set_advance_payment_status`, `delink_advance_entries`, `create_advance_and_reconcile`, `get_advance_payment_doctypes`, `_remove_advance_payment_ledger_entries`, module funcs `get_advance_journal_entries` / `get_advance_payment_entries`.
- **Validator (from `general_ledger.py`):** `validate_disabled_accounts`, `validate_accounting_period`, `validate_cwip_accounts`, `check_freezing_date`, `validate_against_pcv`, `validate_allowed_dimensions`. (Moved in Phase 1.)
- **Balance trio stays in `general_ledger.py` for now** (revised during Phase 1): `get_debit_credit_difference` / `get_debit_credit_allowance` / `raise_debit_credit_not_equal_error`. `get_debit_credit_difference` *mutates* entries (rounds debit/credit in place) and the trio is interleaved with `process_debit_credit_difference``make_round_off_gle` (the round-off *repair* run before and after balancing). It is not a standalone pre-post gate, so it can't move into a pure `validate(gl_entries)` without changing behavior. It travels with round-off when that moves compose-side (see below).
- **Stays in compose (do NOT move to validator):** `process_debit_credit_difference` / `make_round_off_gle` — these *repair* balance by appending a round-off entry (mutation), not validation.
- **Stays in composer (not validator):** row-level checks (right account for a row, dimension applicability) — validator only validates the finished list.
- **Leave in controller:** `validate_company_in_accounting_dimension`, `validate_company` (dimension validation, not GL).
## Phases
Each phase is behavior-preserving, one draft PR, gated by the Phase-0 snapshot suite + `bench run-tests --site test-site-ai`.
### Phase 0 — Safety net (first, mandatory)
Characterization tests snapshotting `gl_entries` output for representative transactions (SI/PI with taxes, multi-currency, advances, discounts, round-off, POS). Every later phase passes iff snapshots are byte-identical.
### Phase 1 — Extract `gl_validator.py` (lowest risk) — DONE
Moved the 6 pure list-level validators to `erpnext/accounts/services/gl_validator.py`; `general_ledger.py` imports and calls them at the existing call sites (no behavior change). A consolidated `gl_validator.validate(gl_entries)` facade is deferred — the current checks run at different points (make_gl_entries / save_entries per-entry / make_reverse_gl_entries), so collapsing them into one call would alter ordering. Verified: all 12 Phase-0 snapshots byte-identical.
### Phase 2 — Pilot composer on Sales Invoice only — DONE
Added `BaseGLComposer` (minimal: holds `self.doc`) and `SalesInvoiceGLComposer`. SI's `get_gl_entries` is a thin shim delegating to `SalesInvoiceGLComposer(self).compose()`. All 11 SI-specific row builders (make_customer/tax/item/internal_transfer/pos/loyalty/write_off/rounding GL entries, stock_delivered_but_not_billed, get_gl_entries_for_fixed_asset, get_gle_for_change_amount) moved onto the composer and operate on `self.doc`. The `super().get_gl_entries()` stock-expense call became `super(SalesInvoice, doc).get_gl_entries()` (MRO-faithful). Bucket-A shared helpers (`get_gl_dict`, `make_discount_gl_entries`, `make_precision_loss_gl_entry`, `set_transaction_currency_and_rate_in_gl_map`, `get_tax_amounts`, `get_amount_and_base_amount`) **stay on the controller** — they're still called via `self.doc` and only lift to `BaseGLComposer` once all doctypes use composers (can't move while other doctypes inherit them). Verified: 12 snapshots + 10 existing SI tests (perpetual `super()`, POS change, write-off, returns, fixed-asset disposal/regain, internal transfer, loyalty) all green.
### Phase 3 — Second doctype: Purchase Invoice (base earns its shape) — DONE
Added `PurchaseInvoiceGLComposer` with all 13 PI GL builders migrated (make_supplier_gl_entry, add_supplier_gl_entry, make_item_gl_entries, make_stock_adjustment_entry, get_provisional_accounts, make_provisional_gl_entry, update_net_purchase_amount_for_linked_assets, make_tax_gl_entries, make_internal_transfer_gl_entries, make_gl_entries_for_tax_withholding, make_payment_gl_entries, make_write_off_gl_entry, make_gle_for_rounding_adjustment). PI.get_gl_entries is a thin shim. **Decision after comparing SI and PI: keep `BaseGLComposer` minimal** (`self.doc` + abstract `compose`). The two flows differ too much to share a template — different step order, different builders, per-doctype `make_regional_gl_entries`. Revisit base-lifting only when a 3rd+ doctype reveals a real common shape. Remaining on doc: Bucket-A helpers (`make_precision_loss_gl_entry`, `set_transaction_currency_and_rate_in_gl_map`, `get_gl_dict`, `get_tax_amounts`, `get_amount_and_base_amount`) and inherited `set_gl_entry_for_purchase_expense`. Verified: 12 snapshots + 80/81 existing PI tests green (1 pre-existing failure in `test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item`, unrelated to refactoring).
### Phase 4 — Roll out composer to remaining GL-posting doctypes
Payment Entry, Journal Entry, Delivery Note, Stock Entry, etc. Mechanical now; one PR per doctype (or small batches), each snapshot-gated.
- **Payment Entry — DONE.** Added `payment_entry/services/gl_composer.py``PaymentEntryGLComposer(BaseGLComposer)`. `compose()` mirrors the old `build_gl_map` (setup party account field, set txn currency/rate, then party/bank/deductions/tax builders, then `add_regional_gl_entries`). The four row builders (`add_party_gl_entries`, `add_bank_gl_entries`, `add_tax_gl_entries`, `add_deductions_gl_entries`) moved onto the composer and operate on `self.doc`; `build_gl_map` is now a thin shim delegating to the composer. **Advance builders stay on the doc** (`make_advance_gl_entries`, `add_advance_gl_entries`, `get_dr_and_account_for_advances`, `add_advance_gl_for_reference`) — they post in a separate pass inside `make_gl_entries`, not part of `compose()`, and belong to the Phase 5 advances service. Shared helpers (`get_gl_dict`, `calculate_base_allocated_amount_for_reference`, `get_exchange_rate`, `get_party_account_for_taxes`) stay on the doc, called via `self.doc`. Extended the Phase-0 snapshot net with 5 PE scenarios (receive-vs-SI, pay-vs-PI, deductions, taxes, multi-currency). Verified: 17 snapshots byte-identical + 53 existing PE tests green.
- **Journal Entry — DONE.** Added `journal_entry/services/gl_composer.py``JournalEntryGLComposer(BaseGLComposer)`. A JE already carries its ledger rows in the `accounts` child table, so `compose()` is a straight projection of those rows into GL dicts via `self.doc.get_gl_dict` (resolving txn currency/rate from the first foreign-currency row, mirroring the former `build_gl_map`). `build_gl_map` is now a thin shim (kept public — JE tests call it directly). Dropped the now-unused `get_advance_payment_doctypes` import from `journal_entry.py`. Extended the snapshot net with 3 JE scenarios (basic two-line, multi-currency, against-SI with party + reference). Verified: 20 snapshots byte-identical + 18 existing JE tests green.
### Phase 5 — Extract `advances.py`
Move the advances cluster. After composers, because advances cross-calls the exchange-gain/loss helper now on `BaseGLComposer`.
### Phase 6 — Extract remaining domain services from `accounts_controller`
`taxes.py`, `budget.py`, etc. Shrink `accounts_controller` to a thin lifecycle base that delegates.
### Phase 7 — Split the rest of the `sales_invoice.py` monolith
Non-GL doctype services: `pos.py`, `loyalty.py`, `status.py`, `inter_company.py`, `fixed_assets.py`, `timesheet_billing.py`. Independent of GL work; can run parallel to 56.
### Phase 8 — Collapse the inheritance chain
Flatten `SellingController` / `BuyingController` layers that are now pass-through. Last, because only safe once the delegated-to services exist.
**Dependencies:** 1→2→3 sequential; 4 and 7 can parallelize once 3 lands; 8 always last.
## Cross-cutting rules
- Public signatures stay stable — keep the `gl_map=` param and `make_gl_entries` intact. The `gl_map → gl_entries` rename is its own deprecation pass, deferred to the end (or excluded).
- Composers are classes (stateful, per-document); sink and validator are stateless module functions.
- Every phase: behavior-preserving, snapshot + `bench run-tests --site test-site-ai` green before merge, draft PR.