Skip to content
ERPNext Data Model
Esc
navigateopen⌘Jpreview
On this page

Fixed Asset Lifecycle & Depreciation Engine

A design reference for registering, depreciating, revaluing, moving, and disposing of fixed assets

1. Requirements

1.1 Functional requirements

  • Register a fixed asset either as a fresh acquisition tied to a purchase document (receipt or invoice) or as an existing asset already owned before the system went live, carrying an opening accumulated-depreciation figure and an opening count of periods already booked elsewhere.
  • Scope every asset to an Asset Category, which supplies default ledger accounts (fixed-asset, accumulated-depreciation, depreciation-expense, and an optional capital-work-in-progress account) and a template set of depreciation policies that seed a new asset’s own configuration.
  • Support capital-work-in-progress (CWIP) accounting: while an asset is not yet available for use, its cost sits in a CWIP account; once its available-for-use date arrives, the cost moves into the fixed-asset account.
  • Support multiple, independent depreciation schedules per asset — one per finance book — so the same physical asset can depreciate on a different method, rate, and timetable per book a legal entity maintains (statutory, tax, management, and so on), with no book’s numbers influencing another’s.
  • Support exactly four depreciation methods (Straight Line, Written Down Value, Double Declining Balance, and Manual), each computing a periodic depreciation amount against a persisted, editable schedule rather than an on-the-fly calculation at posting time.
  • Support shift-based depreciation, where each scheduled period is tagged with a shift (e.g., single/double shift), and the depreciable value for the asset’s remaining life is apportioned across periods in proportion to a configurable per-shift weighting factor rather than split evenly.
  • Allow the shift assigned to future (not-yet-posted) periods to be revised after the fact through a dedicated adjustment record, which reflows the remaining schedule to match the new shift mix while leaving already-posted periods untouched.
  • Support daily pro-rata depreciation as an alternative to fixed equal periodic amounts, apportioning the depreciable value by elapsed calendar days rather than by period count.
  • Track physical location and custodian over an asset’s life through dedicated movement records with four purposes (issue, receipt, transfer, transfer-and-issue), automatically generated once when the asset is first submitted and thereafter created explicitly.
  • Support mid-life value adjustments (impairment write-downs or revaluations) that immediately post to the ledger and cause the remaining depreciation schedule to be regenerated against the new book value.
  • Support disposal through three distinct paths — scrapping, selling (via a sales document referencing the asset as a line), and quantity-based splitting for partial disposal — each of which depreciates the asset up to the disposal date before any gain/loss is recognized, and support restoring a scrapped asset back to service.
  • Post depreciation entries either automatically on a recurring schedule or manually, one period (or a contiguous range) at a time.

1.2 Non-functional requirements

  • Determinism and auditability: the amount for any given future period must be visible and inspectable before it is posted, not computed silently at posting time.
  • Non-destructive regeneration: recomputing a schedule after a change (rate, salvage value, disposal, value adjustment, shift reassignment) must never alter or re-derive an already-posted period; only the unposted tail of the schedule is subject to change.
  • Isolation of failures: posting depreciation across many assets in one run must not let one asset’s failure block or corrupt the others’, and failures must be surfaced to a responsible role rather than silently swallowed.
  • Respect for closed accounting periods: scheduled postings must not write into a period a company has frozen for general users.
  • Consistency across finance books: the schedule, ledger accounts, and postings for one finance book must be fully independent of every other finance book on the same asset — a defect in one book’s numbers must not corrupt another’s.

1.3 Constraints

  • An asset is a single register row even when it represents more than one physical unit (a quantity field); disposing of part of the quantity requires an explicit split into two register rows first.
  • A depreciation schedule is always a materialized, versioned record with one row per period; there is no code path that computes a period’s depreciation amount purely at posting time without a persisted schedule row behind it.
  • The ledger mechanics of how a depreciation posting balances — account selection by root type, currency handling, accounting dimensions — are shared with the rest of the accounting subsystem and are described elsewhere; this document focuses on what gets posted and when, not on ledger balancing itself.

