Fixed assets & depreciation¶
Backend domain: app/graphql/fixed_assets/
Migration: add_fixed_assets
RBAC Path: FIXED_ASSETS
Overview¶
A FixedAsset is registered with cost, useful life, salvage value, depreciation method, and three GL accounts (the asset itself, accumulated depreciation, depreciation expense). Each month, runDepreciation(periodStart, periodEnd) computes depreciation per active asset, creates a DepreciationRun with one DepreciationLine per asset, and posts a single balanced JE (DR expense / CR accumulated depreciation).
Statuses: ACTIVE → FULLY_DEPRECIATED (auto when NBV hits salvage) → DISPOSED (manual via disposeFixedAsset).
Supported methods: STRAIGHT_LINE, DOUBLE_DECLINING_BALANCE. UNITS_OF_PRODUCTION is defined but currently returns 0 (placeholder).
Setup prerequisites¶
- Three GL accounts per asset (or category): asset, accumulated depreciation (contra-asset), depreciation expense.
- Disposal flow needs a "gain/loss on disposal" account (passed at disposal time).
GraphQL surface¶
Queries¶
fixedAssets: [FixedAsset!]!
fixedAsset(assetId: UUID!): FixedAsset!
depreciationRuns: [DepreciationRun!]!
depreciationRun(runId: UUID!): DepreciationRun!
FixedAsset fields: id, assetNumber (auto, FA-00001), name, description, category (BUILDING | VEHICLE | EQUIPMENT | FURNITURE | IT | OTHER), acquisitionDate, acquisitionCost, salvageValue, usefulLifeMonths, depreciationMethod, assetAccountId, accumulatedDepreciationAccountId, depreciationExpenseAccountId, status, disposalDate, disposalProceeds, supplierInvoiceId.
DepreciationRun fields: id, periodStart, periodEnd, totalAmount, status (DRAFT | POSTED | REVERSED), ledgerEntryId, plus nested lines: [DepreciationLine!]!.
DepreciationLine: id, runId, assetId, amount, nbvAfter (net book value after this line).
Mutations¶
registerFixedAsset(fixedAsset: FixedAssetInput!): FixedAsset!
runDepreciation(periodStart: Date!, periodEnd: Date!): DepreciationRun!
disposeFixedAsset(
assetId: UUID!,
disposalDate: Date!,
proceeds: Decimal!,
cashAccountId: UUID!,
gainLossAccountId: UUID!
): FixedAsset!
FixedAssetInput: name, category, acquisitionDate, acquisitionCost, usefulLifeMonths, assetAccountId, accumulatedDepreciationAccountId, depreciationExpenseAccountId, depreciationMethod, plus optional salvageValue (default 0), description, supplierInvoiceId.
Behaviour to handle in the UI¶
- Asset register list with filters by category and status; show NBV (frontend can compute =
acquisitionCost - sum(depreciation_lines.amount), or expose as a backend field — see follow-up). - Asset detail page shows acquisition info plus depreciation schedule (read from
depreciationRun.linesfiltered to this asset across runs). - "Run monthly depreciation" button on the depreciation runs list — opens a date picker pre-filled with the previous month range. Show a preview before posting (would need a separate "dry-run" query — currently the mutation always posts).
- Disposal flow is a modal: enter disposal date + proceeds + cash account + gain/loss account. Show calculated gain/loss preview before confirming.
Error states¶
- Account-type mismatches are not validated server-side yet — accumulator should be a contra-asset, expense should be an expense account. Build client-side hints.
- Running depreciation for an already-run period currently throws on the unique
(period_start, period_end)constraint — surface as "Depreciation already run for that period".
Suggested UX flow¶
- Asset register with kanban-style status columns or a list with status filter.
- Add asset form including the three account pickers; pre-suggest accounts by category once
CompanySettingdefaults exist (not implemented yet — see follow-up). - Monthly close workflow: a "depreciation runs" page where the accountant clicks "Run period" each month-end.
- Disposal modal with calculated gain/loss preview.
Scheduled execution (admin cron)¶
Wire this command into Render as a daily cron service (it gates internally on each tenant's DEPRECIATION_RUN_DAY setting):
uv run python -m app.admin.tasks.fixed_assets_monthly_depreciation_task
# Force a specific YYYY-MM (bypasses the day-of-month gate):
uv run python -m app.admin.tasks.fixed_assets_monthly_depreciation_task --period 2026-04
# Dry-run preview:
uv run python -m app.admin.tasks.fixed_assets_monthly_depreciation_task --dry-run
Per tenant the script:
- Reads
DEPRECIATION_RUN_DAY(default1, clamped to 1–28). Today'sdaymust match — otherwise the tenant is skipped as a no-op. - Opens a session bound to the tenant DB and resolves
FixedAssetServicewith thesupportuser as actor. - Posts the previous calendar month's depreciation run via
run_period(first_of_prev, last_of_prev). - If the period was already posted (
IntegrityErroron the unique(period_start, period_end)), logsalready_run=Trueand continues — re-running the same day is safe. - Emits a summary line:
tenants=N POSTED=L skipped=S failed=F.
Open follow-ups (not yet implemented)¶
- No "dry-run" / preview mutation —
runDepreciationalways posts. AddpreviewDepreciationif a confirm step is required before posting. UNITS_OF_PRODUCTIONreturns 0 — collect per-period units before enabling.- No per-category default-account configuration; the frontend has to ask for the three accounts every time.
- No NBV field exposed directly on
FixedAsset; compute client-side from depreciation lines or ask backend to add a resolver.