dodil/erp-brick-consolidation

DODIL ERP brick: consolidation

tests/test_slice_c1.py

"""Slice C1: the fair-value step-up of an acquisition (CON-02), CON-16 divestiture, CON-17 ownership changes and step acquisitions."""
from decimal import Decimal

from conftest import api
from test_slice_b1 import BASE, add_rules, eliminate, entries, lines, tb
from world import ACC, CTRL, P, acquire, db, group, ok, sign_period, state, world

D = Decimal
CHANGES = "/structure-changes"


def book(key):
    return tb().get(key, D(0))


def nets_to_zero():
    assert sum(tb().values(), D(0)) == 0 and all(sum(e_l["amount"] for e_l in e["lines"]) == 0 for e in entries() if e["document_type"] != "translation")


# ═══ CON-02 (slice B gap): the fair-value step-up enters the consolidated books ══════════════════════════════════════
LAND = {"description": "land", "amount": "100000.00", "tax_rate_pct": "30", "group_account_id": "G-1500"}


def fv_world(gl):
    """exact_world's group, but UK01 bought DE01 with a 100,000.00 EUR land uplift carrying 30% deferred tax."""
    group(gl)
    add_rules(("G-1500", "closing"), ("G-2500", "closing"), roles=[("deferred_tax_liability", "G-2500")])
    acquire("DE01", fair_value_adjustments=[LAND])
    ok(api("POST", "/groups/GRP/group-periods", ACC, {"period_id": P}))
    gl.set_rows("DE01", P, [("G-1100", "1060000.00", "0.00"), ("G-3000", "-500000.00", "0.00"), ("G-3100", "-500000.00", "0.00"),
                            ("G-4000", "-60000.00", "-10000.00")])
    gl.set_rows("UK01", P, [("G-1600", "774000.00", "0.00", "DE01"), ("G-3000", "-774000.00", "0.00")])
    for step in ("collect-group-data", "translate-currencies"):
        ok(api("POST", f"{BASE}/{step}", ACC, {}))


def test_con_02_the_fair_value_step_up_and_its_deferred_tax_enter_the_books_and_goodwill_is_computed_after_them(gl):
    fv_world(gl)
    change = ok(api("GET", f"{CHANGES}/GRP:DE01:2026-04:acquisition", ACC))
    assert (D(change["net_assets"]), D(change["goodwill"]), D(change["nci"])) == (D("1070000.00"), D("44000.00"), D("214000.00"))
    assert (D(change["goodwill_group"]), D(change["nci_group"])) == (D("37840.00"), D("184040.00"))
    eliminate()
    # IAS 21.47: land, its deferred tax and the goodwill are held in EUR and shown at the closing 0.87 (acquired at 0.86):
    # land 100,000 EUR x 0.87 = 87,000.00 up; deferred tax 30% = 26,100.00 down; goodwill 44,000 EUR x 0.87 = 38,280.00
    assert book(("DE01", "G-1500", "", "", "")) == D("87000.00") and book(("DE01", "G-2500", "", "", "")) == D("-26100.00")
    assert book(("DE01", "G-1700", "", "", "")) == D("38280.00") and book(("UK01", "G-1600", "DE01", "", "")) == 0     # exactly zero: the gate
    # the minority: 20% of the adjusted equity at acquisition (184,040.00), of the result to date (60,000 EUR at 0.86 = 51,600.00: 10,320.00) and
    # of the translation difference (10,600.00: 2,120.00)
    assert book(("DE01", "G-3950", "", "", "")) == D("-196620.00") and book(("DE01", "G-3960", "", "", "")) == D("10320.00")        # 140.00 more: 20% of the step-up's 700.00 difference
    [inv] = entries("investment")
    # the translation reserve takes the group's part of what the goodwill (440.00) and the step-up (700.00 less the minority's 140.00) gained
    assert [ln["amount"] for ln in inv["lines"] if ln["group_account_id"] == "G-3900"] == [D("-1000.00")]
    nets_to_zero()


