dodil/erp-brick-consolidation
DODIL ERP brick: consolidation
tests/world.py
"""The group the stories run in: UK01 (GBP) the top company, DE01 (EUR) bought 80% on 2026-04-01 for 900,000.00 EUR, FR01 (EUR)
30% equity; financial year ends in March. Everything goes through the doors; gl is the fake."""
from decimal import Decimal
from conftest import api, as_role
from brick_consolidation.application import use_cases
D = Decimal
ACC = as_role("group_accountant")
CTRL = as_role("group_controller")
CTRL_SAME_EMAIL = as_role("group_controller", email="group_accountant@group.example", sub="someone-else") # same email as ACC
CTRL_SAME_SUB = as_role("group_controller", email="other@group.example", sub="group_accountant@group.example") # same sub as ACC
OWNER = as_role("owner")
AUDITOR = as_role("auditor")
P = "2026-10"
RULES = {
"account_rules": [
{"group_account_id": a, "valid_from_period": "2020-01", "rate_type": r, **flags}
for a, r, flags in [
("G-1100", "closing", {}), ("G-1300", "closing", {"ic": True}), ("G-1400", "closing", {}), ("G-1450", "closing", {}),
("G-1600", "closing", {}), ("G-1700", "closing", {}), ("G-1990", "closing", {}), ("G-2000", "closing", {}),
("G-2300", "closing", {"ic": True}), ("G-3000", "historical", {"equity": True}), ("G-3100", "historical", {"equity": True}),
("G-3900", "closing", {}), ("G-3950", "closing", {}), ("G-3960", "average", {}),
("G-4000", "average", {}), ("G-4100", "average", {"ic": True}), ("G-4300", "average", {}),
("G-5000", "average", {}), ("G-5100", "average", {"ic": True}), ("G-6500", "average", {}), ("G-6900", "average", {})]],
"account_roles": [{"role": r, "valid_from_period": "2020-01", "group_account_id": a} for r, a in [
("cta", "G-3900"), ("nci_equity", "G-3950"), ("nci_result", "G-3960"), ("goodwill", "G-1700"), ("ic_difference_bs", "G-1990"),
("ic_difference_pl", "G-6900"), ("cost_of_sales", "G-5000"), ("inventory", "G-1400"), ("deferred_tax_asset", "G-1450"),
("tax_expense", "G-6500"), ("dividend_income", "G-4300"), ("investment", "G-1600"),
("retained_earnings", "G-3100")]], # the companies' reserves: the year's result is fixed on it at year end (audit MUST-1b)
"elimination_rules": [
{"rule_id": "IC-BAL", "kind": "ic_balance", "left_accounts": "G-1300", "right_accounts": "G-2300", "difference_role": "ic_difference_bs",
"tolerance": "5.00"},
{"rule_id": "IC-PL", "kind": "ic_pl", "left_accounts": "G-4100", "right_accounts": "G-5100", "difference_role": "ic_difference_pl",
"tolerance": "0.00"},
{"rule_id": "DIV", "kind": "dividend", "left_accounts": "G-4300", "right_accounts": "G-3100", "difference_role": "ic_difference_pl",
"tolerance": "0.00"}],
}
# DE01's trial balance at 2026-10 (EUR, debit positive, year to date) — nets to zero
DE01_ROWS = [("G-1100", "1400000.00", "0.00"), ("G-1400", "12000.00", "0.00"), ("G-2000", "-216816.25", "0.00"),
("G-2300", "-997.70", "0.00", "UK01"), ("G-3000", "-500000.00", "0.00"),
("G-3100", "-540000.00", "0.00"), ("G-3100", "40000.00", "40000.00", "", "dividend"),
("G-4000", "-300000.00", "-50000.00"), ("G-5000", "100000.00", "20000.00"), ("G-5100", "5813.95", "813.95", "UK01")]
# UK01's (GBP): receivable 870.00 from DE01, investment 774,000.00, intercompany revenue 5,000.00, dividend income 27,520.00
UK01_ROWS = [("G-1300", "870.00", "0.00", "DE01"), ("G-1600", "774000.00", "0.00", "DE01"), ("G-3000", "-742350.00", "0.00"),
("G-4100", "-5000.00", "-1000.00", "DE01"), ("G-4300", "-27520.00", "-27520.00", "DE01")]
def ok(r):
status, body = r
assert status == 200, (status, body)
return body
def gl_world(gl):
"""gl's companies, rates and figures."""
gl.company("UK01", "GBP", fy_end=3)
gl.company("DE01", "EUR", fy_end=3)
gl.company("FR01", "EUR", fy_end=3)
gl.set_rows("DE01", "2026-03", [("G-1100", "1000000.00", "0.00"), ("G-3000", "-500000.00", "0.00"), ("G-3100", "-500000.00", "0.00")])
gl.set_rows("DE01", P, DE01_ROWS)
gl.set_rows("UK01", P, UK01_ROWS)
gl.rates[("EUR", "spot", "2026-04-01")] = D("0.860000")
gl.rates[("EUR", "closing", "2026-10-31")] = D("0.870000")
for m in ("2026-04", "2026-05", "2026-06", "2026-07", "2026-08", "2026-09", "2026-10"):
gl.rates[("EUR", "average", m)] = D("0.860000")
return gl
def group(gl):
gl_world(gl)
ok(api("POST", "/groups", ACC, {"group_id": "GRP", "name": "Group", "group_currency": "GBP", "ledger_id": "IFRS", "group_chart_id": "GROUP",
"top_company_code": "UK01", "fiscal_year_end_month": "3"}))
ok(api("POST", "/groups/GRP/rules", ACC, RULES))
def acquire(company="DE01", pct="80", consideration="900000.00", method="full", date_="2026-04-01", investor="UK01", approve=True, **extra):
body = {"group_id": "GRP", "kind": "acquisition", "company_code": company, "investor_company_code": investor, "ownership_pct": pct,
"method": method, "effective_date": date_, "consideration": consideration, **extra}
change = ok(api("POST", "/structure-changes", ACC, body))
if approve:
change = ok(api("POST", f"/structure-changes/{change['change_id']}/approve-structure-change", CTRL, {}))
return change
def world(gl, *, acquired=True, opened=True):
group(gl)
if acquired:
acquire()
if opened:
ok(api("POST", "/groups/GRP/group-periods", ACC, {"period_id": P}))
def collected(gl):
world(gl)
return ok(api("POST", f"/groups/GRP/group-periods/{P}/collect-group-data", ACC, {}))
def translated(gl):
collected(gl)
return ok(api("POST", f"/groups/GRP/group-periods/{P}/translate-currencies", ACC, {}))
def state():
with use_cases.repository() as repo:
group_, version = repo.groups.get("GRP")
return group_, version
def period_row():
return next(p for p in state()[0].group_periods if p.period_id == P)
def db(fn):
with use_cases.repository() as repo:
return fn(repo)
def sign_period(period_id):
"""A signed history row (signing itself is CON-14): the group's year closed up to here."""
from brick_consolidation.domain.group import GroupPeriod
with use_cases.repository() as repo:
g, v = repo.groups.get("GRP")
g.group_periods.append(GroupPeriod(group_id="GRP", period_id=period_id, run_no=1, status="signed", signed_by="x", signed_sub="x"))
repo.groups.save(g, v)
def group_structures_without_acquisition():
"""DE01 in the structure with no acquisition behind it (what a bad import would leave): the eliminations must refuse."""
from brick_consolidation.domain.group import GroupStructure
with use_cases.repository() as repo:
g, v = repo.groups.get("GRP")
g.group_structures.append(GroupStructure(group_id="GRP", company_code="DE01", valid_from_period="2026-04", investor_company_code="UK01",
ownership_pct=D("80"), voting_pct=D("80"), method="full", change_id="none"))
repo.groups.save(g, v)