2. High-Level Design

2.1 Component diagram

2.2 Walkthrough — from registration to an active schedule

This is a short annotated procedure, not a graph:

  1. An asset row is created, either from a purchase document line (net purchase amount, quantity, and cost center copied from the matching line) or manually as an existing asset carrying opening figures.
  2. Category defaults are applied if the asset was not given its own finance-book configuration: for every finance-book policy template on the asset’s category, a Finance Book Line is added to the asset, with an expected salvage value computed as the category’s configured salvage percentage against the asset’s net purchase amount.
  3. Auto-rate methods get a computed rate. If a line uses Written Down Value or Double Declining Balance and no rate was entered, one is derived (see §3.2) before the schedule is built.
  4. A schedule header is generated per Finance Book Line. If no schedule exists yet for that (asset, finance book) pair, a new one is created and populated; if one already exists in draft, it is regenerated in place rather than duplicated. A uniqueness rule blocks a second, non-cancelled schedule header from ever existing for the same (asset, finance book) pair.
  5. The asset is submitted. Every draft schedule header becomes Active. From this point, the schedule — not the asset’s own fields — is the record that posting reads from.
  6. Later changes that affect the numbers (a value adjustment, a disposal, a shift reassignment, an asset-repair capitalization, or an edit to core purchase/opening figures) regenerate the schedule: the still-unposted tail is recomputed and the schedule header is swapped for a fresh one, while every already-posted period keeps its original posting link untouched. Swapping the header cancels the old one but does not reverse any ledger posting already made from it — cancellation here means “retire this version of the plan,” not “undo what already happened.”

2.3 Data flow — posting a depreciation entry

Each asset’s rows are posted inside their own attempt in the automatic run: one asset throwing an exception is logged and skipped, and the run continues to the next asset rather than aborting the whole batch.


3. Deep Dive

3.1 Data model

Asset The register row. Carries acquisition figures (net purchase amount, additional cost, purchase date, available-for-use date), a category, a location, an optional custodian, an asset type (a plain new acquisition, an Existing Asset entered with opening depreciation history, a Composite Asset built up from components, or a Composite Component consumed into one — the last two belong to the capitalization process covered in a dedicated document), and a calculate_depreciation flag that gates whether any of the machinery below applies at all. If depreciation is not calculated, the asset still has a single flat value_after_depreciation, adjustable only by hand or by a value adjustment.

Asset Category (+ its account table) A template, not a per-asset record: one row of ledger accounts per company, and a table of finance-book policy templates (method, frequency, count, salvage percentage) that seed a new asset’s own Finance Book Lines. Also carries the switch that turns on CWIP accounting for everything in the category, and a flag marking the whole category non-depreciable (which blocks turning on depreciation for any asset filed under it).

Finance Book Line One row per finance book the asset depreciates under (or a single unscoped row if the company does not use multiple finance books). Holds the method, rate, frequency (in months), total number of periods, an optional shift flag, an optional daily-pro-rata flag, the expected salvage value, the depreciation start date, and — kept in sync as periods post — the running value_after_depreciation and count of periods already booked. This is the row that makes the multi-finance-book story concrete: an asset with three finance books has three of these rows, each independently configured, each driving its own schedule below, with no shared state between them except the asset’s own cost figures.

Depreciation Schedule (header) A submittable record with a Draft → Active → Cancelled lifecycle. Exactly one non-cancelled header may exist for a given (asset, finance book) pair at a time — creating a second is rejected outright. It snapshots the finance-book line’s configuration at generation time (method, rate, frequency, dates, salvage value) and owns the table of period rows described next. Submitting the owning asset activates every draft header it has one-per-book; cancelling the asset cancels every active header (and, unless explicitly suppressed, reverses whatever periods it had already posted).