def test_con_02_without_an_account_for_the_uplift_or_its_tax_the_run_names_what_to_set(gl):
    group(gl)
    acquire("DE01", fair_value_adjustments=[{"description": "land", "amount": "100000.00", "tax_rate_pct": "30"}])
    ok(api("POST", "/groups/GRP/group-periods", ACC, {"period_id": P}))
    gl.set_rows("UK01", P, [("G-1600", "774000.00", "0.00", "DE01"), ("G-3000", "-774000.00", "0.00")])
    gl.set_rows("DE01", P, [("G-1100", "1000000.00", "0.00"), ("G-3000", "-500000.00", "0.00"), ("G-3100", "-500000.00", "0.00")])
    for step in ("collect-group-data", "translate-currencies"):
        ok(api("POST", f"{BASE}/{step}", ACC, {}))
    status, body = api("POST", f"{BASE}/run-eliminations", ACC, {})
    assert status == 409 and "fair_value_step_up" in str(body) and [e for e in db(lambda r: r.data.entries("GRP", P, 1)) if e["document_type"] != "translation"] == []


# ═══ CON-16 divestiture ════════════════════════════════════════════════════════════════════════════════════════════
def sale_world(gl, *, books_sale=True):
    """exact_world with UK01's October books after selling DE01 on 2026-10-31 for 1,200,000.00 EUR (1,044,000.00 GBP at the closing rate 0.87): cash in, the
    investment out, its own gain (270,000.00 over the 774,000.00 cost) on gain_on_disposal against DE01."""
    group(gl)
    add_rules(("G-4900", "average"), roles=[("gain_on_disposal", "G-4900"), ("group_retained_earnings", "G-3200")])
    acquire("DE01")
    ok(api("POST", "/groups/GRP/group-periods", ACC, {"period_id": P}))
    gl.rates[("EUR", "closing", "2026-10-31")] = D("0.870000")
    gl.set_rows("DE01", P, [("G-1100", "1060000.00", "0.00"), ("G-3000", "-500000.00", "0.00"), ("G-3100", "-500000.00", "0.00"),
                            ("G-4000", "-60000.00", "-10000.00")])
    uk = [("G-1100", "1044000.00", "0.00"), ("G-3000", "-774000.00", "0.00"), ("G-4900", "-270000.00", "-270000.00", "DE01")] if books_sale else \
         [("G-1600", "774000.00", "0.00", "DE01"), ("G-3000", "-774000.00", "0.00")]
    gl.set_rows("UK01", P, uk)
    for step in ("collect-group-data", "translate-currencies"):
        ok(api("POST", f"{BASE}/{step}", ACC, {}))


def sell(**extra):
    body = {"group_id": "GRP", "kind": "divestiture", "company_code": "DE01", "investor_company_code": "UK01", "ownership_pct": "80",
            "effective_date": "2026-10-31", "proceeds": "1200000.00", **extra}
    return api("POST", CHANGES, ACC, body)


