Skip to content

Book 4, Chapter 16 — Fixed Assets

1. Purpose

Fixed Assets is where Bizwiz keeps the register of long-lived things a distributor owns — vehicles, forklifts, cold rooms, generators, furniture, IT kit — and tracks each one from the day it is acquired to the day it leaves the business. It answers four everyday questions:

  • What do we own, where is it, and who holds it? (the asset register, with tags, custodians, branches and QR/barcode labels)
  • What is it worth today? (acquisition cost, accumulated depreciation, net book value, revaluations, insurance value)
  • What has the ledger already been told? (capitalization onto the books, monthly/periodic depreciation, gains and losses on disposal)
  • What is the paper trail? (approvals for moving and disposing assets, status history, custodian handovers)

Unlike the legacy wa_* core of the ERP, Fixed Assets is a newer, cleanly-layered module (migrations dated 2026-06 to 2026-08). It has its own controllers, service classes, an account-resolver, dedicated depreciation-method classes, and a state machine — but it still posts its journals into the same GL spine the rest of Finance uses (wa_gl_trans, Chapter 15). So it is modern on the surface and integrated underneath.

The whole area lives under one nav menu — the "Fixed Asset" submenu inside the General Ledger sidebar — gated by the permission fixed-asset___view.


Where it lives (technical anchor). The nav boundary is resources/views/admin/includes/sidebar_includes/general_ledger.blade.php:605–735, opened by isset($my_permissions['fixed-asset___view']) (:605). Routes are all under the fixed-assets.* name prefix in routes/modules/fixed_assets.php:16–147 (prefix admin/fixed-assets, middleware AdminLoggedIn, ip-blocker). Controllers sit in app/Http/Controllers/Admin/FixedAssets/ (11 controllers). Business logic is in app/Services/FixedAssets/ and GL posting in app/Actions/GeneralLedger/PostFixedAsset*. Models are app/Models/FixedAsset*.php.

2. Users & roles

In plain terms there are four kinds of user here:

  • Asset clerks / storekeepers register assets, print labels, record custodian handovers, and raise transfer and disposal requests.
  • Finance / accountants capitalize assets onto the books, run and post depreciation, record valuations, and post disposals.
  • Approvers / managers sit on the maker-checker queues — they approve or reject transfers and disposals.
  • Configurators (senior finance) set up categories, account groups, the depreciation calendar (periods and run types), and the fiscal-year start.

Every controller action calls the helper can($action, $model) (app/helpers.php:4182). The base module permission string is fixed-asset (FixedAssetController::$permission_module, FixedAssetController.php:27), but the sidebar and sub-features gate on more granular model strings. Abilities used:

Ability Model string Where enforced
view fixed-asset asset register/index (FixedAssetController.php:37)
create / edit / delete fixed-asset asset CRUD (FixedAssetController.php:112,176,212)
change-status fixed-asset lifecycle transition (FixedAssetController.php:236)
post-gl fixed-asset capitalization queue — index/preview/post/reverse (FixedAssetCapitalizationController.php:30,45,76,112)
view fixed-asset-transfers transfers listing (sidebar :621)
approve fixed-asset-transfers transfer approve/reject (sidebar :635)
view fixed-asset-depreciation, fixed-asset-periods depreciation runs / calendar (sidebar :644,653,656)
view fixed-asset-valuations valuations (sidebar :666)
view fixed-asset-disposals disposals listing (sidebar :669)
approve fixed-asset-disposals disposal approve (sidebar :683)
view fixed-asset-categories, fixed-asset-account-groups config (sidebar :702,105,106)
view fixed-asset-reports reports group (sidebar :711)

Actions on assets are also audited by user: created_by/updated_by on fixed_assets, changed_by on fixed_asset_status_logs, requested_by/approved_by/reversed_by/posted_by on the transfer, disposal, valuation and depreciation-run tables. User id comes from Auth::id() throughout the service layer.

3. Processes

3.1 Asset lifecycle (register → in service → retire)

An asset is created as a draft, registered, then put into service (which is the capitalization gate — an in-service date is mandatory). From service it can go idle or under repair and back, and ultimately be disposed, written off, or lost. Those last three are terminal.

