dodil/erp-brick-gl

DODIL ERP brick: general ledger

src/brick_gl/application/process.yaml

# The brick's process — module.yaml's workflows, copied (pattern.md §15). BRICK.declare_process serves
# it as BPMN 2.0 at GET /process/bpmn; it is identical to `erp-compose bpmn --from module.yaml`.
# A transition's key (or its derived <entity>_<from>_to_<to>) is frozen once signed: it is the BPMN id.
id: gl
name: Accounting
version: 7
actors:
- id: bookkeeper
  name: Bookkeeper
  does: proposes manual journals, reconciles the receivable control account, reads what the other modules posted
  sees: every account and journal
  permissions:
  - gl:journal:read
  - gl:journal:write
  - gl:period:read
  - gl:fx:read
  - gl:reconcile:run
  - gl:report:read
  - gl:company:read
  - gl:close:task
- id: accountant
  name: Accountant / finance manager
  does: sets up companies, ledgers, the charts and the account map, approves manual journals, recurring entries, accruals and allocation cycles, reverses mistakes, enters rates, revalues, matches intercompany, runs the closing cockpit, closes periods and years, signs off a close, runs the reports
  sees: the companies the owner gives them access to (all by default)
  permissions:
  - gl:journal:read
  - gl:journal:write
  - gl:journal:approve
  - gl:reversal:approve
  - gl:account:write
  - gl:company:read
  - gl:company:write
  - gl:fsv:write
  - gl:period:read
  - gl:period:close
  - gl:period:reopen
  - gl:adjustment:post
  - gl:fx:read
  - gl:fx:write
  - gl:reval:run
  - gl:reconcile:run
  - gl:report:read
  - gl:recurring:write
  - gl:recurring:approve
  - gl:recurring:run
  - gl:accrual:write
  - gl:accrual:approve
  - gl:accrual:run
  - gl:allocation:approve
  - gl:allocation:run
  - gl:budget:approve
  - gl:close:manage
  - gl:close:task
  - gl:close:signoff
  - gl:audit:export
  - gl:settings:write
  - gl:migration:read
