Manufacturing — Direction-B Gap Addenda

The five undecided B-gaps owed since Phase 1: rework flow · co/by-products · multi-plant netting · currency basis · WIP physical count

Module: Manufacturing (Moon ERP) · Phases P0–P6 SHIPPED on feat/manufacturing · This document closes the P1 exit gate (spec-addenda track) · Source analysis §B5/B6/B11/B19/B20 · Each addendum: context → options → recommended v1 stance → schema/action sketch → target phase.

B5 Rework / Repair Order Flow & Costing IMPORTANT

The spec names order_type=Rework, routing_type=Repair and a rework_quantity field, but never defines the flow: how rework quantity spawns or links an order, how it re-enters routing, how its cost is treated, and the normal-vs-abnormal spoilage line.

1 Context — what the spec left unspecified

2 Decision options

3 Recommended v1 stance

RECOMMEND — Option B: manual, linked rework order on the existing rail A rework order is just a production order with order_type=Rework and a parent_order_id. It reuses every landed action — IssueMaterials, ConfirmOperation, ReceiveFinishedGoods, CloseProductionOrder — with their existing GL posting. No new accounting path. Rework conversion cost is capitalised into the reworked unit's FG cost (same WIP→FG flow already shipped). Normal-vs-abnormal spoilage is handled by the existing variance disposition policy (v1: expense to P&L, per B16): the un-recovered scrap value already lands in the period via the close-time variance, so abnormal spoilage is implicitly expensed without a new mechanism.
Documented v1 simplification (state loudly) v1 does not auto-spawn rework from a failed confirmation, does not support partial-routing re-entry (rework runs its own short routing), and does not compute a separate normal-loss threshold — normal vs abnormal spoilage both flow through the existing variance-to-P&L policy. A dedicated normal-loss % and capitalise-vs-expense switch is a P7+ costing-precision item.

4 Schema / action impact sketch

// Production migration (additive, idempotent, sqlite-safe) production_orders: parent_order_id BIGINT NULL FK→production_orders.id // rework links to parent // order_type enum already includes 'Rework' (named in spec); no new enum migration if string-backed // No new Action. Reuses the landed rail: IssueMaterialsConfirmOperationReceiveFinishedGoodsCloseProductionOrder // Rework conversion capitalises into FG via the existing WIP→FG posting. // FE: Orders screen shows a "Rework" chip + "Parent: PRD-…" link when parent_order_id set.
TouchpointChangeRisk
Schema+1 nullable self-FK column on production_ordersLow additive
ActionsNone new — reuse landed railNone
GLNone new — existing WIP→FG + variance postingNone
FERework chip + parent link on Orders screenLow

5 Target phase

v1 — documented now, thin column in a future Production migration The parent_order_id column + Rework chip is a small follow-up (P7 backlog). The flow itself needs zero new code because it rides the shipped rail. Auto-spawn / partial-routing / normal-loss threshold → P7+ costing precision.

B6 Co-product vs By-product + Allocation Method IMPORTANT

P2/P3 shipped graded joint-products with NRV allocation only. The spec leaves open: by-product NRV-credit treatment, an allocation-method enum (market value / physical units / fixed ratio), and a BOM output-structure for secondary products.

1 Context — what the spec left unspecified

2 Decision options

3 Recommended v1 stance

