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

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

สถาปัตยกรรมและวงจรชีวิตถูกสร้างอัตโนมัติจากโค้ด (dispatch, CHECK status enums, การเขียน SET status) ส่วนลำดับการทำงาน (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 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 (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 --> [*]

hr_payroll_employee.status

สถานะ: active (เริ่มต้น) · on_probation · suspended · terminated

hr_payroll_employment_contract.status

สถานะ: draft (เริ่มต้น) · active · superseded · ended

Flow สำคัญ: run_payroll → การ post ลง GL

run_payroll เป็น flow เดียวใน HR ที่ post แบบ double-entry และเป็น idempotent ที่ระดับ งวด (period) ดังนั้นการ retry หลังเกิด error ชั่วคราวตอน finalize จึงไม่มีทางจ่ายเงินซ้ำ

%%{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 ที่สร้างโดย buildPayrollJournalIntent (journal PAY, doctype HRPAYROLL):

ด้าน (Side) transaction_kind จำนวนเงิน
dr payroll_salary_expense salaryExpense + SSO ฝั่งนายจ้าง + PF ฝั่งนายจ้าง
dr payroll_overtime_expense ค่าล่วงเวลา (overtime expense)
cr payroll_net_payable เงินเดือนสุทธิ (net pay)
cr payroll_sso_payable SSO ฝั่งลูกจ้าง + ฝั่งนายจ้าง
cr payroll_pit_payable ภาษีหัก ณ ที่จ่าย PIT (เฉพาะเมื่อ > 0)
cr payroll_pf_payable PF ฝั่งลูกจ้าง + ฝั่งนายจ้าง (เฉพาะเมื่อ > 0)

หมายเหตุ

  • จุดประสงค์: จ่ายเงินเดือนไทยครบวงจร — 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.