# Group consolidation: the module source.
#
# PROVENANCE. Written 2026-10-22 in the SAP full-scope pass (worktree sap-gl), from:
# - the gl spec version 3, which serves this module its data through one exact route
# (GET /group-reporting/balances, GL-38), the intercompany match (GL-52), the companies, the group chart,
# the statement versions and the FX rates by type; consolidation keeps no copy of any of them
# - IFRS 10 (control, consolidation procedures, changes in ownership), IFRS 3 (acquisition method, goodwill,
# non-controlling interest), IAS 28 (equity method), IAS 21 (translation of foreign operations, the translation
# reserve and its recycling on disposal), IAS 12 (deferred tax on eliminated profit), IFRS 11 (joint operations)
# - SAP S/4HANA Group Reporting (consolidation units, data release, currency translation, intercompany matching,
# eliminations, consolidation of investments, the data and consolidation monitors) as a parity reference only
# - Odoo Enterprise account_consolidation (13 to 16) as the migration source; Odoo 17 and later carry no such
# module (to confirm at the door), so their multi-company databases bring only the company list
#
# The decisions a group accountant checks first:
# - consolidation never posts in a company's books: its entries are its OWN journal (cons_journal) in group currency
# - the data comes from gl's universal journal through one route, with a digest; a company outside these books
# uploads the same shape; nothing is re-keyed from a trial balance printout
# - every automatic entry is keyed by group, period, task, pair and run, so a re-run replaces, never doubles
# - translation, eliminations and the sign-off are gated; a second person signs the group close
#
# APQC PCF v8.0: 9.3.3 Perform consolidations and process eliminations (process ids to confirm against the PCF file; pcf_id null until then).
# Version 3 (2026-10-25, DRAFT for sign-off; the fixes the bricks and the end-to-end wiring found, and wave 3's routes):
# - account_role retained_earnings is listed with its meaning (the companies' own reported retained earnings, on
# which each held company's year result is fixed at year end), separate from group_retained_earnings; both may
# name one group account
# - deferred tax rate: the explicit tax_rate_pct on the adjustment wins, else gl's statutory_tax_rate of the
# company on the period's last day, else refused with gate tax_rate_known
# - PROCESS (propose_structure_change, load_cons_from_odoo, run_eliminations): the structure row's source gl |
# upload is exact (what each requires of gl), the gl migration guard names the statuses it refuses, and
# tax_rate_known is a readiness gate
# - business_timezone is read from gl's GET /companies/{top_company_code} per request, never stored; the lines
# drill-down calls gl's route exactly as gl specifies it (from, to, group_account_id)
# - CON-16: goodwill at disposal is the closing carrying amount, retranslated at the closing rate on the disposal
# date, and its translation difference is recycled with the reserve
# - use cases (MCP tools) of the shared families name the module noun (tool_names)
#
id: consolidation
name: Group consolidation
short: Consolidation
version: 3
maturity: planned
summary: >-
Turn the books of every company in the group into one set of group accounts: translated into the group's
currency, with what the companies sold, lent and charged each other taken out, the parent's investments
replaced by the assets behind them, and the minority's share shown, signed off by a second person each period.
facets:
domain: finance
industries: []
customer_words:
- "group accounts"
- "consolidated accounts"
- "we own 80% of the German company"
- "eliminate intercompany"
- "minority interest"
- "goodwill"
- "translation reserve"
- "the group close"
replaces: [spreadsheets, Excel consolidation packs, SAP Group Reporting, SAP BPC, Odoo Consolidation, LucaNet, CCH Tagetik]
pillars: [sql, graph]
first_module_fit: low
depends_on:
- module: gl
why: every number comes from gl's GET /group-reporting/balances (or an upload in the same shape); the companies, group chart, statement versions and rates are gl's
required: true
- module: business-partner
why: a company's partner (bp_id) is how gl recognises intercompany lines; consolidation reads it only through gl
required: recommended
feeds: []
owns_master_data: [group, group_structure]
references_master_data: [company, group_chart, ledger, fx_rate, statement_version]
actors:
- id: group_accountant
name: Group accountant
does: keeps the group structure and acquisitions, collects and uploads the companies' data, translates, enters intercompany inventory, runs the eliminations, proposes manual consolidation entries, prepares the group close
sees: the whole group
permissions: [cons:group:read, cons:structure:write, cons:data:collect, cons:data:upload, cons:translate:run, cons:eliminate:run, cons:inventory:write, cons:journal:write, cons:journal:reverse, cons:period:open, cons:report:read, cons:settings:write, cons:migration:read]
- id: group_controller
name: Group finance controller
does: approves structure changes, uploads and manual consolidation entries, signs off the group close and reopens it
sees: the whole group
permissions: [cons:group:read, cons:structure:approve, cons:data:accept, cons:journal:approve, cons:period:signoff, cons:period:reopen, cons:report:read, cons:migration:read]
note: "the controller never approves what they proposed or prepared: matched on email AND sub"
- id: auditor
name: Group auditor
does: reads the structure, the collected data, every consolidation entry and the trail to gl
sees: everything, changes nothing
permissions: [cons:group:read, cons:report:read, cons:migration:read]
- id: owner
name: The owner (or whoever the owner names)
does: holds the controls on the people who consolidate (second person for entries and sign-off, whether data may be collected from an open company period, the AI step mode) and signs the migration reconciliation
sees: everything
permissions: [cons:group:read, cons:report:read, cons:settings:controls, cons:migration:read, cons:migration:sign]
note: "a group accountant or controller holding cons:settings:controls is a configuration error the access test refuses"
- id: system
name: The system
does: collects the companies' data from gl, posts the automatic consolidation entries, re-runs what a changed digest made stale, derives the group carry-forward, materialises signed statements, numbers consolidation journals, loads and reconciles a migration
sees: the whole group, through its own service account
permissions: [cons:group:read, cons:data:load, cons:journal:post_auto, cons:carry_forward:derive, cons:statement:materialise, cons:number_series:claim, cons:migration:load, cons:migration:reconcile]
note: "each is an exact grant to consolidation's service client, never reached by a wildcard; the same client holds gl:group_data:extract, gl:company:read, gl:fx:read and gl:period:read on gl"
stories:
# ── person ────────────────────────────────────────────────────────────────────────────────
- id: CON-01
kind: person
actor: group_accountant
story: As the group accountant, I want the group's companies, who owns how much of each, and how each is consolidated written down, so that the group accounts include the right companies the right way.
given: gl companies UK01 (GBP), DE01 (EUR) and FR01 (EUR); group GRP with group currency GBP, ledger IFRS, group chart GROUP, top company UK01
when: I record DE01 owned 80% by UK01 (full) and FR01 30% by UK01 (equity), effective 2026-04, and the controller approves
then: >-
the structure in force on a period's last day decides scope and method (full | equity | none); ownership and
voting are NUMERIC(9,6) above 0 and at most 100 (422 otherwise); a company of source gl must exist in gl (GET /companies), and one of source upload (CON-12) must NOT (v2: see CON-12)
and its investor must be in the group; a change takes effect from a period not yet signed (never back into a
signed period); the proposer cannot approve it (email AND sub); a method full needs an acquisition (CON-02)
screen: Structure
reference: not built
customer_varies: who is in the group and how (control decides full, significant influence equity, IFRS 10 / IAS 28)
- id: CON-02
kind: person
actor: group_accountant
story: As the group accountant, I want an acquisition recorded once with the price, the net assets taken over and the fair-value adjustments, so that goodwill and the minority's share are computed, not typed.
given: UK01 bought 80% of DE01 on 2026-04-01 for 900,000.00 EUR (on UK01's group account G-1600 investment, partner DE01); DE01's equity collected for 2026-03 is 1,000,000.00 EUR (capital 500,000.00, reserves 500,000.00); no fair-value adjustment; rate 0.860000 GBP per EUR on 2026-04-01
when: I record the acquisition and the controller approves it
then: >-
goodwill = consideration - 80% of net assets = 100,000.00 EUR (86,000.00 GBP at the acquisition rate), held in
the subsidiary's currency as an asset of the foreign operation (IAS 21.47),
non-controlling interest = 20% of net assets = 200,000.00 EUR (proportionate share, IFRS 3.19); the equity at
acquisition is fixed at its historical rate (cons_historical_amount); a bargain purchase (negative goodwill) is
shown for review and posted to profit only after the controller approves it; fair-value adjustments are lines of
the acquisition with their deferred tax; amounts NUMERIC(18,2) (422 above)
screen: Structure
reference: not built
- id: CON-03
kind: person
actor: group_accountant
story: As the group accountant, I want each company's figures pulled from its books by group account, so that the group starts from exactly what the companies booked.
given: GRP 2026-10 open; UK01 and DE01 closed for 2026-10 in gl's ledger IFRS; FR01 still open
when: I collect, and collect again after DE01 posts nothing more
then: >-
consolidation calls gl's GET /group-reporting/balances per company and stores the rows (year-to-date and period,
by group account, partner company, movement type, segment, in local currency) with gl's digest; FR01 is refused
(gate company_periods_closed) unless allow_open_period_collect is on, when it is collected and flagged open; a
company with unmapped accounts is refused with the accounts named (fix in gl, GL-41); each company's data nets to
zero or is refused; the second collect with an unchanged digest writes nothing; a changed digest marks every later
step of the period stale (CON-20)
screen: Data
reference: not built
- id: CON-04
kind: person
actor: group_accountant
story: As the group accountant, I want each foreign company translated into the group currency by the rules of IAS 21, so that the translation difference lands in equity and not in profit.
given: DE01 collected for 2026-10 in EUR; closing rate 0.870000 on 2026-10-31 and an average rate for each month 2026-04..2026-10 (gl rate types closing and average); equity at acquisition fixed at 0.860000
when: I translate
then: >-
balance-sheet accounts at the closing rate, income and expense period by period at each month's average rate
(summed year to date), equity at its historical amounts, and each account's rule (cons_account_rule) decides;
the balancing difference posts as one automatic entry to the translation reserve (cta) with the NCI's 20% share on
the NCI account; a missing rate refuses with the currency and month named (fix in gl, GL-33); translated data nets
to zero; translating twice replaces the run's entries (keyed derived_id("cons", group, period, "translation",
company, run_no)), never doubles
screen: Translation
reference: not built
- id: CON-05
kind: person
actor: group_accountant
story: As the group accountant, I want what the companies owe each other taken out, pair by pair, so that the group shows only what it owes the outside world.
given: UK01's G-1300 ic receivable partner DE01 870.00 GBP translated; DE01's G-2300 ic payable partner UK01 868.00 GBP translated (the 2.00 gl break accepted in GL-52); tolerance 5.00
when: I run the intercompany elimination of balances
then: >-
per pair and elimination rule (receivable vs payable, loans), an automatic entry removes both sides; the
difference within tolerance posts to the ic_difference account (a difference from translation to the cta);
above tolerance the run is refused for that pair (gate ic_within_tolerance) with gl's break list (GET
/intercompany/reconciliation) and its fix; keyed per pair and run, so a re-run replaces
screen: Eliminations
reference: not built
- id: CON-06
kind: person
actor: group_accountant
story: As the group accountant, I want intercompany sales, recharges, interest and dividends taken out of the group's profit, so that the group does not report revenue it earned from itself.
given: UK01's G-4100 intercompany revenue partner DE01 5,000.00 and DE01's G-5100 intercompany cost partner UK01 5,000.00 (translated); DE01 paid UK01 a dividend of 40,000.00 EUR in 2026-09
when: I run the elimination of revenue and expense
then: >-
each rule pair (revenue vs cost of sales or expense, interest income vs expense) is removed per pair, the
difference to ic_difference_pl; the dividend income in UK01 is eliminated against DE01's dividend movement in
equity, the NCI bearing its 20% of the dividend; re-runs replace
screen: Eliminations
reference: not built
- id: CON-07
kind: person
actor: group_accountant
story: As the group accountant, I want the profit one company made selling to another taken out while the goods are still in stock, so that the group does not book profit on stock it has not sold.
given: DE01 holds 12,000.00 EUR of goods bought from UK01 at 2026-10-31; UK01's intercompany margin 25%; DE01's tax rate 30%
when: I enter the intercompany inventory and run the elimination
then: >-
unrealised profit = 12,000.00 x 25% = 3,000.00 EUR, translated at closing: Dr cost of sales (UK01) / Cr inventory
(DE01), and deferred tax at the holder's rate: Dr deferred tax asset / Cr tax expense 900.00 EUR (IAS 12); last
year's elimination is realised through opening retained earnings (CON-15); for an upstream sale (from a partly
owned seller) the NCI bears its share; margin_pct NUMERIC(9,6) from 0 to 100, amount NUMERIC(18,2) (422 outside);
a pair with intercompany sales and no inventory entry is listed (gate ic_inventory_entered: enter it, or state none).
v3, the deferred tax rate, one rule for every consolidation adjustment that carries deferred tax (this one, and the
fair-value adjustments of CON-02): the explicit tax_rate_pct on the entry (NUMERIC 9,6, percent, 0 to 100) wins;
absent, gl's statutory_tax_rate of the company that holds the temporary difference (the holder here, the acquired
company for a fair-value adjustment) in force on the period's last day (GET /companies/{company_code}?on=, a
decimal fraction: 0.30 = 30%); absent both, the run is refused (gate tax_rate_known, naming the company), never
computed at zero
screen: Eliminations
reference: not built
- id: CON-08
kind: person
actor: group_accountant
story: As the group accountant, I want the parent's investment replaced by the subsidiary's net assets every period, with goodwill and the minority shown, so that the group balance sheet shows what the group controls.
given: the DE01 acquisition of CON-02, DE01's 2026 result to October 60,000.00 EUR translated, its translation reserve
when: I run the consolidation of investments
then: >-
the investment (G-1600 partner DE01) is eliminated against 80% of the equity at acquisition, goodwill and the NCI
at acquisition are posted; the NCI takes 20% of the post-acquisition result (to the NCI result line) and of the
translation reserve; goodwill (and the fair-value adjustments of CON-02) is retranslated each period at the
closing rate (IAS 21.47): 100,000.00 EUR at a closing 0.870000 shows 87,000.00 GBP, and the 1,000.00 GBP
difference goes to the group's translation reserve (under proportionate measurement the goodwill is the
group's alone, so the NCI takes none of it; the NCI takes its 20% of the fair-value adjustments' difference, as
part of the net assets); goodwill is not amortised under IFRS (impairment is a manual entry, CON-10, in the subsidiary's
currency) and is amortised over goodwill_life_years where the group's ledger is a local GAAP that requires it;
the elimination of the investment leaves exactly zero on G-1600 for DE01 (gate investment_eliminated)
screen: Eliminations
reference: not built
- id: CON-09
kind: person
actor: group_accountant
story: As the group accountant, I want an associate carried at our share of its net assets, so that 30% of its profit shows in one line and nowhere else.
given: FR01 (30%, equity method) acquired 2026-04 for 300,000.00 EUR; FR01's result April to October 50,000.00 EUR; a dividend to UK01 of 6,000.00 EUR
when: I run the equity method
then: >-
FR01's lines are not added line by line; one entry raises the investment by 15,000.00 EUR (30% of the result) to
the share of profit of associates, the dividend reduces the investment and eliminates UK01's dividend income, and
30% of FR01's other comprehensive income moves to the group's OCI; translated at the rule's rates; re-runs replace
screen: Eliminations
reference: not built
- id: CON-10
kind: person
actor: group_accountant
story: As the group accountant, I want to post the entries no rule makes (a goodwill impairment, a fair-value unwind, a reclassification) with a second person's approval, so that a judgement is recorded and checked.
given: an impairment of the DE01 goodwill of 10,000.00 GBP for 2026-10
when: I propose a manual consolidation entry (document type manual, period-specific no) and the controller approves it
then: >-
the entry is balanced in group currency, names company and partner per line and a movement type where the account
tracks movements; it posts with its number in the group's book and year; the proposer cannot approve it (email AND
sub); a period-specific entry reverses at the start of the next period, a cumulative one carries through the year
and into the carry-forward; a signed period refuses it; a correction is a reversal, never an edit
screen: Entries
reference: not built
- id: CON-11
kind: person
actor: group_accountant
story: As the group accountant, I want the group's balance sheet, profit and loss, comprehensive income, changes in equity and cash flow from the consolidated figures, so that the group accounts come out of the system, not a spreadsheet.
given: GRP 2026-10 eliminated, gl's statement versions GROUP-IFRS (balance sheet / P&L) and GROUP-CF (cash flow) published on the group chart
when: I run the reports, by company contribution, by segment, and beside the prior year
then: >-
each line is the sum of translated data and consolidation entries through the version's assignment; each opens to
the companies, to the entries and from a company's figure to gl's line history (GL-04); the statement of changes in
equity reads the movement types (opening, result, dividend, capital change, translation, ownership change); the
cash flow (indirect) shows translation differences on cash as their own line (IAS 7.28); a report of a signed
period is materialised and stable; the report names the version, ledger and run
screen: Reports
reference: not built
- id: CON-12
kind: person
actor: group_accountant
story: As the group accountant, I want a company whose books are kept elsewhere to send its figures in the same shape, so that it is consolidated like the others.
given: US01, kept in another package, with a trial balance mapped to group accounts in USD for 2026-10
when: I upload a CSV of group account, partner company, movement type, segment and year-to-date amount, and the controller accepts it
then: >-
the rows are validated (accounts exist in the group chart, amounts NUMERIC(18,2), the data nets to zero, partners
are group companies) and stored as the company's reported data with source upload, the file kept under
raw/consolidation/<group>/<period>/ with its sha-256; the uploader cannot accept it (email AND sub); a new upload
replaces the previous one and marks later steps stale; a company in gl cannot be uploaded (its data comes from gl). v2: a company kept outside gl is identified by a structure row with source upload: its company_code is chosen by the group (1 to 4 upper-case letters or digits, unique in the group) and gl must NOT know it (refused 422 if gl lists it: one company, one source); the row is proposed and approved like any structure row, and an upload for a company with no source upload row is refused. v3: the row is POST /structure-changes with kind acquisition (or method_change for a company moving out of or into gl) and {company_code, source: upload, local_currency, name, investor_company_code, ownership_pct, voting_pct, method, effective_period}; the upload's amounts are in that local_currency and are translated like a gl company's
screen: Data
reference: not built
- id: CON-13
kind: person
actor: group_controller
story: As the group controller, I want the checks the auditor will ask about run before I sign, so that I sign figures that are known to hold.
given: GRP 2026-10 eliminated
when: I open the group close
then: >-
readiness lists each gate as {gate, met, reason, fix}: every company in scope collected and current, each
company's and the translated data nets to zero, intercompany within tolerance, every investment eliminated,
no manual entry waiting, no step stale, the consolidated trial balance nets to zero and the balance sheet balances;
each failing gate names the company, pair or account
screen: Group close
reference: not built
- id: CON-14
kind: person
actor: group_controller
story: As the group controller, I want to sign off the group's period, so that the consolidated figures are frozen once a second person has checked them.
given: GRP 2026-10 with readiness met, prepared by the group accountant
when: I sign it off, and later reopen it with a reason
then: >-
the sign-off records who, when and the digest of the consolidated trial balance; the preparer cannot sign (email
AND sub); signed figures are materialised and stable, and a later gl posting into a company's closed-and-reopened
period shows as a changed digest without changing the signed figures; reopening needs a reason and a controller
other than the signer, is recorded, and makes the period's figures provisional again
screen: Group close
reference: not built
- id: CON-16
kind: person
actor: group_accountant
story: As the group accountant, I want a subsidiary sold during the year taken out of the group with the gain or loss computed, so that the group result shows the disposal correctly.
given: UK01 sells all of DE01 on 2027-06-30 for 1,200,000.00 EUR
when: I record the divestiture and the controller approves it
then: >-
DE01's profit and loss is consolidated up to 2027-06 and its balance sheet no longer from that period's end; the
gain = proceeds - (the group's share of net assets + goodwill) is posted to gain_on_disposal, and DE01's
translation reserve is recycled to profit (IAS 21.48). v3, exactly: every term is in the group currency at the
disposal date: proceeds and net assets at gl's closing rate on the effective date (GET /fx-rates/EUR?type=closing&on=
2027-06-30); goodwill is its CLOSING carrying amount, i.e. the goodwill held in EUR since acquisition (CON-02 v2),
less any impairment and amortisation to date, retranslated at that same closing rate (never the acquisition rate,
never the last period-end rate); the recycled reserve includes the goodwill's and the fair-value adjustments'
accumulated retranslation differences, so the gain plus the recycled reserve equals the gain computed in EUR
translated once; the NCI is derecognised; the structure row has method none
from 2027-07; an effective period already signed is refused
screen: Structure
reference: not built
- id: CON-17
kind: person
actor: group_accountant
story: As the group accountant, I want a change in how much we own handled by the standards, so that buying more of a subsidiary or taking control of an associate is not booked as ordinary profit.
given: UK01 buys another 10% of DE01 (80% to 90%) for 120,000.00 EUR effective 2027-01; and in another case gains control of FR01 (30% to 60%)
when: I record each ownership change and the controller approves it
then: >-
without a change of control the difference between the price and the NCI given up goes to group equity (IFRS
10.23), not to profit, and the NCI share is 10% from 2027-01; on obtaining control the previously held 30% is
remeasured at fair value with the gain in profit (IFRS 3.42), goodwill is computed at that date and the method
becomes full; every change is a dated structure row
screen: Structure
reference: not built
- id: CON-18
kind: person
actor: owner
story: As the owner moving off Odoo, I want the consolidation history reconciled against Odoo and to sign it, so that the prior group figures we compare with are the ones we filed.
given: a loaded consolidation migration run
when: the checks are computed and I sign
then: every check is equal or explained where explaining is allowed; carried consolidated balances per group account and period must be equal; signing records who, when and the run's checksums
screen: Migration
reference: not built
- id: CON-19
kind: person
actor: owner
story: As the owner, I want the controls on the group close held by me, so that the group accountant cannot switch off the second person for their own entries.
given: a group accountant holding cons:settings:write and an owner holding cons:settings:controls
when: the accountant tries to set signoff_second_person false, then the owner sets allow_open_period_collect true from tomorrow
then: the accountant is refused (a control); the owner's row takes effect from tomorrow (business date in business_timezone), never today or earlier (a dated row starts tomorrow by default, and only the FIRST row of a key (how a setting is first set up) may start today, as in config_rows); every control row's history is readable
screen: Settings
reference: not built
# ── system ────────────────────────────────────────────────────────────────────────────────
- id: CON-15
kind: system
actor: system
trigger: when the first period of a group's year opens, and again whenever a signed period of the previous year is reopened and signed again
story: The system carries the consolidation entries' balance-sheet effects into the new year, so that goodwill, NCI, eliminated profit in stock and the translation reserve open where they closed.
given: GRP FY2026 signed through 2026-12 with goodwill, NCI, a 3,000.00 EUR stock profit elimination and a translation reserve
when: FY2027's first period opens, and opens again
then: >-
cumulative entries' balance-sheet lines open FY2027 (per group account, company, partner, segment); their P&L
effect opens on group retained earnings; last year's stock profit elimination is realised this year (the opening
moves to cost of sales when the 2027 elimination is run); re-deriving writes the same rows (keyed, every value in
VALUES); a stale carry-forward blocks the year's first sign-off (gate carry_forward_current)
reference: not built
- id: CON-20
kind: system
actor: system
trigger: whenever a collected company's digest changes, an upload replaces another, the structure changes, or a rate used changes in gl (a new rate for a date already used is refused by gl, so in practice a back-posting)
story: The system marks every later step of the period stale and reverses its automatic entries on the next run, so that the group never shows translation or eliminations computed on yesterday's data.
given: GRP 2026-10 eliminated; DE01 2026-10 reopened in gl and a 500.00 EUR accrual posted
when: the group accountant collects again
then: >-
DE01's new digest is stored, translation and eliminations become stale (the period moves back to collected), the
next translate and eliminate runs take run_no + 1 and their keyed entries replace run 1's (run 1 reversed, never
deleted); a signed period is not touched: its figures stay as signed and the change is shown as a difference
until it is reopened
reference: not built
- id: CON-21
kind: system
actor: system
trigger: on sign-off
story: The system materialises the group's statements of a signed period, so that a signed report reads the same every time.
given: GRP 2026-10 signed
when: the statements are materialised, twice
then: the stored figures equal a fresh computation at the sign-off digest, and the second run changes nothing (keyed report, group, period, node)
reference: not built
# ── integration ───────────────────────────────────────────────────────────────────────────
- id: CON-30
kind: integration
actor: system
trigger: when a company's data is collected (CON-03)
story: Consolidation reads each company's balances by group account from gl, through one exact route, so that nothing is re-keyed.
given: UK01 2026-10 closed in ledger IFRS
when: consolidation calls GET /group-reporting/balances?company_code=UK01&ledger_id=IFRS&period_id=2026-10
then: the rows, the period status, the functional currency, the unmapped accounts and the digest come back; consolidation stores them as reported data and never reads gl_ tables
owner: gl
reference: not built
- id: CON-31
kind: integration
actor: system
trigger: when the intercompany elimination runs (CON-05, CON-06)
story: Consolidation reads gl's intercompany match per pair, so that an accepted difference is not argued again in the group.
given: gl's match UK01/DE01 as at 2026-10-31 with one accepted break of 2.00
when: consolidation calls GET /intercompany/reconciliation?company_code=UK01&partner_company=DE01&as_at=2026-10-31
then: the matched items and breaks come back; an accepted break is eliminated to ic_difference; an unaccepted one above tolerance blocks the pair; no match run answers 404 with its fix (run match_intercompany in gl)
owner: gl
reference: not built
- id: CON-32
kind: integration
actor: system
trigger: when the group translates (CON-04) or records an acquisition (CON-02)
story: Consolidation takes closing, average and spot rates from gl, so that the group and the companies use the same rates.
given: gl rates EUR to GBP of type closing on 2026-10-31 and average for each month
when: consolidation calls GET /fx-rates/EUR?on=2026-10-31&type=closing&to=GBP, and type=average for each month's last day
then: the rates in force come back; a 404 names the currency, type and date and blocks translation with gl's fix
owner: gl
reference: not built
- id: CON-33
kind: integration
actor: system
trigger: when the structure is kept (CON-01) and when reports run (CON-11)
story: Consolidation reads the companies, the group chart and the statement versions from gl, so that it keeps no second copy of any.
given: gl companies, the group chart GROUP and published statement versions on it
when: consolidation calls GET /companies, GET /charts/GROUP/accounts and GET /statement-versions/{fsv_id}
then: the lists come back; a structure row of source gl naming a company gl does not know is refused (a row of source upload, CON-12, is the one way a company gl does not know enters the structure); a report on a version that is not published is refused
owner: gl
reference: not built
- id: CON-34
kind: integration
actor: system
trigger: once at cut-over (and again after a fix, before signing)
story: Consolidation carries Odoo's consolidation charts, periods and journals where the database has account_consolidation, so that the prior group figures are comparable.
given: an Odoo 16 Enterprise database with account_consolidation; gl's migration signed (companies and accounts mapped)
when: the run loads
then: "the structure per period, the account rules and the consolidation journals of closed periods written read-only, keyed on the Odoo record; a second run changes nothing; a signed run is never rewritten"
owner: consolidation
reference: not built
processes:
- key: group_close
pcf_id: null # APQC 9.3.3 Perform consolidations and process eliminations: id to confirm
pcf_name: Perform consolidations and process eliminations
reference_names: ["Record to Report", "Group close"]
brick: consolidation
- key: group_structure
pcf_id: null
pcf_name: Maintain the group structure
reference_names: ["Record to Report"]
brick: consolidation
workflows:
- entity: group_period
states: [open, collected, translated, eliminated, signed]
transitions:
- { key: open_group_period, name: Open the group period, from: none, to: open, by: group_accountant, gate: "the structure is approved for the period; the previous period is signed (or this is the first)", effect: "scope and methods fixed from the structure in force on the period's last day; for a year's first period the group carry-forward derived (CON-15)" }
- { key: upload_reported_data, name: Upload a company's figures, from: open, to: open, by: group_accountant, gate: "the company is in scope with a structure row of source upload; the file validates", effect: "rows stored with source upload, waiting for acceptance (CON-12)" }
- { key: accept_reported_upload, name: Accept the upload, from: open, to: open, by: group_controller, approval: true, gate: "the accepter is not the uploader (email AND sub)", effect: "the rows become the company's reported data" }
- { key: collect_group_data, name: Collect the companies' figures, from: open, to: collected, by: [group_accountant, system], gate: "every company in scope collected from gl or by an accepted upload; each nets to zero; no unmapped accounts; the gl period closed (unless allow_open_period_collect)", effect: "reported data with gl's digest per company (CON-03)" }
- { key: recollect_group_data, name: Collect again, from: [collected, translated, eliminated], to: collected, by: [group_accountant, system], gate: "a company's digest changed, an upload was replaced, or the structure changed", effect: "later steps stale; the next runs take run_no + 1 (CON-20)" }
- { key: translate_currencies, name: Translate into the group currency, from: collected, to: translated, by: group_accountant, gate: "a closing rate on the period end and an average rate for every month of the year so far, for every foreign company's currency", effect: "translated data and one translation-reserve entry per company, with the NCI share (CON-04)" }
- { key: run_eliminations, name: Run the eliminations, from: translated, to: eliminated, by: group_accountant, gate: "intercompany within tolerance or accepted per pair; intercompany inventory entered or stated none per selling pair; an approved acquisition for every full and equity company", effect: "automatic entries for intercompany balances, revenue and expense, dividends, stock profit, investments, NCI and the equity method, keyed per task, pair and run (CON-05 to CON-09)" }
- { key: sign_off_group_close, name: Sign off the group close, from: eliminated, to: signed, by: group_controller, approval: true, gate: "readiness met (CON-13); the signer is not the preparer (email AND sub)", effect: "who, when and the digest recorded; statements materialised (CON-21)" }
- { key: reopen_group_period, name: Reopen the group period, from: signed, to: collected, by: group_controller, requires: reason, approval: true, gate: "the reopener is not who signed it; the next period is not signed", effect: "recorded; the figures provisional; the next runs take run_no + 1" }
readiness:
- { transition: open_group_period, gate: structure_approved, reason: "a structure change effective in the period waits for approval", fix: "Structure: ask the controller to approve it" }
- { transition: open_group_period, gate: prior_signed, reason: "the previous group period is not signed", fix: "sign it first" }
- { transition: collect_group_data, gate: company_periods_closed, reason: "a company's period is open in gl, named", fix: "close it in gl, or the owner allows collecting from open periods" }
- { transition: collect_group_data, gate: group_mapped, reason: "accounts with balances have no group account in gl, named", fix: "gl Accounts: map them (GL-41)" }
- { transition: collect_group_data, gate: data_nets_to_zero, reason: "a company's data does not net to zero, named with the difference", fix: "a gl fault or a wrong upload: fix and collect again" }
- { transition: collect_group_data, gate: all_companies_present, reason: "companies in scope have no data, named", fix: "collect from gl or upload and accept" }
- { transition: translate_currencies, gate: rates_present, reason: "no closing or average rate for a currency and month, named", fix: "gl Currencies: enter the rate (GL-33)" }
- { transition: run_eliminations, gate: ic_within_tolerance, reason: "a pair's difference is above tolerance and not accepted in gl, named", fix: "fix it in gl or accept it there (GL-52)" }
- { transition: run_eliminations, gate: ic_inventory_entered, reason: "a selling pair has intercompany sales and no inventory entry", fix: "Eliminations: enter it, or state none" }
- { transition: run_eliminations, gate: acquisitions_recorded, reason: "a full or equity company has no approved acquisition", fix: "Structure: record it (CON-02)" }
- { transition: run_eliminations, gate: tax_rate_known, reason: "an intercompany inventory entry of {company} has no tax rate, and gl has no statutory_tax_rate for {company} on {period_end}", fix: "enter the rate on the entry, or set statutory_tax_rate in gl (GL settings)" }
- { transition: sign_off_group_close, gate: no_stale_step, reason: "a step is stale after a new collection", fix: "translate and eliminate again" }
- { transition: sign_off_group_close, gate: no_proposed_entry, reason: "manual consolidation entries wait for approval", fix: "approve or reject them (CON-10)" }
- { transition: sign_off_group_close, gate: investment_eliminated, reason: "an investment in a subsidiary is not fully eliminated, named", fix: "run the eliminations; check the acquisition" }
- { transition: sign_off_group_close, gate: trial_balance_zero, reason: "the consolidated trial balance does not net to zero", fix: "a fault: stop and report it" }
- { transition: sign_off_group_close, gate: carry_forward_current, reason: "the group carry-forward is older than a change to the previous year", fix: "the system re-derives it (CON-15)" }
- { transition: sign_off_group_close, gate: second_person, reason: "you prepared this period (ran a step or posted an entry)", fix: "ask another controller" }
- { transition: reopen_group_period, gate: second_person, reason: "you signed it", fix: "ask another controller" }
- entity: cons_journal
states: [proposed, posted, rejected, reversed]
terminal: [rejected, reversed]
transitions:
- { key: post_cons_auto_entry, name: Post an automatic consolidation entry, from: none, to: posted, by: system, gate: "balanced in group currency; the period not signed; keyed derived_id('cons', group, period, task, company, partner, run_no)", effect: "numbered in the group's book and year; a replay answers the entry posted; the previous run's entry for the key reversed" }
- { key: propose_cons_entry, name: Propose a manual consolidation entry, from: none, to: proposed, by: group_accountant, gate: "balanced in group currency; every line names a company in scope; the period not signed", effect: "proposer recorded (email and sub); document type and period-specific flag recorded" }
- { key: approve_cons_entry, name: Approve the entry, from: proposed, to: posted, by: group_controller, approval: true, gate: "the approver is not the proposer (email AND sub) where signoff_second_person is on; the period not signed", effect: "numbered; a period-specific entry's reversal due at the next period's start" }
- { key: reject_cons_entry, name: Reject the entry, from: proposed, to: rejected, by: group_controller, requires: reason }
- { key: reverse_cons_entry, name: Reverse the entry, from: posted, to: reversed, by: [group_accountant, system], requires: reason, gate: "the period not signed", effect: "a mirror entry keyed derived_id(cons_journal_id, 'reversal'); neither deleted" }
readiness:
- { transition: approve_cons_entry, gate: second_person, reason: "you proposed it", fix: "ask another controller" }
- { transition: propose_cons_entry, gate: period_not_signed, reason: "the group period is signed", fix: "post it in the next period, or reopen" }
- entity: structure_change
states: [proposed, effective, rejected]
terminal: [effective, rejected]
transitions:
- { key: propose_structure_change, name: Propose a structure change, from: none, to: proposed, by: group_accountant, gate: "kind acquisition | ownership_change | divestiture | method_change; v3, by the row's source: source gl (the default) = gl knows the company (GET /companies/{company_code} answers 200) and its functional currency is gl's; source upload (CON-12) = gl does NOT know it (404), company_code matches ^[A-Z0-9]{1,4}$ and is unique in the group, and the row names local_currency (ISO 4217) and a name; ownership within (0, 100]; the effective period not signed; every fair-value adjustment has a tax rate (tax_rate_known)", effect: "goodwill, NCI, gain or equity effect computed from the collected data and shown (CON-02, CON-16, CON-17)" }
- { key: approve_structure_change, name: Approve the structure change, from: proposed, to: effective, by: group_controller, approval: true, gate: "the approver is not the proposer (email AND sub); the effective period still not signed", effect: "the dated structure row written; historical amounts fixed; later periods' steps stale" }
- { key: reject_structure_change, name: Reject the structure change, from: proposed, to: rejected, by: group_controller, requires: reason }
readiness:
- { transition: approve_structure_change, gate: second_person, reason: "you proposed it", fix: "ask another controller" }
- { transition: propose_structure_change, gate: period_not_signed, reason: "the effective period is signed", fix: "make it effective from the next unsigned period" }
- { transition: propose_structure_change, gate: equity_collected, reason: "the company's equity at the effective date is not collected", fix: "collect the period before the effective one" }
- { transition: propose_structure_change, gate: source_matches_gl, reason: "source gl names a company gl does not know, or source upload names one gl knows ({company_code})", fix: "source upload for a company kept outside gl (CON-12), source gl for one of gl's companies" }
- { transition: propose_structure_change, gate: tax_rate_known, reason: "a fair-value adjustment has no tax rate, and gl has no statutory_tax_rate for {company} on {effective_date}", fix: "enter the rate on the adjustment, or set it in gl" }
- entity: cons_migration_run
states: [loaded, reconciled, signed, discarded]
terminal: [signed, discarded]
transitions:
- { key: load_cons_from_odoo, name: Load Odoo's consolidation, from: none, to: loaded, by: system, gate: "the doors decided; gl's migration signed (v3: GET /migration/status?db=<the run's Odoo database> answers status signed; none, loaded or reconciled refuse with gate gl_signed naming the status; nothing after gl's cutover_date is loaded)", effect: "group, structure per period, account rules and closed periods' consolidation journals written read-only, keyed on the Odoo record" }
- { key: reconcile_cons_migration, name: Reconcile against Odoo, from: loaded, to: reconciled, by: system, effect: "every migration.odoo.reconcile check computed on both sides" }
- { key: explain_cons_difference, name: Explain each difference, from: reconciled, to: reconciled, by: group_accountant, requires: reason, gate: "the check differs and may be explained" }
- { key: reload_cons_migration, name: Load again after a fix, from: [loaded, reconciled], to: loaded, by: system, gate: "not signed" }
- { key: sign_cons_reconciliation, name: Sign the reconciliation, from: reconciled, to: signed, by: owner, approval: true, gate: "every check equal, or explained where allowed", effect: "who, when and checksums recorded" }
- { key: discard_cons_migration, name: Discard the run, from: [loaded, reconciled], to: discarded, by: owner, requires: reason }
readiness:
- { transition: load_cons_from_odoo, gate: gl_signed, reason: "gl's migration of {db} is {status} (none | loaded | reconciled), not signed", fix: "sign gl's reconciliation of {db} first (gl GL-34)" }
- { transition: sign_cons_reconciliation, gate: zero_unexplained, reason: "checks neither equal nor explained: named", fix: "explain each, or fix and reload" }
- { transition: sign_cons_reconciliation, gate: must_equal, reason: "the carried consolidated balance per group account and period differs", fix: "fix the mapping and reload" }
entities:
- id: group
meaning: a set of companies reported together, in one currency, from one gl ledger
natural_key: group_id VARCHAR ("GRP"; the customer's code)
key_attributes: [name, group_currency (ISO 4217), ledger_id (gl), group_chart_id (gl), top_company_code, fiscal_year_end_month, fsv_id (gl), goodwill_life_years (null = not amortised)]
master_data: SHARED, sole writer
reference_table: cons_group
- id: group_structure
meaning: who owns how much of a company and how it is consolidated, dated by period
natural_key: [group_id, company_code, valid_from_period]
key_attributes: [investor_company_code, ownership_pct (NUMERIC 9,6), voting_pct (NUMERIC 9,6), method (full | equity | none), source (gl | upload; v2: gl = a company of gl, GET /companies; upload = kept outside gl, its data arrives by CON-12; v3: required, default gl), "local_currency (v3: ISO 4217; required for source upload, where gl does not know the company; for source gl it is gl's functional_currency and is not stored)", "name (v3: for source upload only)", change_id]
rules: >-
v3: a company's source applies from the row's valid_from_period. A company kept outside gl that later moves
into gl (its books migrated) gets a new row of source gl from a later unsigned period, by a method_change
structure change; earlier periods keep their accepted uploads and are never recollected from gl. The group's
top company is always of source gl (it carries the business_timezone read, CON-33).
relationships: ["-> company (gl, company_code; only when source is gl)", "-> structure_change"]
master_data: SHARED, sole writer; written only by approve_structure_change
reference_table: cons_group_structure
note: "the structure is also a graph: investor -> company edges with ownership, walked for indirect ownership (UK01 80% of DE01 which owns 100% of DE02 gives 80%)"
- id: structure_change
meaning: an acquisition, an ownership change, a divestiture or a method change, with what it computed
natural_key: change_id = "<group_id>:<company_code>:<effective_period>:<kind>"
key_attributes: [kind, effective_date, consideration, proceeds, previously_held_fair_value, net_assets_json, fair_value_adjustments_json, goodwill, nci, gain_loss, rate, status, proposed_by, proposed_sub, approved_by, approved_sub]
master_data: module-owned
reference_table: cons_structure_change
- id: account_rule
meaning: how a group account is translated and whether it takes part in an elimination
natural_key: [group_id, group_account_id, valid_from_period]
key_attributes: [rate_type (closing | average | historical), ic (bool), equity (bool)]
master_data: module-owned config
reference_table: cons_account_rule
- id: account_role
meaning: which group account receives each kind of consolidation result (translation reserve, NCI, goodwill, differences)
natural_key: [group_id, role, valid_from_period]
key_attributes: [group_account_id]
master_data: module-owned config
reference_table: cons_account_role
note: "roles: cta, nci_equity, nci_result, nci_oci, goodwill, ic_difference_bs, ic_difference_pl, investment, share_of_associates, group_retained_earnings, deferred_tax_asset, tax_expense, cost_of_sales, inventory, dividend_income, gain_on_disposal, bargain_purchase_gain, fair_value_step_up, deferred_tax_liability, oci (v2), retained_earnings (v3). Two retained-earnings roles, kept apart because they hold different things: group_retained_earnings takes the P&L effect of the CONSOLIDATION entries carried into a new year (CON-08) and the reserve recycled on a disposal; retained_earnings is the group account carrying the companies' OWN reported retained earnings, on which, at the group's fiscal year end, each held subsidiary's and associate's result for the year is fixed as a historical amount (translated at the year's rates and never retranslated), so the equity eliminated against the investment stays at historical rates (IAS 21.48 with IAS 27). Both may name the same group account; retained_earnings is required in a year's last period when a full or equity company is held (gate account_roles_complete), group_retained_earnings when a carry-forward runs"
- id: elimination_rule
meaning: a pair of group-account sets eliminated against each other per pair of companies
natural_key: [group_id, rule_id]
key_attributes: [kind (ic_balance | ic_pl | dividend), left_accounts, right_accounts, difference_role, tolerance (NUMERIC 18,2, group currency)]
master_data: module-owned config
reference_table: cons_elimination_rule
- id: group_period
meaning: one period of the group close and where it stands
natural_key: [group_id, period_id]
key_attributes: [status, run_no, stale_from_step, prepared_by, prepared_sub, signed_by, signed_sub, signed_at, tb_digest, reopened_reason]
master_data: module-owned
reference_table: cons_group_period
- id: reported_data
meaning: a company's figures for a period by group account, as gl (or an upload) gave them
natural_key: [group_id, period_id, company_code, group_account_id, partner_company, movement_type, segment]
key_attributes: [ytd_local, period_local, local_currency, source (gl | upload), gl_digest, period_status, collected_at]
master_data: module-owned
reference_table: cons_reported_data
- id: upload
meaning: a file of reported data from a company kept elsewhere, and its acceptance
natural_key: [group_id, period_id, company_code, upload_no]
key_attributes: [object_key, sha256, rows, status (submitted | accepted | replaced), submitted_by, submitted_sub, accepted_by, accepted_sub]
master_data: module-owned
reference_table: cons_upload
- id: translated_data
meaning: a company's figures in the group currency, with the rate that made each
natural_key: [group_id, period_id, run_no, company_code, group_account_id, partner_company, movement_type, segment]
key_attributes: [ytd_group, rate_type, rate]
master_data: module-owned (derived per run)
reference_table: cons_translated_data
- id: historical_amount
meaning: an equity account's amount fixed in group currency at the rate of the day it arose
natural_key: [group_id, company_code, group_account_id, arose_period]
key_attributes: [amount_local, amount_group, rate, change_id]
master_data: module-owned
reference_table: cons_historical_amount
- id: ic_inventory
meaning: stock a company holds that it bought from another group company, and the seller's margin
natural_key: [group_id, period_id, holder_company, seller_company]
key_attributes: [inventory_amount (NUMERIC 18,2, holder's currency), margin_pct (NUMERIC 9,6), "tax_rate_pct (NUMERIC 9,6, percent, optional: when absent, gl's statutory_tax_rate of the holder on the period's last day; v3)", none_stated (bool), entered_by, entered_sub]
master_data: module-owned
reference_table: cons_ic_inventory
- id: cons_journal
meaning: one consolidation entry in group currency, automatic or manual (its own ledger; never in a company's books)
natural_key: 'cons_journal_id BIGINT = derived_id("cons", group_id, period_id, task, company_code, partner_company, run_no) for an automatic entry, derived_id("cons-manual", uuid) for a manual one, derived_id(cons_journal_id, "reversal") for a reversal'
key_attributes: [group_id, period_id, document_type (translation | ic_balance | ic_pl | dividend | stock_profit | investment | nci | equity_method | disposal | ownership_change | manual | carry_forward | migration), auto, run_no, period_specific, status, entry_no, proposed_by, proposed_sub, approved_by, approved_sub, reverses_id, memo, migration_run_id]
relationships: ["1..n cons_journal_line", "-> group_period"]
master_data: module-owned
reference_table: cons_journal
- id: cons_journal_line
meaning: one line of a consolidation entry
natural_key: [cons_journal_id, line_no]
key_attributes: [group_account_id, company_code, partner_company, movement_type, segment, amount (NUMERIC 18,2, group currency, debit positive)]
master_data: module-owned
reference_table: cons_journal_line
- id: number_series
meaning: the consolidation entry numbers of a group and year
natural_key: [group_id, fiscal_year, entry_no]
key_attributes: [cons_journal_id, claimed_at]
master_data: module-owned (state)
reference_table: cons_number_series
note: "claimed with a plain INSERT of MAX+1, retried on conflict; never reused; a rejected manual entry takes none"
- id: carry_forward
meaning: the accumulated balance-sheet effect of consolidation entries at a year's start
natural_key: [group_id, fiscal_year, group_account_id, company_code, partner_company, segment]
key_attributes: [opening, derived_at, source_digest]
master_data: module-owned (derived)
reference_table: cons_carry_forward
- id: statement
meaning: a materialised group statement of a signed period
natural_key: [report, group_id, period_id, node_id]
key_attributes: [amount, prior_year, produced_at, digest]
master_data: module-owned (derived)
reference_table: cons_statement
- id: audit
meaning: what happened in the group close and to each entry (append only)
natural_key: audit_id
key_attributes: [group_id, period_id, subject, event, by, sub, event_at]
master_data: module-owned
reference_table: cons_audit
- id: ai_finding
meaning: an AI explanation of an elimination difference and the person's decision
natural_key: finding_id
key_attributes: [group_id, period_id, pair, model, inputs_digest, finding, evidence, decision, decided_by, decided_at]
master_data: module-owned (append only)
reference_table: cons_ai_finding
- id: settings
meaning: dated settings and owner controls
natural_key: [setting_key, scope, valid_from]
key_attributes: [value, set_by, set_at]
master_data: module-owned config
reference_table: cons_settings
- id: migration_run
meaning: one Odoo consolidation load, its checks and its signature
natural_key: run_id = "odoo:<db>:<cutover_date>"
key_attributes: [status, doors_json, checks_json, signed_by, signed_at, checksums]
master_data: module-owned
reference_table: cons_migration_run
- id: migration_map
meaning: (odoo db, odoo model, odoo id) -> our natural key (shared.yaml conventions.odoo_migration)
natural_key: [odoo_db, odoo_model, odoo_id]
key_attributes: [natural_key, run_id]
master_data: module-owned; read through GET /migration-map/{db}/{model}
reference_table: cons_migration_map
master_data:
owns: [cons_group, cons_group_structure]
references:
- entity: company
owner: gl
how: "company_code on every row, read through GET /companies"
if_absent: "no consolidation: a company exists only in gl"
- entity: group_chart
owner: gl
how: "group_account_id (gl account_id of the group chart), read through GET /charts/{chart_id}/accounts"
if_absent: "nothing to report by"
- entity: fx_rate
owner: gl
how: "GET /fx-rates/{currency}?type=closing | average"
if_absent: "translation is refused with the rate named"
- entity: statement_version
owner: gl
how: "GET /statement-versions/{fsv_id}"
if_absent: "reports by group account only"
warning: >-
The companies, the group chart, the rates and the statement versions are gl's. A consolidation that keeps its own
rate table or its own mapping of company accounts is how the group's figures stop agreeing with the companies'.
jobs:
- story: CON-15
trigger: when a year's first group period opens, and after a re-signed prior-year period
runs: derive the group carry-forward
where_options: [in the open_group_period request, the composition's worker]
idempotent: "keyed [group_id, fiscal_year, group_account_id, company_code, partner_company, segment]; every value in VALUES"
- story: CON-20
trigger: when a collection, upload or structure change changes the inputs
runs: mark later steps stale; reverse and replace their automatic entries on the next run
where_options: [in the collect request]
idempotent: "run_no claimed with a plain INSERT; entries keyed per task, pair and run"
- story: CON-21
trigger: on sign-off
runs: materialise the group statements
where_options: [in the sign-off request]
idempotent: "keyed (report, group_id, period_id, node_id)"
seams:
- story: CON-30
with: gl
owner: gl
route: "GET /group-reporting/balances"
permission: gl:group_data:extract
request: query company_code, ledger_id, period_id (YYYY-MM)
response: '200 {company_code, ledger_id, period_id, fiscal_year, period_status: open | closed, functional_currency, group_chart_id, digest, rows: [{group_account_id, account_type (asset | liability | equity | income | expense, of the group account; v6), sign_class (balance_sheet | income_statement; v6: asset, liability and equity are balance_sheet, income and expense income_statement), partner_company ("" when none), movement_type ("" when none), segment ("" when none), ytd_amount, period_amount}], unmapped: [{account_id, code, ytd_amount}]}; amounts are strings, NUMERIC(18,2), debit positive, in the functional currency; 404 when the company is not visible; 422 {gate: ledger_assigned, reason, fix}'
rule: 'the gl spec''s GL-38 seam; consolidation stores the rows as cons_reported_data and never reads gl_ tables; this call: query company_code, ledger_id (the group''s), period_id'
status: aligned 2026-10-22 with gl GL-38
- story: CON-31
with: gl
owner: gl
route: "GET /intercompany/reconciliation"
permission: gl:group_data:extract
request: query company_code, partner_company, as_at (YYYY-MM-DD)
response: '200 {match_id, as_at, matched: [{key, amount, currency}], breaks: [{key, ours, theirs, currency, difference, within_tolerance, accepted, reason, fix}], run_at}; 404 when no match was run (fix: run match_intercompany)'
rule: 'gl''s GL-52 result; consolidation eliminates accepted differences and blocks the pair on the rest; this call: query company_code, partner_company, as_at'
status: aligned 2026-10-22 with gl GL-38
- story: CON-32
with: gl
owner: gl
route: "GET /fx-rates/{currency}"
permission: gl:fx:read
request: path currency; query on=YYYY-MM-DD (the newest rate of the type dated on or before it) OR, for type=average, month=YYYY-MM (the average dated that month's last day, exactly; v2), to=the group currency
response: "200 {currency, to, type, on, rate_date, rate, source}; 404 {gate: fx_rate, reason, fix}; month is valid only with type=average and never answers a nearer date"
rule: 'consolidation keeps no rates; historical amounts are fixed amounts, not rates; this call: path currency; query on, type (closing | average | spot), to (the group currency)'
status: aligned 2026-10-22 with gl GL-38
- story: CON-33
with: gl
owner: gl
route: "GET /companies"
permission: gl:company:read
request: "none"
response: '200 [{company_code, name, country, functional_currency, chart_id, bp_id, financial_year_end_month, ledgers: [ledger_id]}]'
rule: "a structure row names a company from this list"
status: aligned 2026-10-22 with gl GL-38
- story: CON-33
with: gl
owner: gl
route: "GET /charts/{chart_id}/accounts"
permission: gl:company:read
request: "path chart_id (the group chart)"
response: "200 [{account_id, code, name, type, subtype}]"
rule: "the consolidation items are the group chart's accounts"
status: aligned 2026-10-22 with gl GL-38
- story: CON-33
with: gl
owner: gl
route: "GET /statement-versions/{fsv_id}"
permission: gl:report:read
request: "path fsv_id; query version_no (optional; the published one)"
response: '200 {fsv_id, version_no, kind, chart_id, status, nodes: [{node_id, parent_node_id, seq, label, sign, cash_flow_class}], assignments: [{from_code, to_code, node_id, dc_rule, other_node_id, basis}]}; 404 when not published'
rule: "consolidation applies the published version of the group chart to its consolidated figures; it never edits it"
status: aligned 2026-10-22 with gl GL-53
- story: CON-33
with: gl
owner: gl
route: "GET /periods/open"
permission: gl:period:read
request: "query date (the period's last day), company_code"
response: "200 {date, company_code, period_id, open, status, reason, fix}"
rule: "the gate company_periods_closed"
status: aligned 2026-10-22 with gl GL-38
- story: CON-34
with: gl
owner: gl
route: "GET /migration-map/{db}/{model}?ids="
permission: gl:migration:read
request: path db, model res.company | account.account; query ids=<comma-separated Odoo ids>
response: '200 [{odoo_model, odoo_id, ours: company_code | account_id}]; an id not migrated is omitted'
rule: 'the one map route; consolidation''s migration runs after gl''s; this call: path db, model res.company | account.account; query ids'
status: aligned 2026-10-22 with gl GL-34
# ── added 2026-10-24, seam-gap PR 2 (the gl reads the build found missing) ──
- story: CON-33
with: gl
owner: gl
route: GET /companies/{company_code}
permission: gl:company:read
request: 'path company_code; query on (v3: YYYY-MM-DD, the period''s last day, for statutory_tax_rate)'
request_schema:
- { field: company_code, type: company_code, required: true, in: path }
- { field: "on", type: date, required: false, in: query }
response: '200 {company_code, name, country, functional_currency, chart_id, bp_id, financial_year_end_month, ledgers: [ledger_id], statutory_tax_rate (string or null, the row in force on `on`), business_timezone}; 404 when unknown or not visible'
rule: 'v3: deferred tax uses the entry''s own tax_rate_pct, else this statutory_tax_rate on the period''s last day (a decimal fraction, x 100 for percent), else gate tax_rate_known refuses (CON-07). "today" for dated rows and periods is business_timezone, read from GET /companies/{top_company_code} of the group (the bucket''s one value, shared.yaml conventions.business_timezone) at each request that needs it and never stored; the top company is always of source gl'
status: "added 2026-10-24, seam-gap PR 2"
- story: CON-34
with: gl
owner: gl
route: "GET /migration/status?db="
permission: gl:migration:read
request: 'query db (the Odoo database)'
response: '200 {db, run_id, status (none | loaded | reconciled | signed), cutover_date, reconciled_at, signed_by, signed_at}'
request_schema:
- { field: db, type: string, required: true, in: query }
rule: 'v3: load_cons_from_odoo calls it first with the run''s db; only status signed proceeds; none, loaded or reconciled refuse 409 {gate: gl_signed, met: false, reason (naming the status), fix: "sign gl''s reconciliation of <db> first"}; consolidation loads no period after cutover_date'
status: "added 2026-10-24, seam-gap PR 2"
- story: CON-11
with: gl
owner: gl
route: "GET /accounts/{account_id}/lines"
permission: gl:journal:read
request: 'path account_id (v3: the group account, the same value as group_account_id); query company_code, ledger_id (the group''s ledger_id), from (YYYY-MM-DD: the first day of the company''s fiscal year for a year-to-date figure, of the period for a period figure), to (YYYY-MM-DD: the period''s last day), group_account_id, limit (500), offset'
request_schema:
- { field: account_id, type: account_id, required: true, in: path }
- { field: company_code, type: company_code, required: true, in: query }
- { field: ledger_id, type: ledger_id, required: false, in: query }
- { field: from, type: date, required: true, in: query }
- { field: to, type: date, required: true, in: query }
- { field: group_account_id, type: account_id, required: true, in: query }
- { field: limit, type: integer, required: false, in: query }
- { field: offset, type: integer, required: false, in: query }
response: '200 {account_id, company_code, ledger_id, from, to, opening_balance, lines: [{account_id (v3: the local account), journal_id, entry_no, line_no, journal_date, source_module, source_document, bp_id, memo, debit, credit, running_balance}], closing_balance, more}'
rule: 'the report drill-down from a group figure to the lines behind it, per company (gl GL-04). v3: the call is exactly gl''s spec (from and to, never date_from / date_to); amounts are local currency, before translation; a company of source upload has no gl lines: its drill stops at the accepted upload (object key and row)'
status: "added 2026-10-24, seam-gap PR 2"
tool_names: # shared.yaml conventions.tool_names (2026-10-25): use-case (MCP tool) names unique across a composition; the route and permission are unchanged
- { tool: list_cons_migration_runs, route: "GET /cons-migration-runs", was: "list_cons_migration_runs (unique; the brick's list_cons_load_runs on GET /migration-runs is the same family)" }
- { tool: get_cons_migration_run, route: "GET /cons-migration-runs/{cons_migration_run_id}", was: "get_cons_migration_run" }
- { tool: get_cons_migration_map, route: "GET /migration-maps/{odoo_db}/{odoo_model}/{odoo_id}", was: "get_cons_id_map" }
screens:
- { id: structure, name: Structure, stories: [CON-01, CON-02, CON-16, CON-17], actors: [group_accountant, group_controller, auditor], note: "the group tree with ownership and method per period; acquisitions and changes with what they computed, waiting for approval or effective" }
- { id: data, name: Data, stories: [CON-03, CON-12], actors: [group_accountant, group_controller], note: "per company: source, period status, digest, nets-to-zero, unmapped accounts; uploads waiting for acceptance" }
- { id: translation, name: Translation, stories: [CON-04], actors: [group_accountant], note: "per company: rates used by type, translated totals, the translation reserve and its NCI share" }
- { id: eliminations, name: Eliminations, stories: [CON-05, CON-06, CON-07, CON-08, CON-09], actors: [group_accountant, auditor], note: "per task and pair: what was eliminated, the difference and where it went; intercompany inventory entry" }
- { id: entries, name: Entries, stories: [CON-10], actors: [group_accountant, group_controller, auditor] }
- { id: group_close, name: Group close, stories: [CON-13, CON-14], actors: [group_controller, group_accountant], note: "the readiness panel {gate, met, reason, fix} and the sign-off; reopen with its history" }
- { id: reports, name: Reports, stories: [CON-11], actors: [group_accountant, group_controller, auditor] }
- { id: settings, name: Settings, stories: [CON-19], actors: [group_accountant, owner], note: "account rules, roles, elimination rules, tolerances; the controls to the owner only" }
- { id: migration, name: Migration, stories: [CON-18], actors: [owner, group_accountant] }
permissions:
namespace: cons
catalog:
- cons:group:read
- cons:structure:write
- cons:structure:approve
- cons:data:collect
- cons:data:upload
- cons:data:accept
- cons:translate:run
- cons:eliminate:run
- cons:inventory:write
- cons:journal:write
- cons:journal:approve
- cons:journal:reverse
- cons:period:open
- cons:period:signoff
- cons:period:reopen
- cons:report:read
- cons:settings:write # account rules, roles, elimination rules, tolerances
- cons:settings:controls # signoff_second_person, allow_open_period_collect, AI step mode (business_timezone is the platform's, shared.yaml conventions); never held by whoever they control
- cons:migration:read
- cons:migration:sign
- cons:data:load # machine: store collected data from gl
- cons:journal:post_auto # machine: post automatic consolidation entries
- cons:carry_forward:derive # machine
- cons:statement:materialise # machine
- cons:number_series:claim # machine
- cons:migration:load # machine
- cons:migration:reconcile # machine
machine:
- cons:data:load
- cons:journal:post_auto
- cons:carry_forward:derive
- cons:statement:materialise
- cons:number_series:claim
- cons:migration:load
- cons:migration:reconcile
row_visibility:
rule: "everyone who can consolidate sees the whole group; a person's gl company access does not narrow group figures (they are the group's), but the drill-down from a group figure to gl's lines answers 404 for a company the person cannot see in gl"
enforced: "every write carries its own permission; reads need cons:group:read; the drill-down calls gl under the caller, so gl's company scope applies"
not_found_not_forbidden: true
config_rows:
- table: cons_settings
changes: "how the group close is run; every row dated (valid_from after today; the first row of a key may start today)"
settings:
- { key: signoff_second_person, type: bool, default: true, changes: "whether manual entries, uploads, structure changes and the sign-off need a second person; cons:settings:controls" }
- { key: allow_open_period_collect, type: bool, default: false, changes: "collect from a company whose gl period is still open (flagged provisional); cons:settings:controls" }
- { key: translation_pl_rate, type: str, default: "monthly_average", changes: "monthly_average | ytd_average: how income and expense are translated (IAS 21.40 permits an average that approximates the transaction rates)" }
- { key: nci_measurement, type: str, default: "proportionate", changes: "proportionate share of net assets (IFRS 3.19); full goodwill is an extension" }
- { table: cons_settings, changes: "each AI step's mode (ai_steps: off | suggest)" }
- { table: cons_group, changes: "the group: currency, ledger, group chart, top company, statement version, goodwill life" }
- { table: cons_account_rule, changes: "how each group account is translated and whether it is intercompany or equity" }
- { table: cons_account_role, changes: "which group account takes the translation reserve, NCI, goodwill and differences" }
- { table: cons_elimination_rule, changes: "which account pairs are eliminated against each other, and the tolerance" }
# the structure is NOT a config row: it changes only through an approved structure change.
in_code_not_rows:
- "that consolidation never writes a company's books, only its own journal"
- "that every number enters through gl's route or an accepted upload, never typed"
- "that an automatic entry is keyed per group, period, task, pair and run, so a re-run replaces and never doubles"
- "that translated data and the consolidated trial balance net to zero exactly"
- "that a signed period's figures never change; a later change is a difference until reopened"
- "that no structure change takes effect in a signed period"
- "that 'today' is the business date in business_timezone and a dated row starts after today (v2: except the first row of a key, which may start today)"
- "that an AI finding never changes an entry; a person accepts or dismisses it"
- "that migrated history is read-only and never rewritten by a later run"
ai_steps:
- key: explain_elimination_difference
benefit: "a pair whose intercompany balances differ comes with a likely cause (an invoice in one month and the bill in the next, a different rate, a missing recharge)"
mode: suggest
modes_allowed: ["off", suggest]
safe_for_auto: false
inputs: [the pair's reported data, gl's intercompany match for the pair, the two companies' rates]
output: "a cause and its evidence; the group accountant decides the fix"
fallback: "the group accountant works the difference by hand"
consent: "nothing is posted or accepted by the step"
audit: "cons_ai_finding: model, inputs digest, finding, evidence, decision, who, when"
eval: "seeded differences of each kind; accuracy of the cause"
note: "an assistant task beside the process, not a step in the signed lifecycle"
deviation_support:
- deviation: "a company kept in another package"
how: "the reference: upload in the same shape (CON-12)"
- deviation: "consolidation from open company periods for a fast flash close"
how: "configuration (depth 1): allow_open_period_collect; the figures are flagged provisional and the sign-off still waits for closed periods"
- deviation: "local-GAAP group accounts that amortise goodwill"
how: "configuration: goodwill_life_years on the group, a group on the local ledger"
- deviation: "full-goodwill NCI measurement (IFRS 3.19 option)"
how: "extension (depth 2): nci_measurement full_goodwill computes NCI at fair value; the default is proportionate"
tests:
acceptance: "one per story id above, named for it"
access:
- "a group accountant cannot approve a structure change, an upload or a manual entry, nor sign off or reopen a period"
- "a group controller cannot propose a manual entry or collect data"
- "an auditor cannot change anything"
- "cons:settings:write cannot change a control; a group accountant or controller holding cons:settings:controls is refused at configuration"
- "the machine permissions are refused to every person role and to a wildcard grant"
- "the drill-down from a group figure to gl's lines answers 404 for a company the caller cannot see in gl"
invariants:
- "self-approval: the proposer of a structure change, upload or manual entry, and the preparer of a period, cannot approve or sign it under a second identity (matched on email AND sub); a reopener is not the signer"
- "every company's reported data, the translated data and the consolidated trial balance net to zero exactly, no float tail"
- "running translate or eliminate twice, or twice at once, gives one set of entries per key (run_no claimed with a plain INSERT; entries keyed per task, pair and run); a re-run reverses the previous run's entries and never deletes them"
- "a collection with an unchanged gl digest writes nothing; a changed digest makes every later step stale until re-run"
- "a signed period's materialised statements equal a fresh computation at the signed digest and never change"
- "no structure change takes effect in a signed period; a change dated before the first unsigned period is refused (never backwards)"
- "today is the business date in business_timezone, not the UTC date; a dated settings row starts after today, except the first row of a key, which may start today"
- "ownership and voting above 100 or at most 0, a margin outside 0..100, an amount above NUMERIC(18,2) is refused 422, never a 500"
- "goodwill = consideration - share of net assets at acquisition, NCI = (100% - share) x net assets, to the penny in local and group currency"
- "goodwill and fair-value adjustments are held in the subsidiary's currency and shown at each period's closing rate; the change since the previous period goes to the translation reserve (IAS 21.47), never to profit"
- "after the investment elimination the investment account for each full subsidiary is exactly zero"
- "consolidation entry numbers per group and year are never reused and have no gaps; a rejected manual entry takes none"
- "a later migration run never rewrites a signed run's rows; the carried consolidated balance per group account and period equals Odoo's"
- "translation uses only gl's rates: no rate is stored here, and a rate missing in gl blocks with the currency and month named"
scenarios:
- id: SC-CON-01
name: first consolidation of an 80% subsidiary
stories: [CON-01, CON-02, CON-03, CON-04, CON-08]
given: UK01 (GBP) acquires 80% of DE01 (EUR) on 2026-04-01 for 900,000.00 EUR, net assets 1,000,000.00 EUR
when: the structure and acquisition are approved, 2026-04 to 2026-10 are collected, translated and eliminated
then: goodwill 100,000.00 EUR, 86,000.00 GBP at 0.860000 on acquisition and retranslated at each closing rate with the difference in the translation reserve; NCI 20% of net assets plus 20% of post-acquisition result and translation reserve; the investment nets to zero; the consolidated balance sheet balances
- id: SC-CON-02
name: intercompany out of balance, then fixed
stories: [CON-05, CON-31, CON-20]
given: a UK01/DE01 pair 50.00 apart (tolerance 5.00)
when: eliminations run, are refused, the difference is posted in gl, the data re-collected and eliminations run again twice
then: the first run refused ic_within_tolerance; after the fix one set of elimination entries (run 2) and run 1's reversed
- id: SC-CON-03
name: stock profit, deferred tax, next year's realisation
stories: [CON-07, CON-15]
given: DE01 holds 12,000.00 EUR of UK01 goods at 25% margin, tax 30%, at 2026-12-31
when: 2026-12 is eliminated and signed, FY2027 opens, the goods are sold and 2027-01 eliminated with no stock left
then: 3,000.00 EUR eliminated with 900.00 deferred tax in 2026; in 2027 the opening is realised into cost of sales and the group result rises by 2,100.00 net
- id: SC-CON-04
name: sign-off under a second login and a late change
stories: [CON-13, CON-14, CON-20]
given: GRP 2026-10 eliminated by the group accountant
when: the same person (same sub, other email) tries to sign, another controller signs, then DE01 reopens in gl and posts
then: the first sign refused second_person; after signing the figures stay; the new digest shows a difference until the period is reopened by a controller other than the signer
- id: SC-CON-05
name: divestiture recycles the translation reserve
stories: [CON-16]
given: DE01 consolidated with a translation reserve of 12,000.00 GBP
when: it is sold on 2027-06-30
then: the reserve is recycled to profit within the disposal gain; DE01's balance sheet leaves the group from 2027-06; its P&L to June stays
scale:
reads: "the tables reader serves 16 concurrent reads; a collection reads one company at a time from gl (a semaphore below 16 on the cons client), and translation and eliminations read the period's rows in one query each"
hot_paths: ["the collection per company", "the translation per company", "the elimination per pair", "the consolidated report"]
workers: "none required; a group of hundreds of companies collects and eliminates in the composition's worker"
volumes: "reported data is per group account, not per line: a company with a 2,000-account chart sends a few thousand rows a period"
platform_constraints:
- constraint: "No read-your-writes inside an open transaction."
today: "a run writes its translated data, commits, then computes the translation reserve in a second transaction"
- constraint: "ON CONFLICT DO UPDATE SET accepts only EXCLUDED.<col>."
today: "reported data, carry-forward and statements upsert with every value in VALUES; entries are plain INSERTs (a conflict is the replay)"
- constraint: "graph_khop() takes an integer-literal start node."
today: "indirect ownership walks the structure graph by company node ids derived from company_code, one walk per top company"
customer_varies:
- question: "v3: Which tax rate does deferred tax on consolidation adjustments use?"
changes: tax_rate_pct on an intercompany inventory entry or a fair-value adjustment, gl's statutory_tax_rate (CON-07, CON-02)
default: "the rate entered on the adjustment wins; else gl's statutory_tax_rate of the company on the period's last day; else the run is refused (gate tax_rate_known), never zero (chosen 2026-10-25)"
- question: "v3: Do the companies' own retained earnings and the group's consolidation reserve sit on one group account or two?"
changes: account_role retained_earnings and group_retained_earnings
default: "two roles, which may name the same group account; both are set before the first year end (gate account_roles_complete)"
- question: Which companies does the group own, how much of each, and does it control them?
changes: group_structure (CON-01), the method (full, equity, none)
standard: "IFRS 10 (control), IAS 28 (significant influence, usually 20% or more), IFRS 11 (joint arrangements)"
- question: In which currency do you present the group, and which ledger feeds it?
changes: cons_group group_currency, ledger_id
- question: Do any companies keep their books outside this system?
changes: uploads (CON-12)
- question: How do you measure the minority's share, and do you amortise goodwill?
changes: nci_measurement, goodwill_life_years
standard: "IFRS 3.19 (proportionate share or fair value); IFRS: goodwill not amortised, tested for impairment (IAS 36); FRS 102 section 19 and HGB section 309 amortise it"
- question: Do your companies sell goods to each other that may still be in stock at period end?
changes: CON-07, ic_inventory, the deferred tax rate per company
- question: How often do you consolidate (monthly, quarterly, yearly)?
changes: which group periods are opened and signed
typical:
owner_managed_group: yearly, for the statutory accounts
investor_backed_group: monthly or quarterly management consolidation
- question: Who prepares the group close and who signs it?
changes: group_accountant, group_controller, signoff_second_person
variation_points:
- node: Task_sign_off_group_close
op: narrow
default: "a second person signs"
depth: 1
guideline: "warn - one person preparing and signing the group accounts is the first thing an auditor reports"
- node: Task_collect_group_data
op: narrow
default: "only from closed company periods"
depth: 1
guideline: "warn - figures from open periods change under the group; allow it only for a flash close"
- node: Task_run_eliminations
op: narrow
default: "refuse a pair above tolerance"
depth: 1
requirement: "refuse - an elimination that does not eliminate leaves intercompany in the group accounts"
migration:
odoo:
runs_after: [gl] # companies and accounts through gl's map
reads:
- model: consolidation.chart
fields: [id, name, currency_id, company_ids]
filter: "where Enterprise account_consolidation is installed (13 to 16; field names to verify against the database's version at the door)"
why: "the group, its currency and companies"
- model: consolidation.account
fields: [id, chart_id, code, name, currency_mode (end | avg | hist), account_ids, group_id, invert_sign]
filter: "the chart's accounts"
why: "the group chart is gl's (gl's migration carries it); here the translation rule per account"
- model: consolidation.period
fields: [id, chart_id, date_analysis_begin, date_analysis_end, state, company_period_ids]
filter: "closed periods of the door's years"
why: "the group periods carried as signed history"
- model: consolidation.company_period
fields: [id, period_id, company_id, rate_consolidation, currency_rate_avg, currency_rate_end, date_company_begin, date_company_end]
filter: "the periods above"
why: "scope and ownership per period; Odoo's rate_consolidation scales every line (proportional consolidation)"
- model: consolidation.journal / consolidation.journal.line
fields: [id, period_id, chart_id, auto_generated, company_period_id, name; line: journal_id, account_id, amount, currency_amount, note]
filter: "journals of the periods above"
why: "the consolidation entries and the consolidated balances, as history"
maps:
- { from: "consolidation.chart", to: "group", rule: "group_id named at the door; currency_id -> group_currency; ledger the gl leading ledger" }
- { from: "consolidation.account.currency_mode", to: "account_rule.rate_type", rule: "end -> closing, avg -> average, hist -> historical; the account through gl's map of the group chart" }
- { from: "consolidation.company_period", to: "group_structure", rule: "ownership_pct = rate_consolidation; method full where 100, else decided at the door (Odoo consolidated it proportionally); valid_from_period the period's first month" }
- { from: "consolidation.period (closed)", to: "group_period", rule: "status signed, signed_by migration, digest of the carried balances" }
- { from: "consolidation.journal.line", to: "cons_journal (document_type migration)", rule: 'cons_journal_id = derived_id("odoo:<db>:consolidation.journal:<id>"); amount to the penny in group currency, never recomputed' }
- { from: "the consolidated balance per consolidation account and period", to: "cons_statement (report migration)", rule: "carried as the signed figures of the period; never re-translated or re-eliminated" }
door:
- "which consolidation chart is the group, and its group_id"
- "for each company with rate_consolidation below 100: full with NCI, equity, or none from the first period after cut-over (history stays proportional, as Odoo computed it)"
- "how many closed periods to carry (recommended: the current and the prior financial year)"
- "Odoo 17 and later (no account_consolidation): start the group from the cut-over with the companies gl carried"
reconcile:
- { check: "consolidated balance per group account and carried period", odoo: "SUM of company-period amounts x rate and consolidation journal lines per consolidation account", ours: "cons_statement (report migration) per group account and period", must: equal }
- { check: "companies in scope and ownership per period", odoo: "consolidation.company_period per period", ours: "group_structure in force per period", must: equal }
- { check: "consolidation journal count and total per period", odoo: "search_count and SUM(amount) of consolidation.journal.line per period", ours: "cons_journal (migration) per period", must: equal }
- { check: "companies consolidated proportionally in Odoo and fully or by equity here from cut-over", odoo: "rate_consolidation below 100", ours: "the method decided at the door", must: explained }
not_carried:
- "eliminations as rules: Odoo keeps journals, not rules; the elimination rules are set up here"
- "non-controlling interest, goodwill, translation reserve: Odoo's consolidation has no such computation; the first period after cut-over computes them from the acquisition recorded here"
- "Odoo's own consolidation rates (currency_rate_avg/end): gl's migration carries res.currency.rate; the carried periods are history at Odoo's amounts"
- "draft (open) consolidation periods: re-run here"
idempotent_on: 'group on group_id; structure on [group_id, company_code, valid_from_period]; group_period on [group_id, period_id]; cons_journal_id = derived_id("odoo:<db>:consolidation.journal:<id>"); lines on [cons_journal_id, line_no]; cons_migration_map on [odoo_db, odoo_model, odoo_id]; the run on run_id'
reference:
repos: [] # brick-consolidation is not built
packages: []
contracts: []
build_guide: null
known_gaps:
- "nothing is built; this is version 1 of the spec"
- "the data route GET /group-reporting/balances and GET /statement-versions/{fsv_id} are specified in gl version 3, not built"
- "plan and forecast consolidation (budget versions) is deferred to M6: actuals first, then the same engine with a version key over gl's budgets (GL-55)"
- "full-goodwill NCI measurement, multiple group currencies and hyperinflation (IAS 29) are not specified"
- "APQC ids are null until verified against PCF v8.0"
# SAP S/4HANA Group Reporting parity at mid-market scope (no industry solutions).
sap_parity:
- { sap: "Consolidation units, groups and hierarchies", ours: covered, where: "group, group_structure, CON-01" }
- { sap: "Consolidation methods: full and equity", ours: covered, where: "CON-01, CON-08, CON-09" }
- { sap: "Proportional consolidation", ours: different, where: "gl (the investor's own books)", why: "IFRS 11 removed proportional consolidation of joint ventures (equity method, CON-09); a joint operation's share is recognised in the investor's own books" }
- { sap: "Data release from the universal journal (real-time)", ours: covered, where: "CON-03, CON-30, gl GL-38" }
- { sap: "Flexible upload of reported data", ours: covered, where: "CON-12, upload" }
- { sap: "Data validation of reported and consolidated data", ours: covered, where: "CON-13, group_period readiness" }
- { sap: "Currency translation (closing, average, historical) with translation differences", ours: covered, where: "CON-04, account_rule, historical_amount" }
- { sap: "Intercompany matching and reconciliation (ICMR)", ours: covered, where: "gl GL-52, CON-31" }
- { sap: "Intercompany elimination of balances, revenue and expense, dividends", ours: covered, where: "CON-05, CON-06, elimination_rule" }
- { sap: "Elimination of intercompany profit in inventory", ours: covered, where: "CON-07, ic_inventory" }
- { sap: "Consolidation of investments: first consolidation, goodwill, NCI, subsequent", ours: covered, where: "CON-02, CON-08, structure_change" }
- { sap: "Equity method", ours: covered, where: "CON-09" }
- { sap: "Changes in ownership, step acquisitions and divestitures", ours: covered, where: "CON-16, CON-17, structure_change" }
- { sap: "Manual consolidation entries with document types and approval", ours: covered, where: "CON-10, cons_journal" }
- { sap: "Deferred taxes on consolidation entries", ours: covered, where: "CON-07, CON-02 (fair-value adjustments)", why: "automatic on eliminated stock profit; on purchase-price adjustments as lines of the acquisition" }
- { sap: "Consolidation monitor and task sequence (data monitor, consolidation monitor)", ours: covered, where: "group_period workflow, CON-13, CON-14" }
- { sap: "Group reporting: statements, changes in equity, cash flow, segments", ours: covered, where: "CON-11, cons_statement" }
- { sap: "Matrix consolidation (profit centre / segment)", ours: different, where: "reported_data.segment, cons_journal_line.segment", why: "segment carried on reported data and on elimination lines for segment reports; no separate consolidation of profit-centre units" }
- { sap: "Consolidation versions (actual, plan, forecast)", ours: deferred, milestone: "M6", where: "", why: "actuals first; plan consolidation is the same engine with a version key over gl's budgets (GL-55), worth building once actuals are signed" }
- { sap: "Multiple group currencies and ledgers per group", ours: different, where: "group", why: "a group has one currency and one ledger; a second presentation currency or GAAP is a second group over the same structure" }
- { sap: "Hyperinflation accounting (IAS 29)", ours: not_planned, where: "", why: "applies only to operations in hyperinflationary economies; outside the mid-market groups this method targets" }
- { sap: "Audit trail and drill-down to the source document", ours: covered, where: "cons_audit, CON-11 (to gl GL-04)" }