def test_con_16_the_gain_the_recycled_reserve_and_the_minority_are_shown_to_the_penny_then_the_controller_approves(gl):
    sale_world(gl)
    status, shown = sell()
    assert status == 200, shown
    figs = __import__("json").loads(shown["figures_json"])
    # v3: every term at gl's CLOSING rate on the disposal date (0.87): net assets 1,060,000 EUR = 922,200.00; the group's 80% = 737,760.00; the goodwill's
    # closing carrying amount 100,000 EUR x 0.87 = 87,000.00 (never the acquisition rate 0.86); proceeds 1,200,000 EUR = 1,044,000.00
    assert (figs["net_assets_group"], figs["share_of_net_assets"], figs["goodwill_group"], figs["proceeds_group"]) == \
        ("922200.00", "737760.00", "87000.00", "1044000.00")
    assert figs["gain"] == "219240.00"                                               # proceeds - (share of net assets + goodwill)
    # the reserve recycled: the company's 8,480.00 and the 1,000.00 goodwill's retranslation (87,000.00 - 86,000.00) put there since acquisition
    assert (figs["cta_recycled"], figs["total_gain"], figs["nci_derecognised"]) == ("9480.00", "228720.00", "184440.00")
    assert D(shown["gain_loss"]) == D("228720.00") and shown["status"] == "proposed"
    approved = ok(api("POST", f"{CHANGES}/{shown['change_id']}/approve-structure-change", CTRL, {}))
    assert approved["status"] == "effective"
    g, _ = state()
    row = next(r for r in g.group_structures if r.company_code == "DE01" and r.method == "none")
    assert row.valid_from_period == "2026-11"                                         # the structure row is method none from the next period
    eliminate()
    [entry] = entries("disposal")
    got = lines(entry)
    assert got[("G-4900", "UK01", "DE01")] == D("-219240.00") + D("-8480.00") + D("-1000.00") + D("270000.00") - D("270000.00") + D("270000.00")
    assert book(("UK01", "G-4900", "DE01", "", "")) == D("-228720.00")                # the group's gain replaces UK01's own: gain + recycled reserve
    assert book(("DE01", "G-1100", "", "", "")) == 0 and book(("DE01", "G-3000", "", "", "")) == 0     # its balance sheet no longer from the period's end
    assert book(("DE01", "G-4000", "", "", "")) == D("-51600.00")                    # its profit and loss is consolidated up to the sale
    assert book(("DE01", "G-3900", "", "", "")) == 0 and book(("DE01", "G-3950", "", "", "")) == 0     # the reserve is recycled, the minority derecognised
    assert book(("DE01", "G-3960", "", "", "")) == D("10320.00")                      # the minority's share of the profit to the sale
    assert not [ln for ln in entry["lines"] if ln["group_account_id"] == "G-3200"]    # the entry balanced by itself: no plug on retained earnings
    nets_to_zero()
    assert not entries("investment") and not entries("nci")                           # the sold company is not eliminated as a subsidiary


def test_con_16_a_sale_dated_inside_a_signed_period_or_not_on_a_period_end_is_refused(gl):
    sale_world(gl)
    status, body = sell(effective_date="2026-10-15")
    assert status == 409 and "last day" in str(body)
    sign_period("2026-10")
    status, body = sell()
    assert status == 409 and "signed" in str(body)


def test_con_16_only_the_whole_holding_of_a_subsidiary_and_with_proceeds(gl):
    sale_world(gl)
    assert sell(ownership_pct="50")[0] == 409
    assert sell(proceeds="")[0] == 422 and sell(proceeds="12345678901234567.00")[0] == 422


def test_con_16_the_later_months_of_the_year_keep_what_the_company_earned_until_the_sale(gl):
    sale_world(gl)
    ok(api("POST", CHANGES, ACC, {"group_id": "GRP", "kind": "divestiture", "company_code": "DE01", "investor_company_code": "UK01",
                                  "ownership_pct": "80", "effective_date": "2026-10-31", "proceeds": "1200000.00"}))
    ok(api("POST", f"{CHANGES}/GRP:DE01:2026-10:divestiture/approve-structure-change", CTRL, {}))
    eliminate()
    ok(api("POST", f"{BASE}/sign-off-group-close", CTRL, {}))
    ok(api("POST", "/groups/GRP/group-periods", ACC, {"period_id": "2026-11"}))
    gl.set_rows("UK01", "2026-11", [("G-1100", "1044000.00", "0.00"), ("G-3000", "-774000.00", "0.00"), ("G-4900", "-270000.00", "0.00", "DE01")])
    nov = "/groups/GRP/group-periods/2026-11"
    for step in ("collect-group-data", "translate-currencies", "run-eliminations"):
        ok(api("POST", f"{nov}/{step}", ACC, {}))
    g, _ = state()
    gp = next(x for x in g.group_periods if x.period_id == "2026-11")
    from brick_consolidation.application import reports
    books = db(lambda r: reports.consolidated(r, g, gp)[0])
    assert books[("DE01", "G-4000", "", "", "")] == D("-51600.00") and books[("UK01", "G-4900", "DE01", "", "")] == D("-228720.00")
    assert not [k for k in books if k[0] == "DE01" and k[1] in ("G-1100", "G-3000", "G-3900")] and sum(books.values(), D(0)) == 0


# ═══ CON-17 ownership changes ═════════════════════════════════════════════════════════════════════════════════════
def buy_more(**extra):
    body = {"group_id": "GRP", "kind": "ownership_change", "company_code": "DE01", "investor_company_code": "UK01", "ownership_pct": "90",
            "effective_date": "2026-10-01", "consideration": "120000.00", **extra}
    return api("POST", CHANGES, ACC, body)


