Fixed Assets & Depreciation
Business view
Dental equipment — X-ray machines, chairs, scanners, sterilizers — is expensive and lasts years, so it's not expensed all at once when purchased. Instead it's capitalized (recorded as an asset) and its cost is spread out over its useful life as depreciation, a small monthly expense.
This module handles the full lifecycle: record the asset when it's bought, run depreciation automatically every month, and record what happens when the asset is eventually sold, scrapped, or otherwise disposed of — including the gain or loss versus its remaining book value.
Technical view
FixedAsset (packages/prisma/schema.prisma:8878-8909) — acquisitionDate, acquisitionCost, salvageValue, usefulLifeMonths, depreciationMethod (DepreciationMethod: only STRAIGHT_LINE and DECLINING_BALANCE are supported, :8836-8839), decliningFactor (default 2, i.e. double-declining by default), accumulatedDepreciation (cached — the schema comment explicitly notes the GL, account 1590 tagged by fixedAssetId, is the real source of truth), lastDepreciationPeriod (a 'YYYY-MM' cursor used for catch-up), status (ACTIVE | FULLY_DEPRECIATED | DISPOSED).
Lifecycle
- Acquisition —
addFixedAsset(packages/server/src/resolvers/mutations/fixedAssetMutations.js:86) validates cost > 0, integer life ≥ 1,0 ≤ salvage < cost(salvage must be strictly less than cost, not merely ≤), factor > 0, then posts Dr Fixed Asset (1510) / Cr Cash (if a treasury was given) or Cr Accounts Payable (if not). - Depreciation —
packages/server/src/accounting/fixedAssets/depreciation.jscomputes the monthly charge (straight-line = cost/life; declining-balance = net book value × factor/life; the final scheduled month always writes off the exact remainder rather than asymptotically approaching it).runDepreciation(packages/server/src/accounting/fixedAssets/runDepreciation.js) is a catch-up batch: for every active asset it posts one entry per un-depreciated month sincelastDepreciationPeriod, each posting Dr Depreciation Expense (5500) / Cr Accumulated Depreciation (1590), idempotent per(asset, period). Marks the assetFULLY_DEPRECIATEDonce net book value reaches salvage value. - Disposal —
disposeFixedAssetfirst auto-catches-up depreciation for just that one asset (so the gain/loss calculation uses current, not stale, numbers), then posts Cr Fixed Asset (cost) / Dr Accumulated Depreciation / Dr Cash (proceeds) / and Cr Gain (4400) or Dr Loss (5900) on disposal versus net book value. Disposing an already-disposed asset is blocked.
Depreciation runs both manually (a "Run Depreciation" button on fixedAssets/FixedAssetsScreen.js, gated MANAGE_FIXED_ASSETS) and automatically — a monthly cron on the 1st at 07:00 targets the previous completed month, Redis-locked to prevent overlap.
editFixedAsset only allows changing name, description, asset code, category, and branch — cost, salvage value, useful life, method, and declining factor are all immutable after creation, to protect the depreciation schedule from mid-life corruption.
Permissions
addFixedAsset/editFixedAsset/disposeFixedAsset require MANAGE_FIXED_ASSETS. runDepreciation requires MANAGE_FIXED_ASSETS or CLOSE_PERIOD. Read queries only require VIEW_REPORTS.
Gaps to know about
- Nav/permission mismatch — mobile only. Web fixed this: the "Fixed Assets" tab is gated on
viewReports(accountingNav.js:88), matching the read permission. Mobile still gates its menu entry onmanageFixedAssetsonly (Accounting.js:65) — a mobile user with view-only reporting access has no way to reach the screen despite being authorized for the data. - Treasury is never actually selected on acquisition or disposal.
treasuryIdexists in the schema and the posting logic uses it to decide Cash vs. Accounts Payable — but neither the mobile nor the web "add asset" or "dispose asset" form collects it. In practice, every fixed-asset acquisition through the UI books to Accounts Payable, never Cash, even if the clinic paid cash on the spot; disposal proceeds always land in the generic default cash account rather than a specific treasury the user picked. - No end-to-end test coverage exists for acquisition/disposal posting or the full monthly depreciation batch — only the pure depreciation-math functions (straight-line, declining-balance, final-month write-off) are unit tested.