Skip to content

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

sales_quotation.status

States: draft (initial) · sent · accepted · expired · lost · cancelled

sales_order.status

States: draft (initial) · confirmed · on_hold · closed · cancelled

sales_reservation_entry.status

States: reserved (initial) · consumed · released · expired

Key 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, มูลค่าคิดจาก AVCO computeValuation), 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 ก่อน แล้ว ค่อย flip docstatus 0->1 แบบ compare-and-set; เฉพาะผู้ชนะ (meta.changes === 1) เท่านั้นที่เขียน reservation consumed + สร้าง backorder — กัน concurrent submit สอง request หักจองซ้อน (มี comment อธิบาย round-4 #9 ในโค้ด). cancelDelivery ก็ใช้ CAS 1->2 คู่กับ cancelDocumentIdempotent เพื่อคืนจอง exactly-once.
  • Gate + guards: ต้องมี company (multi-tenant) และ capability erp / 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.