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

Accounting (บัญชี) — Process flows

สถาปัตยกรรม & วงจรชีวิตสร้างอัตโนมัติจากโค้ด (dispatch, CHECK status enums, SET status writes) ส่วนลำดับการทำงาน (Sequences) & หมายเหตุเขียนกำกับเอง

สถาปัตยกรรม (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 accounting<br/>bolt_session auth · company_id scope"]
  A --> D["dispatch<br/>void · reopen"]
  D --> K["Kernel<br/>erp_document + journal<br/>atomic GL / stock"]
  K --> DB[("D1 tables")]
  D --> DB
  D --> EV["events<br/>append audit trail"]
  EV --> DB
  DB -.-> T["accounting_payment<br/>accounting_open_item_settlement<br/>accounting_payment_allocation<br/>accounting_payment_void<br/>accounting_wht_certificate<br/>budget<br/>posting_lock<br/>accounting_fx_reval_run"]

วงจรชีวิต (Lifecycle)

วงจรชีวิตเอกสารระดับ Kernel (erp_document.docstatus)

เอกสารทุกใบที่ขยับเงิน/สต็อกในโมดูลนี้จะโพสต์ผ่าน 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 --> [*]

accounting_period.status

สถานะ: open (เริ่มต้น) · soft-closed · locked

status ตั้งค่าโดย (grep จาก SET status='…')
locked closePeriod
open reopenPeriod

accounting_fx_reval_run.status

สถานะ: queued (เริ่มต้น) · running · posted · failed · cancelled

accounting_tax_return_export.status

สถานะ: preview (เริ่มต้น) · finalized · filed · amended

Flow สำคัญ: submitPayment (ใบเสร็จรับเงิน / การจ่ายเงิน)

เป็นการเรียกแบบครบวงจร (end-to-end) ครั้งเดียว โปรดสังเกตว่า GL journal จะโพสต์ผ่าน kernel ก่อน batch ของ subledger เสมอ หาก batch ล้มเลิก (abort) โดยที่ยังไม่มี payment row ใดถูก commit ระบบจะกลับรายการ journal ที่ค้างเป็นเด็กกำพร้าด้วย cancelDocumentIdempotent

%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","actorBkg":"#2d2d30","actorBorder":"#569cd6","actorTextColor":"#d4d4d4","signalColor":"#9cdcfe","signalTextColor":"#d4d4d4","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa","labelBoxBkgColor":"#3a3d41","labelBoxBorderColor":"#dcdcaa"},"sequence":{"useMaxWidth":false}}}%%
sequenceDiagram
  participant C as Client
  participant F as accounting Fn
  participant L as PostingLock
  participant K as Kernel GL
  participant D as Accounting subledger
  participant W as WHT

  C->>F: POST PAYMENT or RECEIPT with idempotency_key
  F->>F: require idempotency_key and functional currency
  F->>L: assertPostingOpen posting_date
  L-->>F: open or 409 locked
  F->>F: aggregate allocations per open item
  F->>D: openItemSnapshot check aggregate le open amount
  F->>F: build balanced dr cr lines assertBalanced
  F->>K: createDocument then submitDocument
  K->>K: post erp_journal_entry idempotent on key
  K-->>F: submitted doc id and journal entry id
  F->>F: validate wht_code read-only (no number drawn)
  F->>D: pre-check payment by kernel_document_id
  alt already posted replay
    D-->>F: prior payment
    F->>W: ensureWhtCertificate reuse existing
    F-->>C: ok payment idempotent true
  else fresh post
    F->>D: batch insert payment + allocations + open_item_settlement
    alt batch abort
      opt concurrent winner already committed
        F->>W: ensureWhtCertificate reuse existing
        F-->>C: ok payment idempotent true (no journal reversal)
      end
      F->>K: cancelDocumentIdempotent reverse orphan journal
      alt settlement CHECK abort
        F-->>C: 409 concurrently settled retry
      else other abort cause
        F-->>C: rethrow original error
      end
    else committed
      F->>W: ensureWhtCertificate draw gap-free cert_no
      F-->>C: ok payment and wht_certificate
    end
  end

หมายเหตุ (Notes)

  • วัตถุประสงค์. ระบบบัญชีคู่ (double-entry) ที่อิงกับ kernel สำหรับ ERP ไทยแบบ multi-tenant: ใบสำคัญบันทึกบัญชี (journal vouchers), การตัดชำระ AR/AP (payment/receipt), การปิดงวดและการล็อกการโพสต์ (period close & posting locks), งบประมาณ (budgeting), หนังสือรับรองหัก ณ ที่จ่าย (WHT certificates), การตีมูลค่าอัตราแลกเปลี่ยนที่ยังไม่เกิดขึ้นจริง (FX unrealized reval), การนำเข้า statement ธนาคาร + การกระทบยอด (reconciliation), รายการบัญชีประจำ (recurring journals), และรายงานภาษี PP30/PND การยืนยันตัวตนต้องมี capability finance, accounting, หรือ manage และทุก query จะถูกจำกัดขอบเขตด้วย company_id
  • Kernel มาก่อน, subledger มาทีหลัง, ทำแบบ atomic. ตัวจัดการ (handler) จะสร้างบรรทัด GL ที่สมดุล (assertBalanced) แล้วโพสต์ผ่าน kernel.createDocument+submitDocument (ซึ่งเป็นเจ้าของ erp_journal_entry และการ dedup แบบ idempotency) จากนั้นจึงบันทึก row ของตัวเองใน env.DB.batch เดียว หาก batch นั้นล้มเลิกและยังไม่มี payment row ใด commit journal ที่โพสต์ไปแล้วจะถูกกลับรายการด้วย cancelDocumentIdempotent เพื่อไม่ให้ GL ค้างสภาพที่มี journal แต่ไม่มี subledger row (round-7 LL-2) หมายเหตุ: submitPayment จะไม่ปล่อย domain event ใด ๆ เมื่อสำเร็จ — emitEvent จะยิงเฉพาะจากการปิดงวด, FX reval, และการ match/unmatch statement ธนาคารเท่านั้น
  • Idempotency เป็นข้อบังคับสำหรับการเคลื่อนย้ายเงิน. submitPayment จะปฏิเสธทันที (400) หากไม่มี idempotency_key ก่อนที่จะแตะ DB ใด ๆ — key นี้เองคือสิ่งที่ทำให้ submitDocument dedup การ retry จากเครือข่ายแทนที่จะโพสต์ payment + journal + settlement ซ้ำสองครั้ง ส่วน FX reval และ recurring journals จะสร้าง key แบบ deterministic (fx-reval:period:runNo, recurring:template_id:run_date) เพื่อให้การรันที่ crash ไปสามารถ replay journal เดิมได้ แทนที่จะโพสต์ใบที่สอง
  • บัญชีแบบ append-only, ตารางข้างเคียงสำหรับกลับรายการที่แก้ไขได้. erp_accounting_payment, payment_allocation, wht_certificate, vat_return_pp30, และ row ของ FX reval มี trigger BEFORE UPDATE/DELETE ที่ RAISE(ABORT) ดังนั้นการ void จึงไม่เคยแก้ไขตัว payment เลย แต่จะ insert ลงในตารางที่แก้ไขได้ erp_accounting_payment_void และเปิด erp_accounting_open_item_settlement ใหม่ ส่วน row ของหนังสือรับรอง WHT และเลขที่ตามกฎหมายจะถูกเก็บรักษาไว้เสมอ (ตามกฎหมายไทย) แม้จะถูก void ก็ตาม
  • ตัวป้องกันการทำงานพร้อมกัน (Concurrency guards). การจัดสรรเกิน (over-allocation) ในการตัดชำระถูกทำให้เป็นลำดับ (serialize) ด้วย ledger open_item_settlement ที่มี CHECK (batch ทั้งก้อนของฝ่ายแพ้จะ abort และ journal ถูกกลับรายการ → 409 retry) ส่วนการ match บรรทัดธนาคารเดี่ยวถูกทำให้เป็นลำดับด้วย PK anchor ที่แก้ไขได้ reconciliation_active (ตาราง match แบบ append-only ยังคงเป็น audit-only) โดย unmatch จะลบ anchor และพลิกบรรทัดกลับเป็น unreconciled แต่ไม่กลับรายการ GL
  • จุดที่ต้องระวังที่พบในโค้ด (Gotchas). withTransaction เป็น no-op ที่ตายแล้ว (มันไปคุม env.db.exec แต่ binding จริงคือ env.DB) — ความเป็น atomicity มาจาก env.DB.batch ไม่ใช่ตัว wrapper นั้น การตัดชำระข้ามสกุลเงิน (multi-currency) ถูกปฏิเสธโดยเจตนา: สกุลเงินของ payment ต้องเท่ากับ functional currency มิฉะนั้นจะเป็นการบันทึกตัวเลขต่างสกุลเป็น THB ส่วนการปิดงวดจะถูกปฏิเสธ (409, บันทึกเป็น state rejected ของ period_close_run) หากยังมี draft ที่เกี่ยวกับการโพสต์ค้างอยู่ และการ reopen จะหมดอายุ (expire) posting lock แทนที่จะลบทิ้ง เพราะ period_close_run แบบ append-only ถือ FK แบบแข็งไปยัง erp_posting_lock