Schedule Period Rows The actual per-period table: a date, a depreciation amount, a running accumulated-depreciation total, an optional shift tag, and — once posted — a link to the ledger posting that booked it. A row with a posting link is treated as immutable history; every regeneration algorithm in this subsystem starts from “the first row without a posting link” and only ever recomputes forward from there.

Asset Shift Factor / Asset Shift Allocation A small reference table of named shifts, each with a numeric weighting factor (exactly one may be marked default), plus an adjustment record that lets a user retroactively change which shift a not-yet-posted period was run under. Changing the mix triggers a reflow: periods are added or removed so that the sum of remaining shift factors still matches the asset’s remaining depreciable value, and the adjustment record itself, on submission, swaps in a freshly built schedule header carrying the revised periods forward (again without touching what was already posted).

Asset Value Adjustment A standalone record capturing a manual revaluation: a new asset value versus the system’s current one, a difference account, and a date. On submission it posts the difference immediately (debiting the fixed-asset account and crediting the difference account for an increase, or the reverse for a write-down), proportionally adjusts the finance book’s expected salvage value by the same percentage the salvage value already represented of cost, and regenerates the remaining schedule against the new book value. Cancellation reverses all of it symmetrically.

Asset Movement / Asset Movement Item A submittable record with a purpose of Issue, Receipt, Transfer, or Transfer-and-Issue, listing one or more assets with a source/target location and/or a from/to employee. A Receipt movement is generated automatically the moment an asset with a purchase-side reference is first submitted, establishing its starting location and custodian; every later change is a fresh movement record. Recomputing “where is this asset now” after a cancellation re-derives the latest still-submitted movement rather than reverting to a prior known state, which matters if movements were submitted out of strict date order.

Location / Linked Location A hierarchical location tree, with an optional geolocation polygon whose area is computed automatically and rolled up to ancestor locations. Linked Location is a legacy child-table shape carried over from an older, unrelated module rename; nothing in current asset code writes to or reads from it.

Asset Activity An append-only log entry (subject, user, timestamp) written by nearly every state-changing action described above. It has no lifecycle of its own; it is purely an audit trail.

3.2 Algorithm — the four depreciation methods

The schema offers four options. Two of them do not implement independent arithmetic at all — this is the central, easy-to-get-wrong fact about this subsystem.

Straight Line. The depreciable value (book value minus salvage) is split across the remaining periods. Three sub-modes exist under this one method:

  • Fixed periodic: amount = depreciable_value / pending_periods, where pending_periods is the remaining period count adjusted for frequency — a flat amount every period.
  • Daily pro-rata: a yearly (or, if a global setting says so, whole-remaining-life) rate is divided by the number of days it covers, then multiplied by the actual days in each period — so a shorter or longer calendar period gets a proportionally smaller or larger charge.
  • Shift-based: the depreciable value is divided by the sum of the shift-weighting factors across all still-open periods, then multiplied by each period’s own factor — a period tagged with a heavier shift absorbs more of the total depreciable value, which shortens how many periods it takes to reach salvage value if heavier shifts run more often.

Written Down Value. A classic declining-balance calculation: once per fiscal year (not once per period), a yearly amount is derived as book value at the start of that fiscal year × rate / 100, then divided down to a per-period amount by the period frequency; every period within the same fiscal year reuses that same derived amount without re-deriving it from a shrinking balance period-by-period. If no rate was entered, one is computed once, from the ratio of salvage value to current book value, solved so that book value lands exactly on salvage after the remaining useful life: rate = 100 × (1 − (salvage ÷ current_value) ^ (1 ÷ pending_years)).

Double Declining Balance. This method is not a separate arithmetic branch — it runs through the exact same declining-balance code as Written Down Value, fiscal-year re-basing and all. The only place it differs is the rate: if no rate was entered, Double Declining Balance’s auto-computed rate is a fixed formula, 200 ÷ useful_life_in_years, rather than Written Down Value’s salvage-derived rate. Anywhere a rate is entered explicitly, the two methods are computationally identical. Both methods are also the only two that trigger a pro-rata check when the depreciation frequency is not exactly annual.

