dodil/erp-brick-consolidation

DODIL ERP brick: consolidation

src/brick_consolidation/application/process.yaml

# The consolidation process as the customer signed it (module.yaml workflows, copied; tests/test_process.py compares the two).
id: consolidation
name: Group consolidation
version: 3
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
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