Skip to content

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_kind inventory, 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 จะ mark PRODUCED เฉพาะเมื่อ 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 ซ้ำจาก state CANCELLED = idempotent no-op.
  • BOM loop guard: create BOM ตรวจ self-consume และ multi-level cycle (wouldCreateBomCycle) และ BOM output cost_share_pct รวมต้อง < 100 เพื่อให้ main good เหลือ share บวกเสมอ.