Manual. Also not a separate arithmetic branch — a schedule built under this method is computed with the identical fixed-periodic Straight Line formula described above. What “Manual” actually changes is when the schedule is allowed to regenerate: for the other three methods, any relevant change (rate, frequency, count, dates, salvage value, or the underlying purchase/opening figures) forces the schedule to be rebuilt from the first unposted row forward. For Manual, once a schedule already has rows, saving the asset again leaves it alone unless the asset’s core purchase or opening-depreciation figures actually changed — which is what makes it safe for a user to open the schedule and hand-edit individual period amounts without those edits being silently overwritten on the next save. The label describes a protection policy, not a distinct formula.

Two mechanics apply across all four methods. First, the last-period true-up: whatever amount is left in the pending balance once the second-to-last scheduled period rounds to precision is dumped into the final period so accumulated depreciation lands exactly on the expected salvage value, and no further periods are generated past that point — this is also what lets a declining-balance schedule finish early if rounding brings it to salvage value ahead of its nominal period count. Second, pro-rata on the first period: if the asset became available for use partway through what would otherwise be a full period, the first scheduled amount is shortened proportionally to the days actually in service.

3.3 Adapter/API contract — schedule regeneration entry points

Every lifecycle event that changes future depreciation numbers funnels through the same regeneration routine rather than each implementing its own recompute:

regenerate_schedule(asset, finance_book_row, reason_note, disposal_date=None):
  current = find_active_or_draft_schedule(asset, finance_book_row)
  if disposal_date and book_value_already_at_or_below_salvage:
      return                                   # nothing left to depreciate
  new_schedule = copy_or_create(current)
  if method in (Written Down Value, Double Declining Balance):
      new_schedule.rate = recompute_rate(finance_book_row)
  new_schedule.build_periods(from_row = first_unposted_row_of(current))
  new_schedule.notes = reason_note
  if current and current.is_active:
      current.flag_do_not_reverse_postings()
      current.cancel()                         # retires the version, not the postings
  new_schedule.submit()

Callers supply only the reason text and, for disposal, a cut-off date beyond which no further periods are generated (the remaining pending balance is instead pro-rated up to that date and booked as the final period). Value adjustments, asset repairs that capitalize their cost, shift reassignments, and disposal/restoration all call this same routine — none of them contains its own copy of the period-building logic.

3.4 Error handling

  • Duplicate schedules: creating a second non-cancelled schedule header for the same (asset, finance book) pair is rejected at validation, not silently merged or ignored.
  • Batch posting isolation: the recurring posting job wraps each asset’s postings in its own attempt; an exception on one asset is logged, flags that asset with a failure status, and is reported to a responsible role, while every other asset in the run still posts normally.
  • Frozen periods: a company that has frozen postings up to a date is excluded from the due-row scan for periods on or before that date, for any user without the override role.
  • Low-value guard: if the depreciable value is too small to produce a nonzero amount over the configured period count (a risk with a small purchase amount and a long schedule), generation throws rather than silently producing zero-amount rows.
  • Cancellation ordering: a schedule header cannot be cancelled while any period still carries a draft (unsubmitted) posting — that posting must be resolved first, preventing a half-finished journal entry from being orphaned.
  • Disposal timing guards: a scrap or sale date must fall on or after the purchase date, must not be in the future, and cannot fall before the asset’s last already-posted period.
  • Regional override seam: a hook point exists (a no-op by default) that lets country-specific logic suppress depreciation entirely for the fiscal year an asset is disposed in — the base engine always books the pro-rated final period regardless.