stateDiagram-v2
    [*] --> draft: create (auto tag CODE-NNN)
    draft --> registered
    draft --> written_off
    registered --> in_service: requires in_service_date
    registered --> written_off
    registered --> lost
    in_service --> idle
    in_service --> under_repair
    idle --> in_service
    idle --> under_repair
    under_repair --> in_service
    under_repair --> idle
    in_service --> disposed
    idle --> disposed
    under_repair --> disposed
    in_service --> written_off
    in_service --> lost
    disposed --> [*]
    written_off --> [*]
    lost --> [*]

The state machine is defined declaratively on the model: FixedAsset::STATUSES and FixedAsset::TRANSITIONS (app/Models/FixedAsset.php:79,88). Every move goes through FixedAssetService::transitionStatus() (FixedAssetService.php:106), which rejects illegal moves, enforces the in-service-date gate (:119), and writes a fixed_asset_status_logs row. Depreciation only touches assets in DEPRECIABLE_STATUSES = [in_service, idle, under_repair] (FixedAsset.php:103).

3.2 Capitalization (bringing cost onto the books)

Registering an asset does not touch the GL. Capitalization is a separate, deliberate finance step that posts the acquisition cost to the ledger. Finance opens the Capitalization queue (all assets with capitalization_gl_status = 'unposted' and a cost > 0), picks a credit/clearing account, previews the journal, and posts. They can instead mark-external (the cost already reached the GL via AP or an opening take-on — post nothing) or reverse a posted one.

sequenceDiagram
    actor Finance
    participant Q as Capitalization queue
    participant A as PostFixedAssetCapitalization
    participant GL as wa_gl_trans
    Finance->>Q: select asset(s) + credit account
    Q->>A: post(asset, creditAccountId)
    A->>A: resolve asset-cost account (resolver chain)
    A->>GL: insert Dr Asset cost / Cr Accum (if b/f) / Cr Clearing
    A->>A: status=posted, ref, lock cost & b/f

Posting logic: PostFixedAssetCapitalization::post() (PostFixedAssetCapitalization.php:38) builds balanced rows via CapitalizationGlPostingService::buildCapitalizationEntries() and inserts them with WaGlTran::insert($entries) (PostFixedAssetCapitalization.php:83). The journal is Dr Asset cost / Cr Clearing for a normal asset; for a take-on asset with brought-forward accumulated depreciation it becomes Dr Asset cost / Cr Accumulated depreciation (b/f) / Cr Clearing (net) (CapitalizationGlPostingService.php:54–73).

3.3 A depreciation run over a period

Depreciation is run per accounting period, in bulk, as a preview → post → (reverse) cycle. The engine walks every depreciating asset, computes the period's charge with the asset's method, and — on post — writes one balanced journal for the whole run.

flowchart TD
    P[Periods configured\nrun type + fiscal start] --> Prev[Preview run]
    Prev -->|per asset| Calc[DepreciationCalculator: SL or RB\nopening NBV, remaining depreciable]
    Calc --> Sched[fixed_asset_depreciation_schedules\nstatus=projected]
    Sched --> Post[Post run]
    Post --> GL[wa_gl_trans:\nDr Depreciation expense\nCr Accumulated depreciation]
    Post --> Run[run status=posted, gl_batch_ref]
    Post -.reverse.-> Rev[contra journal + status=reversed]

The run header is fixed_asset_depreciation_runs; the per-asset lines are fixed_asset_depreciation_schedules (unique on fixed_asset_id + accounting_period_no, the idempotency guarantee that a period can never be double-posted per asset — migration ...120100...:41). GL posting is Dr depreciation expense (ROLE_DEPRECIATION) / Cr accumulated depreciation (ROLE_ACCUMULATED) via DepreciationGlPostingService (DepreciationGlPostingService.php:71–72), inserted as one WaGlTran::insert() batch under number series FAD (DepreciationGlPostingService.php:22).

3.4 A disposal with approval + gain/loss

Disposing is maker-checker. A clerk raises a disposal (sale, scrap, write-off, trade-in), the system computes catch-up depreciation to the disposal date, net book value, and the resulting gain or loss (proceeds − NBV), then an approver posts it. Posting derecognizes cost and accumulated depreciation and books the gain/loss.

