dodil/erp-brick-gl
DODIL ERP brick: general ledger
tests/test_audit1.py
"""Audit 1: four proven findings."""
from decimal import Decimal
from conftest import ACCOUNTANT, ACCOUNTANT2, OWNER, SYSTEM, api, must, on_day, refused
from sqlalchemy import text
from test_intercompany import recharge, two_companies
from world import books, close_through, line, post
TOMORROW = "2026-10-23T09:00:00+00:00"
def test_soft_close_is_an_owner_control_and_still_waits_for_the_second_approver():
books()
close_through("2026-03")
refused(api("POST", "/settings", ACCOUNTANT, {"key": "soft_close", "value": True}), 403, "control")
must(api("POST", "/controls", OWNER, {"key": "soft_close", "value": True}))
must(api("POST", "/controls", OWNER, {"key": "manual_journal_approval", "value": "always"}))
lines = [line(account="6100", debit="5000.00"), line(account="2100", credit="5000.00")]
with on_day(TOMORROW):
out = must(api("POST", "/journals/propose-manual", ACCOUNTANT, {"journal_date": "2026-03-20", "lines": lines}))
assert out["status"] == "proposed" and out["needs_approval"] is True
done = must(api("POST", f"/journals/{out['journal_id']}/approve-manual", ACCOUNTANT2, {}))
assert done["status"] == "posted"
def test_post_manual_never_posts_one_side_of_a_cross_company_document():
two_companies()
p = must(recharge())
must(api("POST", "/controls", OWNER, {"key": "manual_journal_approval", "value": "none", "valid_from": "2026-10-23"}))
with on_day(TOMORROW):
out = api("POST", f"/journals/{p['journal_ids']['UK01']}/post-manual", SYSTEM, {})
statuses = {c: must(api("GET", f"/journals/{p['journal_ids'][c]}", ACCOUNTANT))["status"] for c in ("UK01", "DE01")}
assert statuses["UK01"] == statuses["DE01"], (out, statuses)
def test_approval_needed_honours_cross_company():
from types import SimpleNamespace
from brick_gl.application.posting import approval_needed
s = SimpleNamespace(manual_journal_approval="above_threshold", manual_journal_threshold=100)
assert approval_needed(s, Decimal("500"), cross_company=True) is True
assert approval_needed(s, Decimal("50"), cross_company=True) is False
s.manual_journal_approval = "none"
assert approval_needed(s, Decimal("500"), cross_company=True) is True and approval_needed(s, Decimal("500")) is False
def test_the_hash_chain_covers_the_split_lines(engine):
books()
post("INV-S1", "issue", "2026-03-10", [line(purpose="receivable", debit="1200.00"), line(purpose="revenue", credit="1000.00"),
line(account="2200", credit="200.00")])
assert must(api("GET", "/audit/verify-chain", ACCOUNTANT_AUD(), {"company_code": "UK01"}))["met"] is True
with engine.begin() as c:
c.execute(text("UPDATE gl_split_line SET debit = debit + 1, functional_debit = functional_debit + 1 WHERE debit > 0"))
out = must(api("GET", "/audit/verify-chain", ACCOUNTANT_AUD(), {"company_code": "UK01"}))
assert out["met"] is False
def ACCOUNTANT_AUD():
from conftest import AUDITOR
return AUDITOR
def test_a_replay_with_a_different_reverse_next_period_is_a_conflict():
from datetime import date
from brick_gl.domain import journal as jr
base = dict(company_code="UK01", journal_date=date(2026, 3, 1), currency="GBP", lines=[])
assert jr.body_digest(jr.Draft(**base)) == jr.body_digest(jr.Draft(**base, reverse_next_period=False))
assert jr.body_digest(jr.Draft(**base)) != jr.body_digest(jr.Draft(**base, reverse_next_period=True))
books()
lines = [line(account="6100", debit="10.00"), line(account="2100", credit="10.00")]
a = post("ACC-1", "x", "2026-03-31", lines, source_module="gl", reverse_next_period=False)
assert a[0] == 200, a
b = post("ACC-1", "x", "2026-03-31", lines, source_module="gl", reverse_next_period=True)
refused(b, 409, "journal_conflict")