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 นี้เองคือสิ่งที่ทำให้submitDocumentdedup การ 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 มี triggerBEFORE 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, บันทึกเป็น staterejectedของperiod_close_run) หากยังมี draft ที่เกี่ยวกับการโพสต์ค้างอยู่ และการ reopen จะหมดอายุ (expire) posting lock แทนที่จะลบทิ้ง เพราะperiod_close_runแบบ append-only ถือ FK แบบแข็งไปยังerp_posting_lock