sequenceDiagram
    actor Clerk
    actor Approver
    participant D as Disposal
    participant GL as wa_gl_trans
    Clerk->>D: create (proceeds, buyer, type) — status=pending
    Note over D: computes cost, accum, catch-up dep, NBV, gain_loss
    Approver->>D: approve
    D->>GL: catch-up depreciation (if any)
    D->>GL: Dr Accum dep / Dr Disposal(proceeds) / Cr Asset cost / Dr|Cr Gain-Loss
    D->>D: status=posted, asset status=disposed/written_off

Disposal record: fixed_asset_disposals with status pending | posted | reversed (...120000...:34) and dedicated columns for cost_at_disposal, catch_up_depreciation, accumulated_at_disposal, nbv_at_disposal, gain_loss (:24–28). Approval (can('approve','fixed-asset-disposals'), FixedAssetDisposalController.php:57) recomputes the figures and posts up to three GL batches via PostFixedAssetDisposal::approve():

  1. Catch-up depreciation (series FAD, only if catch_up_depreciation > 0): Dr Depreciation expense / Cr Accumulated depreciation (DisposalGlPostingService::buildCatchUpEntries).
  2. Derecognition (series DSP): Cr Asset cost (−cost_at_disposal), Dr Accumulated depreciation (+accumulated_at_disposal), Dr Cash/Bank/Receivable (+proceeds, proceeds_account), Cr VAT output (if any), and the gain-or-loss balancer to the disposal account (ROLE_DISPOSAL) (DisposalGlPostingService::buildDerecognitionEntries).
  3. Revaluation-reserve release (only for a revalued asset with remaining surplus): Dr Revaluation reserve / Cr Retained earnings.

Gain/loss is net_proceeds − nbv where net_proceeds = proceeds − vat_amount and nbv = cost − accumulated. On post the asset moves to its resulting_status (disposed/written_off/lost), stored alongside previous_status. A reversal contras every leg (sign −1.0) and restores the pre-disposal status.

4. Tables touched & key data