def stake_world(gl):
    """UK01 holds 80% of DE01 and buys another 10% on 2026-10-01 for 120,000.00 EUR (104,400.00 GBP at 0.87). DE01's net assets at the end of
    September: capital and reserves 1,000,000.00 + the result to date 50,000.00 = 1,050,000.00 EUR."""
    world(gl, opened=False)
    gl.rates[("EUR", "spot", "2026-10-01")] = D("0.870000")
    gl.set_rows("DE01", "2026-09", [("G-1100", "1050000.00", "0.00"), ("G-3000", "-500000.00", "0.00"), ("G-3100", "-500000.00", "0.00"),
                                    ("G-4000", "-50000.00", "-10000.00")])
    add_rules(roles=[("group_retained_earnings", "G-3200")])


def test_con_17_without_a_change_of_control_the_difference_goes_to_equity_and_the_minority_is_10_percent(gl):
    stake_world(gl)
    status, shown = buy_more()
    assert status == 200, shown
    figs = __import__("json").loads(shown["figures_json"])
    # net assets 1,050,000 EUR x 0.87 = 913,500.00; the 10% bought is 91,350.00 of minority given up for 104,400.00: 13,050.00 to equity, no goodwill
    assert (figs["net_assets_group"], figs["nci_given_group"], figs["price_group"], figs["equity_group"]) == ("913500.00", "91350.00", "104400.00", "13050.00")
    assert shown["goodwill"] is None and shown["gain_loss"] is None
    ok(api("POST", f"{CHANGES}/{shown['change_id']}/approve-structure-change", CTRL, {}))
    g, _ = state()
    assert next(r for r in g.group_structures if r.company_code == "DE01" and r.valid_from_period == "2026-10").ownership_pct == D("90")
    ok(api("POST", "/groups/GRP/group-periods", ACC, {"period_id": P}))
    gl.set_rows("DE01", P, [("G-1100", "1060000.00", "0.00"), ("G-3000", "-500000.00", "0.00"), ("G-3100", "-500000.00", "0.00"),
                            ("G-4000", "-60000.00", "-10000.00")])
    gl.set_rows("UK01", P, [("G-1600", "878400.00", "0.00", "DE01"), ("G-3000", "-878400.00", "0.00")])
    for step in ("collect-group-data", "translate-currencies"):
        ok(api("POST", f"{BASE}/{step}", ACC, {}))
    eliminate()
    [entry] = entries("ownership_change")
    assert lines(entry) == {("G-1600", "UK01", "DE01"): D("-104400.00"), ("G-3950", "DE01", ""): D("91350.00"), ("G-3200", "DE01", ""): D("13050.00")}
    assert book(("UK01", "G-1600", "DE01", "", "")) == 0 and book(("DE01", "G-1700", "", "", "")) == D("87000.00")      # the goodwill is untouched, shown at the closing rate
    assert book(("DE01", "G-3960", "", "", "")) == D("5160.00")                       # 10% of 51,600.00 from October, not 20%
    assert not [ln for e in entries() for ln in e["lines"] if ln["group_account_id"] == "G-4900"]       # never profit
    nets_to_zero()


def test_con_17_a_smaller_holding_a_missing_price_and_a_signed_period_are_refused(gl):
    stake_world(gl)
    assert buy_more(ownership_pct="70")[0] == 409 and buy_more(consideration="")[0] == 422
    assert api("POST", CHANGES, ACC, {"group_id": "GRP", "kind": "ownership_change", "company_code": "DE01", "investor_company_code": "UK01",
                                      "ownership_pct": "101", "effective_date": "2026-10-01", "consideration": "1.00"})[0] == 422
    sign_period("2026-10")
    status, body = buy_more()
    assert status == 409 and "signed" in str(body)


