Inventory & Warehouse — ขั้นตอนการทำงาน (Process flows)
สถาปัตยกรรมและวงจรชีวิตสร้างอัตโนมัติจากโค้ด (dispatch, CHECK status enums, การเขียน SET status) ส่วนลำดับการทำงานและหมายเหตุเป็นการเขียนกำกับเอง
สถาปัตยกรรม (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 inventory<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["item<br/>item_variant<br/>inventory_cycle_count<br/>inventory_cycle_count_line<br/>inventory_reservation<br/>inventory_pick_list<br/>inventory_pick_list_line"]
วงจรชีวิต (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 --> [*]
inventory_pick_list.status
สถานะ: draft (เริ่มต้น) · allocated · ready · in_progress · posted · cancelled
inventory_pick_ticket.status
สถานะ: queued (เริ่มต้น) · allocated · posted · cancelled
inventory_repost_request.status
สถานะ: queued (เริ่มต้น) · in_progress · completed · failed
Flow สำคัญ: การจ่ายสินค้าออก Goods Issue (submitIssue)
การจ่ายสินค้าออก (goods-issue) เป็นการปลดมูลค่าสินค้าคงคลังอย่างถาวร จึงเคลื่อนไหวทั้ง subledger และ โพสต์ GL journal ที่สมดุลด้วยต้นทุนขาออกที่ kernel ตีมูลค่าไว้ (เมื่อใดก็ตามที่มูลค่านั้น > 0)
%%{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 Inventory Fn
participant S as Stock subledger
participant P as Removal policy
participant K as Kernel
participant G as GL journal
C->>F: POST issue/submit qty variant location
F->>F: auth + capability check (inventory/erp/manage)
F->>S: availability = onHand minus reserved
Note over F,S: reserved = WMS + sales hard reservations
alt qty > available
F-->>C: 400 insufficient ATP for issue
end
F->>P: resolveRemovalPolicy (returns strategy)
F->>S: pickLots order lots FIFO/FEFO/LIFO (CLOSEST degrades to FIFO here)
F->>S: unitCostForOutbound = value / qty
F->>K: createDocument WMS-ISSUE docstatus 0
F->>K: submitDocument stockLines + journal (journal only if amount > 0)
K->>K: idempotency fingerprint + posting-period lock
K->>S: insert qty_delta value_delta (negative)
K->>G: balanced Dr/Cr from reason (amount re-derived from stock)
K->>K: flip docstatus 0 to 1
K-->>F: document + stock_rows
F-->>C: ok strategy picks document stock_rows
หมายเหตุ (Notes)
- วัตถุประสงค์. endpoint WMS เพียงจุดเดียวครอบคลุมการเคลื่อนไหวสต็อกทั้งหมด — โอนย้าย (transfer), ปรับปรุง (adjustment), ตรวจนับ (cycle-count), จ่ายออก (issue), จัดเก็บเข้าที่ (putaway), หยิบ (pick) และการจอง (reservation) — พร้อมทั้ง surface สำหรับอ่าน (
GETบน reservation / cycle-count(+line) / pick-list(+line) / putaway-rule / removal-policy / adjustment-reason /erp_stock_ledger_entry) ทุกอย่างถูก scope ด้วยcompanyเพื่อแยกข้อมูลแบบ multi-tenant; surfacePOSTที่แก้ไขข้อมูลถูกกั้นด้วย capabilityinventory/erp/manageส่วน surfaceGETที่อ่านอย่างเดียวกั้นด้วยmanage/erp/finance/accounting - การโพสต์ผ่าน Kernel. handler จะไม่ insert ลง
erp_stock_ledger_entryหรือerp_journal_entryเองโดยตรง แต่จะสร้างintent(stockLines+journalแบบ optional) แล้วเรียกpostKernelDocument→kernel.createDocument(สร้างerp_documentที่docstatus 0) →kernel.submitDocument(พลิก0→1, เขียน subledger + GL) วงจรชีวิตเอกสารของ kernel คือ0 draft → 1 submitted → 2 cancelledซึ่งบังคับด้วย triggererp_document_docstatus_lifecycle(erp_document_immutable_after_submitล็อกเอกสารที่ submit แล้ว) - Subledger แบบ append-only + การตีมูลค่าด้วยต้นทุนเฉลี่ย.
erp_stock_ledger_entryเป็น immutable — triggerBEFORE UPDATE/BEFORE DELETEจะRAISE(ABORT)ยอดคงเหลือ (on-hand) และมูลค่าเป็นค่าที่ derived เสมอ:kernel.getOnHandรวมqty_delta/value_delta; ต้นทุนต่อหน่วยเฉลี่ยสำหรับขาออก =value / qtyการจัดลำดับ lot สำหรับการจ่ายออกทำในpickLotsซึ่งรองรับเฉพาะ FIFO / FEFO / LIFO เท่านั้น — removal policy แบบCLOSESTจะตกไปใช้ลำดับ FIFO ในเส้นทางการจ่ายออก (CLOSEST ถูกใช้จริงเฉพาะในstockQuantRowsซึ่งเป็นตัวจัดสรรของ pick-list/putaway) - ความปลอดภัยด้านเงิน / idempotency.
idempotency_keyตัดการซ้ำ (dedupe) ของ replay ด้วยการตรวจ fingerprint — หากนำ key เดิมไปใช้ซ้ำกับ operation ที่ต่างออกไป จะถูกปฏิเสธอย่างชัดเจน (409) แทนที่จะคืน rows ของเอกสารเก่าที่ค้าง (stock rows แบบ legacy ที่มี fingerprint เป็น NULL จะ replay ได้โดยไม่ผ่านการตรวจนี้) การล็อกงวดการโพสต์ (posting-period lock) บังคับทั่วทั้ง kernel บน ทั้งposting_dateและeffective_dateดังนั้นeffective_dateที่ย้อนหลังจึงไม่สามารถแอบดันสต็อกเข้าเดือนที่ปิดไปแล้วได้ - ATP ข้ามโมดูล.
reservedQtyหักลบทั้งการจอง WMS (erp_inventory_reservation, สถานะactive/partially_consumed) และ การจองแบบ hard ฝั่งขายที่ยัง active อยู่ (erp_sales_reservation_entry,reservation_kind='hard', ยังไม่หมดอายุ) ดังนั้น WMS จึงไม่มีทางบริโภคสต็อกที่ฝ่ายขายได้สัญญาไว้แล้ว - ข้อควรระวังด้านความถูกต้องของมูลค่า. ใบรับจากการโอน (transfer receipts) สืบทอดต้นทุนที่บริโภคจริงของขาจ่าย (
value_from_issue) แทนที่จะใช้ค่าเฉลี่ยทั้ง pool ทำให้มูลค่าใน subledger สอดคล้องกับ GL บนการโอนแบบ FIFO; ขนาดจำนวนเงินของ journal ถูก derive ใหม่โดย kernel จากการเคลื่อนไหวมูลค่าสต็อก (amount_from_stock) การตรวจนับ (cycle counts) บังคับการแยก maker/checker ผ่าน DB CHECKcounted_by <> approved_by(และการตรวจล่วงหน้าฝั่ง JS) และจะโพสต์รายการปรับปรุงผลต่าง (variance adjustment) เฉพาะเมื่อdelta != 0เท่านั้น