The module owns ~14 tables, all created 2026-06 → 2026-08 (database/migrations/*fixed_asset*).

fixed_assets — the register master (...100100...)

Key columns: asset_tag (unique, CODE-NNN), name, serial_no, quantity, category_id, sub_category_id, polymorphic assetable_type/assetable_id (link to a Vehicle etc.), acquisition_date, in_service_date, acquisition_cost(15,2), accumulated_depreciation_bf (opening/imported accum. dep.), useful_life_months, end_of_life_value (residual), depreciation_method, status (the state-machine column; default changed to draft by ...110000_change_fixed_assets_status_default), placement fields location_id/department_id/cost_center_id/custodian_id, per-asset GL overrides asset_account/depreciation_account/accumulated_depreciation_account, revaluation fields revalued_amount/revaluation_reserve/last_valuation_date, insurance insured_value/insured_value_date, and the capitalization block capitalization_gl_status (unposted|posted|external), capitalization_gl_ref, capitalization_credit_account, capitalization_posted_at/_by (...100400...). Soft-deletes.

Note: the model constant FixedAsset::CAP_UNPOSTED/POSTED/EXTERNAL (FixedAsset.php:111–113) matches these string values exactly.

fixed_asset_categories — config (...100000...)

name, code, parent_id (self-referential sub-categories), is_depreciable, default_depreciation_method, default_useful_life_months, plus run_type_id and default_depreciation_rate(8,4) (added ...100000_add_depreciation_config...), and four default GL account ids: default_asset_account, default_depreciation_account, default_accumulated_account, default_disposal_account. Also account_group_id (added ...100100...).

fixed_asset_account_groups — reusable GL posting groups (...100000...)

asset_account, depreciation_account, accumulated_account, disposal_account (all COA ids); later extended with revaluation_reserve_account, revaluation_loss_account, impairment_account, retained_earnings_account (...110200_add_valuation_accounts...).

fixed_asset_transfers — movement maker-checker (...110200...)

fixed_asset_id, from_*/to_* for location/department/custodian/cost_center, effective_date, status (default pending), rejection_reason, requested_by/approved_by/approved_at. Later gains quantity/source_quantity for partial (split) transfers (...100300..., ...100500...).

Depreciation calendar & engine

  • fixed_asset_run_types (...100600...): name, code, months_per_period (1=monthly, 3=quarterly, 12=annual…), is_default, is_active; later auto_mode (...100200...).
  • fixed_asset_periods (...100700...): generated tiling of a fiscal year — run_type_id, period_code (unique), label, fiscal_year, sequence, months, start_date, end_date. Comment notes this is independent of wa_accounting_periods, which still governs where the GL journal lands.
  • fixed_asset_depreciation_runs (...120000...): batch header — accounting_period_no (→ WaAccountingPeriod.period_no), run_date, status (draft|previewed|posted|reversed), gl_batch_ref, reversal_batch_ref, asset_count, total_depreciation.
  • fixed_asset_depreciation_schedules (...120100...): one line per asset per period — depreciation_run_id, accounting_period_no, period_seq (asset's own 1..N counter that drives method math), depreciation_method (snapshot), opening_nbv, depreciation_amount, accumulated_depreciation, closing_nbv, status (projected|posted|reversed), gl_batch_ref. Unique (fixed_asset_id, accounting_period_no).

fixed_asset_valuations (...110000...)

valuation_type (insurance|revaluation|impairment), valuation_date, valuer_name, carrying_amount_before, valued_amount, difference, and the split of where the difference lands: to_reserve, from_reserve, to_profit_loss, accumulated_eliminated; status (pending|posted|reversed|recorded — insurance stays recorded, posts nothing), gl_batch_ref, reversal_batch_ref. Revaluation/impairment post under series FAV using the netting method (accumulated depreciation eliminated against cost, asset restated to fair value): an increase credits the revaluation reserve (ROLE_REVALUATION_RESERVE), a decrease debits the reserve first then the excess to P&L via ROLE_REVALUATION_LOSS or ROLE_IMPAIRMENT depending on valuation_type (ValuationGlPostingService, PostFixedAssetValuation::approve()). Only the most recent posted valuation can be reversed. A class revaluation (valuations.class/class-store) raises one pending valuation per asset in a category, each approved independently (IAS 16).

fixed_asset_disposals (...120000...)

Covered in §3.4. disposal_type values include sale|scrap|donation|write_off|lost. Three ref columns: gl_batch_ref (derecognition, series DSP) + depreciation_batch_ref (the catch-up depreciation batch, series FAD) + reversal_batch_ref.

Audit/support tables

fixed_asset_status_logs (fixed_asset_id, from_status, to_status, reason, changed_by), fixed_asset_custodian_logs, fixed_asset_documents (with later transfer_id/valuation_id links so a document can evidence a specific event).

5. Interactions with other modules

General Ledger (Chapter 15) — the posting spine. Every money event posts into wa_gl_trans via WaGlTran::insert(), using the app-wide signed-amount convention (positive = debit, negative = credit; a batch balances when the signed sum is zero — CapitalizationGlPostingService.php:9–21, assertBalanced() at :125). The GL header pulls the current accounting period from WaAccountingPeriod::where('is_current_period','1') (PostFixedAssetCapitalization.php:207) and a transaction number from NumberSeriesService — one series per event type: FAC capitalization, FAD depreciation (and disposal catch-up), DSP disposal derecognition, FAV valuation. So the fixed-asset periods drive what to depreciate, but wa_accounting_periods drives where the journal lands — two separate calendars, by design.

Account mapping (config → GL). Which COA code each leg posts to is resolved by FixedAssetAccountResolver::accountCodeFor() (FixedAssetAccountResolver.php:80) following a four-level fallback: asset override → category default → category's account group → GlConfiguration system code (e.g. FA_ASSET_COST, FA_DEPRECIATION_EXPENSE, FA_ACCUMULATED_DEPRECIATION, FA_DISPOSAL, FA_REVALUATION_RESERVE, FA_IMPAIRMENT_LOSS, FA_RETAINED_EARNINGS) — throwing a clear GlPostingException if nothing resolves (:36–61,132). Stored values are COA ids; the resolver maps to the account_code string that wa_gl_trans.account expects (:94).

Chart of Accounts / branches. Asset forms and the capitalization credit picker read WaChartsOfAccount (is_parent = 0) (FixedAssetController.php:448, FixedAssetCapitalizationController.php:38). Branch on GL lines uses the asset's cost_center_id or location_id as restaurant_id/tb_reporting_branch (CapitalizationGlPostingService.php:101,114).

Procurement / AP link (Book 3). There is no hard FK from an LPO/GRN to an asset. Instead the design uses the capitalization_gl_status = 'external' flag (mark-external) so an asset whose cost already reached the GL through an AP invoice or opening take-on is not double-posted (PostFixedAssetCapitalization.php:106, comment at file header). fixed_assets carries supplier_id and purchase_reference for a soft reference to the purchase. (No petty-cash link found — see §7.)

Reports feed Finance statements. Depreciation expense and accumulated depreciation land in the same GL that produces the P&L and Balance Sheet (Ch15). The module's own reports (Asset Register, Depreciation Forecast, Movement roll-forward, Disposals, By Placement, Valuations — FixedAssetReportController, routes :80–87) are read-and-aggregate views, each also serving ?type=pdf|excel.

Operational records (Fleet etc.). An asset can be polymorphically linked to a source record via assetable_type/assetable_id — the only wired type today is Vehicle (FixedAssetController::searchVehicles :270, field-map on FixedAssetLinkableModel enum). Blank standardized fields (name/serial) auto-fill from the linked record (FixedAsset::populateFromAssetable :298). This is the seam to the Fleet module.

6. Alternatives & variants

  • Depreciation methods. Two are live: straight-line — equal monthly (cost − residual) / useful_life_months, truing up the final month to residual (StraightLineDepreciation.php:29); and reducing (declining) balance — opening NBV × monthly rate, where the monthly rate is the category's default_depreciation_rate ÷ 12 if set, otherwise double-declining 2 / useful_life_months (ReducingBalanceDepreciation.php:37–39). units_of_production and none are recognized keys but units-of-production is not yet supported and throws (DepreciationCalculator.php:42); none/null yields NoDepreciation.
  • Where the rate/life lives. Useful life and method exist on both the asset and the category (asset overrides category default). The reducing-balance annual rate lives only on the category (default_depreciation_rate), not the asset.
  • Depreciation cadence. Configurable via run types (months_per_period) — monthly, quarterly, half-yearly, yearly, or bespoke — with a per-category run_type_id. Periods are generated from a fiscal-year start (add_fixed_asset_fiscal_year_setting). auto_mode on a run type hints at scheduled/automatic runs vs manual.
  • Take-on / migrated assets. accumulated_depreciation_bf seeds opening accumulated depreciation; openingNbv() starts the engine from cost − b/f (FixedAsset.php:203). Capitalization of a take-on adds the extra Cr Accumulated depreciation leg.
  • Revaluation vs impairment vs insurance all share fixed_asset_valuations: insurance records posts nothing; revaluation restates carrying amount (surplus → equity reserve, IAS 16); impairment writes down to P&L (IAS 36). After a revaluation, depreciableBase() switches from cost to revalued_amount (FixedAsset.php:182) so future charges change prospectively.
  • Change of estimate. Editing useful life/method/residual after depreciation has started applies prospectively — posted periods are never rewritten, and the change is logged (FixedAssetService.php:74,174).
  • Partial actions via split. A grouped asset (quantity > 1) can be split proportionally for a partial transfer or partial disposal (FixedAssetService::splitAsset :221); splitting is blocked once depreciation history exists.
  • Locks. Cost & b/f are locked once capitalization is posted; b/f is locked once any non-reversed depreciation line exists; placement changes must go through the transfer workflow, never a silent edit (FixedAssetService.php:59–67).
  • Tenant differences. Account mapping and whether FA_* GL config codes exist will vary per distributor; behaviour degrades to a clear GlPostingException rather than a silent mis-post.

7. Open questions to confirm

CODE-PROVEN - State machine, statuses, transitions, terminal/depreciable sets — FixedAsset.php:79–108. - Capitalization posts Dr Asset / Cr Clearing (+ Cr Accum for take-on) into wa_gl_trans, locks cost/b-f, series FAC — PostFixedAssetCapitalization.php:38–98, CapitalizationGlPostingService.php:54–73. - Depreciation posts Dr Depreciation expense / Cr Accumulated depreciation into wa_gl_trans, series FAD — DepreciationGlPostingService.php:22,71–72. - Two depreciation methods (SL, reducing/double-declining); units-of-production unsupported — DepreciationCalculator.php:14–46, StraightLineDepreciation.php, ReducingBalanceDepreciation.php. - Four-level account resolution (asset → category → account group → GlConfiguration) — FixedAssetAccountResolver.php:36–129. - Transfer/disposal maker-checker states and approver columns — fixed_asset_transfers (...110200...), fixed_asset_disposals (...120000...). - Two calendars: fixed_asset_periods (module) vs wa_accounting_periods (GL) — migration comment ...100700...:7–11, PostFixedAssetCapitalization.php:205. - Disposal GL legs — three batches (catch-up dep FAD, derecognition DSP with Cr Asset cost / Dr Accum / Dr proceeds / Cr VAT / ± gain-loss to ROLE_DISPOSAL, plus reserve release) — PostFixedAssetDisposal.php:56–196, DisposalGlPostingService.php:46–127. - Valuation GL legs — netting method under series FAV; increase → Cr revaluation reserve; decrease → Dr reserve then P&L via ROLE_REVALUATION_LOSS/ROLE_IMPAIRMENT by valuation_type — PostFixedAssetValuation.php:42–105, ValuationGlPostingService.php:49–113. Insurance is record-only (recorded, no GL). - Auto modes on run types — auto_mode ∈ {manual, auto_preview, auto_post} (FixedAssetRunType constants) drive manual vs scheduled preview/post.

INFERENCE / needs human validation - Who exactly approves (which role beyond the approve ability) and whether an approver may be the same user as the requester — no self-approval guard was confirmed in the code read here. - The scheduled job/cron that acts on auto_preview/auto_post run types — the mode flag is proven, but the actual scheduler entry was not located. - Procurement/petty-cash origination — only a soft supplier_id/purchase_reference + the external capitalization flag were found; no hard LPO/GRN→asset creation path. Capitalization is a manual step from the register. Confirm whether procurement ever auto-creates a fixed_assets row. - Whether fixed-asset-* granular permission strings are seeded per tenant or fall back to the base fixed-asset module — seeding not inspected.

8. Source references

  • routes/modules/fixed_assets.php:16–147 — all fixed-assets.* routes.
  • resources/views/admin/includes/sidebar_includes/general_ledger.blade.php:605–735 — nav + permission gates.
  • app/Http/Controllers/Admin/FixedAssets/FixedAssetController.php — register CRUD, lifecycle transition (:234), labels/handover.
  • app/Http/Controllers/Admin/FixedAssets/FixedAssetCapitalizationController.php — capitalization queue (post-gl).
  • app/Http/Controllers/Admin/FixedAssets/{FixedAssetTransferController,FixedAssetDepreciationController,FixedAssetDisposalController,FixedAssetValuationController,FixedAssetPeriodConfigController,FixedAssetCategoryController,FixedAssetAccountGroupController,FixedAssetReportController}.php.
  • app/Models/FixedAsset.php:79–232 — state machine, capitalization flags, openingNbv()/depreciableBase().
  • app/Services/FixedAssets/FixedAssetService.php — create/update/transition/split, locks.
  • app/Services/FixedAssets/FixedAssetAccountResolver.php:36–129 — account resolution chain.
  • app/Services/FixedAssets/CapitalizationGlPostingService.php + app/Actions/GeneralLedger/PostFixedAssetCapitalization.php — capitalization GL.
  • app/Services/FixedAssets/DepreciationGlPostingService.php + DepreciationRunService.php + Depreciation/{DepreciationCalculator,StraightLineDepreciation,ReducingBalanceDepreciation}.php — depreciation engine + GL.
  • app/Services/FixedAssets/{FixedAssetDisposalService,DisposalGlPostingService,FixedAssetValuationService,ValuationGlPostingService,FixedAssetTransferService,FixedAssetPeriodService}.php.
  • app/Actions/GeneralLedger/{PostFixedAssetDepreciation,PostFixedAssetDisposal,PostFixedAssetValuation}.php — the WaGlTran::insert() seams for depreciation (FAD), disposal (DSP+FAD), valuation (FAV).
  • Migrations: database/migrations/2026_06_10..2026_08_18_*fixed_asset* (14 create + delta migrations).