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)