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

Project — 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 project<br/>bolt_session auth · company_id scope"]
  A --> D["dispatch<br/>create_project · create_task · update_task_progress · book_timesheet · set_resource_capacity · allocate_resource · add_project_budget · create_milestone …"]
  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["project_helpdesk_project<br/>project_helpdesk_task<br/>project_helpdesk_timesheet_entry<br/>project_helpdesk_resource_capacity<br/>project_helpdesk_resource_allocation<br/>project_helpdesk_project_budget_entry<br/>project_helpdesk_project_milestone<br/>project_helpdesk_billing_run"]

Lifecycle — วงจรชีวิต

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

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

project_helpdesk_billing_run.status

สถานะ: draft (เริ่มต้น) · submitted · failed · cancelled

status ตั้งค่าโดย (grep จาก SET status='…')
failed completeMilestone, billTimesheets

project_helpdesk_ticket.status

สถานะ: new (เริ่มต้น) · open · in_progress · waiting_customer · waiting_third_party · resolved · closed · merged · spam

project_helpdesk_project.status

สถานะ: draft (เริ่มต้น) · active · on_hold · closing · closed · cancelled

Key flow: bill_timesheets → GL — flow สำคัญ: bill_timesheets → GL

%%{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 project fn
  participant D as D1 tables
  participant K as erp-kernel
  participant G as GL

  C->>F: POST action=bill_timesheets (project_id)
  F->>F: auth: company + hasCap(erp|manage)
  F->>F: reject if project.billing_mode = non_billable
  F->>D: SELECT approved billable entries (hours>0)<br/>net = hours + Σ approved reversals<br/>LEFT JOIN billing_line WHERE bl.id IS NULL
  D-->>F: candidate entries
  F->>F: keep net_hours>0; totalHours, totalAmount = totalHours*rate<br/>(require rate_per_hour>0, totalHours>0)
  F->>K: resolveAccount(AR kind), resolveAccount(revenue kind)
  F->>D: INSERT billing_run (status=draft)
  Note over F,G: GL posted FIRST, before any billing_line is written
  F->>K: resolveAccount(AR), resolveAccount(revenue) [again, in postProjectBilling]
  F->>K: createDocument(doctype=PROJECT_BILLING, idempotency_key)
  F->>K: submitDocument(intent: Dr AR / Cr Revenue)
  K->>G: atomic double-entry journal
  G-->>K: posted
  K-->>F: created + submitted
  F->>D: UPDATE billing_run status=submitted, kernel_document_id
  F->>D: INSERT billing_line per entry (hours>0, UNIQUE timesheet_entry_id)
  F-->>C: {billing_run, created, submitted}
  Note over F,D: on kernel failure → billing_run status=failed (+error), entries stay billable

Notes

  • จุดประสงค์: โมดูลรวม professional-services + helpdesk เข้าด้วยกัน — บริหารโครงการ (WBS task, timesheet, resource capacity/allocation, budget, milestone) และงานซัพพอร์ต (ticket + SLA) แล้วออกบิลเข้า GL ผ่าน kernel เป็น double-entry (Dr accounts_receivable / Cr service_revenue, ตั้งค่าได้ต่อ project ผ่าน ar_transaction_kind / revenue_transaction_kind).
  • Money-safety / ordering (สองflowต่างกัน — อย่ารวมเป็นอันเดียว):
  • bill_timesheets post GL ก่อน แล้วค่อยเขียน billing_line. timesheet billing_line เป็น append-only (trigger บล็อก UPDATE/DELETE) และ UNIQUE(company_id, timesheet_entry_id) — ถ้าเขียน line ก่อนแล้ว kernel fail จะทำให้ entry ผูกกับ run ที่ตายและ re-bill ไม่ได้ จึง post ก่อนเสมอ; fail แล้ว run กลายเป็น failed และ entry ยัง billable.
  • complete_milestone ทำกลับกัน: INSERT billing_run(draft) + milestone billing_line ก่อน แล้วค่อย postProjectBilling (post GL). ทำได้ปลอดภัยเพราะ milestone line มี timesheet_entry_id = NULL (source_kind=milestone) → ไม่ติด UNIQUE(timesheet_entry_id) (NULL distinct ใน SQLite) และเมื่อ fail run→failed แต่ milestone ยัง status=reached (ไม่ใช่ invoiced) จึง re-bill ได้ → ไม่มีปัญหา stranding แบบ timesheet.
  • Idempotency: ส่ง idempotency_key เข้า kernel.createDocument/submitDocument (default project-helpdesk-billing:<billingRunId> และ project-helpdesk-milestone:<milestoneId>) — กันโพสต์ซ้ำที่ชั้น kernel.
  • Reversal correctness: timesheet ที่เป็น reversal (reverses_timesheet_id) ถูกบังคับให้ hours < 0, ต้องอยู่ task เดียวกัน และ abs(hours) ห้ามเกิน hours ของ entry ต้นทาง. bill_timesheets คิดเป็น net of approved reversals (subquery SUM ของ reversal ที่ status=approved) และข้าม entry ที่ net ≤ 0 เพื่อไม่ให้ AR โดนชาร์จงานที่ถูก reverse (schema billing_line มี CHECK hours > 0 — ห้าม negative/zero line).
  • SLA engine: create_ticket สร้าง metric first_response/resolution พร้อม due_at = opened_at + minutes*60 จาก policy. transition_ticket pause/resume นาฬิกาตาม pause_statuses, fulfill resolution เมื่อ resolved/closed (set closed_at เมื่อ closed), และ set sla_state=completed เมื่อทุก metric terminal. record_first_response fulfill metric first_response. check_sla mark breached เมื่อเลย due และ escalate priority (low→medium→high→urgent) พร้อมบันทึก escalation_event. สถานะ merged/spamsla_state=cancelled.
  • Auth / tenancy: ทุก query scope ด้วย company_id; POST ต้องมี capability erp หรือ manage; GET (read surface) รับ manage/erp/finance/accounting และ whitelist ชื่อ table (กัน SQL injection). Milestone billing จำกัดเฉพาะ billing_mode = fixed_fee หรือ milestone; non_billable project ออกบิล timesheet ไม่ได้.