def test_con_17_taking_control_of_an_associate_remeasures_the_interest_held_in_profit_and_computes_goodwill_at_the_date(gl):
    world(gl)
    add_rules(("G-4900", "average"), roles=[("gain_on_disposal", "G-4900")])
    gl.set_rows("FR01", "2026-03", [("G-1100", "300000.00", "0.00"), ("G-3000", "-300000.00", "0.00")])
    acquire("FR01", "30", "150000.00", "equity")
    sign_period("2026-03")
    db(lambda r: r.data.add_translated([{"group_id": "GRP", "period_id": "2026-03", "run_no": 1, "company_code": "UK01", "group_account_id": "G-1600",
                                         "partner_company": "FR01", "movement_type": "", "segment": "", "ytd_group": D("129000.00"), "rate_type": "closing",
                                         "rate": D("1.000000")}]))                       # the 30% carried at 150,000 EUR x 0.86
    status, shown = api("POST", CHANGES, ACC, {"group_id": "GRP", "kind": "ownership_change", "company_code": "FR01", "investor_company_code": "UK01",
                                               "ownership_pct": "60", "method": "full", "effective_date": "2026-04-01", "consideration": "200000.00",
                                               "previously_held_fair_value": "250000.00"})
    assert status == 200, shown
    # price 200,000 + the 30% held at fair value 250,000 = 450,000; 60% of net assets 300,000 = 180,000: goodwill 270,000 EUR, minority 120,000 EUR
    assert (D(shown["goodwill"]), D(shown["nci"]), D(shown["goodwill_group"]), D(shown["nci_group"])) == \
        (D("270000.00"), D("120000.00"), D("232200.00"), D("103200.00"))
    assert D(shown["gain_loss"]) == D("86000.00")                                         # 250,000 x 0.86 = 215,000.00 less the 129,000.00 carried
    ok(api("POST", f"{CHANGES}/{shown['change_id']}/approve-structure-change", CTRL, {}))
    g, _ = state()
    row = next(r for r in g.group_structures if r.company_code == "FR01" and r.valid_from_period == "2026-04")
    assert (row.method, row.ownership_pct) == ("full", D("60"))
    gl.set_rows("FR01", P, [("G-1100", "300000.00", "0.00"), ("G-3000", "-300000.00", "0.00")])
    gl.set_rows("DE01", P, [("G-1100", "1060000.00", "0.00"), ("G-3000", "-500000.00", "0.00"), ("G-3100", "-500000.00", "0.00"),
                            ("G-4000", "-60000.00", "-10000.00")])
    gl.set_rows("UK01", P, [("G-1600", "774000.00", "0.00", "DE01"), ("G-1600", "301000.00", "0.00", "FR01"), ("G-3000", "-1075000.00", "0.00")])
    for step in ("collect-group-data", "translate-currencies"):
        ok(api("POST", f"{BASE}/{step}", ACC, {}))
    eliminate()
    assert book(("UK01", "G-1600", "FR01", "", "")) == 0 and book(("FR01", "G-1700", "", "", "")) == D("234900.00")
    assert book(("FR01", "G-4900", "", "", "")) == D("-86000.00")                         # the gain on the interest held before, in profit (IFRS 3.42)
    [inv] = [e for e in entries("investment") if any(ln["company_code"] == "FR01" for ln in e["lines"])]
    # price + fair value = parent's share + goodwill at acquisition; the goodwill's gain since (270,000 EUR: 232,200.00 to 234,900.00) is the only reserve line
    assert [ln["amount"] for ln in inv["lines"] if ln["group_account_id"] == "G-3900"] == [D("-2700.00")]
    assert not entries("equity_method")
    nets_to_zero()


