From 064340cafb326e9d162a87205d331fe88446a190 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 27 May 2026 00:49:48 +0530 Subject: [PATCH] test: add Phase 0 GL characterization safety net Golden-master snapshot harness (GLSnapshot / assert_gl_snapshot) plus 12 characterization scenarios for Sales and Purchase Invoice (basic, taxes, multi-currency, returns, round-off, discount accounting, advance, POS). Locks current GL Entry output so the upcoming GL pipeline refactor (composer / validator / sink) can be verified byte-identical. Regenerate goldens with REGEN_GL_SNAPSHOTS=1. --- erpnext/accounts/gl_snapshot.py | 110 ++++++++++ erpnext/accounts/gl_snapshots/pi_basic.json | 30 +++ .../gl_snapshots/pi_multi_currency.json | 30 +++ erpnext/accounts/gl_snapshots/pi_return.json | 30 +++ .../accounts/gl_snapshots/pi_with_taxes.json | 58 ++++++ erpnext/accounts/gl_snapshots/si_basic.json | 30 +++ .../gl_snapshots/si_multi_currency.json | 30 +++ erpnext/accounts/gl_snapshots/si_pos.json | 86 ++++++++ erpnext/accounts/gl_snapshots/si_return.json | 30 +++ .../accounts/gl_snapshots/si_round_off.json | 58 ++++++ .../gl_snapshots/si_with_advance.json | 30 +++ .../gl_snapshots/si_with_discount.json | 44 ++++ .../accounts/gl_snapshots/si_with_taxes.json | 44 ++++ erpnext/accounts/test_gl_characterization.py | 189 ++++++++++++++++++ 14 files changed, 799 insertions(+) create mode 100644 erpnext/accounts/gl_snapshot.py create mode 100644 erpnext/accounts/gl_snapshots/pi_basic.json create mode 100644 erpnext/accounts/gl_snapshots/pi_multi_currency.json create mode 100644 erpnext/accounts/gl_snapshots/pi_return.json create mode 100644 erpnext/accounts/gl_snapshots/pi_with_taxes.json create mode 100644 erpnext/accounts/gl_snapshots/si_basic.json create mode 100644 erpnext/accounts/gl_snapshots/si_multi_currency.json create mode 100644 erpnext/accounts/gl_snapshots/si_pos.json create mode 100644 erpnext/accounts/gl_snapshots/si_return.json create mode 100644 erpnext/accounts/gl_snapshots/si_round_off.json create mode 100644 erpnext/accounts/gl_snapshots/si_with_advance.json create mode 100644 erpnext/accounts/gl_snapshots/si_with_discount.json create mode 100644 erpnext/accounts/gl_snapshots/si_with_taxes.json create mode 100644 erpnext/accounts/test_gl_characterization.py diff --git a/erpnext/accounts/gl_snapshot.py b/erpnext/accounts/gl_snapshot.py new file mode 100644 index 00000000000..1bb81384f45 --- /dev/null +++ b/erpnext/accounts/gl_snapshot.py @@ -0,0 +1,110 @@ +"""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}'") diff --git a/erpnext/accounts/gl_snapshots/pi_basic.json b/erpnext/accounts/gl_snapshots/pi_basic.json new file mode 100644 index 00000000000..7e999295def --- /dev/null +++ b/erpnext/accounts/gl_snapshots/pi_basic.json @@ -0,0 +1,30 @@ +[ + { + "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" + } +] diff --git a/erpnext/accounts/gl_snapshots/pi_multi_currency.json b/erpnext/accounts/gl_snapshots/pi_multi_currency.json new file mode 100644 index 00000000000..e20e31810bb --- /dev/null +++ b/erpnext/accounts/gl_snapshots/pi_multi_currency.json @@ -0,0 +1,30 @@ +[ + { + "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" + } +] diff --git a/erpnext/accounts/gl_snapshots/pi_return.json b/erpnext/accounts/gl_snapshots/pi_return.json new file mode 100644 index 00000000000..ffc8afc9a03 --- /dev/null +++ b/erpnext/accounts/gl_snapshots/pi_return.json @@ -0,0 +1,30 @@ +[ + { + "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" + } +] diff --git a/erpnext/accounts/gl_snapshots/pi_with_taxes.json b/erpnext/accounts/gl_snapshots/pi_with_taxes.json new file mode 100644 index 00000000000..5cb6ca60a3e --- /dev/null +++ b/erpnext/accounts/gl_snapshots/pi_with_taxes.json @@ -0,0 +1,58 @@ +[ + { + "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" + } +] diff --git a/erpnext/accounts/gl_snapshots/si_basic.json b/erpnext/accounts/gl_snapshots/si_basic.json new file mode 100644 index 00000000000..48bcf835043 --- /dev/null +++ b/erpnext/accounts/gl_snapshots/si_basic.json @@ -0,0 +1,30 @@ +[ + { + "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" + } +] diff --git a/erpnext/accounts/gl_snapshots/si_multi_currency.json b/erpnext/accounts/gl_snapshots/si_multi_currency.json new file mode 100644 index 00000000000..637eb8110b0 --- /dev/null +++ b/erpnext/accounts/gl_snapshots/si_multi_currency.json @@ -0,0 +1,30 @@ +[ + { + "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" + } +] diff --git a/erpnext/accounts/gl_snapshots/si_pos.json b/erpnext/accounts/gl_snapshots/si_pos.json new file mode 100644 index 00000000000..153c15cf334 --- /dev/null +++ b/erpnext/accounts/gl_snapshots/si_pos.json @@ -0,0 +1,86 @@ +[ + { + "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" + } +] diff --git a/erpnext/accounts/gl_snapshots/si_return.json b/erpnext/accounts/gl_snapshots/si_return.json new file mode 100644 index 00000000000..477f83d50c5 --- /dev/null +++ b/erpnext/accounts/gl_snapshots/si_return.json @@ -0,0 +1,30 @@ +[ + { + "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" + } +] diff --git a/erpnext/accounts/gl_snapshots/si_round_off.json b/erpnext/accounts/gl_snapshots/si_round_off.json new file mode 100644 index 00000000000..c994ced1fec --- /dev/null +++ b/erpnext/accounts/gl_snapshots/si_round_off.json @@ -0,0 +1,58 @@ +[ + { + "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" + } +] diff --git a/erpnext/accounts/gl_snapshots/si_with_advance.json b/erpnext/accounts/gl_snapshots/si_with_advance.json new file mode 100644 index 00000000000..48bcf835043 --- /dev/null +++ b/erpnext/accounts/gl_snapshots/si_with_advance.json @@ -0,0 +1,30 @@ +[ + { + "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" + } +] diff --git a/erpnext/accounts/gl_snapshots/si_with_discount.json b/erpnext/accounts/gl_snapshots/si_with_discount.json new file mode 100644 index 00000000000..29a2d25ac0e --- /dev/null +++ b/erpnext/accounts/gl_snapshots/si_with_discount.json @@ -0,0 +1,44 @@ +[ + { + "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" + } +] diff --git a/erpnext/accounts/gl_snapshots/si_with_taxes.json b/erpnext/accounts/gl_snapshots/si_with_taxes.json new file mode 100644 index 00000000000..228e76cf295 --- /dev/null +++ b/erpnext/accounts/gl_snapshots/si_with_taxes.json @@ -0,0 +1,44 @@ +[ + { + "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" + } +] diff --git a/erpnext/accounts/test_gl_characterization.py b/erpnext/accounts/test_gl_characterization.py new file mode 100644 index 00000000000..891d906e794 --- /dev/null +++ b/erpnext/accounts/test_gl_characterization.py @@ -0,0 +1,189 @@ +"""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.purchase_invoice.purchase_invoice import make_debit_note +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.doctype.sales_invoice.sales_invoice 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 + +POSTING_DATE = "2024-01-15" +COMPANY = "_Test Company" +CUSTOMER = "_Test Customer" + + +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 + + +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)