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():
- Catch-up depreciation (series
FAD, only ifcatch_up_depreciation > 0): Dr Depreciation expense / Cr Accumulated depreciation (DisposalGlPostingService::buildCatchUpEntries). - 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). - 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; laterauto_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 ofwa_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'sdefault_depreciation_rate ÷ 12if set, otherwise double-declining2 / useful_life_months(ReducingBalanceDepreciation.php:37–39).units_of_productionandnoneare recognized keys but units-of-production is not yet supported and throws (DepreciationCalculator.php:42);none/null yieldsNoDepreciation. - 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-categoryrun_type_id. Periods are generated from a fiscal-year start (add_fixed_asset_fiscal_year_setting).auto_modeon a run type hints at scheduled/automatic runs vs manual. - Take-on / migrated assets.
accumulated_depreciation_bfseeds opening accumulated depreciation;openingNbv()starts the engine fromcost − 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 torevalued_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 clearGlPostingExceptionrather 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— allfixed-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— theWaGlTran::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).