diff --git a/examples/workbook/ch04_closing_and_post_close/config/chart_of_accounts.yaml b/examples/workbook/ch04_closing_and_post_close/config/chart_of_accounts.yaml new file mode 100644 index 0000000..93a9bd7 --- /dev/null +++ b/examples/workbook/ch04_closing_and_post_close/config/chart_of_accounts.yaml @@ -0,0 +1,56 @@ +schema_id: ledgerloom.chart_of_accounts.v1 +accounts: + # Assets + - code: Assets:Cash + name: Cash + type: asset + - code: Assets:AccountsReceivable + name: Accounts Receivable + type: asset + - code: Assets:Supplies + name: Supplies + type: asset + - code: Assets:Equipment + name: Equipment + type: asset + + # Liabilities + - code: Liabilities:AccountsPayable + name: Accounts Payable + type: liability + - code: Liabilities:NotesPayable + name: Notes Payable + type: liability + + # Equity + - code: Equity:OwnerCapital + name: Owner Capital + type: equity + - code: Equity:RetainedEarnings + name: Retained Earnings + type: equity + - code: Equity:Dividends + name: Owner Draws / Dividends + type: equity + + # Revenue + - code: Revenue:ServiceRevenue + name: Service Revenue + type: revenue + + # Expenses + - code: Expenses:Rent + name: Rent Expense + type: expense + - code: Expenses:Wages + name: Wages Expense + type: expense + - code: Expenses:Supplies + name: Supplies Expense + type: expense + - code: Expenses:Utilities + name: Utilities Expense + type: expense + - code: Expenses:Depreciation + name: Depreciation Expense + type: expense diff --git a/examples/workbook/ch04_closing_and_post_close/inputs/2026-01/adjustments.csv b/examples/workbook/ch04_closing_and_post_close/inputs/2026-01/adjustments.csv new file mode 100644 index 0000000..f29c68a --- /dev/null +++ b/examples/workbook/ch04_closing_and_post_close/inputs/2026-01/adjustments.csv @@ -0,0 +1,9 @@ +entry_id,date,narration,account,debit,credit +A001,2026-01-31,Adjust supplies used,Expenses:Supplies,150.00, +A001,2026-01-31,Adjust supplies used,Assets:Supplies,,150.00 +A002,2026-01-31,Record monthly depreciation,Expenses:Depreciation,50.00, +A002,2026-01-31,Record monthly depreciation,Assets:Equipment,,50.00 +A003,2026-01-31,Accrue utilities expense,Expenses:Utilities,75.00, +A003,2026-01-31,Accrue utilities expense,Liabilities:AccountsPayable,,75.00 +A004,2026-01-31,Accrue earned service revenue,Assets:AccountsReceivable,200.00, +A004,2026-01-31,Accrue earned service revenue,Revenue:ServiceRevenue,,200.00 diff --git a/examples/workbook/ch04_closing_and_post_close/inputs/2026-01/transactions.csv b/examples/workbook/ch04_closing_and_post_close/inputs/2026-01/transactions.csv new file mode 100644 index 0000000..c8e5fbe --- /dev/null +++ b/examples/workbook/ch04_closing_and_post_close/inputs/2026-01/transactions.csv @@ -0,0 +1,19 @@ +entry_id,date,narration,account,debit,credit +T001,2026-01-02,Owner investment,Assets:Cash,10000, +T001,2026-01-02,Owner investment,Equity:OwnerCapital,,10000 +T002,2026-01-03,Buy supplies (cash),Assets:Supplies,800, +T002,2026-01-03,Buy supplies (cash),Assets:Cash,,800 +T003,2026-01-05,Buy equipment on account,Assets:Equipment,5000, +T003,2026-01-05,Buy equipment on account,Liabilities:AccountsPayable,,5000 +T004,2026-01-10,Service revenue (cash),Assets:Cash,2500, +T004,2026-01-10,Service revenue (cash),Revenue:ServiceRevenue,,2500 +T005,2026-01-12,Service revenue (on account),Assets:AccountsReceivable,1200, +T005,2026-01-12,Service revenue (on account),Revenue:ServiceRevenue,,1200 +T006,2026-01-15,Pay rent,Expenses:Rent,600, +T006,2026-01-15,Pay rent,Assets:Cash,,600 +T007,2026-01-20,Pay accounts payable,Liabilities:AccountsPayable,1000, +T007,2026-01-20,Pay accounts payable,Assets:Cash,,1000 +T008,2026-01-25,Collect accounts receivable,Assets:Cash,700, +T008,2026-01-25,Collect accounts receivable,Assets:AccountsReceivable,,700 +T009,2026-01-28,Owner withdrawal (dividends),Equity:Dividends,300.00, +T009,2026-01-28,Owner withdrawal (dividends),Assets:Cash,,300.00 diff --git a/examples/workbook/ch04_closing_and_post_close/ledgerloom.yaml b/examples/workbook/ch04_closing_and_post_close/ledgerloom.yaml new file mode 100644 index 0000000..26702aa --- /dev/null +++ b/examples/workbook/ch04_closing_and_post_close/ledgerloom.yaml @@ -0,0 +1,20 @@ +schema_id: ledgerloom.project_config.v2 + +project: + name: Workbook Ch04 Closing and Post-Close + period: 2026-01 + currency: USD + +chart_of_accounts: config/chart_of_accounts.yaml +build_profile: workbook + +sources: + - source_type: journal_entries.v1 + name: Transactions + file_pattern: inputs/{period}/transactions.csv + entry_kind: transaction + + - source_type: journal_entries.v1 + name: Adjustments + file_pattern: inputs/{period}/adjustments.csv + entry_kind: adjustment diff --git a/tests/test_example_workbook_ch04_closing_and_post_close.py b/tests/test_example_workbook_ch04_closing_and_post_close.py new file mode 100644 index 0000000..8e0d44d --- /dev/null +++ b/tests/test_example_workbook_ch04_closing_and_post_close.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import hashlib +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pandas as pd + + +def _sha256_file(path: Path) -> str: + h = hashlib.sha256() + h.update(path.read_bytes()) + return h.hexdigest() + + +def _run(cmd: list[str], *, env: dict[str, str]) -> None: + res = subprocess.run(cmd, capture_output=True, text=True, env=env) + if res.returncode != 0: + raise AssertionError( + f"Command failed: {' '.join(cmd)}\n" + f"--- stdout ---\n{res.stdout}\n" + f"--- stderr ---\n{res.stderr}\n" + ) + + +def test_example_workbook_ch04_closing_and_post_close_cli_is_runnable_and_deterministic(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[1] + src = repo_root / "examples" / "workbook" / "ch04_closing_and_post_close" + assert src.exists(), "Expected examples/workbook/ch04_closing_and_post_close to exist in the repo" + + project_root = tmp_path / "ch04_closing_and_post_close" + shutil.copytree(src, project_root) + + # Ensure subprocess sees the src-layout package even if tests are running without an installed wheel. + env = dict(os.environ) + env["PYTHONPATH"] = str(repo_root / "src") + os.pathsep + env.get("PYTHONPATH", "") + + # 1) check (gatekeeper) should be runnable via the CLI + _run([sys.executable, "-m", "ledgerloom", "check", "--project", str(project_root)], env=env) + + # 2) build twice with different run IDs; manifests should be byte-identical. + for run_id in ("r1", "r2"): + _run( + [ + sys.executable, + "-m", + "ledgerloom", + "build", + "--project", + str(project_root), + "--run-id", + run_id, + ], + env=env, + ) + + run_root = project_root / "outputs" / run_id + assert (run_root / "artifacts" / "entries.csv").exists() + assert (run_root / "artifacts" / "trial_balance_unadjusted.csv").exists() + assert (run_root / "artifacts" / "trial_balance_adjusted.csv").exists() + assert (run_root / "artifacts" / "closing_entries.csv").exists() + assert (run_root / "artifacts" / "trial_balance_post_close.csv").exists() + + # Post-close TB should be Balance-Sheet-only + tb_pc = pd.read_csv(run_root / "artifacts" / "trial_balance_post_close.csv", dtype=str) + roots = set(tb_pc["root"].tolist()) if not tb_pc.empty else set() + assert roots.issubset({"Assets", "Liabilities", "Equity"}) + + # Trust manifest exists and tracks artifacts + manifest_path = run_root / "trust" / "manifest.json" + assert manifest_path.exists() + manifest = manifest_path.read_text(encoding="utf-8") + assert "artifacts/entries.csv" in manifest + assert "artifacts/trial_balance_post_close.csv" in manifest + + assert _sha256_file(project_root / "outputs" / "r1" / "trust" / "manifest.json") == _sha256_file( + project_root / "outputs" / "r2" / "trust" / "manifest.json" + )