# ═══ CON-15 the year-end carry-forward ═══════════════════════════════════════════════════════════════════════════════
def year_world(gl, extra=0):
    """A calendar-year group of two sterling companies: UK01 bought 80% of DE01 on 2026-01-01 for 900,000.00 (goodwill 100,000.00, minority
    200,000.00). December 2026: DE01 earned 50,000.00 (the minority's share 10,000.00) and holds 12,000.00 of UK01 goods at 25% margin, tax 30%
    (3,000.00 eliminated, 900.00 deferred tax). FY2026 is signed through 2026-12."""
    from world import RULES
    gl.company("UK01", "GBP", fy_end=12)
    gl.company("DE01", "GBP", fy_end=12)
    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": "12"}))
    ok(api("POST", "/groups/GRP/rules", ACC, {**RULES, "account_roles": RULES["account_roles"] + [{"role": "group_retained_earnings", "valid_from_period": "2020-01",
                                                                                                    "group_account_id": "G-3200"}]}))
    gl.set_rows("DE01", "2025-12", [("G-1100", "1000000.00", "0.00"), ("G-3000", "-500000.00", "0.00"), ("G-3100", "-500000.00", "0.00")])
    acquire("DE01", date_="2026-01-01")
    held = []
    for i in range(extra):                                        # more sterling subsidiaries, bought whole for 100.00
        code = f"X{i:02d}"
        gl.company(code, "GBP", fy_end=12)
        gl.set_rows(code, "2025-12", [("G-1100", "100.00", "0.00"), ("G-3000", "-100.00", "0.00")])
        gl.set_rows(code, "2026-12", [("G-1100", "100.00", "0.00"), ("G-3000", "-100.00", "0.00")])
        acquire(code, "100", "100.00", date_="2026-01-01")
        held.append(("G-1600", "100.00", "0.00", code))
    gl.set_rows("UK01", "2026-12", [("G-1600", "900000.00", "0.00", "DE01"), ("G-3000", str(D("-900000.00") - 100 * extra), "0.00")] + held)
    gl.set_rows("DE01", "2026-12", [("G-1100", "1038000.00", "0.00"), ("G-1400", "12000.00", "0.00"), ("G-3000", "-500000.00", "0.00"),
                                    ("G-3100", "-500000.00", "0.00"), ("G-4000", "-50000.00", "-50000.00")])
    ok(api("POST", "/groups/GRP/group-periods", ACC, {"period_id": "2026-12"}))
    close_period("2026-12", inventory=True)


def at(period):
    return f"/groups/GRP/group-periods/{period}"


def close_period(period, *, inventory=False, signer=CTRL):
    for step in ("collect-group-data", "translate-currencies"):
        ok(api("POST", f"{at(period)}/{step}", ACC, {}))
    if inventory:
        ok(api("POST", f"{at(period)}/ic-inventory", ACC, {"holder_company": "DE01", "seller_company": "UK01", "inventory_amount": "12000.00",
                                                            "margin_pct": "25", "tax_rate_pct": "30"}))
    ok(api("POST", f"{at(period)}/run-eliminations", ACC, {}))
    ok(api("POST", f"{at(period)}/sign-off-group-close", signer, {}))


def carried(fiscal_year="FY2027"):
    return {(r["group_account_id"], r["company_code"], r["partner_company"]): D(r["opening"])
            for r in db(lambda rp: rp.data.carry_forward("GRP", fiscal_year)) if D(r["opening"])}


def january(gl):
    gl.set_rows("UK01", "2027-01", [("G-1600", "900000.00", "0.00", "DE01"), ("G-3000", "-900000.00", "0.00")])
    gl.set_rows("DE01", "2027-01", [("G-1100", "1050000.00", "0.00"), ("G-3000", "-500000.00", "0.00"), ("G-3100", "-550000.00", "0.00")])


def test_con_15_the_closing_balance_sheet_effects_open_the_next_year_and_their_profit_and_loss_opens_on_retained_earnings(gl):
    year_world(gl)
    january(gl)
    opened = ok(api("POST", "/groups/GRP/group-periods", ACC, {"period_id": "2027-01"}))
    assert opened["result"]["carry_forward"]["from_period"] == "2026-12"
    # the stock profit (3,000.00 and its 900.00 deferred tax), the minority's 10,000.00 share of the year's result, and what hit profit and loss
    assert carried() == {("G-1400", "DE01", "UK01"): D("-3000.00"), ("G-1450", "DE01", "UK01"): D("900.00"), ("G-3950", "DE01", ""): D("-10000.00"),
                         ("G-3200", "DE01", ""): D("9100.00"), ("G-3200", "UK01", ""): D("3000.00")}
    assert sum(carried().values(), D(0)) == 0
    before = db(lambda r: r.data.carry_forward("GRP", "FY2027"))
    again = ok(api("POST", "/groups/GRP/group-periods", ACC, {"period_id": "2027-01"}))             # opens again: the same rows, nothing changed
    assert again["result"]["carry_forward"]["changed"] is False
    after = db(lambda r: r.data.carry_forward("GRP", "FY2027"))
    assert [(r["group_account_id"], r["opening"]) for r in after] == [(r["group_account_id"], r["opening"]) for r in before] and len(after) == len(before)


