dodil/erp-brick-fixed-assets
DODIL ERP brick: fixed assets
src/brick_fixed_assets/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: fixed-assets
name: Fixed assets & depreciation
version: 3
actors:
- id: asset_accountant
name: the person who keeps the asset register (financial accountant, bookkeeper)
does: creates assets and classes' defaults, posts manual acquisitions, settles assets under construction, runs and simulates depreciation, proposes retirements, transfers, revaluations and impairments, opens asset counts and acts on their differences
sees: every asset of the companies they keep
permissions:
- fa:asset:write
- fa:asset:read
- fa:acquisition:write
- fa:auc:settle
- fa:depreciation:simulate
- fa:depreciation:run
- fa:transaction:write
- fa:count:manage
- fa:report:read
- fa:capital_allowance:read
- id: finance_approver
name: the financial controller (finance manager)
does: approves depreciation postings, retirements, transfers between companies, revaluations and impairments; approves the capital allowances computation; closes the asset year; keeps day-to-day settings; explains migration differences
sees: everything
permissions:
- fa:asset:read
- fa:depreciation:approve
- fa:transaction:approve
- fa:capital_allowance:approve
- fa:capital_allowance:read
- fa:year:close
- fa:report:read
- fa:settings:write
- fa:migration:read
- id: tax_preparer
name: whoever prepares the corporation tax computation (in-house tax, or the external accountant)
does: decides per addition which allowance is claimed (AIA, full expensing, first-year, pool), keeps the opening pools, prepares the capital allowances computation for each chargeable period
sees: the tax area of every asset; not the book areas' journals
permissions:
- fa:asset:read
- fa:capital_allowance:write
- fa:capital_allowance:read
- fa:report:read
- id: asset_custodian
name: whoever looks after the assets on a site or in a department (site manager, office manager, IT)
does: records units used for assets depreciated by use, counts the assets in their location or cost centre, reports one missing or damaged
sees: the assets in their own location or cost centre
permissions:
- fa:asset:read_own
- fa:usage:write
- fa:count:write
- id: owner
name: The owner (or whoever the owner names)
does: holds the controls on the people who value assets (the depreciation areas and the ledger each posts to, the depreciation keys, the account determination per class, the capitalisation threshold, number formats, whether a depreciation posting needs a second person) and signs the migration reconciliation
sees: everything
permissions:
- fa:asset:read
- fa:report:read
- fa:settings:controls
- fa:migration:read
- fa:migration:sign
note: 'the controls are never held by whoever they control: an asset_accountant, finance_approver or tax_preparer holding fa:settings:controls is a configuration error the access test refuses'
- id: system
name: The system
does: stamps numbers, takes acquisitions from purchasing, computes planned depreciation, posts journals to gl per area, answers gl's reconciliation and close questions, suggests a class and life (suggest), loads and reconciles a migration
sees: everything, through its own service account
permissions:
- fa:number_series:claim
- fa:acquisition:intake
- fa:journal:post
- fa:depreciation:read
- fa:asset:ai_suggest
- fa:migration:load
- fa:migration:reconcile
note: 'each is an exact grant to one service client, never reached by a wildcard (permissions.machine): fa:acquisition:intake to purchasing''s client, fa:depreciation:read (and fa:asset:read for GET /assets/balances) to gl''s client, the rest to this module''s own'
workflows:
- entity: asset
process: acquire
states:
- planned
- active
- retired
- deactivated
terminal:
- deactivated
transitions:
- key: create_asset
name: Create the asset
from: none
to: planned
by: asset_accountant
gate: a class in force today and a company
effect: asset_no claimed from the class series; sub-number 0; defaults per area copied from the class; keyed on the request id
- key: add_sub_asset
name: Add a component or a later addition
from: planned
to: planned
by: asset_accountant
gate: the main asset exists in the same company and class
effect: the next sub_no, its own key and life per area
- key: ai_suggest_asset_class
name: AI suggests the class, life and tax pool (suggest)
from: planned
to: planned
by: system
gate: the step is on
effect: a suggestion per field with its evidence, in fa_ai_finding
- key: accept_asset_suggestion
name: Accept or correct each suggested field
from: planned
to: planned
by: asset_accountant
effect: each decision recorded; nothing applied without the accountant
- key: take_acquisition
name: Take the acquisition from purchasing
from:
- planned
- active
to: active
by: system
gate: the asset is of the bill's company and not retired; the period is open in every posting area's ledger
effect: a transaction kind acquisition per area keyed acquisition_ref; journals per posting area (debit class cost, credit asset clearing); capitalised_on set by the first acquisition
- key: post_manual_acquisition
name: Post a manual acquisition
from:
- planned
- active
to: active
by: asset_accountant
gate: an offset account other than asset clearing; the value date today or earlier; above the capitalisation threshold or the class is LVA
effect: a transaction kind acquisition (or grant, negative) per area; journals per posting area
- key: change_asset_master
name: Change location
cost centre or life from a date: null
from:
- planned
- active
to:
- planned
- active
by: asset_accountant
gate: starts after today
effect: a new dated row; depreciation recalculated prospectively from its date; posted periods never recomputed
- key: record_usage
name: Record units used this period
from: active
to: active
by: asset_custodian
gate: the asset's key is units_of_production; the period is not after today's; the units fit what remains
effect: a usage row (asset, period, seq)
- key: retire_fully
name: The last value leaves the register
from: active
to: retired
by: system
gate: an approved retirement or transfer out leaves cost 0 in every area
effect: status retired; derived from the transactions
- key: deactivate_asset
name: Deactivate a planned asset never capitalised
from: planned
to: deactivated
by: asset_accountant
requires: reason
gate: no transaction in any area
effect: kept with its number; never reused
note: an asset's values per area are the SUM of its transactions; active and retired are derived from them, never set by hand
readiness:
- transition: create_asset
gate: class_in_force
reason: the class has no row in force today
fix: 'Settings: asset classes (owner)'
- transition: create_asset
gate: number_series
reason: the class has no number series for this year
fix: 'Settings: number formats (owner)'
- transition: take_acquisition
gate: same_company
reason: the asset belongs to another company than the bill
fix: assign the order line to an asset of the bill's company
- transition: take_acquisition
gate: not_retired
reason: the asset is fully retired
fix: assign a new asset, or a new sub-number
- transition: take_acquisition
gate: period_open
reason: a posting area's ledger period for the bill date is closed
fix: ask the accountant to reopen the period in gl; the hand-over is retried
- transition: post_manual_acquisition
gate: offset_not_clearing
reason: 'asset clearing is purchasing''s: a manual acquisition offsets another account'
fix: pick the account the money or credit really came from
- transition: post_manual_acquisition
gate: not_future
reason: an acquisition cannot be dated after today
fix: post it on the day of the document
- transition: post_manual_acquisition
gate: above_threshold
reason: the amount is below the capitalisation threshold for a class that is not LVA
fix: expense it, or use class LVA
- transition: change_asset_master
gate: starts_after_today
reason: a change starts tomorrow at the earliest
fix: pick a later date
- transition: record_usage
gate: within_total
reason: the units are more than the asset has left
fix: check the reading, or extend the total units (a dated life change)
- transition: deactivate_asset
gate: never_capitalised
reason: the asset has transactions
fix: retire it instead (FA-16)
- entity: auc_settlement
process: acquire
states:
- draft
- settled
- reversed
terminal:
- reversed
transitions:
- key: draft_auc_settlement
name: Draft the settlement rule
from: none
to: draft
by: asset_accountant
gate: an AuC asset with unsettled line items
effect: per line item, receivers (asset, percent or amount), keyed (auc asset, settlement date, seq)
- key: settle_auc
name: Settle into the finished assets
from: draft
to: settled
by: asset_accountant
gate: each line item distributed exactly 100% or by amounts not above what is unsettled; every receiver is an asset of the same company; the period is open
effect: a claim row per (line item, receiver); credit the AuC and debit each receiver per area; the receivers' capitalisation date = the settlement date
- key: reverse_auc_settlement
name: Reverse a settlement
from: settled
to: reversed
by: asset_accountant
requires: reason
gate: no receiver has been depreciated, retired or transferred since
effect: reversing transactions dated today; the line items are unsettled again
readiness:
- transition: settle_auc
gate: fully_distributed
reason: a line item's rule does not add up to 100% (or exceeds what is unsettled)
fix: complete the rule
- transition: settle_auc
gate: not_settled_twice
reason: a line item is already settled (another settlement claimed it)
fix: reverse that settlement first
- transition: reverse_auc_settlement
gate: receivers_untouched
reason: a receiver has been depreciated or changed since
fix: retire or transfer from the receiver instead
- entity: depreciation_run
process: asset_accounting
states:
- simulated
- approved
- posted
- reversed
terminal:
- reversed
transitions:
- key: simulate_depreciation
name: Simulate the period
from: none
to: simulated
by: asset_accountant
gate: a period of an open asset year, not after today's period
effect: planned-to-date minus posted per asset, area and period; keyed (company_code, period_id, run_no); nothing posted
- key: resimulate_depreciation
name: Simulate again after a change
from: simulated
to: simulated
by: asset_accountant
effect: the figures recomputed from rows
- key: approve_depreciation_run
name: Approve the run
from: simulated
to: approved
by: finance_approver
approval: true
gate: the approver did not start the run (email AND sub); no gate fails (usage_missing, account_determination)
effect: who, when (today in the business time zone)
- key: post_depreciation_run
name: Post the run per area to its ledger
from: approved
to: posted
by: system
gate: each posting area's ledger period is open
effect: one journal per area, keyed derived_id(run_id, area); posted rows per asset, area and period
- key: post_unapproved_run
name: Post without a second person (owner switched it off)
from: simulated
to: posted
by: system
gate: the owner's control depreciation_needs_approval is false
effect: as post_depreciation_run; the starter recorded as poster
- key: reverse_depreciation_run
name: Reverse the run
from: posted
to: reversed
by: finance_approver
approval: true
requires: reason
gate: the year is not closed; the period of today is open
effect: reversing journals per area dated today; reversing posted rows; the next run plans the period again
readiness:
- transition: simulate_depreciation
gate: not_in_advance
reason: the period is after the current period
fix: run it in its own month
- transition: approve_depreciation_run
gate: second_person
reason: you started this run (same email or same sign-in)
fix: another approver
- transition: approve_depreciation_run
gate: usage_missing
reason: a units-of-production asset has no reading for the period
fix: the custodian records the units (zero is a reading)
- transition: approve_depreciation_run
gate: account_determination
reason: a class has no account in force for an area
fix: 'Settings: account determination (owner)'
- transition: post_depreciation_run
gate: period_open
reason: a posting area's ledger period is closed
fix: reopen the period in gl, or post in the next run
- transition: reverse_depreciation_run
gate: year_open
reason: the asset year is closed
fix: correct it in the next year's run
- entity: asset_transaction
process: asset_accounting
states:
- proposed
- approved
- posted
- rejected
- reversed
terminal:
- rejected
- reversed
transitions:
- key: propose_retirement
name: Propose a sale or a scrapping
from: none
to: proposed
by: asset_accountant
gate: the retirement date today or earlier; by amount, percent or quantity, not above what remains
effect: depreciation to the retirement date by the key's convention; gain or loss per area computed
- key: propose_asset_transfer
name: Propose a transfer to another asset or company
from: none
to: proposed
by: asset_accountant
gate: the receiving asset, or company and class; a transfer variant (gross | net); the transfer date falls in a period whose depreciation is posted (transfer_period_posted)
effect: the values leaving and arriving per area
- key: propose_revaluation
name: Propose a revaluation
from: none
to: proposed
by: asset_accountant
requires: evidence
gate: the area's policy is the revaluation model
effect: the change per area; reserve and profit or loss split
- key: propose_impairment
name: Propose an impairment or its reversal
from: none
to: proposed
by: asset_accountant
requires: evidence
gate: not above the NBV; a reversal not above what the carrying amount would have been
effect: the loss or reversal per area
- key: approve_asset_transaction
name: Approve the transaction
from: proposed
to: approved
by: finance_approver
approval: true
gate: the approver is not the proposer (email AND sub); the asset's version unchanged since the proposal
effect: who and when recorded
- key: reject_asset_transaction
name: Reject with a reason
from: proposed
to: rejected
by: finance_approver
requires: reason
- key: post_asset_transaction
name: Post per area to its ledger
from: approved
to: posted
by: system
gate: every company's and area's ledger period open
effect: journals per company and posting area keyed derived_id(transaction_id, area); the tax area recorded, never posted
- key: reverse_asset_transaction
name: Reverse a posted transaction
from: posted
to: reversed
by: finance_approver
approval: true
requires: reason
gate: the year is open; nothing later on the asset depends on it
effect: a reversing transaction dated today
note: a transfer between companies is ONE transaction with two legs (retire in the sender, acquire in the receiver); both post or neither
readiness:
- transition: propose_retirement
gate: within_remaining
reason: more than what remains of the asset (another retirement claimed it)
fix: lower the amount or percent
- transition: propose_retirement
gate: not_future
reason: a retirement cannot be dated after today
fix: record it on the day it left
- transition: propose_impairment
gate: not_above_nbv
reason: the impairment is more than the net book value
fix: lower it to the NBV at most
- transition: approve_asset_transaction
gate: second_person
reason: you proposed this (same email or same sign-in)
fix: another approver
- transition: approve_asset_transaction
gate: asset_unchanged
reason: the asset changed since the proposal
fix: propose again from the current values
- transition: post_asset_transaction
gate: period_open
reason: a company's or area's ledger period is closed
fix: reopen it in gl
- entity: capital_allowance_computation
process: asset_accounting
states:
- draft
- prepared
- approved
- filed
terminal:
- filed
transitions:
- key: draft_ca_computation
name: Open the computation for a chargeable period
from: none
to: draft
by: tax_preparer
gate: the previous period filed, or opening pools entered
effect: keyed (company_code, period_start, period_end); pools, additions and disposals derived from the tax area
- key: choose_allowance
name: Choose the allowance for an addition
from: draft
to: draft
by: tax_preparer
gate: the claim fits the asset (AIA not on a car; full expensing only for a company's new and unused main-pool plant)
effect: a claim row per asset (aia | full_expensing | fya_50 | pool | single_asset | sba | none) and amount
- key: prepare_ca_computation
name: Prepare the computation
from: draft
to: prepared
by: tax_preparer
gate: every addition has a claim; the AIA claimed within the company's allocated limit for the period
effect: WDA, balancing allowances and charges, closing pools computed and kept
- key: approve_ca_computation
name: Approve the computation
from: prepared
to: approved
by: finance_approver
approval: true
gate: the approver is not the preparer (email AND sub)
- key: return_ca_computation
name: Send back with a reason
from:
- prepared
- approved
to: draft
by: finance_approver
requires: reason
- key: file_ca_computation
name: Mark it filed with the return
from: approved
to: filed
by: finance_approver
requires: return_reference
effect: frozen; later changes land in the next period as adjustments
readiness:
- transition: choose_allowance
gate: claim_allowed
reason: this allowance is not available for this asset (a car cannot take AIA)
fix: pick the pool the asset belongs to
- transition: prepare_ca_computation
gate: aia_within_limit
reason: AIA claimed is above the company's share of the limit for this period
fix: move the excess to the pool, or change the group allocation (owner)
- transition: approve_ca_computation
gate: second_person
reason: you prepared this computation (same email or same sign-in)
fix: another approver
- entity: asset_count
process: acquire
states:
- open
- counted
- closed
terminal:
- closed
transitions:
- key: open_asset_count
name: Open a count for a location
from: none
to: open
by: asset_accountant
gate: a location or cost centre and a custodian
effect: keyed (location, count_date); the expected list fixed
- key: count_asset
name: Record what is found
from: open
to: open
by: asset_custodian
gate: the count date today or earlier; the custodian's own location
effect: a result per line (found | not_found | damaged | found_unlisted) with who counted
- key: finish_asset_count
name: Hand the count back
from: open
to: counted
by: asset_custodian
gate: every expected line has a result
- key: decide_count_difference
name: Decide each difference
from: counted
to: counted
by: asset_accountant
requires: decision
effect: a proposal (retirement, transfer, new asset) or a note per difference
- key: close_asset_count
name: Close the count
from: counted
to: closed
by: asset_accountant
gate: every difference has a decision
effect: last-counted date set on every found asset
readiness:
- transition: finish_asset_count
gate: all_counted
reason: 'lines without a result: named'
fix: count them, or record not_found
- transition: close_asset_count
gate: differences_decided
reason: a difference has no decision
fix: decide it (retire, transfer, add, or note)
- entity: fa_migration_run
process: asset_accounting
states:
- loaded
- reconciled
- signed
- discarded
terminal:
- signed
- discarded
transitions:
- key: load_assets_from_odoo
name: Load the Odoo asset register
from: none
to: loaded
by: system
gate: the doors are decided; gl has mapped every account and the fiscal year; purchasing has mapped the bills used
effect: classes, assets and takeover values written read-only, keyed odoo:<db>:<model>:<id>; a re-run changes nothing (FA-30)
- key: reconcile_assets_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_assets_difference
name: Explain each difference
from: reconciled
to: reconciled
by: finance_approver
requires: reason
gate: a check is not equal and may be explained
- key: reload_assets_migration
name: Load again after a fix
from:
- loaded
- reconciled
to: loaded
by: system
gate: not signed (a load after the signature is refused 409 not_signed)
- key: sign_assets_migration
name: Sign the reconciliation
from: reconciled
to: signed
by: owner
approval: true
gate: every check equal, or explained where explained is allowed
effect: who (email and sub), when and the run's checksums recorded; depreciation runs here from the next period
- key: discard_assets_migration
name: Discard the run
from:
- loaded
- reconciled
to: discarded
by: owner
requires: reason
readiness:
- transition: load_assets_from_odoo
gate: doors_decided
reason: a migration door is undecided
fix: 'Migration: answer it'
- transition: load_assets_from_odoo
gate: accounts_mapped
reason: Odoo asset, depreciation or expense accounts used have no gl account_id
fix: run the gl migration first
- transition: load_assets_from_odoo
gate: asset_not_taken_over
reason: this asset was taken over by a signed run (409 not_signed)
fix: adjust it here; a signed run's assets are never reloaded
- transition: load_assets_from_odoo
gate: not_signed
reason: 'this run is signed: signed history is never rewritten'
fix: start a new run for what changed after cutover
- transition: sign_assets_migration
gate: zero_unexplained
reason: 'checks neither equal nor explained: named'
fix: explain each, or fix and reload
- transition: sign_assets_migration
gate: must_equal
reason: a check that must be equal differs
fix: fix the mapping and reload