Skip to content

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

  1. Three GL accounts per asset (or category): asset, accumulated depreciation (contra-asset), depreciation expense.
  2. 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.lines filtered 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

  1. Asset register with kanban-style status columns or a list with status filter.
  2. Add asset form including the three account pickers; pre-suggest accounts by category once CompanySetting defaults exist (not implemented yet — see follow-up).
  3. Monthly close workflow: a "depreciation runs" page where the accountant clicks "Run period" each month-end.
  4. 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:

  1. Reads DEPRECIATION_RUN_DAY (default 1, clamped to 1–28). Today's day must match — otherwise the tenant is skipped as a no-op.
  2. Opens a session bound to the tenant DB and resolves FixedAssetService with the support user as actor.
  3. Posts the previous calendar month's depreciation run via run_period(first_of_prev, last_of_prev).
  4. If the period was already posted (IntegrityError on the unique (period_start, period_end)), logs already_run=True and continues — re-running the same day is safe.
  5. Emits a summary line: tenants=N POSTED=L skipped=S failed=F.

Open follow-ups (not yet implemented)

  • No "dry-run" / preview mutation — runDepreciation always posts. Add previewDepreciation if a confirm step is required before posting.
  • UNITS_OF_PRODUCTION returns 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.