Sales — ผังกระบวนการ
สถาปัตยกรรมและวงจรชีวิตถูกสร้างอัตโนมัติจากโค้ด (dispatch, CHECK status enums, การเขียน SET status) ส่วนลำดับการทำงาน (sequences) และหมายเหตุเขียนกำกับเอง
สถาปัตยกรรม
%%{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 sales<br/>bolt_session auth · company_id scope"]
A --> D["dispatch<br/>REST resources"]
D --> K["Kernel<br/>erp_document + journal<br/>atomic GL / stock"]
K --> DB[("D1 tables")]
D --> DB
DB -.-> T["sales_quotation<br/>sales_quotation_line<br/>sales_order<br/>sales_order_line<br/>sales_reservation_entry<br/>sales_delivery<br/>sales_delivery_line<br/>sales_invoice"]
วงจรชีวิต (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 --> [*]
sales_quotation.status
สถานะ: draft (เริ่มต้น) · sent · accepted · expired · lost · cancelled
sales_order.status
สถานะ: draft (เริ่มต้น) · confirmed · on_hold · closed · cancelled
sales_reservation_entry.status
สถานะ: reserved (เริ่มต้น) · consumed · released · expired
Flow สำคัญ: Order to cash (SO submit -> delivery post -> invoice post)
%%{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 Sales Function
participant D as D1 sales tables
participant K as Kernel
participant S as Stock AVCO
participant G as GL journal
C->>F: POST sales-order/submit
F->>F: creditDecision
alt credit hold
F->>D: status on_hold hold_reason
else ok
F->>K: computeAtp per line
F->>D: insert reservation_entry reserved
F->>D: status confirmed docstatus 1
end
C->>F: POST delivery/submit
F->>F: guard qty and not-held
F->>K: getOnHand + computeValuation buildDeliveryPosting
F->>K: createDocument SALES-DELIVERY
F->>K: submitDocument stockLines + journal
K->>S: deduct stock qty_delta neg
K->>G: dr cogs / cr inventory amount_from_stock
F->>D: CAS flip posted then consume reservation + backorders
C->>F: POST invoice/submit
F->>F: base + vat gross round6
F->>K: createDocument SALES-INVOICE
F->>K: submitDocument journal only
K->>G: dr ar / cr revenue / cr vat_output
F->>D: status posted docstatus 1
Notes
- หน้าที่ของโมดูล: order-to-cash เต็มสาย — quotation -> sales order -> delivery -> invoice บวก return และ credit note. ทุก endpoint คือ
POST /api/erp/sales/<resource>/<action>(create/accept/convert/submit/release/cancel) และอ่านย้อนผ่านGET /api/erp/sales/<resource>(whitelist ตารางของตัวเอง, กัน SQL injection) รวมถึงGET /atp. - แยกเงิน/สต๊อกออกจาก handler: sales ไม่โพสต์บัญชีเอง — สร้าง
intent(stockLines + journal double-entry) แล้วเรียกkernel.createDocument+kernel.submitDocument. Delivery โพสต์ COGS (dr cogs / cr inventory, มูลค่าคิดจาก AVCOcomputeValuation), Invoice โพสต์ dr ar / cr revenue / cr vat_output, Return กลับรายการ (dr inventory / cr cogs + คืนสต๊อก), Credit Note กลับ AR (dr revenue / dr vat / cr ar). - Idempotency สองชั้น: (1) ทุก submit เช็ก
docstatus === 1แล้วคืนidempotent: trueทันที; (2) ส่งidempotency_keyเช่นsales-invoice:<id>ให้ kernel replay เอกสารเดิมแทนโพสต์ซ้ำ. ทั้ง 4 submit (delivery/invoice/return/credit-note) ตั้งidempotency_key; เฉพาะ delivery (operation_group: sales-delivery:<id>) และ return (operation_group: sales-return:<id>) เท่านั้นที่ตั้งoperation_groupเพิ่ม — invoice และ credit note พึ่งidempotency_keyอย่างเดียว. - CAS flip กัน double-relief:
submitDeliveryโพสต์ kernel ก่อน แล้ว ค่อย flipdocstatus 0->1แบบ compare-and-set; เฉพาะผู้ชนะ (meta.changes === 1) เท่านั้นที่เขียน reservationconsumed+ สร้าง backorder — กัน concurrent submit สอง request หักจองซ้อน (มี comment อธิบาย round-4 #9 ในโค้ด).cancelDeliveryก็ใช้ CAS1->2คู่กับcancelDocumentIdempotentเพื่อคืนจอง exactly-once. - Gate + guards: ต้องมี
company(multi-tenant) และ capabilityerp/manage/sales; ก่อนโพสต์มี guard เชิงธุรกิจ — ATP check ตอน SO submit/release,assertSellableLocation, guard qty ของ delivery/invoice/return/credit-note, credit hold, และcogsAmount > 0. kit-phantom line จะระเบิดเป็น BOM components ตอนตัดสต๊อก (explodeDeliveryTarget). - การเชื่อมข้ามโมดูล: quotation
convertผูกconverted_sales_order_idและ SO ถือorigin_ref(เช่นQUOTE:<id>) ที่ใช้โยงกลับ CRM opportunity; analytic dimensions (channel/branch/salesperson) ไหลจาก SO custom_fields -> delivery/invoice -> GL journal lines ผ่านparseDims/soDims.