def test_con_15_last_years_stock_profit_is_realised_this_year_the_group_result_rises_by_the_net_2100(gl):
    year_world(gl)
    january(gl)
    ok(api("POST", "/groups/GRP/group-periods", ACC, {"period_id": "2027-01"}))
    for step in ("collect-group-data", "translate-currencies"):
        ok(api("POST", f"{at('2027-01')}/{step}", ACC, {}))
    ok(api("POST", f"{at('2027-01')}/ic-inventory", ACC, {"holder_company": "DE01", "seller_company": "UK01", "none_stated": True}))
    ok(api("POST", f"{at('2027-01')}/run-eliminations", ACC, {}))
    g, _ = state()
    gp = next(x for x in g.group_periods if x.period_id == "2027-01")
    from brick_consolidation.application import reports
    books = db(lambda r: reports.consolidated(r, g, gp)[0])
    assert sum(books.values(), D(0)) == 0
    assert books[("UK01", "G-5000", "DE01", "", "")] == D("-3000.00") and books[("DE01", "G-6500", "UK01", "", "")] == D("900.00")     # into cost of sales and tax
    assert books.get(("DE01", "G-1400", "UK01", "", ""), D(0)) == 0 and books.get(("DE01", "G-1450", "UK01", "", ""), D(0)) == 0     # the opening is cleared
    assert -sum((v for (c, a, p, m, s), v in books.items() if a in ("G-5000", "G-6500", "G-3960")), D(0)) == D("2100.00")        # the group result rises by 2,100.00 net
    assert ok(api("GET", f"{at('2027-01')}/readiness", CTRL))["met"] is True


def test_con_15_a_reopened_and_re_signed_year_end_makes_the_carry_forward_stale_and_blocks_the_first_sign_off(gl):
    from test_slice_b2 import CTRL2
    year_world(gl)
    january(gl)
    ok(api("POST", "/groups/GRP/group-periods", ACC, {"period_id": "2027-01"}))
    for step in ("collect-group-data", "translate-currencies"):
        ok(api("POST", f"{at('2027-01')}/{step}", ACC, {}))
    ok(api("POST", f"{at('2027-01')}/ic-inventory", ACC, {"holder_company": "DE01", "seller_company": "UK01", "none_stated": True}))
    ok(api("POST", f"{at('2027-01')}/run-eliminations", ACC, {}))
    ok(api("POST", f"{at('2026-12')}/reopen-group-period", CTRL2, {"reason": "a 500.00 accrual was missed"}))
    gate = next(g for g in ok(api("GET", f"{at('2027-01')}/readiness", CTRL))["gates"] if g["gate"] == "carry_forward_current")
    assert gate["met"] is False and "2026-12" in gate["reason"]
    status, body = api("POST", f"{at('2027-01')}/sign-off-group-close", CTRL2, {})
    assert status == 409 and "carry_forward_current" in str(body)
    gl.set_rows("DE01", "2026-12", [("G-1100", "1038000.00", "0.00"), ("G-1400", "12000.00", "0.00"), ("G-2000", "-500.00", "0.00"),
                                    ("G-3000", "-500000.00", "0.00"), ("G-3100", "-500000.00", "0.00"), ("G-4000", "-49500.00", "-49500.00")])
    ok(api("POST", f"{at('2026-12')}/collect-group-data", ACC, {}))
    for step in ("translate-currencies", "run-eliminations"):
        ok(api("POST", f"{at('2026-12')}/{step}", ACC, {}))
    signed = ok(api("POST", f"{at('2026-12')}/sign-off-group-close", CTRL2, {}))
    assert signed["result"]["carry_forward"]["changed"] is True                                  # derived again from the new signing
    assert carried()[("G-3950", "DE01", "")] == D("-9900.00")                                    # the minority's 20% of 49,500.00
    assert next(x for x in state()[0].group_periods if x.period_id == "2027-01").status == "collected"      # 2027-01 stood on the old opening: stale