RECOMMEND — Option A: by-product role flag + allocation-method enum (default NRV) The landed NRV allocation stays the default and untouched. Add two small declarations:
  1. allocation_method on the BOM (or a production setting) — enum {nrv, physical, market, fixed}, default nrv, so existing behaviour is byte-for-byte unchanged.
  2. output_role on each receipt/grade line — enum {co_product, by_product}, default co_product. A by_product line does not absorb a cost share; instead its NRV credits WIP (reducing the main output's absorbed cost), then is received into stock at that NRV. This is the textbook by-product NRV-credit method and reuses the existing grade-line posting structure.
Documented v1 simplification (state loudly) v1 implements only the nrv branch fully (the default) plus the by-product NRV-credit case. The physical/market/fixed enum values are declared but routed to the NRV implementation with a docblock until P7 wires their math — the enum exists so data captured now is forward-compatible. No expected-ratio validation on the BOM in v1.

4 Schema / action impact sketch

// Production migration (additive) mfg_boms: allocation_method ENUM('nrv','physical','market','fixed') DEFAULT 'nrv' mfg_goods_receipt_lines: output_role ENUM('co_product','by_product') DEFAULT 'co_product' // Costing action (extends the SHIPPED NRV allocator — does not replace it): allocateJointCost(totalCost, lines, method='nrv'): byproducts = lines.where(output_role='by_product') coproducts = lines.where(output_role='co_product') netCost = totalCost − Σ(byproducts.qty × byproducts.nrv) // NRV-credit switch method: 'nrv' (default) → existing P2/P3 NRV split of netCost over coproducts 'physical'|'market'|'fixed' → // v1: fall through to NRV + Log::info (docblock)
TouchpointChangeRisk
Schema+1 enum on mfg_boms, +1 enum on receipt lines (both defaulted)Low additive, default = current behaviour
CostingExtend landed NRV allocator with by-product credit + method switchMed money-asserted tests required
GLBy-product NRV credit reuses existing grade-line FG postingLow
FEOutput-role selector + method label on receipt/BOMLow

5 Target phase

v1 partial — enum + by-product NRV-credit default-NRV enum and by-product credit are a self-contained costing follow-up (P7 costing precision). Full multi-output BOM structure with expected-ratio validation and the physical/market/fixed math → P8+.

B11 Multi-plant / Warehouse Netting IMPORTANT

The MPS carries a plant_id, but MRP (shipped P4) nets against a single on_hand. There is no warehouse dimension and no transfer-order sourcing — it must be reconciled with Moon's existing branch + warehouse model.

1 Context — what the spec left unspecified

2 Decision options

3 Recommended v1 stance

RECOMMEND — Option A: branch-scoped MRP, "plant" = branch, single source warehouse Treat a "plant" as a branch (Moon's system-of-record for site separation, consistent with the B17 cost-center decision that Accounting/host owns the cross-cutting dimension). MRP accepts an optional branch_id scope and nets against that branch's designated manufacturing warehouse on-hand (falling back to company-wide when no branch given — preserving today's behaviour). No new plant entity, no transfer sourcing, no warehouse-pooling in v1. This aligns with how Inventory, Sales and the rest of the manufacturing rail already stamp and scope by branch.
Documented v1 simplification (state loudly) v1 nets against one warehouse per planning run (the branch's manufacturing warehouse, or company-wide on no branch). It does not pool multiple warehouses, does not emit internal transfer-order suggestions, and introduces no separate plant entity — "plant" is an alias for "branch". Multi-warehouse pooled netting + transfer sourcing is explicitly P8+ (pairs with time-phased supply already deferred in P4).

4 Schema / action impact sketch

// No new schema — reuse Inventory warehouses + branches. // ComputeMrp / netting engine gains an optional scope: RunMrp(branch_id?): warehouse = branch_id ? branch.manufacturing_warehouse // designated mfg WH for the branch : null // null → company-wide on_hand (today's path) onHand = StockService.balance(product, warehouse) // per-WH when scoped net = demand − supply − onHand // unchanged formula // production setting: production.default_planning_branch_id (nullable) // FE: MRP screen gains a branch filter (reuses BranchContext pattern).
TouchpointChangeRisk
SchemaNone — reuse warehouses/branches; +1 nullable settingNone
MRP engineOptional branch_id → per-warehouse on_hand; null preserves today's pathMed golden-fixture re-run needed
SourcingNo transfer suggestions in v1None
FEBranch filter on MRP screen (BranchContext reuse)Low

5 Target phase

v1 — branch scope as a thin engine option (backward-compatible) Branch-scoped netting is a P7-backlog enhancement to the shipped MRP engine (null branch = current behaviour, zero regression). Pooled multi-warehouse netting + transfer-order sourcing → P8+ (with time-phased supply).

B19 Multi-currency Costing Basis NICE-TO-HAVE

Material price currency basis is undefined. Moon's CreateJournalEntry already handles FX, but standard costs should be declared base-currency-only.

1 Context — what the spec left unspecified

2 Decision options

3 Recommended v1 stance

RECOMMEND — Option A: standard cost is base-currency-only (declare loudly) Declare that all standard costs are expressed in the company base currency (currencies.is_base). When a material's source price is in a foreign currency, it is converted to base at standard-set time using the host exchange rate; the roll-up never mixes currencies. Runtime FX differences are not a costing concern — they land as a normal purchase/price variance through the already-shipped CreateJournalEntry FX path. This matches P3's roll-up exactly and needs no schema and no new action — it is a stated policy plus a docblock on the roll-up.
Documented v1 simplification (state loudly) v1 holds no per-currency standard cost and performs no standard-cost revaluation on rate movement. The base-currency figure is authoritative; rate drift is expensed/absorbed via GL variance, not re-standardised. Per-currency standards and scheduled revaluation are out of scope (P8+ / likely never for v1 factories).

4 Schema / action impact sketch

// No schema change. Policy + guard only. // Standard-cost roll-up (SHIPPED P3) gains a docblock + optional guard: rollUpStandardCost(product): /** v1: standard costs are BASE-CURRENCY ONLY (currencies.is_base). Foreign source prices are converted to base at set-time via the host rate. Runtime FX → purchase/price variance through CreateJournalEntry. */ base = Currency.where(is_base=true).first() // component costs assumed already in base; convert at capture if sourced FX // CreateJournalEntry already resolves is_base + posts FX per line.currency_id (unchanged).
TouchpointChangeRisk
SchemaNoneNone
Roll-upDocblock + (optional) base-currency assertion guardLow
GLNone — existing CreateJournalEntry FX path unchangedNone
FEShow "(base currency)" label on standard-cost screensLow

5 Target phase

v1 — policy ratified now (no code beyond a docblock/label) This is a documentation-grade decision; the shipped P3 roll-up and GL already behave this way. Per-currency standards + revaluation → deferred indefinitely (P8+).

B20 WIP Physical Count Procedure NICE-TO-HAVE

No period-end WIP inventory / count procedure is defined. WIP is currently a purely perpetual balance accumulated by issues and relieved by receipts; there is no way to physically count shop-floor WIP and reconcile a difference.

1 Context — what the spec left unspecified

2 Decision options

3 Recommended v1 stance

RECOMMEND — Option A: a read-only period-end WIP reconciliation report v1 ships a WIP reconciliation report: for every open production order, compute book WIP = (issued material value + confirmed labor/overhead) − (FG received value), list per order, and total it against the GL WIP control account balance. Any difference is surfaced for finance to investigate and adjust via a normal manual journal entry. This is fully derivable from already-shipped data (issue/confirm/receipt postings) — no schema, no new GL action — and gives finance the period-end visibility they need. A formal count-and-auto-adjust document is the next step, not v1.
Documented v1 simplification (state loudly) v1 provides visibility, not enforcement: it reports book WIP vs the GL control account but does not capture a physical count and does not auto-post a WIP adjustment JE. Reconciling a real difference is a manual journal entry by finance. The countable WIP document + automatic write-down/up posting is a later phase. WIP is never pushed into the Inventory schema (preserves the zero-Inventory-risk rule held across P0–P6).

4 Schema / action impact sketch

// No schema, no new posting action. A read-only aggregator + endpoint: GET production/reports/wip-reconciliation?as_of=YYYY-MM-DD per open order: bookWip = Σ(issue.value) + Σ(confirmation.labor+overhead) − Σ(receipt.value) report: rows[] = { order, product, bookWip, status } total = Σ bookWip glControlBalance = Account(production.wip_account_id).balanceAsOf(as_of) difference = total − glControlBalance // finance investigates // perm: production.costing · FE: "WIP Reconciliation" report screen (read-only table + total).
TouchpointChangeRisk
SchemaNoneNone
Reporting+1 read-only aggregator endpoint (book WIP vs GL control)Low read-only
GLNone auto-posted; manual JE by finance for differencesNone
FE"WIP Reconciliation" report screenLow

5 Target phase

v1 — read-only reconciliation report The aggregator fits the existing reporting pattern (like the variance / WIP-aging reports flagged in B24) and is a clean P7-backlog item. Count document + auto-adjustment JE → P8+.

Closing Summary

Each gap resolves to a thin, additive, backward-compatible v1 stance that reuses landed P0–P6 rails. Nothing here forces a new GL path, an Inventory-schema change, or a re-architecture.

GapTitleSeverityRECOMMENDED v1 stanceNew schema?New GL?Target phase
B5Rework / repair flowImportant Manual rework order + parent_order_id, rides the shipped Issue→Confirm→Receive→Close rail; spoilage via existing variance-to-P&L +1 nullable FKNoP7 backlog
B6Co/by-product + methodImportant By-product NRV-credit flag + allocation_method enum defaulting to NRV (current behaviour); non-NRV methods declared, routed to NRV until later +2 enums (defaulted)NoP7 costing
B11Multi-plant nettingImportant "Plant" = branch; optional branch_id scope nets one mfg warehouse; null = today's company-wide path; no pooling / transfers None (+setting)NoP7 / P8 pooling
B19Currency basisNice Standard costs are base-currency-only; FX absorbed as purchase/price variance through CreateJournalEntry (already behaves this way) NoneNov1 policy now
B20WIP physical countNice Read-only period-end WIP reconciliation report (book WIP vs GL control); manual JE for differences; no Inventory-schema touch NoneNoP7 report / P8 count doc
P1 exit gate — CLOSED With these five one-pagers signed off, every Direction-B gap named without a decision row now has a ratified v1 stance. The common thread: document the simplification loudly, reuse the shipped rail, defer anything needing a new schema dimension to a named phase. This completes the spec-addenda track that gated Phase-1 exit.
Manufacturing B-Gap Addenda · Moon ERP · branch feat/manufacturing · P0–P6 SHIPPED · Closes the P1 exit gate (analysis §B5/B6/B11/B19/B20). Stances are v1 recommendations consistent with the landed rails — not yet implemented code.