ข้ามไปที่เนื้อหา

Fixed Assets — กระบวนการทำงาน (Process flows)

สถาปัตยกรรม & lifecycle สร้างอัตโนมัติจากโค้ด (dispatch, CHECK status enums, การเขียน SET status) ส่วน sequences & notes เขียนกำกับเอง

สถาปัตยกรรม (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 (erp_document.docstatus)

ทุกเอกสารในโมดูลนี้ที่ทำให้เกิดการเคลื่อนไหวของเงิน/สต็อก จะ post ผ่าน kernel ที่ใช้ร่วมกัน โดยเดินไปตาม 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

สถานะ: registered (เริ่มต้น) · depreciating · fully_depreciated · disposed

status กำหนดโดย (grep จาก SET status='…')
disposed disposeAsset

Key flow: การรันค่าเสื่อมราคา (POST /depreciation/run)

การคิดค่าเสื่อมราคาแบบเส้นตรงหนึ่งงวดสำหรับสินทรัพย์รายการเดียว จำนวนเงินถูก clamp ไว้ไม่ให้ Σ เกิน cost − salvage ได้เด็ดขาด และงวดสุดท้ายจะรับ remainder ที่ทศนิยม 6 ตำแหน่งพอดี คู่รายการ GL (Dr expense / Cr accumulated) ถูก post ผ่าน kernel ภายใต้ idempotency key แบบ deterministic จากนั้น status ที่ cache ไว้ของสินทรัพย์จะถูกคำนวณใหม่ จากผลรวมของ entry ทั้งหมด — ไม่ใช่การบวกเพิ่มแบบไม่ตรวจสอบ

%%{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) ไม่ทับ.