dodil/erp-brick-gl
DODIL ERP brick: general ledger
tests/test_ledgers_and_assets.py
"""GL-01 the chart, GL-42 a ledger added later, GL-37 fixed-assets posting per ledger."""
from conftest import ACCOUNTANT, ACCOUNTANT2, api, must, refused
from erp_platform.seam import derived_id
from world import account_id, books, close_through, line, post
def test_an_account_a_journal_has_used_keeps_its_type_and_code_and_is_deactivated_from_tomorrow():
books()
post("INV-1", "x", "2026-10-10", [line(account="6100", debit="10.00"), line(account="2100", credit="10.00")], source_module="inventory")
refused(api("POST", "/accounts", ACCOUNTANT, {"chart_id": "MAIN", "code": "6100", "type": "income"}), 409, "type is fixed")
refused(api("POST", "/accounts/change-code", ACCOUNTANT, {"chart_id": "MAIN", "code": "6100", "new_code": "6101"}), 409, "code is fixed")
must(api("POST", "/accounts", ACCOUNTANT, {"chart_id": "MAIN", "code": "6100", "name": "Premises"}))
must(api("POST", "/accounts/change-code", ACCOUNTANT, {"chart_id": "MAIN", "code": "6400", "new_code": "6410"}))
refused(api("POST", "/accounts", ACCOUNTANT, {"chart_id": "MAIN", "code": "6100", "active_until": "2026-10-22"}), 422, "tomorrow")
must(api("POST", "/accounts", ACCOUNTANT, {"chart_id": "MAIN", "code": "6100", "active_until": "2026-10-23"}))
assert post("INV-2", "x", "2026-10-22", [line(account="6100", debit="1.00"), line(account="2100", credit="1.00")], source_module="inventory")[0] == 200
s, b = post("INV-3", "x", "2026-10-23", [line(account="6100", debit="1.00"), line(account="2100", credit="1.00")], source_module="inventory")
refused((s, b), 422, "[account_active]")
refused(api("POST", "/accounts", ACCOUNTANT, {"chart_id": "MAIN", "code": "7000", "type": "asset", "subtype": "bank", "currency": "gbp"}), 422)
def test_a_ledger_added_later_starts_a_year_and_opens_with_the_leading_ledgers_carry_forward():
books()
must(api("POST", "/fiscal-years/open-year", ACCOUNTANT, {"fiscal_year": "FY2027"}))
must(api("POST", "/ledgers", ACCOUNTANT, {"ledger_id": "LOCAL", "kind": "non_leading", "accounting_principle": "FRS102"}))
must(api("POST", "/companies/UK01/ledgers", ACCOUNTANT, {"ledger_id": "LOCAL", "from_fiscal_year": "FY2027"}))
post("INV-1", "issue", "2026-03-10", [line(purpose="receivable", debit="120.00"), line(purpose="revenue", credit="100.00"), line(account="2200", credit="20.00")])
post("LEASE", "capitalise", "2026-10-15", [line(account="1400", debit="500.00"), line(account="2100", credit="500.00")], source_module="gl", ledger_id="LEAD")
refused(post("LOCAL-PRE", "x", "2026-10-16", [line(account="6100", debit="1.00"), line(account="2100", credit="1.00")], source_module="gl", ledger_id="LOCAL"),
422, "[ledger_assigned]")
close_through("2026-12")
must(api("POST", "/fiscal-years/FY2026/close-year", ACCOUNTANT, {}))
lead = {r["code"]: r["opening"] for r in must(api("GET", "/reports/trial-balance", ACCOUNTANT, {"as_at": "2027-01-31", "ledger_id": "LEAD"}))["rows"]}
local = {r["code"]: r["opening"] for r in must(api("GET", "/reports/trial-balance", ACCOUNTANT, {"as_at": "2027-01-31", "ledger_id": "LOCAL"}))["rows"]}
assert lead == local and lead["1400"] == "500.00" # LOCAL opens with the leading ledger's figures, lease included
post("LOCAL-ADJ", "x", "2027-01-05", [line(account="1400", credit="500.00"), line(account="6100", debit="500.00")], source_module="gl", ledger_id="LOCAL")
after = {r["code"]: r["closing"] for r in must(api("GET", "/reports/trial-balance", ACCOUNTANT, {"as_at": "2027-01-31", "ledger_id": "LOCAL"}))["rows"]}
assert after["1400"] == "0.00" and after["6100"] == "500.00"
def test_fixed_assets_posts_per_ledger_idempotently_and_its_accounts_take_no_manual_line():
books(extra_map={"asset_cost": "1400", "asset_clearing": "2100", "depreciation_expense": "6300"})
must(api("POST", "/accounts", ACCOUNTANT, {"chart_id": "MAIN", "code": "1500", "name": "Vehicles cost", "type": "asset", "subtype": "asset_cost"}))
must(api("POST", "/accounts", ACCOUNTANT, {"chart_id": "MAIN", "code": "1590", "name": "Vehicles depreciation", "type": "asset", "subtype": "accumulated_depreciation"}))
must(api("POST", "/account-map", ACCOUNTANT, {"chart_id": "MAIN", "purpose": "asset_cost", "qualifier": "VEH", "account_id": str(account_id("1500")), "valid_from": "2026-01-01"}))
must(api("POST", "/account-map", ACCOUNTANT, {"chart_id": "MAIN", "purpose": "accumulated_depreciation", "qualifier": "VEH", "account_id": str(account_id("1590")), "valid_from": "2026-01-01"}))
must(api("POST", "/ledgers", ACCOUNTANT, {"ledger_id": "LOCAL", "kind": "non_leading"}))
must(api("POST", "/companies/UK01/ledgers", ACCOUNTANT, {"ledger_id": "LOCAL", "from_fiscal_year": "FY2026"}))
from conftest import POSTER
s, acq = post("fa:T1", "acquire", "2026-10-02", [{"purpose": "asset_cost", "asset_class": "VEH", "debit": "30000.00", "asset_id": "UK01-1-0", "movement_type": "addition"},
{"purpose": "asset_clearing", "credit": "30000.00"}], source_module="fixed-assets", who=POSTER)
assert s == 200, acq
runs = {}
for lg, amount in (("LEAD", "500.00"), ("LOCAL", "625.00")):
body = [{"purpose": "depreciation_expense", "debit": amount, "asset_id": "UK01-1-0", "movement_type": "depreciation", "cost_centre": None},
{"purpose": "accumulated_depreciation", "asset_class": "VEH", "credit": amount, "asset_id": "UK01-1-0", "movement_type": "depreciation"}]
s, b = post(f"dep:UK01:{lg}:2026-10:1", "depreciate", "2026-10-31", body, source_module="fixed-assets", ledger_id=lg, who=POSTER)
assert s == 200 and b["replay"] is False, b
runs[lg] = b
s2, again = post(f"dep:UK01:{lg}:2026-10:1", "depreciate", "2026-10-31", body, source_module="fixed-assets", ledger_id=lg, who=POSTER)
assert s2 == 200 and again["replay"] is True
assert runs["LEAD"]["journal_id"] == str(derived_id("dep:UK01:LEAD:2026-10:1", "depreciate", "LEAD"))
lead = {r["code"]: r["closing"] for r in must(api("GET", "/reports/trial-balance", ACCOUNTANT, {"as_at": "2026-10-31", "ledger_id": "LEAD"}))["rows"]}
local = {r["code"]: r["closing"] for r in must(api("GET", "/reports/trial-balance", ACCOUNTANT, {"as_at": "2026-10-31", "ledger_id": "LOCAL"}))["rows"]}
assert lead["1500"] == local["1500"] == "30000.00" and lead["1590"] == "-500.00" and local["1590"] == "-625.00" # the acquisition is common
j = must(api("GET", f"/journals/{acq['journal_id']}", ACCOUNTANT))
assert [(ln["asset_id"], ln["movement_type"]) for ln in j["lines"]][0] == ("UK01-1-0", "addition")
refused(api("POST", "/journals/propose-manual", ACCOUNTANT, {"journal_date": "2026-10-20", "lines": [line(account="1500", debit="5.00"), line(account="2100", credit="5.00")]}),
422, "[source_only_account]")
refused(api("POST", "/journals/propose-manual", ACCOUNTANT, {"journal_date": "2026-10-20", "lines": [line(account="1590", credit="5.00"), line(account="6100", debit="5.00")]}),
422, "[source_only_account]")
s, b = post("fa:T2", "retire", "2026-10-02", [{"purpose": "asset_cost", "asset_class": "BOAT", "credit": "5.00"}, {"purpose": "disposal_gain", "asset_class": "BOAT", "debit": "5.00"}],
source_module="fixed-assets", who=POSTER)
refused((s, b), 422, "[purposes_mapped]", "disposal_gain (BOAT)")
_ = ACCOUNTANT2