Manufacturing — 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 mfg<br/>bolt_session auth · company_id scope"]
A --> D["dispatch<br/>work-center-create · routing-create · bom-create · work-order-create · work-order-consume · work-order-produce · work-order-cancel · create-work-center …"]
D --> K["Kernel<br/>erp_document + journal<br/>atomic GL / stock"]
K --> DB[("D1 tables")]
D --> DB
DB -.-> T["mfg_work_center<br/>mfg_routing<br/>mfg_routing_operation<br/>mfg_bom<br/>mfg_bom_component<br/>mfg_cost_hook<br/>mfg_bom_output<br/>mfg_work_order"]
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 --> [*]
mfg_work_order_operation.status
States: PENDING (initial) · READY · RUNNING · DONE · SKIPPED
mfg_bom.status
States: DRAFT (initial) · ACTIVE · ARCHIVED
mfg_routing.status
States: DRAFT (initial) · ACTIVE · ARCHIVED
Key flow: work-order-produce (backflush → WIP → finished goods)
work-order-produce is the money flow. From a DRAFT order it first backflushes the consume (unless backflush:false), then posts the finished-good receipt with an actual-cost GL journal.
%%{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 M as mfg Function
participant D as D1 (mfg tables)
participant K as erp-kernel
participant S as Stock ledger
participant G as GL journal
C->>M: POST work-orders/{id}/produce
M->>D: getWorkOrderHeader (assert state)
alt state DRAFT and backflush != false
M->>M: consumeWorkOrder(...)
M->>K: MFG-CONSUME create+submit (material out)
K->>S: -qty from input_location
opt wip_location set
M->>K: MFG-CONSUME create+submit (into WIP)
K->>S: +qty into wip_location
M->>D: wip_movement MATERIAL_TO_WIP
end
M->>D: lines CONSUMED, state=CONSUMED
end
M->>M: completeOperationsAndComputeLabor (hours from body*routing rate)
M->>D: operations DONE + labor_cost
M->>M: totalCost = material + labor + overhead
M->>K: resolveAccount FG/WIP/inventory/overhead
M->>K: MFG-PRODUCE create+submit (stock_lines + journal)
K->>S: +FG (+co-products/scrap), -WIP
K->>G: Dr FG total; Cr material-release/labor-absorbed/overhead-applied
alt posted (submitted, not idempotent replay, docstatus==1)
M->>D: state=PRODUCED, qty_produced, costs, variance
M->>D: wip_movement LABOR_TO_WIP + FG_FROM_WIP, scrap, variance, rollup
M-->>C: 200 work_order + costs
else idempotent replay / unposted
M-->>C: 409 produce did not post (state unchanged)
end
Notes
- จุดประสงค์: แปลงวัตถุดิบเป็นสินค้าสำเร็จรูปตาม BOM + routing แล้วรับเข้าคลังพร้อมต้นทุนจริง (material + labor + overhead) ผ่าน kernel — โมดูลนี้เป็น subledger ล้วน ไม่เขียน SLE/journal เอง (ทุก stock/GL effect โพสต์ผ่าน
erp-kernel). - Actual-cost (Model-B) GL: produce เดบิต FG ที่
totalCostแล้วเครดิตแหล่งมูลค่าจริง —material-release(transaction_kindinventory, inventory control),labor-absorbed(manufacturing_wip_clearing),overhead-applied(manufacturing_overhead_applied) — เพื่อให้ทุก inventory control account เท่ากับ perpetual subledger. Standard-cost variance บันทึกไว้บน WO +erp_mfg_cost_varianceเพื่อรายงาน แต่ ยังไม่โพสต์เข้า GL (deferred โดยตั้งใจ). - Money-safety guards ที่เห็นในโค้ด:
PRODUCE_YIELD_CAP = 2เพดาน good+scrap ไม่เกิน 200% ของ qty_ordered (กัน over-production เป่ามูลค่า FG); overhead ที่ caller ส่งมาต้อง<=configured (โพสต์น้อยกว่าได้ แต่ห้ามมากกว่า); labor rate มาจาก work-center (cost_rate_per_hour + overhead_rate_per_hour) เท่านั้น caller ตั้งเองไม่ได้ — แต่ชั่วโมง (setup/run hours) caller override ได้ผ่านbody.operations[]/operation_actuals[]และไม่มี cap, ดังนั้น labor cost ไม่ได้ถูกล็อกจาก caller เต็มตัวแบบ overhead/yield; co-product valued ตามcost_share_pctแล้วยกเศษที่เหลือให้ main good เป๊ะ เพื่อผลรวม stock-line = FG GL debit. - Idempotency: ทุก kernel post ใช้
operation_group+idempotency_keyที่ namespace ด้วยwo.id(mfg:wo:{id}:consume|wip-in|produce) กัน replay ข้าม WO; produce จะ markPRODUCEDเฉพาะเมื่อ submit จริง (docstatus===1, ไม่ใช่submitted.idempotent) ไม่งั้น throw 409 state เดิม. - doc_no series-owned: caller ส่ง
doc_noมาเองไม่ได้ (400); WO number จองMFG-WOแบบ provisional (atSubmit:false) ตอน create เพื่อไม่ให้ draft ที่ถูกทิ้งเผาเลขแบบ gap-free. - Cancel = reverse fragile leg first: ยกเลิกไล่ produce → wip → consume ด้วย
cancelDocumentIdempotent; produce (FG receipt) เป็นขาที่อาจ 409 (negative-stock ถ้า FG ถูกใช้ต่อ) จึงกลับก่อน, ห้ามสลับเป็น consume-first; cancel ซ้ำจาก stateCANCELLED= idempotent no-op. - BOM loop guard: create BOM ตรวจ self-consume และ multi-level cycle (
wouldCreateBomCycle) และ BOM outputcost_share_pctรวมต้อง< 100เพื่อให้ main good เหลือ share บวกเสมอ.