4. Scale and Reliability

  • Load pattern: depreciation posting is a scheduled, batch-shaped workload (one run per day across every company and asset), not a request/response hot path; movements, value adjustments, and disposals are comparatively rare, human-triggered events.
  • Horizontal scaling: the recurring posting job iterates assets sequentially with a commit per asset (outside test mode), bounding the blast radius of any single failure but also capping throughput at what a single worker can push through in the scheduled window — a company with a very large depreciable-asset count is the pressure point to watch.
  • CWIP settlement is its own small daily sweep: assets still parked in a capital-work-in-progress account are re-scanned once a day for ones whose available-for-use date has just arrived, keeping the expensive “am I ready to capitalize” check off the hot path of asset creation.
  • Idempotency: the due-row scan only ever selects rows with no posting link yet, so re-running the job after a partial failure re-attempts only what genuinely failed, never double-posting a period that already succeeded.
  • Failure visibility: a per-asset posting-failure marker plus a notification to a configured role is the detection mechanism; there is no automatic retry beyond the next scheduled run, so a persistently failing asset (e.g., a missing account configuration) re-surfaces in every run’s failure list until someone fixes the cause.
  • Monitoring: alert on assets whose posting-failure marker is set, and on the age of the oldest unposted-but-due row — a growing backlog usually means the scheduled job has stopped running, not that individual assets are failing.

5. Trade-off Analysis

Decision Trade-off
Persisted, per-period schedule rather than computing amounts at posting time Every future amount is inspectable before it is booked, and regeneration can safely leave posted history alone — at the cost of a synchronization problem: every lifecycle event that changes the numbers must remember to call the same regeneration routine, and a caller that forgets leaves a stale schedule that silently disagrees with the asset.
Double Declining Balance implemented as Written Down Value with a different default rate Almost no duplication between two textbook-distinct methods — at the cost of the option list implying four independent algorithms when the engine really has two (declining-balance and straight-line), which is easy to document wrong.
“Manual” reusing the Straight Line formula, distinguished only by a no-regenerate policy Lets a user hand-edit a generated schedule without a separate free-form entry mode — at the cost of the initial numbers looking identical to Straight Line until someone edits a row, which can read as a bug rather than the intended starting point.
One schedule header per (asset, finance book), enforced by a uniqueness check Clean, provably independent books with no shared mutable state — at the cost of every regeneration needing to loop over every finance book separately; a bug that forgets to loop can silently leave one book stale.
Schedule replacement (cancel-and-resubmit) instead of in-place editing of an active schedule Every version of the plan is a distinct, auditable document, and posted periods are structurally protected from being altered — at the cost of one cancelled header per regeneration, so reconstructing “what did this look like on date X” means walking cancelled history.
Failure isolation per asset in the daily posting run, with no automatic retry beyond the next scheduled run A stuck asset (bad account configuration, frozen period) cannot block every other asset’s depreciation — at the cost of that asset quietly falling behind unless someone is watching the failure notification.

6. What to Revisit as the System Grows

  • Collapse the method taxonomy in the UI, not just the engine. Since Double Declining Balance and Written Down Value already share one implementation, and Manual shares another with Straight Line, exposing “declining-balance rate source” and “regeneration policy” as the two real axes — rather than four flat method names — would make the actual behavior harder to misconfigure.
  • Automatic retry for failed postings. A failed asset simply reappears in the next scheduled run’s failure list today; a bounded retry with backoff, or a manual “retry this asset now” action, would shorten how long a misconfigured asset sits un-depreciated.
  • Throughput of the daily posting sweep. The current per-asset sequential loop is simple and safe but will need parallelization (by company, or by asset-name hash) once a single company’s depreciable-asset count makes the scheduled window a binding constraint.
  • A queryable “schedule history” view. Because regeneration cancels and resubmits whole headers, reconstructing “what was the plan before this adjustment” means walking cancelled documents; a purpose-built history view over that chain would make audits far faster to resolve.
  • Explicit modeling of the regional depreciation-suppression seam. It exists only as a silent no-op override point today; documenting which jurisdictions use it, and what “correct” looks like, would reduce the risk of a future extension mishandling the pro-rated final period.

Was this page helpful?