- id: controller
  name: Controller (management accountant)
  does: keeps the allocation cycles and the statistical key figures, simulates and posts allocations, keeps the budget, reads plan against actual by cost and profit centre
  sees: the companies the owner gives them access to (all by default)
  permissions:
  - gl:journal:read
  - gl:period:read
  - gl:company:read
  - gl:report:read
  - gl:allocation:write
  - gl:allocation:simulate
  - gl:allocation:run
  - gl:keyfigure:write
  - gl:budget:write
  - gl:close:task
  note: a controller who wrote a cycle cannot approve it (gl:allocation:approve is the accountant's, and the approver must be another person)
- id: auditor
  name: Auditor
  does: reads the books and the trail behind any number, verifies the hash chain, exports the FEC or SAF-T file
  sees: everything, changes nothing
  permissions:
  - gl:journal:read
  - gl:period:read
  - gl:fx:read
  - gl:report:read
  - gl:company:read
  - gl:audit:export
  - gl:audit:verify
  - gl:migration:read
- id: owner
  name: The owner (or whoever the owner names)
  does: holds the controls on the people who keep the books (time zone, second approver for manual journals, recurring entries, accruals, allocation cycles and reopening, journal numbering, document splitting, who may see which company, the AI step modes) and signs the migration reconciliation
  sees: everything
  permissions:
  - gl:journal:read
  - gl:report:read
  - gl:company:read
  - gl:settings:controls
  - gl:migration:read
  - gl:migration:sign
  note: 'the controls are never held by whoever they control: a bookkeeper or accountant holding gl:settings:controls is a configuration error the access test refuses'
- id: system
  name: The system
  does: posts what other modules send (fixed-assets included), splits and re-derives balances, derives the carry-forward, posts pending reversals when a period opens, re-splits open items by profit centre at period end, checks the closing cockpit's system tasks, materialises statements, serves consolidation its group data, checks a manual journal before approval, numbers journals, loads and reconciles a migration
  sees: everything, through its own service account
  permissions:
  - gl:journal:read
  - gl:period:read
  - gl:fx:read
  - gl:company:read
  - gl:journal:post
  - gl:balance:derive
  - gl:carry_forward:derive
  - gl:resplit:post
  - gl:close:check
  - gl:group_data:extract
  - gl:statement:materialise
  - gl:journal:check
  - gl:number_series:claim
  - gl:migration:load
  - gl:migration:reconcile
  - gl:cost_centre:read
  note: each is an exact grant to one service client, never reached by a wildcard (permissions.machine); gl:journal:post is held by each posting module's client (ar, payments, tax, purchasing, inventory, fixed-assets), gl:group_data:extract by consolidation's client only
workflows:
- entity: journal
  states:
  - proposed
  - posted
  - rejected
  - reversed
  terminal:
  - rejected
  - reversed
  transitions:
  - key: post_from_source
    name: Post what another module sent
    from: none
    to: posted
    by: system
    gate: balanced in document and functional currency; every purpose mapped on the journal date; the company's period open; the ledger (if named) assigned to the company; source-only accounts posted only by their module; field status met; journal_id = derived_id(source_document, event[, ledger_id])
    effect: lines written with the source named; split to universal lines (GL-22); hash chain extended; number claimed per company, book and year; balances re-derived in a second transaction; a replay answers the journal already posted
  - key: propose_manual
    name: Propose a manual journal
    from: none
    to: proposed
    by:
    - bookkeeper
    - accountant
    gate: balanced; dated in an open period and not after today; the proposer has access to the company; no source-only or group account on a line
    effect: proposer recorded (email and sub); a ledger may be named (ledger-specific); reverse_next_period may be flagged (GL-50)
  - key: propose_cross_company
    name: Propose a cross-company journal
    from: none
    to: proposed
    by:
    - bookkeeper
    - accountant
    gate: lines in two or more companies the proposer has access to; ic_receivable and ic_payable mapped for every pair; each company's period open
    effect: one ic_document_id; the due-to and due-from lines computed and shown before approval (GL-51)
  - key: propose_audit_adjustment
    name: Propose a year-end audit adjustment
    from: none
    to: proposed
    by: accountant
    gate: dated in the last period of an open financial year; that period closed
    effect: flagged adjustment, book adjustment; always waits for a second person (GL-56)
  - key: ai_check_journal
    name: AI checks the journal (suggest)
    from: proposed
    to: proposed
    by: system
    gate: the check is on
    effect: findings listed for the approver
  - key: accept_journal_ai_findings
    name: Accept or dismiss each AI finding
    from: proposed
    to: proposed
    by: accountant
    gate: the journal has findings (GL-15)
    effect: each decision recorded in gl_ai_finding
  - key: post_manual
    name: Post without a second approver
    from: proposed
    to: posted
    by: system
    gate: manual_journal_approval is none, or the total is at or below the threshold; never an adjustment or a cross-company journal above the threshold
    effect: numbered; split; chained; balances re-derived; an auto-reversal posted or held (GL-24)
  - key: approve_manual
    name: Approve the manual journal
    from: proposed
    to: posted
    by: accountant
    approval: true
    gate: the approver is not the proposer (email AND sub); every company's period is still open (the last period closed but the year open, for an adjustment)
    effect: numbered; approver recorded; one journal per company for a cross-company document, in one transaction; balances re-derived; the carry-forward marked stale for an adjustment
  - key: reject_manual
    name: Reject the manual journal
    from: proposed
    to: rejected
    by: accountant
    requires: reason
  - key: reverse_journal
    name: Reverse the journal
    from: posted
    to: reversed
    by: accountant
    requires: reason
    gate: the reversal date (today, or the first day of the next open period) is in an open period; no auto-reversal pending
    effect: a mirror journal keyed derived_id(journal_id, 'reversal'), split the same way; neither is deleted; a cross-company journal reverses with its partners
  - key: post_pending_reversal
    name: Post the auto-reversal
    from: posted
    to: reversed
    by: system
    gate: the journal asked for a reversal on the next period's first day (accrual, revaluation, re-split) and that period is open
    effect: the mirror journal dated that day, once (GL-24)
  note: 'There is no edit: a journal exists or it does not, and a correction is another journal. A journal''s date is an accounting fact dated by its document (an invoice''s issue date, a receipt''s received date, a manual accrual''s month): the one kind of date allowed earlier than today, and only into an open period (or, for an audit adjustment, the closed last period of an open year). A migrated journal (source_module odoo) is read-only history: it is reversed like any other, never re-posted. Recurring entries, accrual releases, allocations and revaluations post through the same use case as post_from_source, with gl itself as the source.'
  readiness:
  - transition: post_from_source
    gate: balanced
    reason: debits and credits differ by the amount named
    fix: the calling module's journal builder
  - transition: post_from_source
    gate: purposes_mapped
    reason: a purpose (or purpose and qualifier) has no account in force on the journal date in the company's chart
    fix: 'Account map: map it (GL-12)'
  - transition: post_from_source
    gate: period_open
    reason: the journal date falls in a closed period of the company, or in no period
    fix: post it dated in an open period, or ask the accountant to reopen
  - transition: post_from_source
    gate: fx_rate
    reason: a foreign journal with no rate given and no rate in force on its date
    fix: 'Currencies: enter the rate for the date'
  - transition: post_from_source
    gate: ledger_assigned
    reason: the named ledger is not assigned to the company for the journal's year
    fix: 'Companies: assign the ledger, or post to all ledgers'
  - transition: post_from_source
    gate: company_known
    reason: the company_code is not a company of these books
    fix: 'Companies: add it, or send the default'
  - transition: post_from_source
    gate: field_status
    reason: the account requires (or forbids) a cost centre, profit centre, partner or tax code on the line
    fix: the calling module's builder, or the account's field status
  - transition: post_from_source
    gate: profit_centre_valid
    reason: the profit centre is not valid for the company on the journal date
    fix: 'Dimensions: extend it, or send another'
  - transition: propose_manual
    gate: source_only_account
    reason: an account on a line is kept by its module (receivable, payable, asset cost, accumulated depreciation)
    fix: post it in that module, or use a non-control account
  - transition: propose_manual
    gate: group_chart_not_postable
    reason: a group-chart account cannot carry a line
    fix: use the operational account mapped to it
  - transition: propose_manual
    gate: company_access
    reason: you have no access to a company on a line
    fix: ask the owner (GL-60)
  - transition: propose_cross_company
    gate: ic_accounts_mapped
    reason: no ic_receivable or ic_payable account for the pair named
    fix: 'Account map: map them per partner company'
  - transition: propose_cross_company
    gate: ic_periods_open
    reason: the period is closed in one of the companies, named
    fix: date it in a period open in both
  - transition: propose_audit_adjustment
    gate: year_open
    reason: the financial year is closed
    fix: post a prior-period correction in the current year
  - transition: approve_manual
    gate: second_person
    reason: you proposed it, and the owner asks for a second person (always, for an audit adjustment)
    fix: ask another accountant
  - transition: approve_manual
    gate: period_open
    reason: its period closed while it waited
    fix: reject it and propose it in an open period
  - transition: reverse_journal
    gate: not_reversed
    reason: already reversed by the journal named
    fix: none needed
  - transition: reverse_journal
    gate: no_pending_reversal
    reason: an auto-reversal of this journal is pending for the next period
    fix: open the next period; the reversal posts itself
  - transition: post_pending_reversal
    gate: next_period_open
    reason: the period of the reversal date is not open yet
    fix: 'Periods: open it (the reversal posts then)'
- entity: period
  states:
  - open
  - closed
  transitions:
  - key: open_periods
    name: Open the year's periods
    from: none
    to: open
    by: accountant
    gate: the company's fiscal year is opened (GL-13)
    effect: one period per month of the year, period_id YYYY-MM, per company; pending reversals dated in them posted (GL-24)
  - key: reconcile_receivable
    name: Reconcile the receivable control account
    from: open
    to: open
    by: bookkeeper
    effect: the breaks against payments' settlement recorded as at the period end (GL-08)
  - key: reconcile_payable
    name: Reconcile the payables control account
    from: open
    to: open
    by: bookkeeper
    gate: purchasing is installed
    effect: the breaks against purchasing's GET /bills/open recorded as at the period end (GL-08, GL-36)
  - key: reconcile_assets
    name: Reconcile the asset accounts
    from: open
    to: open
    by: accountant
    gate: fixed-assets is installed
    effect: the breaks per asset class and ledger against fixed-assets' register (GL-39)
  - key: run_recurring_entries
    name: Run the recurring entries
    from: open
    to: open
    by: accountant
    gate: the period is open
    effect: one journal per due approved template; twice posts once (GL-45)
  - key: run_accruals
    name: Release accruals and deferrals
    from: open
    to: open
    by: accountant
    gate: the period is open
    effect: one journal per active registration; twice posts once (GL-49)
  - key: run_fx_revaluation_balances
    name: Revalue foreign balances
    from: open
    to: open
    by: accountant
    gate: revaluation is on and a closing rate is in force on the period end for every currency held
    effect: per ledger, an unrealised gain or loss journal by the ledger's method, reversing next period (GL-09)
  - key: run_fx_revaluation_open_items
    name: Revalue foreign open items
    from: open
    to: open
    by: accountant
    gate: revaluation is on; a closing rate for every open item's currency
    effect: per ledger, one journal of item lines on the control accounts, reversing next period (GL-48)
  - key: match_intercompany
    name: Match intercompany
    from: open
    to: open
    by: accountant
    gate: the company has a partner company with intercompany lines
    effect: the pair's match result as at the period end (GL-52)
  - key: accept_ic_difference
    name: Accept an intercompany difference
    from: open
    to: open
    by: accountant
    requires: reason
    gate: the break is above tolerance and the accepter is not who posted the difference
    effect: recorded beside the break; consolidation reads it
  - key: resplit_open_items
    name: Re-split open items by profit centre
    from: open
    to: open
    by: system
    gate: document splitting is on for the company's year
    effect: one adjustment journal on the control accounts, reversing next period (GL-25)
  - key: close_period
    name: Close the month
    from: open
    to: closed
    by: accountant
    gate: readiness met
    effect: operational postings dated in it refused for the company; statements materialised (GL-21)
  - key: materialise_statements
    name: Materialise the statements
    from: closed
    to: closed
    by: system
    effect: keyed (report, company, ledger, period); a second run changes nothing
  - key: reopen_period
    name: Reopen the month
    from: closed
    to: open
    by: accountant
    requires: reason
    approval: true
    gate: the year is not closed; the reopener is not who closed it where the owner asks for it
    effect: recorded; the materialised statements and the close sign-off marked stale
  readiness:
  - transition: close_period
    gate: prior_closed
    reason: the previous period is still open
    fix: close it first
  - transition: close_period
    gate: no_proposed
    reason: manual journals dated in the period are waiting for approval
    fix: approve or reject them (GL-11)
  - transition: close_period
    gate: receivable_reconciled
    reason: the receivable control account has breaks as at the period end
    fix: 'Reconcile: work the breaks or accept the difference with a reason'
  - transition: close_period
    gate: payable_reconciled
    reason: the payables control account has breaks against purchasing's open bills as at the period end
    fix: 'Reconcile: work the breaks or accept the difference with a reason (only when purchasing is installed)'
  - transition: close_period
    gate: revalued
    reason: foreign balances or open items are not revalued at the period end in a ledger, named
    fix: 'Currencies: revalue (or revaluation is off)'
  - transition: close_period
    gate: recurring_posted
    reason: approved recurring entries due in the period are not posted
    fix: 'Recurring: run them (GL-45)'
  - transition: close_period
    gate: accruals_posted
    reason: active accruals or deferrals have no release in the period
    fix: 'Accruals: run them (GL-49)'
  - transition: close_period
    gate: allocations_posted
    reason: approved allocation cycles have no posted run for the period
    fix: 'Allocations: post them (GL-46), or retire the cycle'
  - transition: close_period
    gate: depreciation_posted
    reason: fixed-assets has not posted the period's depreciation in a ledger (its readiness named)
    fix: run depreciation in fixed-assets (only when it is installed)
  - transition: close_period
    gate: ic_matched
    reason: intercompany breaks above tolerance and not accepted, per partner
    fix: 'Intercompany: fix or accept them (ic_match_required refuse; warn lists them)'
  - transition: close_period
    gate: resplit_done
    reason: open items are not re-split by profit centre as at the period end
    fix: the system runs it as a close task (splitting on only)
  - transition: close_period
    gate: pending_reversals_posted
    reason: reversals due on the period's first day are not posted
    fix: 'a platform fault if the period is open: report it'
  - transition: close_period
    gate: carry_forward_current
    reason: the opening balances of this year are older than a posting into the previous year
    fix: the system re-derives them (GL-23)
  - transition: close_period
    gate: close_signed_off
    reason: the close run is not signed off by a second person
    fix: 'Close cockpit: finish the tasks and ask another accountant to sign (close_cockpit on)'
  - transition: close_period
    gate: trial_balance_zero
    reason: the trial balance does not net to zero, in a ledger or a segment
    fix: 'a platform fault: stop and report it'
  - transition: reopen_period
    gate: year_open
    reason: the financial year is closed
    fix: post a correction in the current year
  - transition: reopen_period
    gate: second_person
    reason: you closed it, and the owner asks for a second person
    fix: ask another accountant
  - transition: accept_ic_difference
    gate: second_person
    reason: you posted the difference you are accepting
    fix: ask another accountant
- entity: fiscal_year
  states:
  - open
  - closed
  transitions:
  - key: open_year
    name: Open the financial year
    from: none
    to: open
    by: accountant
    gate: the start month follows the company's previous year end (financial_year_end_month)
    effect: twelve periods opened for the company; the carry-forward from the previous year derived (GL-23)
  - key: derive_carry_forward
    name: Derive the carry-forward
    from: open
    to: open
    by: system
    gate: the previous year has postings
    effect: opening balances per ledger, account, profit centre, segment and partner company; P&L to retained earnings; re-derivation writes the same rows
  - key: close_year
    name: Close the year
    from: open
    to: closed
    by: accountant
    gate: every period of the year closed; no audit adjustment waiting; retained_earnings mapped
    effect: closed to adjustments; the carry-forward to the next year derived for every ledger; the next year opened if it is not; no closing journal
  readiness:
  - transition: close_year
    gate: periods_closed
    reason: 'periods of the year are still open: named'
    fix: close them (GL-07)
  - transition: close_year
    gate: no_proposed_adjustment
    reason: an audit adjustment is waiting for approval
    fix: approve or reject it (GL-56)
  - transition: close_year
    gate: retained_earnings_mapped
    reason: no retained_earnings account in the map on the year's last day
    fix: Account map (GL-12)
- entity: recurring_journal
  states:
  - proposed
  - approved
  - rejected
  - ended
  terminal:
  - rejected
  - ended
  transitions:
  - key: propose_recurring
    name: Propose a recurring entry
    from: none
    to: proposed
    by: accountant
    gate: balanced; company, ledger and accounts valid; first and last period within open or future years
    effect: version 1, proposer recorded (email and sub)
  - key: approve_recurring
    name: Approve the recurring entry
    from: proposed
    to: approved
    by: accountant
    approval: true
    gate: the approver is not the proposer (email AND sub) where manual_journal_approval asks for it
    effect: runs from its first period
  - key: reject_recurring
    name: Reject the recurring entry
    from: proposed
    to: rejected
    by: accountant
    requires: reason
  - key: post_recurring_period
    name: Post the period's entry
    from: approved
    to: approved
    by:
    - accountant
    - system
    gate: due in the period; the period open; not posted for it
    effect: a journal keyed derived_id('recurring', recurring_id, version, period_id), through the posting use case
  - key: revise_recurring
    name: Revise the recurring entry
    from: approved
    to: ended
    by: accountant
    requires: reason
    effect: this version ends at the change; version + 1 is proposed and needs approval
  - key: end_recurring
    name: End the recurring entry
    from: approved
    to: ended
    by: accountant
    requires: reason
    effect: no period after the end posts; posted journals untouched
  readiness:
  - transition: approve_recurring
    gate: second_person
    reason: you proposed it, and the owner asks for a second person
    fix: ask another accountant
  - transition: post_recurring_period
    gate: period_open
    reason: the period is closed
    fix: post it in an open period by revising the template, or reopen
- entity: accrual
  states:
  - proposed
  - active
  - rejected
  - terminated
  - completed
  terminal:
  - rejected
  - terminated
  - completed
  transitions:
  - key: register_accrual
    name: Register an accrual or deferral
    from: none
    to: proposed
    by: accountant
    gate: amount above zero and within NUMERIC(18,2); start period not after end; accounts valid in the company's chart
    effect: the schedule computed per period (straight-line or daily), the last period taking the remainder
  - key: approve_accrual
    name: Approve the registration
    from: proposed
    to: active
    by: accountant
    approval: true
    gate: the approver is not the registrant (email AND sub) above manual_journal_threshold
    effect: released from its start period
  - key: reject_accrual
    name: Reject the registration
    from: proposed
    to: rejected
    by: accountant
    requires: reason
  - key: release_accrual_period
    name: Release the period's share
    from: active
    to: active
    by:
    - accountant
    - system
    gate: the period open and in the schedule; not released for it
    effect: a journal keyed derived_id('accrual', accrual_id, period_id)
  - key: complete_accrual
    name: Complete the registration
    from: active
    to: completed
    by: system
    gate: every period of the schedule released
    effect: released total equals the amount to the penny
  - key: terminate_accrual
    name: Terminate early
    from: active
    to: terminated
    by: accountant
    requires: reason
    gate: the termination period open
    effect: the remainder released in the termination period; nothing after
  readiness:
  - transition: approve_accrual
    gate: second_person
    reason: you registered it, and it is above the threshold
    fix: ask another accountant
  - transition: release_accrual_period
    gate: period_open
    reason: the period is closed
    fix: release it in the next open period by terminating and re-registering, or reopen
- entity: allocation_cycle
  states:
  - draft
  - approved
  - retired
  terminal:
  - retired
  transitions:
  - key: define_allocation_cycle
    name: Define an allocation cycle
    from: none
    to: draft
    by: controller
    gate: every segment has senders, receivers and a rule; receivers valid; percentages total 100 where the rule is percentages
    effect: version recorded with its author (email and sub)
  - key: approve_allocation_cycle
    name: Approve the cycle
    from: draft
    to: approved
    by: accountant
    approval: true
    gate: the approver is not the author (email AND sub) where allocation_cycle_approval is on
    effect: runnable from its valid_from period
  - key: retire_allocation_cycle
    name: Retire the cycle
    from:
    - draft
    - approved
    to: retired
    by: accountant
    requires: reason
    effect: no new run; posted runs untouched; a change is a new cycle version
  readiness:
  - transition: approve_allocation_cycle
    gate: second_person
    reason: you wrote the cycle, and the owner asks for a second person
    fix: ask another accountant
- entity: allocation_run
  states:
  - simulated
  - posted
  - reversed
  terminal:
  - reversed
  transitions:
  - key: simulate_allocation
    name: Simulate the cycle
    from: none
    to: simulated
    by: controller
    gate: the cycle approved; the period open; key figures recorded for every receiver the rules need
    effect: senders, receivers and amounts computed and shown with a digest of the sender balances; nothing posted
  - key: post_allocation
    name: Post the allocation
    from: simulated
    to: posted
    by:
    - controller
    - accountant
    gate: the simulation is current (sender digest unchanged); no posted run for the cycle, period and ledger; the period open
    effect: one journal per segment keyed derived_id('allocation', cycle_id, period_id, ledger_id, run_no, segment); key figures used are frozen
  - key: reverse_allocation
    name: Reverse the allocation run
    from: posted
    to: reversed
    by: accountant
    requires: reason
    gate: the period open
    effect: mirror journals of every segment; the key figures unfrozen; run_no + 1 may post
  readiness:
  - transition: simulate_allocation
    gate: key_figures_present
    reason: a receiver has no key figure value for the period, named
    fix: 'Allocations: record it (GL-47)'
  - transition: post_allocation
    gate: simulation_current
    reason: postings on the senders since the simulation changed their balance
    fix: simulate again
  - transition: post_allocation
    gate: prior_run_reversed
    reason: a run of this cycle is posted for the period and ledger
    fix: reverse it first
  - transition: reverse_allocation
    gate: period_open
    reason: the period is closed
    fix: reopen it, or leave the run
- entity: statement_version
  states:
  - draft
  - published
  - superseded
  terminal:
  - superseded
  transitions:
  - key: draft_statement_version
    name: Draft a statement version
    from: none
    to: draft
    by: accountant
    effect: nodes and account assignments (ranges, group accounts, debit/credit-dependent); kind balance_sheet_pl or cash_flow
  - key: publish_statement_version
    name: Publish the version
    from: draft
    to: published
    by: accountant
    gate: fsv_complete
    effect: reports may run on it; it is never edited again
  - key: supersede_statement_version
    name: Supersede the version
    from: published
    to: superseded
    by: accountant
    effect: version_no + 1 drafted from it; reports already run name the old version
  readiness:
  - transition: publish_statement_version
    gate: fsv_complete
    reason: accounts with a balance unassigned or assigned twice, named
    fix: assign each exactly once
- entity: budget
  states:
  - draft
  - submitted
  - approved
  - superseded
  terminal:
  - superseded
  transitions:
  - key: draft_budget
    name: Draft a budget
    from: none
    to: draft
    by: controller
    gate: company, ledger and fiscal year exist
    effect: lines by account, profit centre, cost centre and period, typed or imported
  - key: submit_budget
    name: Submit the budget
    from: draft
    to: submitted
    by: controller
    gate: every amount within NUMERIC(18,2)
    effect: submitter recorded (email and sub)
  - key: approve_budget
    name: Approve the budget
    from: submitted
    to: approved
    by: accountant
    approval: true
    gate: the approver is not the submitter (email AND sub)
    effect: frozen; plan/actual reads it
  - key: return_budget
    name: Return the budget
    from: submitted
    to: draft
    by: accountant
    requires: reason
  - key: revise_budget
    name: Revise the budget
    from: approved
    to: superseded
    by: controller
    requires: reason
    effect: version + 1 drafted from it
  readiness:
  - transition: approve_budget
    gate: second_person
    reason: you submitted it
    fix: ask another accountant
- entity: close_run
  states:
  - in_progress
  - ready
  - signed
  transitions:
  - key: start_close_run
    name: Start the close run
    from: none
    to: in_progress
    by: accountant
    gate: a task list exists for the company and the kind (month or year end)
    effect: tasks instantiated with owner, due business day and dependencies
  - key: complete_close_task
    name: Complete a manual task
    from: in_progress
    to: in_progress
    by:
    - bookkeeper
    - controller
    - accountant
    gate: the tasks it depends on are done
    effect: who, when and the note or evidence key recorded
  - key: skip_close_task
    name: Mark a task not applicable
    from: in_progress
    to: in_progress
    by: accountant
    requires: reason
  - key: mark_close_ready
    name: All tasks done
    from: in_progress
    to: ready
    by: system
    gate: every task done or not applicable; every system task's gate met on read
    effect: the trial balance digest taken
  - key: sign_off_close
    name: Sign off the close
    from: ready
    to: signed
    by: accountant
    approval: true
    gate: the signer is not the run's starter nor a task's completer (email AND sub); the digest unchanged
    effect: who, when and the digest recorded; close_period may proceed
  - key: invalidate_close_signoff
    name: The sign-off goes stale
    from:
    - ready
    - signed
    to: in_progress
    by: system
    gate: a posting landed in the period after the digest
    effect: the run shows what changed and asks for a new sign-off
  readiness:
  - transition: complete_close_task
    gate: dependencies_done
    reason: a task it depends on is open, named
    fix: finish that first
  - transition: sign_off_close
    gate: second_person
    reason: you started the run or completed a task in it
    fix: ask another accountant
- entity: migration_run
  states:
  - loaded
  - reconciled
  - signed
  - discarded
  terminal:
  - signed
  - discarded
  transitions:
  - key: load_gl_from_odoo
    name: Load the Odoo books
    from: none
    to: loaded
    by: system
    gate: the doors decided; business-partner and tax mapped every partner and tax the lines use
    effect: companies, accounts, account map, fiscal years, periods (closed up to Odoo's lock date), rates, recurring entries, deferrals and posted journals written read-only, keyed on the Odoo record; a re-run changes nothing
  - key: reconcile_gl_migration
    name: Reconcile against Odoo
    from: loaded
    to: reconciled
    by: system
    effect: every migration.odoo.reconcile check computed on both sides, with the difference
  - key: explain_gl_difference
    name: Explain each difference
    from: reconciled
    to: reconciled
    by: accountant
    requires: reason
    gate: the check differs and may be explained
    effect: the explanation recorded beside the check
  - key: reload_gl_migration
    name: Load again after a fix
    from:
    - loaded
    - reconciled
    to: loaded
    by: system
    gate: not signed
    effect: the same keys; explanations on unchanged checks kept
  - key: sign_gl_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; postings from the composed modules accepted from the day after cut-over
  - key: discard_gl_migration
    name: Discard the run
    from:
    - loaded
    - reconciled
    to: discarded
    by: owner
    requires: reason
    effect: the run's rows removed; nothing was posted on them
  readiness:
  - transition: load_gl_from_odoo
    gate: doors_decided
    reason: a migration door is undecided
    fix: 'Migration: answer it'
  - transition: load_gl_from_odoo
    gate: partners_mapped
    reason: Odoo partners on receivable or payable lines have no bp_id
    fix: run the business-partner migration first
  - transition: load_gl_from_odoo
    gate: taxes_mapped
    reason: Odoo taxes on tax lines have no tax_code
    fix: run the tax migration first
  - transition: sign_gl_reconciliation
    gate: zero_unexplained
    reason: 'checks neither equal nor explained: named'
    fix: explain each, or fix and reload
  - transition: sign_gl_reconciliation
    gate: must_equal
    reason: a check that must be equal differs (the trial balance per company, account and period)
    fix: fix the mapping and reload; it cannot be explained away