# Fixed assets & depreciation: the module source.
#
# Version 1 (2026-10-22, DRAFT for sign-off; the SAP full-scope pass): SAP FI-AA at mid-market scope, to the
# conventions of the signed tax, sales-order, ar and payments specs (shared.yaml conventions):
# - an ASSET is a company's thing with a value that is used up over years; its number (asset_no) and sub-number
# (sub_no 0 = the main asset, 1.. = a component or a later addition depreciated on its own, IAS 16.43) come from
# the class's series and are never reused
# - values are kept per DEPRECIATION AREA: book (01, the leading ledger's GAAP), IFRS or any ledger-specific area
# (30, a non-leading ledger), and the UK tax area (CA, capital allowances, which never posts). Each posting area
# names ONE gl ledger (gl's ledger_id); a journal for an area goes to that ledger only
# - an acquisition arrives from a purchasing bill (POST /acquisitions, served here: purchasing debits the asset
# clearing account, this module credits it and debits the class's cost account, per area) or as a manual
# posting; an asset under construction collects costs and is settled into the final assets
# - depreciation is DERIVED from the asset's values, its key and its life, cumulatively, so no period drifts by a
# penny; a run simulates, a second person approves, the system posts, a reversal is a reversing run
# - revaluation, impairment, partial or full retirement with gain or loss, transfers between cost centres and
# between companies: each a transaction with values per area, approved by a second person
# - the UK capital allowances are a TAX AREA: pools (main, special rate, single asset), AIA, full expensing and
# first-year allowances, writing-down allowances, balancing charges, per company and chargeable period
# - the asset history sheet, the reconciliation of NBV per class to gl, and a physical inventory of assets
# - dated rows, no back-dating ("today" in the one business_timezone); facts dated by their own document are the
# exception, said where they occur: the bill's date of an acquisition, the date an asset was sold or scrapped,
# the date of a count. Corrections are reversing transactions, never edits; machine actions are exact-grant
# machine permissions; AI steps suggest
# - the Odoo slice (account.asset and its depreciation moves -> assets with accumulated depreciation at cut-over;
# NBV reconciled per asset and per class to gl) under `migration:`
#
# Four decisions an auditor checks first:
# - an asset's value is the SUM of its transactions per area; nothing stores a net book value that rows do not explain
# - depreciation posted to a period is planned-to-date minus already-posted, so a repeat run posts only the difference
# - the person who proposed a retirement, revaluation, impairment, transfer or depreciation posting never approves it
# - the tax area never posts to a ledger; the book areas always reconcile to gl per class account, to zero
#
# APQC PCF v8.0: 19207 (10.0 Acquire, Construct, and Manage Assets) and 17058 (9.0 Manage Financial Resources).
# Lower-level PCF ids are not cited because they were not verified (erp-bench standards/processes.yaml carries the
# verified category ids only).
# 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):
# - use cases (MCP tools) of the shared families name the module noun (tool_names): get_migration_map becomes
# get_assets_migration_map, as gl's get_migration_map is gl's (no process change)
#
id: fixed-assets
name: Fixed assets & depreciation
short: Fixed assets
version: 3
maturity: planned # no package and no brick; this spec is the draft for sign-off
summary: >-
Know what the business owns, what it cost, what it is worth in the books, in IFRS and for tax, and depreciate it
every month without a spreadsheet: from the supplier's invoice or a project under construction to the day it is sold
or scrapped, with the capital allowances worked out and every number tying to the ledger.
facets:
domain: finance
industries: [construction, manufacturing, professional_services, retail, hospitality, wholesale, logistics]
customer_words:
- fixed asset register
- depreciation
- what is the van worth
- asset under construction
- capital allowances
- annual investment allowance
- writing down allowance
- we sold the old machine
- impairment
- asset count
- net book value
replaces: [the fixed asset register spreadsheet, the accountant's year-end depreciation journal, Sage Fixed Assets, Odoo Assets]
pillars: [sql]
first_module_fit: low
depends_on:
- module: gl
why: every acquisition, depreciation, retirement and revaluation is a journal; the ledgers a depreciation area posts to, periods, cost centres and company codes are gl's
required: true
- module: purchasing
why: most assets arrive on a supplier's invoice; an order line with account assignment asset hands its value here
required: recommended
- module: business-partner
why: the supplier an asset came from, and the party it was sold to
required: optional
- module: inventory
why: the receiving location a delivered asset sits in (a location code on the asset)
required: optional
feeds: [gl]
owns_master_data: [asset_class, asset, depreciation_area, depreciation_key]
references_master_data: [business_partner, account, cost_centre, company_code, location, period]
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'
stories:
# ── the register ────────────────────────────────────────────────────────────────────────
- id: FA-01
kind: person
actor: owner
story: As the owner, I want the kinds of assets we keep, and how each is valued in the books, in IFRS and for tax, set once, so that every asset of a kind is treated the same and the accountant cannot quietly change a policy.
given: "classes BUILD (buildings), PLANT, VEH (vehicles), IT, AUC (assets under construction) and LVA (low value); areas 01 book -> ledger 0L, 30 IFRS -> ledger 2L, CA UK tax -> no ledger; key LIN5 (straight line, 5 years, full month)"
when: the owner records, from tomorrow, class IT with LIN3 in area 01, LIN4 in area 30, pool main in area CA, and its accounts (cost 0160, accumulated depreciation 0161, depreciation expense 7710, gain 4910, loss 7910, revaluation reserve 3200, impairment 7720)
then: "a dated class row and a dated account-determination row per area exist; an asset created in IT from tomorrow takes those defaults; an asset_accountant who tries is refused (fa:settings:write cannot change a control); a row dated today or earlier is refused (a first row of a class may start today); an area naming a ledger gl does not have is refused naming it (GET /ledgers); area CA is refused a ledger (the tax area never posts)"
screen: Settings
reference: "SAP asset classes (OAOA), chart of depreciation and depreciation areas (EC08, OADB), account determination (AO90), area-to-ledger assignment in S/4 (accounting principle per ledger)"
customer_varies: "which areas exist: most SMEs keep book and tax only; a group reporting in IFRS adds area 30"
- id: FA-02
kind: person
actor: owner
story: As the owner, I want depreciation methods defined once as keys, so that "straight line over 5 years from the month of purchase" means exactly one thing everywhere.
given: "keys LIN5 (straight_line, 60 periods, convention full_month), DB25 (declining_balance 25%, switch to straight line when higher, convention full_month), UOP (units_of_production), HALF (straight_line, convention half_year), DAY (straight_line, convention daily), NONE (land, assets under construction), IMM (low value: fully in the period of acquisition)"
when: the owner records key DB25 from tomorrow
then: "the key is a dated row with method, rate or life, base (cost | net book value), switch rule, acquisition convention and retirement convention, scrap handling and whether it stops at zero or at scrap; a key used by an asset can be superseded by a new dated row but never edited, and the change applies prospectively from its date (IAS 8 change in estimate); a rate outside 0..100 is a 422 naming the field"
screen: Settings
reference: "SAP depreciation keys (AFAMS) with calculation methods (base, declining-balance, maximum amount, multi-level, period control: AFAMA / AFAPC)"
- id: FA-03
kind: person
actor: asset_accountant
story: As the asset accountant, I want to create an asset with its sub-numbers for the parts that wear out at different rates, so that the roof and the building, or the engine and the lorry, are depreciated over their own lives.
given: "class PLANT, company UK01, a press costing 80,000.00 of which the hydraulic unit 20,000.00 lasts 4 years and the frame 10 years"
when: the accountant creates the asset with sub-number 0 (the frame) and sub-number 1 (the hydraulic unit, LIN4 in areas 01 and 30)
then: "asset_no 2026-PLANT-0007 is stamped once from the class series and sub-numbers 0 and 1 exist as asset_ids UK01-2026-PLANT-0007-0 and -1, each with its own key and life per area copied from the class and overridable; tag (inventory_no), serial number, location, cost centre and custodian are on the master; a second create with the same request id returns the same asset; nothing is capitalised until an acquisition posts"
screen: Assets
reference: "SAP asset master (AS01, AS11 sub-number, ANLA / ANLB / ANLZ with time-dependent data), IAS 16.43-47 componentisation"
- id: FA-04
kind: person
actor: asset_accountant
story: As the asset accountant, I want to change where an asset is, who looks after it, or its remaining life, from a date, so that the register stays true without rewriting history.
given: "a van in cost centre SITE-A with 30 of 60 periods left; an engineer's report says it lasts 12 periods more than planned"
when: "the accountant moves it to cost centre SITE-B from 1 December and changes area 01's life to 72 periods from 1 December"
then: "both are new dated rows starting after today (refused if today or earlier); depreciation from December is charged to SITE-B; from December the remaining NBV is spread over the new remaining 42 periods (prospective, IAS 8.36); posted periods are never recomputed; the same change for every asset of a class is one mass change listing each asset it touched"
screen: Assets
reference: "SAP time-dependent master data (ANLZ), mass change (AR31) and change of useful life with recalculation (AFAR)"
- id: FA-05
kind: system
actor: system
trigger: when an asset is created without a class, life or tax pool, and the step is on
story: The system suggests the class, the useful life and the capital allowance pool from the description, the supplier and the amount, so that the accountant picks rather than looks up, and nothing is decided without them.
given: "a bill line 'Ford Transit 350 L3H2, CO2 210 g/km' for 32,000.00"
when: the AI suggestion step runs
then: "a suggestion: class VEH, LIN5 in area 01, pool main in area CA (a van, not a car), with its evidence; the accountant accepts, corrects or dismisses each field; nothing is applied without them; every decision is recorded in fa_ai_finding"
# ── acquisition ─────────────────────────────────────────────────────────────────────────
- id: FA-06
kind: integration
actor: system
owner: fixed-assets
trigger: when purchasing approves a supplier bill (or a credit) whose line has account assignment asset
story: Purchasing hands the value of an asset bought on a supplier's invoice to the asset register, so that it is capitalised once, in every area, without anyone re-keying it.
given: "PO-2026-0051 line 1 assigned to asset UK01-2026-VEH-0003-0; the supplier's bill INV-77 dated 2026-10-20 for 32,000.00 net plus 6,400.00 VAT reclaimable; areas 01 and 30 post, CA is statistical"
when: "purchasing calls POST /acquisitions with acquisition_ref pur:BILL-2026-00031:1"
then: "a transaction of kind acquisition dated 2026-10-20 (the bill's date: a fact dated by its document) with 32,000.00 in areas 01, 30 and CA; journals to ledger 0L and to ledger 2L each debit the class cost account 0150 and credit asset clearing 32,000.00 (purchasing's bill journal debited asset clearing in every ledger, so clearing nets to zero); capitalised_on and the depreciation start are set by the key's convention; a retry with the same acquisition_ref posts nothing new; a credit from the supplier is the same call with a negative amount and reduces cost in every area; an asset that is retired, or of another company, is refused with its reason and the bill stays approved with the hand-over listed"
reference: "SAP integrated acquisition from MIRO with account assignment A (movement type 100 / credit 105), acquisition with clearing offset (ABZON)"
- id: FA-07
kind: person
actor: asset_accountant
story: As the asset accountant, I want to post an acquisition that did not come through purchasing, so that an asset paid by card, built in-house or brought in by a grant still lands in the register with the right offset.
given: "a laptop bought on the company card for 1,450.00 (expense claim already posted to card clearing); and a government grant of 10,000.00 against the press"
when: "the accountant posts a manual acquisition of 1,450.00 offset to account 2210 card clearing, and a grant of -10,000.00 on the press offset to grants receivable"
then: "each is a transaction with values per area and a journal per posting area; the offset account is the one named, never asset clearing (that belongs to purchasing); the grant reduces cost in areas 01 and 30 under the deduction method (IAS 20.24, FRS 102 24.5) and does not reduce the tax area unless the tax preparer says so; the value date is the document's (a fact: today or earlier, never later); an amount that does not fit NUMERIC(18,2) is a 422 naming the field"
screen: Acquisitions
reference: "SAP manual acquisition with offset (ABZON / F-90), investment support (ABIF)"
- id: FA-08
kind: person
actor: asset_accountant
story: As the asset accountant, I want an item bought below our capitalisation threshold kept out of the register, or written off at once, so that the register is not full of staplers.
given: "a capitalisation threshold of 500.00 (owner's control) and class LVA with key IMM"
when: "an order line for a 300.00 monitor is assigned to asset class IT, and another for 450.00 to class LVA"
then: "the IT line is refused at the purchasing order with the gate below_threshold (expense it instead, or a second person accepts it with a reason); the LVA asset is created and depreciated in full in its acquisition period, appearing in the history sheet as an addition and a charge in the same year"
screen: Acquisitions
reference: "SAP low-value assets (maximum amount check, OAYK) and the capitalisation policy"
- id: FA-09
kind: person
actor: asset_accountant
story: As the asset accountant, I want costs of something we are still building collected on an asset under construction and settled into the finished assets, so that the new warehouse starts depreciating the day it is in use and not before.
given: "AUC asset UK01-2026-AUC-0001-0 with three bills (steel 120,000.00, roofing 60,000.00, electrics 30,000.00) and a manual acquisition of own labour 15,000.00"
when: "on completion the accountant settles 100% of the steel and roofing to a new BUILD asset, the electrics 100% to a new PLANT asset (integral features), and the own labour 80% / 20% to the two"
then: "each AuC line item is settled once (a claim row per line item and receiver), the AuC is credited and each receiver debited per area with the settlement date as capitalisation date; BUILD receives 192,000.00 and PLANT 33,000.00; the AuC itself never depreciates (key NONE); settling more than 100% of a line item, or one already settled, is refused; two accountants settling the same line at once settle it once; a partial settlement leaves the rest on the AuC"
screen: Acquisitions
reference: "SAP assets under construction with line-item settlement (AIAB / AIBU), settlement rules, transaction types 335 / 336"
# ── depreciation ────────────────────────────────────────────────────────────────────────
- id: FA-10
kind: system
actor: system
trigger: on read, and when an acquisition, transaction or master change posts
story: The system computes each asset's planned depreciation per area and period from its values, its key and its life, so that every figure in a run is explained by rows and a repeat never drifts.
given: "a van 12,000.00 acquired 2026-10-20, scrap 0, LIN5 (60 periods); and the same van under keys with each convention"
when: the plan is computed for October and November 2026
then: "planned depreciation to date is CUMULATIVE: round_half_up((cost - scrap) x periods_elapsed / life_periods) minus what the earlier periods were planned, so the 60 periods sum to exactly 12,000.00. Conventions: full_month (from the month of acquisition): October 200.00; next_month: October 0.00, November 200.00; mid_month (acquired after the 15th: from the next month): October 0.00; daily (days in use / days in the period): October 200.00 x 12 / 31 = 77.42; half_year: the first fiscal year gets half a year's charge spread over the periods from acquisition to year end; on retirement the same convention applies the other way, stated precisely in FA-16. Declining balance DB25: the year's charge is 25% of the NBV at the start of the year (or of cost in the year of acquisition, pro-rated by the convention), switching to straight line over the remaining life in the first year straight line is higher. Units of production: (cost - scrap) x units this period / total units"
reference: "SAP depreciation calculation (AFAMA base methods, declining-balance with switch, unit-of-production, period control: acquisition / retirement / transfer conventions)"
- id: FA-11
kind: person
actor: asset_custodian
story: As the custodian of an asset depreciated by use, I want to record the hours or miles it did this month, so that it is depreciated for what it did.
given: "a CNC machine 90,000.00, key UOP, 30,000 machine hours in total; 410 hours run in October"
when: the custodian records 410 hours for October
then: "a usage row (asset, period, units) dated by the period; October's depreciation is 90,000.00 x 410 / 30,000 = 1,230.00; a second reading for the same period replaces nothing (a correction is a new row with its reason, and the run posts the difference); units beyond the remaining total are refused; a run with no reading for a UOP asset shows the gate usage_missing"
screen: Counts
reference: "SAP unit-of-production depreciation (AFAMA method with units per period, ANLC / ANLP)"
- id: FA-12
kind: person
actor: asset_accountant
story: As the asset accountant, I want to simulate a period's depreciation before it posts, so that I see the charge per class and cost centre and catch a wrong life before it is in the books.
given: "company UK01, period 2026-10, 412 active assets, areas 01 and 30"
when: the accountant starts a run for 2026-10 and reads its simulation
then: "a run in state simulated lists, per area, class and cost centre, the planned-to-date minus already-posted for each asset, with the gates of each (usage_missing, period_open per ledger, account_determination); nothing is posted; a second simulation of the same period returns the same figures until a row changes; a run for a period after the current one is refused (no depreciation in advance)"
screen: Depreciation
reference: "SAP depreciation run in test mode (AFAB test run), depreciation simulation (S_ALR_87012936)"
- id: FA-13
kind: person
actor: finance_approver
story: As the financial controller, I want to approve a depreciation run before it posts, so that a wrong charge is a question, not a reversal.
given: "the simulated run for 2026-10 started by the accountant: area 01 41,275.33, area 30 40,912.08"
when: the controller approves it
then: "the run is approved with their email, sub and today's date; the system posts one journal per area to that area's ledger (debit depreciation expense per class and cost centre, credit accumulated depreciation per class), keyed derived_id(run_id, area); the posted amount per asset, area and period is recorded; the accountant who started the run cannot approve it (email OR sub matching is refused); approving twice changes nothing; a ledger whose period is closed is refused for that area and named"
screen: Depreciation
customer_varies: "whether a depreciation posting needs a second person (default yes; the owner may switch it off, warned)"
- id: FA-14
kind: person
actor: asset_accountant
story: As the asset accountant, I want to run depreciation again after a late invoice or a change, and to reverse a run posted by mistake, so that the books are right without editing a posted journal.
given: "2026-10 posted; on 3 November a supplier bill dated 20 October capitalises a new van of 12,000.00 (full_month)"
when: "the November run is simulated and approved; separately, a run posted in error is reversed"
then: "November's run posts the van's October catch-up (200.00) plus its November charge (200.00) as planned-to-date minus posted, dated in November (no back-posting into a closed or posted period); the reversal is a reversing run with its own journals dated today in an open period, the posted amounts per asset set back by reversing rows, and the original journals untouched; a run is never posted twice for the same planned amount"
screen: Depreciation
reference: "SAP repeat run and catch-up depreciation (AFAB repeat, catch-up method), reversal of the depreciation run in S/4 (AFAB reverse)"
- id: FA-15
kind: person
actor: finance_approver
story: As the financial controller, I want to close the asset year after the last run, so that nobody posts into a year the auditors have signed.
given: "fiscal year 2026 (gl's), every period's depreciation posted for every posting area, no transaction pending approval"
when: the controller closes the year
then: "the year is closed in this module (an asset transaction dated in it is refused, naming the year); the next year's opening values per asset and area are the closing values (derived, nothing copied); closing with a period not posted, or a transaction awaiting approval, is refused with each named; a closed year is never reopened here once gl has closed the same year"
screen: Depreciation
reference: "SAP fiscal year change (FAA_CMP / AJRW) and year-end closing (FAA_CMP / AJAB)"
# ── changes in value ────────────────────────────────────────────────────────────────────
- id: FA-16
kind: person
actor: asset_accountant
story: As the asset accountant, I want to sell or scrap an asset, all of it or part, and have the gain or loss worked out, so that the disposal is right in every area without a spreadsheet.
given: "a van cost 12,000.00, accumulated depreciation 7,200.00 at 30 September 2026 (LIN5 full_month), sold on 15 October 2026 for 5,500.00 net (invoiced through ar to account asset sale clearing)"
when: "the accountant proposes a retirement with revenue 5,500.00 dated 15 October, and later a partial retirement of 25% of a press"
then: "depreciation up to the retirement by the key's retirement convention (full_month: October 200.00 charged); NBV 4,600.00; gain 900.00 in area 01 posted debit asset sale clearing 5,500.00, debit accumulated depreciation 7,400.00, credit cost 12,000.00, credit gain 900.00; area 30 the same with its own values; the tax area records a disposal value for its pool (FA-21); the partial retirement retires 25% of cost and of accumulated depreciation proportionally; the retirement date is the date it left (a fact: today or earlier); with retirement convention half_year the retirement fiscal year gets six periods of life in total (of a twelve-period year): the retirement period carries what the earlier periods of that fiscal year left of six, i.e. charge = max(0, 6 - n) x the planned charge of one period, where n is the number of periods of that fiscal year already charged before the retirement period; never negative, and charges already planned or posted for earlier periods are not taken back. Worked example: the van above (12,000.00, 60 periods, 200.00 a period) retired in period 4 with periods 1 to 3 charged (600.00): the retirement period carries (6 - 3) x 200.00 = 600.00, the year totals 1,200.00 = six periods. Retired in period 8 with seven periods charged (1,400.00): the retirement period carries max(0, 6 - 7) = 0.00 and the 1,400.00 stays; retiring more than what remains, or two partial retirements together over 100%, is refused (the asset's version is the claim); it posts only after a second person approves"
screen: Transactions
reference: "SAP retirement with revenue (ABAON / F-92), scrapping (ABAVN), partial retirement by amount, percentage or quantity, transaction types 210 / 200"
- id: FA-17
kind: person
actor: finance_approver
story: As the financial controller, I want every retirement, revaluation, impairment and transfer between companies approved by somebody other than who proposed it, so that one person cannot write an asset off the books.
given: "a proposed scrapping of a 9,000.00 NBV machine proposed by the accountant"
when: "the accountant tries to approve it, then the controller approves it"
then: "the accountant is refused (second_person: same email or same sub); the controller's approval records who, email and sub, today's date and posts the transaction's journals per area; a rejected proposal keeps its reason and posts nothing; an approved transaction is never edited (a correction is a reversing transaction)"
screen: Transactions
- id: FA-18
kind: person
actor: asset_accountant
story: As the asset accountant, I want to move an asset to another cost centre, or to another company in the group, so that the charge and the value follow the asset.
given: "a forklift cost 24,000.00, accumulated depreciation 9,600.00, in company UK01; it moves to UK02 on 1 November; transfer variant gross (cost and accumulated depreciation carried)"
when: "the accountant proposes the inter-company transfer, the controller approves"
then: "UK01 retires it (credit cost 24,000.00, debit accumulated depreciation 9,600.00, debit inter-company clearing 14,400.00) and UK02 acquires a new asset with the same values (debit cost 24,000.00, credit accumulated depreciation 9,600.00, credit inter-company clearing 14,400.00), in each posting area to each company's ledger, the remaining life carried; transfer variant net carries 14,400.00 as cost; both companies' periods must be open (gate period_open, per company); the tax area records the transfer for both companies' pools (FA-21); a move between cost centres in one company is a dated master change (FA-04), not a transfer"
screen: Transactions
reference: "SAP intra-company transfer (ABUMN, transaction types 300 / 310) and inter-company transfer with transfer variants (ABT1N, gross / net method)"
- id: FA-19
kind: person
actor: asset_accountant
story: As the asset accountant, I want to revalue land and buildings to a valuer's figure where our policy is the revaluation model, so that the IFRS and statutory accounts show fair value with the surplus in the right place.
given: "a building in area 30 (IFRS, revaluation model) cost 500,000.00, accumulated depreciation 50,000.00; a valuer's report of 600,000.00 dated 30 September"
when: the accountant proposes a revaluation to 600,000.00 in area 30 with the report attached, and the controller approves
then: "the increase of 150,000.00 is credited to the revaluation reserve (other comprehensive income), the accumulated depreciation eliminated against cost (IAS 16.35(b)); future depreciation is on 600,000.00 over the remaining life; a later decrease goes first against that asset's reserve and the rest to profit or loss (IAS 16.40); area 01 (cost model) is untouched; the report is kept as an object raw/fixed-assets/valuations/<sha256>.pdf; the revaluation dated by the valuation is a fact, posted in an open period"
screen: Transactions
reference: "SAP revaluation (transaction type 800 group, ABZU / ABMA for the area), IAS 16.31-42, FRS 102 17.15B-17.15F"
- id: FA-20
kind: person
actor: asset_accountant
story: As the asset accountant, I want to record an impairment, and its reversal when things recover, so that an asset is never carried above what it can recover.
given: "a production line NBV 300,000.00 in area 01; an impairment review (a lost contract) gives a recoverable amount of 180,000.00"
when: the accountant proposes an impairment of 120,000.00 with the review attached, the controller approves; a year later a reversal is proposed
then: "120,000.00 is charged to impairment loss (or first against a revaluation reserve for that asset), future depreciation is on 180,000.00 over the remaining life; the reversal is capped at what the carrying amount would have been without the impairment (IAS 36.117) and shown separately in the history sheet; an impairment larger than the NBV is refused"
screen: Transactions
reference: "SAP unplanned depreciation and write-up (ABMA, ABZU; transaction types 640 / 700), IAS 36, FRS 102 section 27"
# ── capital allowances (the UK tax area) ────────────────────────────────────────────────
- id: FA-21
kind: person
actor: tax_preparer
story: As whoever prepares the corporation tax computation, I want the capital allowances worked out from the register, per pool, for each chargeable period, so that the claim comes from the same assets as the accounts.
given: "company UK01, chargeable period 1 April 2026 to 31 March 2027 (12 months); opening main pool 50,000.00 and special rate pool 10,000.00; additions: plant 120,000.00 (main), a car 30,000.00 at 110 g/km (special rate); disposal of plant cost 15,000.00 for 8,000.00"
when: "the preparer claims AIA on the plant and prepares the computation"
then: "AIA 120,000.00 (within the 1,000,000.00 annual limit, pro-rated for a short period and shared across the group per the owner's allocation); main pool 50,000.00 - 8,000.00 = 42,000.00, WDA 18% = 7,560.00, closing 34,440.00; special rate pool 10,000.00 + 30,000.00 = 40,000.00, WDA 6% = 2,400.00, closing 37,600.00; total allowances 129,960.00; a car cannot take AIA (refused, naming the rule); a disposal value is the lower of proceeds and the asset's cost; a pool of 1,000.00 or less may be written off (small pools allowance); a balancing allowance or charge arises when a pool ends or a single-asset pool's asset is sold; full expensing (100%) and the 50% first-year allowance are choices for a company's new and unused plant, with their own disposal rule (proceeds become a balancing charge rather than a pool reduction); the rates and the AIA limit are dated owner-controlled rows, not code"
screen: Capital allowances
reference: "SAP tax depreciation area with a UK localisation; Capital Allowances Act 2001 Part 2 (s51A AIA, s56 WDA, s56A small pools, s104D special rate), structures and buildings allowance (Part 2A)"
customer_varies: "whether the computation is prepared here or by the external accountant from the register's export (the export is the same rows)"
- id: FA-22
kind: person
actor: finance_approver
story: As the financial controller, I want to approve the capital allowances computation before it goes into the tax return, so that what we claim is what we agreed and can never move afterwards.
given: "the computation of FA-21 prepared by the tax preparer"
when: the controller approves it, and later marks it filed with the return's reference
then: "approval records who (email and sub) and when; the preparer cannot approve their own (email OR sub); filed freezes it: an asset transaction dated in that chargeable period that would change the computation is refused for the tax area and lands in the next period as an adjustment, named; the computation's closing pools are the next period's opening pools (derived); the difference between book NBV and tax written-down value per company is shown for the deferred tax calculation (FRS 102 section 29, IAS 12)"
screen: Capital allowances
# ── seeing it and checking it ───────────────────────────────────────────────────────────
- id: FA-23
kind: person
actor: asset_accountant
story: As the asset accountant, I want the asset history sheet for the year, per class and area, so that the fixed assets note in the accounts is a report, not a spreadsheet.
given: "company UK01, fiscal year 2026, area 01"
when: the accountant opens the history sheet
then: "per class: cost at the start of the year, additions, retirements, transfers in and out, revaluations, cost at the end; accumulated depreciation at the start, the charge for the year, impairments and reversals, depreciation on retirements and transfers, at the end; net book value at the start and the end; every column the SUM of transactions in rows, and the closing of one year equal to the opening of the next; exportable as CSV; the same for area 30 and any period range"
screen: Reports
reference: "SAP asset history sheet (S_ALR_87011990, RAGITT_ALV01); IAS 16.73(e), FRS 102 17.31(e) (the reconciliation of carrying amount)"
- id: FA-24
kind: person
actor: asset_accountant
story: As the asset accountant, I want the register reconciled to the ledger per class account, so that the balance sheet's fixed assets are the register's to the penny.
given: "class VEH: register cost 184,000.00 and accumulated depreciation 61,300.00 at 31 October in area 01; ledger 0L shows the same on 0150 / 0151, except a manual journal of 500.00 an accountant posted to 0150"
when: the reconciliation is read as of 31 October
then: "per class account and ledger: the register's figure (this module), gl's figure (gl reads ours through GET /assets/balances and compares, the way it reconciles payables against purchasing), and the difference with its fix; the 500.00 is a break named 'posted to an asset account outside the register: reverse it and post a manual acquisition'; asset clearing's balance is listed per ledger, zero when every asset bill was handed over"
screen: Reports
reference: "SAP reconciliation of asset accounting with the general ledger (FAA_GL_RECON), the asset reconciliation accounts are posted only by FI-AA"
- id: FA-25
kind: person
actor: asset_accountant
story: As the asset accountant, I want to open a count of the assets on a site, so that what the register says we own is checked against what is there.
given: "location SITE-A with 37 assets in the register, last counted 14 months ago"
when: the accountant opens a count for SITE-A dated 10 November, assigned to the site's custodian
then: "a count lists the 37 assets (tag, description, serial number) expected; it is keyed (location, count date); a second open of the same count returns it; the asset list is fixed when the count opens, and an asset moved in during the count is listed separately"
screen: Counts
reference: "SAP physical inventory of assets (inventory number, last inventory date ANLA-IVDAT, inventory list RAINVE00 / S_ALR_87011979)"
- id: FA-26
kind: person
actor: asset_custodian
story: As the custodian of a site, I want to tick off what I find, and say what I cannot find or what is there and not listed, so that the register is put right by the accountant rather than by guesswork.
given: "the open count for SITE-A"
when: "the custodian records 34 found (scanning tags), 2 not found, 1 damaged, and a pressure washer with no tag found on site"
then: "each line has a result (found | not_found | damaged | found_unlisted) dated the count date (a fact: today or earlier), with who counted; found assets get their last-counted date; a not_found or damaged line becomes a proposal for the accountant (retirement as scrapping, FA-16, or a transfer); a found_unlisted line becomes a draft asset or a note; the count closes only when every line has a result and every difference a decision; a custodian sees only their own location's counts"
screen: Counts
# ── migration ───────────────────────────────────────────────────────────────────────────
- id: FA-30
kind: system
actor: system
trigger: once at cutover, and again after a fix until signed
story: The system loads the Odoo asset register read-only, so that every asset continues here with its cost and its accumulated depreciation at cut-over, and depreciates from the next period without a gap or a double charge.
given: "an Odoo database with asset models, running and closed assets and their posted depreciation entries up to the cutover date 2026-09-30"
when: the migration run loads twice
then: "every asset is keyed odoo:<db>:account.asset:<id>; each running asset is carried as a takeover transaction in area 01 (cost = original_value, accumulated depreciation at the start of the fiscal year, depreciation posted in the fiscal year to the cutover date, from its posted depreciation moves plus already_depreciated_amount_import), with its remaining life = the periods Odoo had not yet depreciated; the second load changes nothing; takeover values post NO journals (gl migrates the journals); October 2026 is depreciated here, not in Odoo; a run after signature never rewrites signed history"
- id: FA-31
kind: person
actor: owner
story: As the owner, I want to sign the asset migration only when every asset's net book value and every class's total agree with Odoo and the ledger, or are explained, so that the register I start from is one I agreed.
given: "a reconciled run with one explained difference (an Odoo asset whose account had a manual journal)"
when: the owner signs
then: "the signature records who (email and sub), when and the run's checksums; signing is refused while a must-equal check differs (NBV per asset, cost and accumulated depreciation per class account against Odoo's trial balance)"
screen: Migration
processes:
- key: acquire
pcf_id: 19207
pcf_name: Acquire, Construct, and Manage Assets
reference_names: ["Acquire to Retire", "Acquire to Decommission", "Asset lifecycle"]
brick: fixed-assets
- key: asset_accounting
pcf_id: 17058
pcf_name: Manage Financial Resources (fixed-asset accounting)
reference_names: ["Record to Report (fixed assets)", "Depreciation run", "Capital allowances"]
brick: fixed-assets
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, 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" }
entities:
- id: depreciation_area
meaning: one way an asset is valued (book, IFRS, UK tax), and the gl ledger it posts to
natural_key: (company_code, area_code, valid_from)
key_attributes: [company_code (shared.yaml keys.company_code; one ledger per company per area), name, purpose (book | ifrs | ledger_specific | tax), "ledger_id (gl's; NULL for the tax area, which never posts)", posting (per_run | none), "value_model (cost | revaluation)", "values_from (own | area 01: an area may copy acquisition values from another)", currency]
relationships: [values every asset per area, posts to a gl ledger]
master_data: module-owned
reference_table: fa_area
- id: depreciation_key
meaning: a depreciation method, defined once (straight line over 5 years from the month of purchase)
natural_key: (key_code, valid_from)
key_attributes: [method (straight_line | declining_balance | declining_then_straight | units_of_production | immediate | none), "rate_pct (declining)", "life_periods (default; the asset's own overrides)", base (cost | net_book_value), "acquisition_convention (full_month | next_month | mid_month | daily | half_year)", "retirement_convention (same set)", "stop_at (zero | scrap)"]
relationships: [drives an asset's planned depreciation per area]
master_data: module-owned
reference_table: fa_dep_key
- id: asset_class
meaning: a kind of asset, with its defaults per area and its accounts
natural_key: (class_code, valid_from)
key_attributes: [name, "is_auc", "is_low_value", "default key and life per area", "default capital allowance pool (main | special_rate | single_asset | sba | none)", number_format]
relationships: [has account_determination rows, classifies assets]
master_data: module-owned
reference_table: fa_class
- id: account_determination
meaning: which gl accounts a class posts to in one area
natural_key: (class_code, area_code, valid_from)
key_attributes: ["cost_account_id, accumulated_depreciation_account_id, depreciation_expense_account_id, gain_account_id, loss_account_id, revaluation_reserve_account_id, impairment_account_id, impairment_reversal_account_id (BIGINT, gl's account_id)"]
relationships: [maps a class and area to gl accounts]
master_data: "module-owned; the accounts are references to gl"
reference_table: fa_account_map
- id: asset
meaning: one thing the company owns and uses up over years, or one component of it
natural_key: 'asset_id = "<company_code>-<asset_no>-<sub_no>" (migrated: "odoo:<db>:account.asset:<id>" with asset_no carried); asset_no stamped once from the class series'
key_attributes: [company_code, asset_no, sub_no, class_code, description, serial_no, inventory_no (the tag), bp_id (supplier), capitalised_on (derived from the first acquisition), status (derived), last_counted_on, quantity, request_id]
relationships: [has asset_master_rows over time, has asset_area_rows, has transactions]
master_data: "module-owned master data: fixed-assets is the sole writer"
reference_table: fa_asset
- id: asset_master_row
meaning: where an asset is and who answers for it, from a date
natural_key: (asset_id, valid_from)
key_attributes: [cost_centre (gl's), location_id (inventory's, or a site code), custodian_email, set_by_email]
relationships: [describes an asset over time]
master_data: module-owned
reference_table: fa_asset_master
- id: asset_area_row
meaning: how one asset is depreciated in one area, from a date
natural_key: (asset_id, area_code, valid_from)
key_attributes: [key_code, life_periods, "total_units (units of production)", scrap_value, dep_start (derived by the convention)]
relationships: [drives planned_depreciation]
master_data: module-owned
reference_table: fa_asset_area
- id: asset_transaction
meaning: "something that changed an asset's value: acquisition, grant, settlement, transfer, retirement, revaluation, impairment, takeover; or its reversal"
natural_key: 'transaction_id, derived from its source ("acq:<acquisition_ref>", "set:<settlement_id>:<seq>", "ret:<asset_id>:<proposed moment>", "odoo:<db>:account.asset:<id>:takeover")'
key_attributes: [asset_id, kind, value_date (a fact for acquisitions and retirements), posting_period_id, status, proposed_by_email, proposed_by_sub, approved_by_email, approved_by_sub, approved_on, reverses_transaction_id, evidence_key, source_ref]
relationships: [has transaction_area_values, posts journals per area]
master_data: module-owned
reference_table: fa_transaction
- id: transaction_area_value
meaning: what one transaction did to an asset in one area
natural_key: (transaction_id, area_code)
key_attributes: ["cost (NUMERIC(18,2), signed)", "accumulated_depreciation (signed)", "depreciation_in_year (takeover)", "revaluation_reserve (signed)", "gain_loss (signed)", "proceeds", journal_id, ledger_id]
relationships: [one area of an asset_transaction]
master_data: module-owned
reference_table: fa_transaction_area
- id: acquisition_claim
meaning: "one hand-over from purchasing, so a retried bill capitalises once"
natural_key: acquisition_ref ("pur:<bill_id>:<line_no>")
key_attributes: [bill_id, line_no, asset_id, amount, transaction_id, taken_at]
relationships: [becomes one acquisition transaction]
master_data: module-owned
reference_table: fa_acquisition_claim
- id: auc_settlement
meaning: how an asset under construction's costs were distributed to the finished assets
natural_key: (auc_asset_id, settlement_date, seq)
key_attributes: [status, receivers (asset_id, percent | amount per line item), settled_by_email]
relationships: [has settlement_claims, posts transactions]
master_data: module-owned
reference_table: fa_auc_settlement
- id: settlement_claim
meaning: a line item of an AuC claimed by one settlement for one receiver, so it is never settled twice
natural_key: (auc_asset_id, line_item_transaction_id, receiver_asset_id)
key_attributes: [settlement_seq, percent, amount]
relationships: [claims an AuC line item]
master_data: module-owned
reference_table: fa_settlement_claim
- id: usage_reading
meaning: units an asset did in a period (hours, miles, pieces)
natural_key: (asset_id, period_id, seq)
key_attributes: [units (NUMERIC(19,4)), recorded_by_email, reason (a correction)]
relationships: [drives units-of-production depreciation]
master_data: module-owned
reference_table: fa_usage
- id: depreciation_run
meaning: one period's depreciation for one company, simulated, approved and posted
natural_key: (company_code, period_id, run_no)
key_attributes: [run_id (derived), status, started_by_email, started_by_sub, approved_by_email, approved_by_sub, approved_on, reverses_run_id, totals per area]
relationships: [has depreciation_postings, posts one journal per area]
master_data: module-owned
reference_table: fa_dep_run
- id: depreciation_posting
meaning: what a posted run charged one asset in one area for one period
natural_key: (asset_id, area_code, period_id, run_id)
key_attributes: [amount (signed; a reversal negative), cost_centre, journal_id]
relationships: [posted by a depreciation_run]
master_data: module-owned
reference_table: fa_dep_posting
- id: fiscal_year_status
meaning: whether an asset year is open in this module, per company
natural_key: (company_code, fiscal_year)
key_attributes: [status (open | closed), closed_by_email, closed_by_sub, closed_on]
relationships: [gates transactions dated in it]
master_data: module-owned
reference_table: fa_year
- id: capital_allowance_computation
meaning: one company's capital allowances for one chargeable period
natural_key: (company_code, period_start, period_end)
key_attributes: [status, months, aia_limit_allocated, pools (opening, additions, disposals, allowances, balancing, closing per pool), prepared_by_email, prepared_by_sub, approved_by_email, approved_by_sub, return_reference]
relationships: [has ca_claims, derived from the tax area]
master_data: module-owned
reference_table: fa_ca_computation
- id: ca_claim
meaning: which allowance is claimed on one addition, and how much
natural_key: (company_code, period_start, asset_id)
key_attributes: [claim (aia | full_expensing | fya_50 | pool | single_asset | sba | none), pool (main | special_rate | single_asset | sba), amount, disposal_value]
relationships: [one line of a capital_allowance_computation]
master_data: module-owned
reference_table: fa_ca_claim
- id: ca_rate
meaning: the dated rates and limits of the capital allowances (owner-controlled rows, not code)
natural_key: (rate_key, valid_from)
key_attributes: ["rate_key (aia_limit | wda_main_pct | wda_special_pct | sba_pct | small_pool_limit | fya_full_expensing_pct | fya_50_pct)", value, legislation_ref]
relationships: [used by capital_allowance_computation]
master_data: module-owned
reference_table: fa_ca_rate
- id: asset_count
meaning: a physical check of the assets at one location on one date
natural_key: (location_id, count_date)
key_attributes: [custodian_email, status, opened_by_email]
relationships: [has count_lines]
master_data: module-owned
reference_table: fa_count
- id: count_line
meaning: one asset expected or found in a count, and what was decided
natural_key: (location_id, count_date, line_no)
key_attributes: [asset_id (null for found_unlisted), result, counted_by_email, decision, decision_ref]
relationships: [belongs to an asset_count]
master_data: module-owned
reference_table: fa_count_line
- id: number_series
meaning: the next asset number per class and year
natural_key: (series_id, year)
key_attributes: [format, high_water_mark]
relationships: [stamps asset.asset_no]
master_data: module-owned
reference_table: fa_number_series
- id: ai_finding
meaning: what the AI suggested for an asset, and what the accountant decided
natural_key: (subject_ref, step, run_no, finding_no)
key_attributes: [model, inputs_digest, field, value, evidence, decision (accepted | corrected | dismissed), decided_by_email, decided_at]
relationships: [about an asset]
master_data: module-owned
reference_table: fa_ai_finding
- id: migration_map
meaning: (odoo db, odoo model, odoo id) -> our natural key
natural_key: (odoo_db, odoo_model, odoo_id)
key_attributes: [natural_key, run_id]
relationships: [read by later migrations]
master_data: module-owned
reference_table: fa_migration_map
master_data:
owns: [fa_area, fa_dep_key, fa_class, fa_account_map, fa_asset, fa_asset_master, fa_asset_area, fa_ca_rate]
references:
- entity: account
owner: gl
how: "fa_account_map.*_account_id = gl account_id (BIGINT); offsets of manual acquisitions"
if_absent: "nothing can post: gl is required"
- entity: ledger
owner: gl
how: "fa_area.ledger_id, validated against gl's GET /ledgers"
if_absent: "one ledger: area 01 posts to it and no other area posts"
- entity: cost_centre
owner: gl
how: "fa_asset_master.cost_centre; carried on each depreciation line"
- entity: company_code
owner: gl
how: "fa_asset.company_code; every journal names it"
- entity: business_partner
owner: business-partner
how: "fa_asset.bp_id (the supplier), BIGINT"
- entity: location
owner: inventory
how: "fa_asset_master.location_id where inventory is composed; else a site code validated against nothing"
warning: >-
an asset register kept in a spreadsheet beside this one, or depreciation journals typed into gl by hand, breaks
the reconciliation per class account the first month: the asset accounts are posted only by this module
jobs:
- story: FA-10
trigger: on read, and after a value or master change commits
runs: planned depreciation per asset, area and period
where_options: [derived on read, in the same request after commit]
idempotent: "a function of rows; nothing stored until a run posts"
- story: FA-06
trigger: when purchasing calls POST /acquisitions after a bill approval commits
runs: one acquisition transaction and its journals
where_options: [in the same request]
idempotent: 'keyed acquisition_ref "pur:<bill_id>:<line_no>" (a credit: "pur:<credit_id>:<line_no>"); a retry returns the same transaction'
- story: FA-30
trigger: once at cutover, and again until signed
runs: the Odoo load
where_options: [a one-off request per run, the composition's worker for a large register]
idempotent: 'every row keyed "odoo:<db>:<model>:<id>" and recorded in fa_migration_map'
seams:
# ── inbound: a neighbour calls this module ────────────────────────────────────────────────
- story: FA-06
with: purchasing
owner: fixed-assets
direction: inbound
route: "POST /acquisitions"
request: "{acquisition_ref: \"pur:<bill_id>:<line_no>\", source_kind: purchase_bill | purchase_credit, asset_id (or class_code + description to create the asset), company_code, bp_id, bill_id, line_no, purchase_order_id, value_date (the bill's invoice date, YYYY-MM-DD), amount (string, NUMERIC 18,2: net plus any non-reclaimable VAT; negative for a credit), currency, quantity (string), cost_centre}"
response: "201 {acquisition_ref, transaction_id, asset_id, areas: [{area_code, cost, journal_id, ledger_id}]}; 200 the same with replay: true; 409 {gate: same_company | not_retired | period_open, met: false, reason, fix}; 422 {gate: bounds, reason, fix}"
permission: fa:acquisition:intake
rule: "called by purchasing after approve_bill commits, per line with account assignment asset; purchasing's own journal debits purpose asset_clearing in every ledger and this module credits it per posting area, so clearing nets to zero; purchasing never writes fa_ tables; idempotent on acquisition_ref"
status: aligned 2026-10-22 with purchasing PUR-56
- story: FA-06
with: purchasing
owner: fixed-assets
direction: inbound
route: "GET /assets/{asset_id}"
request: "path asset_id"
response: "{asset_id, company_code, asset_no, sub_no, class_code, description, status, is_auc, cost_centre, capitalisation_threshold, readiness: [{gate, met, reason, fix}]}"
permission: fa:asset:read
rule: "purchasing's asset_assignable gate at confirm_purchase_order: the asset exists, is of the order's company and not retired, and the line is above the threshold or the class is LVA"
status: aligned 2026-10-22 with purchasing PUR-55
- story: FA-06
with: purchasing
owner: fixed-assets
direction: inbound
route: "GET /asset-classes"
request: "query on (YYYY-MM-DD, default today)"
response: "{classes: [{class_code, name, is_auc, is_low_value, capitalisation_threshold}]}"
permission: fa:asset:read
rule: "the order line's asset picker (a new asset per line by class, or an existing asset_id)"
status: aligned 2026-10-22 with purchasing PUR-55
- story: FA-24
with: gl
owner: fixed-assets
direction: inbound
route: "GET /assets/balances"
request: "query {company_code, ledger_id, as_of (YYYY-MM-DD)}"
response: "{company_code, ledger_id, as_of, accounts: [{account_id, role (cost | accumulated_depreciation), class_code, balance}], clearing: {account_purpose: asset_clearing, open_acquisitions: [{acquisition_ref, amount}]}}"
permission: fa:asset:read
rule: "gl reconciles each asset account against this, the way it reconciles payables against purchasing's GET /bills/open (GL-31's twin); this module calls no gl balance route and gl never reads fa_ tables"
status: aligned 2026-10-22; gl GL-39 asks it
- story: FA-15
with: gl
owner: fixed-assets
direction: inbound
route: "GET /depreciation/status"
request: query {company_code, period_id, ledger_id (optional; absent = every area)}
response: "{company_code, period_id, areas: [{area_code, ledger_id, posted: bool, run_id}], pending_transactions: n, readiness: [{gate, met, reason, fix}]}"
permission: fa:depreciation:read
rule: "gl's period close readiness asks it (gate depreciation_posted): a period closes in a ledger only when every area posting to it has posted"
status: aligned 2026-10-22; gl GL-39 asks it for its close gate depreciation_posted
- story: FA-30
with: gl
owner: fixed-assets
direction: inbound
route: "GET /migration-map/{db}/{model}?ids="
request: "path db, model (account.asset); query ids=<comma-separated Odoo ids>"
response: "[{odoo_model, odoo_id, ours}]; an id not migrated is omitted"
permission: fa:migration:read
rule: "the one map route (shared.yaml conventions.odoo_migration.map_route)"
# ── outbound: this module calls a neighbour ───────────────────────────────────────────────
- story: FA-13
with: gl
owner: gl
direction: outbound
route: "POST /journal-entry/journals"
request: '{journal_id (BIGINT as string; = derived_id(source_document, event, ledger_id) when a ledger is named, else derived_id(source_document, event); gl checks it), source_module: "fixed-assets", source_document: "fa:<transaction_id>" for an asset event or "fa:<run_id>" for a depreciation run (suffixed ":<class>" when a run is split by asset class), event (acquire | depreciate | transfer | revalue | impair | retire | settle | reverse), company_code, ledger_id (the depreciation area''s ledger, for an event that differs per area; absent when common to every ledger, as an acquisition), journal_date, currency, memo, lines: [{purpose (asset_cost | accumulated_depreciation | depreciation_expense | asset_clearing | disposal_gain | disposal_loss | impairment_loss | revaluation_reserve) qualified by the asset class, or account_id, debit | credit, asset_id, cost_centre, profit_centre (optional), movement_type (addition | depreciation | disposal | transfer | revaluation | impairment), memo}]}'
response: '201 {journal_id, entry_no, period_id, status: posted, replay: false, lines: n}; 200 the same with replay: true; 409 {gate: period_open | journal_conflict, reason, fix}; 422 {gate: balanced | purposes_mapped | ledger_assigned | bounds, reason, fix}'
permission: gl:journal:post
rule: one journal per transaction (or run) and posting area, to that area's ledger only; the tax area never posts; at most 5,000 lines per call (a larger run is split by asset class); fixed-assets never writes gl_ tables
status: aligned 2026-10-22 with gl GL-37
- story: FA-01
with: gl
owner: gl
direction: outbound
route: GET /companies/{company_code}/ledgers
request: path company_code
response: '200 {company_code, functional_currency, fiscal_year_end_month, ledgers: [{ledger_id, kind: leading | non_leading, accounting_principle, from_fiscal_year, until_fiscal_year}]}; 404 when the company is unknown or not visible'
permission: gl:company:read
rule: an area's ledger_id must be one of these; the leading ledger is area 01's default; an area whose ledger the company does not keep is refused
status: aligned 2026-10-22; one form, gl's GET /companies/{company_code}/ledgers (the GET /ledgers the p2p draft assumed is dropped)
- story: FA-13
with: gl
owner: gl
direction: outbound
route: "GET /periods/open"
request: query date=YYYY-MM-DD, company_code (optional; the default company), ledger_id (optional; the leading ledger)
response: '200 {date, company_code, period_id, open: bool, status: open | closed | none, reason, fix}'
permission: gl:period:read
rule: "the period_open gate per posting area; ledger_id is the multi-ledger extension (omitted = the leading ledger)"
status: aligned 2026-10-22 with gl GL-32
- story: FA-03
with: gl
owner: gl
direction: outbound
route: "GET /fx-rates/{currency}"
request: path currency ISO 4217; query on=YYYY-MM-DD, type=spot | average | closing (optional, spot), to=ISO 4217 (optional; the company's functional currency)
response: '200 {currency, to, type, on, rate_date, rate: string (NUMERIC 19,6), source}; 404 {gate: fx_rate, reason, fix}'
permission: gl:fx:read
rule: "only where an acquisition arrives in a foreign currency: the value is translated once at the value date and kept; assets are non-monetary and never revalued for exchange rates"
status: aligned 2026-10-22 with gl GL-33
- story: FA-25
with: inventory
owner: inventory
direction: outbound
route: "GET /locations"
request: query usage (internal | receiving), warehouse_id (optional)
response: '{locations: [{location_id, warehouse_id, name, usage, default: bool}]}'
permission: inv:stock:read
rule: the location picker on the asset master and the count (usage internal); without inventory a site code is free text
status: aligned 2026-10-22 with inventory INV-01
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: get_assets_migration_map, route: "GET /migration-map/{db}/{model}", was: "get_migration_map" }
screens:
- { id: assets, name: Assets, stories: [FA-03, FA-04], actors: [asset_accountant, finance_approver, asset_custodian], note: "per asset: master over time, values per area (cost, accumulated depreciation, NBV), planned vs posted per year, transactions; the AI suggestion panel" }
- { id: acquisitions, name: Acquisitions, stories: [FA-07, FA-08, FA-09], actors: [asset_accountant], note: "hand-overs from purchasing (taken or refused, with the reason); manual acquisitions and grants; AuC line items and settlement rules" }
- { id: depreciation, name: Depreciation, stories: [FA-12, FA-13, FA-14, FA-15], actors: [asset_accountant, finance_approver], note: "per run: simulation by area, class and cost centre, gates, approval, journals per ledger; the year close with its readiness" }
- { id: transactions, name: Transactions, stories: [FA-16, FA-17, FA-18, FA-19, FA-20], actors: [asset_accountant, finance_approver], note: "proposals with their values per area, evidence, approval; reversals" }
- { id: capital_allowances, name: Capital allowances, stories: [FA-21, FA-22], actors: [tax_preparer, finance_approver], note: "per company and chargeable period: pools, additions with the claim chosen, disposals, allowances, closing pools; the book-tax difference" }
- { id: counts, name: Counts, stories: [FA-11, FA-25, FA-26], actors: [asset_accountant, asset_custodian], note: "usage readings for units-of-production assets; counts per location with each line's result and decision" }
- { id: reports, name: Reports, stories: [FA-23, FA-24], actors: [asset_accountant, finance_approver, tax_preparer, owner], note: "history sheet per class and area, reconciliation per class account and ledger, depreciation forecast (simulated future years)" }
- { id: settings, name: Settings, stories: [FA-01, FA-02], actors: [finance_approver, owner], note: "every config_row with its valid_from; the controls section is shown to the owner only" }
- { id: migration, name: Migration, stories: [FA-31], actors: [owner, finance_approver], note: "the doors, the run's checks (Odoo, ours, difference, must, explanation), the sign button with its readiness" }
permissions:
namespace: fa
catalog:
- fa:asset:read
- fa:asset:read_own # the custodian: assets in their own location or cost centre
- fa:asset:write
- fa:acquisition:write
- fa:auc:settle
- fa:usage:write
- fa:depreciation:simulate
- fa:depreciation:run
- fa:depreciation:approve
- fa:transaction:write
- fa:transaction:approve
- fa:capital_allowance:write
- fa:capital_allowance:read
- fa:capital_allowance:approve
- fa:count:manage
- fa:count:write
- fa:report:read
- fa:year:close
- fa:settings:write # day-to-day: class names, AI step modes
- fa:settings:controls # areas and their ledgers, keys, account determination, capitalisation threshold, capital allowance rates and group AIA allocation, number formats, depreciation_needs_approval
- fa:migration:read
- fa:migration:sign
- fa:number_series:claim # machine: asset_no at create
- fa:acquisition:intake # machine: purchasing's client hands over an asset bill line
- fa:journal:post # machine: post journals to gl per area
- fa:depreciation:read # machine: gl's client reads the depreciation status (its close gate depreciation_posted)
- fa:asset:ai_suggest # machine: the AI class / life / pool suggestion (suggest)
- fa:migration:load # machine: write migrated history; never posts
- fa:migration:reconcile # machine: compute the run's checks
machine:
- fa:number_series:claim
- fa:acquisition:intake
- fa:journal:post
- fa:depreciation:read
- fa:asset:ai_suggest
- fa:migration:load
- fa:migration:reconcile
row_visibility:
rule: "accountants, approvers, the tax preparer and the owner see every asset of the companies in their pool; a custodian sees only assets whose current location or cost centre is theirs, and only their own counts"
enforced: "in the use case, from the caller's email and sub; not_found, not forbidden, for a row the caller may not see"
not_found_not_forbidden: true
config_rows:
- table: fa_settings
changes: "how this business keeps its assets; every row dated (valid_from after today; the first row of a key may start today)"
settings:
- { key: capitalisation_threshold, type: decimal, default: "500.00", changes: "below this a line is expensed (or class LVA); fa:settings:controls" }
- { key: depreciation_needs_approval, type: bool, default: true, changes: "a run posts only after a second person approves; fa:settings:controls" }
- { key: asset_number_format, type: str, default: "{yyyy}-{class}-{seq:4}", changes: "stamped at create_asset per class; a change starts a new series from its valid_from; fa:settings:controls" }
- { key: intercompany_clearing_account_id, type: str, default: null, changes: "per company_code: the gl account that takes the other side of an inter-company transfer (gross or net); a transfer is refused (gate clearing_account) while it is unset; fa:settings:controls" }
- { key: asset_clearing_account_id, type: str, default: null, changes: "per company_code: the gl account this module credits when it takes an acquisition from purchasing (the account purchasing's bill debits as asset clearing); fa:settings:controls" }
- { key: asset_sale_clearing_account_id, type: str, default: null, changes: "per company_code: the gl account a retirement with revenue debits for the proceeds invoiced through ar (asset sale clearing); fa:settings:controls" }
- { key: transfer_variant_default, type: str, default: "gross", changes: "gross | net for transfers between companies; fa:settings:controls" }
- { key: aia_group_allocation, type: str, default: "all to the first company", changes: "how the group's one AIA limit is shared between its companies per chargeable period; fa:settings:controls" }
- { table: fa_settings, changes: "each AI step's mode (ai_steps: off | suggest)" }
- { table: fa_area, changes: "the depreciation areas per company and the gl ledger each posts to (one ledger per company per area); fa:settings:controls" }
- { table: fa_dep_key, changes: "the depreciation keys (method, rate, life, conventions); fa:settings:controls" }
- { table: fa_class, changes: "asset classes with their defaults per area and capital allowance pool; fa:settings:controls" }
- { table: fa_account_map, changes: "per class and area, the cost, accumulated depreciation, expense, gain, loss, revaluation reserve and impairment accounts; fa:settings:controls" }
- { table: fa_ca_rate, changes: "AIA limit, WDA rates (main 18%, special rate 6%), SBA 3%, small pools limit, first-year rates, each from a date with its legislation reference; fa:settings:controls" }
in_code_not_rows:
- "that an asset's values per area are the SUM of its transactions, and NBV is never stored"
- "the depreciation arithmetic: cumulative planned-to-date, rounded once per period, last period absorbing nothing because the cumulative is exact; a run posts planned-to-date minus posted"
- "that a change of life, key or scrap applies prospectively from its date and never recomputes a posted period"
- "that the tax area never posts a journal"
- "that a second person is a different email AND a different sub, for every approval in this module"
- "that the person who proposed a transaction or started a run never approves it"
- "that asset clearing is credited only by an acquisition from purchasing, and a manual acquisition offsets another account"
- "that 'today' is the date in the shared business_timezone, never the UTC date, and a dated row starts after today"
- "that a control (fa:settings:controls) is refused to fa:settings:write and to whoever it controls"
- "that an AI finding never changes an asset; a person accepts, corrects or dismisses it, recorded"
- "that migrated takeover values post no journals and a signed run is never rewritten"
ai_steps:
- key: ai_suggest_asset_class
benefit: "the accountant stops looking up which class, life and tax pool a new asset belongs to"
mode: suggest
modes_allowed: ["off", suggest]
safe_for_auto: false # a class decides the accounts, the life and the tax claim
inputs: [the description, the supplier, the amount, the bill line's expense category, the class definitions, the last 50 assets with their classes]
output: "per field (class, key, life per area, capital allowance pool) a suggestion with its evidence"
fallback: "the class defaults"
consent: "the owner turns the step on (fa_settings ai.ai_suggest_asset_class.mode); the accountant accepts, corrects or dismisses each field"
audit: "fa_ai_finding: model, inputs digest, field, value, evidence, decision, who, when"
eval: "a labelled set of 200 asset descriptions; class and pool accuracy, and cars put in the main pool per 100 (the costly error)"
clarifications: # declared deviations of the build from the first draft; each is a rule, not new scope
- { id: CL-FA-01, about: FA-18, rule: "a transfer's date must fall in a period whose depreciation is posted for the asset's area; otherwise the transfer is refused 409 transfer_period_posted (the values moved would not match the posted ones)" }
- { id: CL-FA-02, about: FA-30, rule: "a load after the run is signed is refused 409 not_signed; signed history is never rewritten" }
- { id: CL-FA-03, about: FA-31, rule: "explaining a reconciliation difference needs fa:migration:read (not a write permission): explaining records a reason and changes no value" }
- { id: CL-FA-04, about: FA-19, rule: "a revaluation decrease that exceeds the asset's own reserve goes to profit or loss on the impairment account of the class (fa_account_map), not a separate loss account" }
- { id: CL-FA-05, about: FA-30, rule: "a later migration run refuses (409 not_signed) the assets a signed run already took over: they are listed, never rewritten" }
deviation_support:
- deviation: "book and tax only, no IFRS"
how: "configuration (depth 1): area 30 is not defined; area 01 posts to the one ledger"
- deviation: "the external accountant does capital allowances"
how: "configuration: the tax area is kept (it is the register) and the tax_preparer role is given to the accountant; or the computation is not used and the tax area exported"
- deviation: "no purchasing module: assets are added by hand"
how: "composition: FA-07 manual acquisitions only; asset clearing is unused"
- deviation: "single-person finance team"
how: "configuration: depreciation_needs_approval false (warned); retirements and revaluations still need a second person, who may be the owner"
tests:
acceptance: "one per story id above, named for it"
access:
- "a custodian sees only assets in their own location or cost centre; another site's asset is not found, not forbidden"
- "self-approval is refused under a second identity: same email with another sub, and same sub with another email, are both refused for approve_depreciation_run, approve_asset_transaction, approve_ca_computation and reverse_depreciation_run of one's own run"
- "fa:settings:write cannot change a control; a person holding a role a control governs and fa:settings:controls is refused at configuration"
- "the tax preparer cannot post or approve a book-area transaction; the asset accountant cannot approve the capital allowances computation"
- "the machine permissions are refused to every person role and to a wildcard grant; fa:acquisition:intake is held by purchasing's client only"
- "only fa:migration:sign signs a reconciliation; fa:migration:load cannot post"
invariants:
- "per asset and area: cost, accumulated depreciation and NBV equal the SUM of transaction_area_values plus depreciation_postings, always; the history sheet's closing of one year equals the next year's opening"
- "planned depreciation over an asset's life sums to exactly cost - scrap (cumulative rounding, half-up to the minor unit); a run posts planned-to-date minus posted; running the same period twice posts the difference only (zero when nothing changed)"
- "a period is posted at most once per asset, area and run; two runs for the same period approved at once post once (the run key (company_code, period_id, run_no) and the posting key (asset_id, area_code, period_id, run_id) collide; the loser re-reads and posts the difference, zero)"
- "two concurrent partial retirements of one asset cannot retire more than 100% (the asset's version is bumped with optimistic concurrency; the loser retries, re-reads and is refused within_remaining)"
- "two settlements of the same AuC line item settle it once (settlement_claim PK); the AuC's cost after settlement = cost - settled, never negative"
- "the same acquisition_ref is one transaction; asset clearing per ledger = purchasing's debits - this module's credits, zero when every hand-over is taken"
- "the tax area posts no journal; an area with no ledger cannot post"
- "'today' is the date in the shared business_timezone: at 23:30 UTC on 31 July with Europe/London in BST (00:30 on 1 August local), today is 1 August; a dated row for 1 August is refused as not after today; today never goes backwards (the last today seen is kept and a smaller one refused)"
- "an acquisition's value date, a retirement date and a count date may be today or earlier, never later; every other dated row starts after today"
- "every amount, unit count, rate and life is bounded before it is stored: a value that does not fit NUMERIC(18,2) (NUMERIC(19,4) for units), a rate outside 0..100 or a life of 0 or above 1,200 periods is a 422 with the field named, never a 500"
- "asset numbers come from the class series with no reuse and no gap-filling: a retry of create_asset returns the number already stamped; a deactivated asset keeps its number; a gap only appears where a claim was abandoned, and is listed"
- "a reversal is a new transaction or run with negated values; a posted transaction or run is never edited"
- "capital allowances: AIA never exceeds the company's allocated limit pro-rated by months/12; WDA = pool x rate x months/12; a disposal value never exceeds the asset's cost; a filed computation never changes"
- "a revaluation decrease goes against that asset's own reserve first; an impairment reversal never takes the carrying amount above what it would have been without the impairment"
- "a migrated takeover posts no journal; a second load changes nothing; a load after the run is signed writes nothing to signed rows and lists any difference"
scenarios:
- id: SC-FA-01
name: bought on an order, depreciated, sold at a gain
stories: [FA-06, FA-10, FA-12, FA-13, FA-16, FA-17]
given: "class VEH (LIN5 full_month, areas 01 -> 0L and CA); a van ordered on PO-2026-0051 with account assignment asset; the bill 12,000.00 net approved 20 October 2026"
when: "purchasing hands over the bill line; October to September next year are run and approved by another person; the van is sold for 5,500.00 on 15 October 2027"
then: "cost 12,000.00 in 0L, clearing nets to zero; 12 runs of 200.00 and October 2027's 200.00 (full_month retirement); NBV 9,400.00 and a loss of 3,900.00 posted; the history sheet for 2027 (calendar fiscal year) shows a charge of 2,000.00 (January to October) and the retirement of cost 12,000.00 with accumulated depreciation 2,600.00; the tax area shows a main-pool disposal of 5,500.00"
- id: SC-FA-02
name: a warehouse built, settled, then a late invoice
stories: [FA-09, FA-14, FA-06]
given: "AUC with 210,000.00 of bills and 15,000.00 own labour; settlement to BUILD and PLANT on 1 November"
when: "settled, November run posted, then a 3,000.00 electrics bill dated 28 October arrives on the AuC and is settled to PLANT on 20 November"
then: "BUILD 192,000.00 and PLANT 33,000.00 then 36,000.00; the December run posts PLANT's catch-up for the 3,000.00 from its capitalisation; two settlements of the same line item are refused"
- id: SC-FA-03
name: the business date, not the UTC date
stories: [FA-04, FA-16, FA-26]
given: "business_timezone Europe/London; the clock at 2026-07-31T23:30Z (00:30 on 1 August in London)"
when: "a cost-centre change dated 1 August, a retirement dated 1 August and a count dated 2 August are entered"
then: "the change is refused (not after today), the retirement accepted (today), the count refused (later than today)"
- id: SC-FA-04
name: approving under a second identity
stories: [FA-13, FA-17, FA-22]
given: "an accountant with a second login of the same email and another sub, and another with the same sub"
when: "each tries to approve their own run, their own retirement and a computation they prepared"
then: "all refused second_person; a different person is accepted"
- id: SC-FA-05
name: book, IFRS and tax side by side
stories: [FA-01, FA-19, FA-20, FA-21, FA-22, FA-23]
given: "areas 01 (cost model), 30 (revaluation model) and CA; a building and plant"
when: "the building is revalued in area 30, the plant impaired in 01 and 30, and the capital allowances computed and approved"
then: "area 30's history sheet shows the revaluation in the reserve; area 01 untouched by it; both show the impairment; the tax area shows pools and allowances; the book-tax difference per company is listed"
- id: SC-FA-06
name: Odoo asset register reconciled and signed
stories: [FA-30, FA-31, FA-24]
given: "an Odoo database with 3 asset models, 120 running assets, 14 closed, cutover 2026-09-30, gl and purchasing migrated"
when: "the run is loaded twice, reconciled, one difference explained, the owner signs, and October is depreciated here"
then: "NBV per asset equals Odoo's book_value at cutover; cost and accumulated depreciation per class account equal Odoo's trial balance; no journal posted by the load; October's charge is the next planned period, never Odoo's September again"
- id: SC-FA-07
name: half-year retirement, never negative
stories: [FA-16]
given: "a van cost 12,000.00, 60 periods, retirement convention half_year, charged 200.00 a period"
when: "it is retired in period 4 after three periods were charged, and a twin van in period 8 after se