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/ Crservice_revenue, ตั้งค่าได้ต่อ project ผ่านar_transaction_kind/revenue_transaction_kind). - Money-safety / ordering (สองflowต่างกัน — อย่ารวมเป็นอันเดียว):
bill_timesheetspost 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ทำกลับกัน: INSERTbilling_run(draft) + milestonebilling_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(defaultproject-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 (schemabilling_lineมี CHECKhours > 0— ห้าม negative/zero line). - SLA engine:
create_ticketสร้าง metricfirst_response/resolutionพร้อมdue_at= opened_at + minutes*60 จาก policy.transition_ticketpause/resume นาฬิกาตามpause_statuses, fulfillresolutionเมื่อ resolved/closed (set closed_at เมื่อ closed), และ setsla_state=completedเมื่อทุก metric terminal.record_first_responsefulfill metricfirst_response.check_slamarkbreachedเมื่อเลย due และ escalate priority (low→medium→high→urgent) พร้อมบันทึก escalation_event. สถานะmerged/spam→sla_state=cancelled. - Auth / tenancy: ทุก query scope ด้วย
company_id; POST ต้องมี capabilityerpหรือmanage; GET (read surface) รับmanage/erp/finance/accountingและ whitelist ชื่อ table (กัน SQL injection). Milestone billing จำกัดเฉพาะbilling_mode=fixed_feeหรือmilestone;non_billableproject ออกบิล timesheet ไม่ได้.