Skip to content

HR — 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 hr<br/>bolt_session auth · company_id scope"]
  A --> D["dispatch<br/>create_employee · accrue_leave · apply_leave · leave_balance · record_attendance · enroll_pf · generate_statutory_filing · run_payroll"]
  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["party<br/>hr_payroll_employee<br/>hr_payroll_employment_contract<br/>hr_payroll_leave_ledger_entry<br/>hr_payroll_leave_request<br/>hr_payroll_pf_enrollment<br/>hr_payroll_run<br/>hr_payroll_payslip"]

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 --> [*]

hr_payroll_employee.status

States: active (initial) · on_probation · suspended · terminated

hr_payroll_employment_contract.status

States: draft (initial) · active · superseded · ended

Key flow: run_payroll → GL posting

run_payroll is the only HR flow that posts double-entry. It is idempotent at the period grain, so a retry after a transient finalize error never double-pays.

%%{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
  autonumber
  participant C as Client
  participant HR as Pages Fn (hr-payroll)
  participant D1 as D1 (HR tables)
  participant K as Kernel / GL

  C->>HR: POST run_payroll {period_start, period_end, run_type}
  HR->>D1: SELECT run WHERE period+run_type AND (state=posted OR doc_id set)
  alt already posted
    D1-->>HR: existing run
    HR->>D1: recomputeEmployeeYtd (self-heal)
    HR-->>C: {row, payslips, idempotent:true}
  else new period
    HR->>D1: INSERT run state=calculating
    loop each selected employee
      HR->>D1: compileWorkEntry (attendance + unpaid leave)
      HR->>HR: computeSso / annualized PIT / providentFund
      HR->>D1: INSERT payslip + payslip_line (clipped gross)
    end
    HR->>K: resolveAccount per line + createDocument HRPAYROLL
    HR->>K: submitDocument (journal PAY, period idempotency_key)
    K->>D1: post balanced GL dr expense / cr net+sso+pit+pf (atomic)
    K-->>HR: doc_no
    HR->>D1: UPDATE run state=posted, doc_id, totals
    HR->>D1: recomputeEmployeeYtd (derived from posted payslips)
    HR-->>C: {row, payslips, submitted}
  end

GL journal lines built by buildPayrollJournalIntent (journal PAY, doctype HRPAYROLL):

Side transaction_kind Amount
dr payroll_salary_expense salaryExpense + SSO employer + PF employer
dr payroll_overtime_expense overtime expense
cr payroll_net_payable net pay
cr payroll_sso_payable SSO employee + employer
cr payroll_pit_payable PIT withheld (only if > 0)
cr payroll_pf_payable PF employee + employer (only if > 0)

Notes

  • จุดประสงค์: จ่ายเงินเดือนไทยครบวงจร — attendance/leave feed เข้า payslip, คำนวณ SSO + PIT (annualized ภ.ง.ด.1) + Provident Fund, แล้ว post เป็น double-entry GL ผ่าน kernel เดียว. generate_statutory_filing สรุป posted payslips เป็น pnd1 / pnd1_kor / sso_1_10 / pf_remittance.
  • Idempotency = period-scoped: guard match run ที่ state=posted หรือ doc_id IS NOT NULL ก่อนเริ่ม; journal idempotency_key = hr-payroll:{company}:{start}:{end}:{run_type}:journal ทำให้ retry (ถึงจะ mint run ใหม่) ให้ kernel replay journal เดิม ไม่ post ซ้ำ = ไม่จ่ายซ้ำ.
  • YTD เป็น projection ไม่ใช่ increment: recomputeEmployeeYtd re-derive YTD = SUM ของ posted payslips ทั้งปี ทุกครั้ง → self-healing, drift-proof, และรันเป็น best-effort นอก catch (GL commit แล้วห้าม fail เพราะ YTD สะดุด).
  • Money-safety ใน payslip: deduction ถูก cap ที่ earned pay (unpaid leave แล้วค่อย other) และ expense debit สะสมจาก clipped gross ไม่ใช่ raw components — กันกรณีหักเกินจ่ายจนทำให้ Σdr ≠ Σcr แล้ว journal strand.
  • Fail handling รอบ submit: pre-post error (เช่น resolveAccount หา posting account ไม่เจอ) mark run failed; แต่หลัง submitDocument commit GL แล้ว การ finalize/YTD ที่พลาดจะ surface เป็น error แต่ ไม่ mark run กลับเป็น failed (กันคนกด retry แล้วจ่ายซ้ำ). Run ที่ค้าง calculating แต่ doc_id มีค่า = healed เป็น posted ตอน retry.
  • Gotchas: attendance ใช้ INSERT OR IGNORE กับ (source, source_event_id) เป็น idempotency key; apply_leave แบบ paid เช็ค balance (SUM ledger ≤ asof) ก่อน แล้วเขียนทั้ง leave_request (approved) + consumption ledger entry ติดลบ; create_employee สร้าง erp_party (kind person) + erp_party_role employee ก่อน insert employee row; ensureCompanyPayrollSeeds รันทุก POST เพื่อ seed tax/sso/scheme ต่อ tenant.