Skip to content

Fixed Assets — Process flows

Architecture & lifecycle auto-generated from code (dispatch, CHECK status enums, SET status writes). Sequences & notes are authored.

Architecture

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","secondaryColor":"#3a3d41","secondaryBorderColor":"#dcdcaa","tertiaryColor":"#252526","tertiaryBorderColor":"#569cd6","clusterBkg":"#252526","clusterBorder":"#569cd6","titleColor":"#569cd6","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa"},"flowchart":{"useMaxWidth":false},"state":{"useMaxWidth":false}}}%%
flowchart LR
  C["Client"] --> A["Pages Fn fixed-assets<br/>bolt_session auth · company_id scope"]
  A --> D["dispatch<br/>REST resources"]
  D --> K["Kernel<br/>erp_document + journal<br/>atomic GL / stock"]
  K --> DB[("D1 tables")]
  D --> DB
  DB -.-> T["fixed_asset<br/>fixed_asset_depreciation<br/>fixed_asset_disposal"]

Lifecycle

Kernel document lifecycle (erp_document.docstatus)

Every money/stock-moving document in this module posts through the shared kernel, walking a fixed docstatus:

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","secondaryColor":"#3a3d41","secondaryBorderColor":"#dcdcaa","tertiaryColor":"#252526","tertiaryBorderColor":"#569cd6","clusterBkg":"#252526","clusterBorder":"#569cd6","titleColor":"#569cd6","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa"},"flowchart":{"useMaxWidth":false},"state":{"useMaxWidth":false}}}%%
stateDiagram-v2
  [*] --> Draft: createDocument (docstatus 0)
  Draft --> Submitted: submitDocument (docstatus 1, journal posted)
  Submitted --> Cancelled: cancelDocument (docstatus 2, reversed)
  Submitted --> [*]
  Cancelled --> [*]

fixed_asset.status

States: registered (initial) · depreciating · fully_depreciated · disposed

status set by (grepped SET status='…')
disposed disposeAsset

Key flow: Depreciation run (POST /depreciation/run)

One period of straight-line depreciation for a single asset. The amount is clamped so Σ can never exceed cost − salvage, and the final period takes the exact 6dp remainder. The GL pair (Dr expense / Cr accumulated) posts through the kernel under a deterministic idempotency key, then the asset's cached status is recomputed from the sum of entries — never blindly incremented.

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","secondaryColor":"#3a3d41","secondaryBorderColor":"#dcdcaa","secondaryTextColor":"#d4d4d4","tertiaryColor":"#252526","tertiaryBorderColor":"#569cd6","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa","clusterBkg":"#252526","clusterBorder":"#569cd6","titleColor":"#569cd6","actorBkg":"#2d2d30","actorBorder":"#569cd6","actorTextColor":"#d4d4d4","signalColor":"#9cdcfe","signalTextColor":"#d4d4d4","noteBkgColor":"#3a3d41","labelBoxBkgColor":"#3a3d41","labelBoxBorderColor":"#dcdcaa"},"flowchart":{"useMaxWidth":false},"sequence":{"useMaxWidth":false},"state":{"useMaxWidth":false},"er":{"useMaxWidth":false}}}%%
sequenceDiagram
  participant C as Client
  participant F as runDepreciation
  participant DEP as erp_fixed_asset_depreciation
  participant K as kernel (GL)
  participant JE as erp_journal_entry
  participant A as erp_fixed_asset

  C->>F: POST asset_id, period_code
  F->>A: loadAsset (reject if disposed)
  F->>DEP: existing entry for (asset,period)?
  alt already posted
    DEP-->>F: row
    F-->>C: 200 idempotent
  else new period
    F->>DEP: depreciationTotals (periods, accumulated)
    Note over F: amount = final ? remainder : min(monthly, remaining)
    F->>K: postJournal FA_DEPRECIATION<br/>key fa-dep:id:period
    K->>JE: Dr expense / Cr accumulated (balanced, atomic)
    JE-->>K: journalEntryId + documentId
    K-->>F: posted refs
    F->>DEP: INSERT entry (UNIQUE asset,period)
    F->>A: syncAssetDepreciation -> depreciating / fully_depreciated
    F-->>C: 201 entry + status + book_value
  end

Notes

  • จุดประสงค์: ทะเบียนสินทรัพย์ถาวร + ค่าเสื่อมราคาแบบเส้นตรง (straight-line เท่านั้นใน phase นี้) + การจำหน่าย โดยผูก GL ผ่าน kernel ล้วนๆ ทั้ง 3 doctype: FA_CAPITALIZATION, FA_DEPRECIATION, FA_DISPOSAL.
  • โมดูลนี้ไม่แตะ stock ledger เลย และไม่เขียน erp_journal_entry(_line) ตรง — ทุก posting ผ่าน postJournal → kernel.createDocument + submitDocument ซึ่ง assert Σdr==Σcr และ commit แบบ atomic (assertBalanced ยังกันเองอีกชั้นก่อนส่ง).
  • Money-safety (accumulated ไม่ drift): accumulated_depreciation บน erp_fixed_asset เป็นแค่ cache — recompute จาก SUM(erp_fixed_asset_depreciation.amount) ทุกครั้งใน syncAssetDepreciation (ไม่เคย +=). Depreciable base = cost − salvage_value, งวดสุดท้ายเก็บ remainder 6dp พอดี ไม่มีเศษค้าง.
  • Idempotency: deterministic key ต่อ business event — fa-cap:{code}, fa-dep:{id}:{period}, fa-disp:{id} — บวก UNIQUE constraints ((company,asset,period), (company,asset) สำหรับ disposal). ถ้า crash คั่นกลางระหว่าง kernel post กับ INSERT ของเรา retry จะ replay journal เดิมที่ kernel ไม่ double-post; postJournal ยัง lookup journal จริงด้วย idempotency_key เผื่อ replay path ที่ submitDocument ไม่คืน .journal.
  • Account wiring validated, zero hard-coded: requireAccount เช็คว่า code มีจริงใน chart of accounts, status='active', และ kind ถูกชนิด (asset/expense/income) — asset account จะถูกผูกกับ income account ไม่ได้.
  • Gotchas: capitalization posting เกิดเฉพาะเมื่อ caller ระบุ capitalization_account/clearing_account (ไม่งั้น register แค่บันทึกทะเบียน status='registered' ไม่ post GL); disposal ต้องส่ง clearing_account เมื่อ proceeds>0 และ gain_account/loss_account ตาม gain/loss ที่คำนวณได้; รัน depreciation ก่อนเดือน acquire ไม่ได้ และรันซ้ำงวดเดิมคืน entry เดิม (idempotent) ไม่ทับ.