# 32 — ERP Evidence for the Client Cycle (Pharma Toll / Contract Manufacturing)

> Addendum to the published analysis. This file answers ONE question with code citations:
> **for the pharma toll-manufacturing client's two cycles (Exploratory Costing + Make-to-Order
> Production), what building blocks already EXIST in Moon ERP, and what is MISSING?**
>
> Client cycles (verbatim from owner interview):
> - **CYCLE 1 — Exploratory Costing**: customer brings a product + composition → R&D drafts a BOM →
>   Accounting computes preliminary cost → preliminary price returned as a quote.
> - **CYCLE 2 — Make-to-Order Production**: customer orders N units; everything ties to that customer
>   order. (a) **TRIAL**: R&D produces a real sample batch (real cost → own trial cycle).
>   (b) On agreement → formal order → R&D finalizes BOM → Planning sizes & checks readiness →
>   customer pays a **DEPOSIT / down-payment** (factory buys raw materials on the customer's behalf;
>   customer MAY also supply his own materials which remain HIS property) → if finances OK →
>   **RESERVE** materials → create manufacturing order → issue → manufacture → finish → receive into
>   finished-goods. Client also wants **dedicated screens per production stage**.
>
> Read-only review of `/home/moonui/moon-erp-be`. Verdict legend: ✅ EXISTS (reuse as-is) ·
> 🟡 PARTIAL (primitive present, needs wiring/extension) · ❌ MISSING (build new).

---

## Executive verdict

| # | Building block the cycle needs | Verdict | Where it lives / what's missing |
|---|---|---|---|
| 1 | Sales quotation + quotation→order conversion (CYCLE 1 quote, CYCLE 2 formal order) | ✅ EXISTS | `Modules/Sales` full lifecycle Draft→Sent→Accepted→Converted; `convertFromQuotation` action |
| 2 | Customer deposit / down-payment with correct GL (advance liability, not AR settlement) | 🟡 PARTIAL | `ReceiptVoucher` + COA `2104 Unearned Revenue` exist; **NOT linked to a sales/mfg order**, and `SalesPayment` is invoice-bound only |
| 3 | Customer-owned (consignment / toll) raw materials kept as the customer's property | ❌ MISSING | `Warehouse.type` enum has no `consignment`/owner concept; no ownership flag on stock |
| 4 | R&D / project / trial-batch workflow | 🟡 PARTIAL | `BomStatus::Draft` exists for draft BOM; CMMS = asset maintenance (not R&D); no trial-batch entity |
| 5 | Approval workflow to gate stages (BOM sign-off, deposit-gate, order release) | 🟡 PARTIAL | Generic engine in `Modules/Core` (ApprovalWorkflow/Log) — but only `Sales`/`Purchases` modules registered, **no Production** |
| 6 | CRM pipeline for RFQ intake | ❌ MISSING | `Modules/CRM` is support/ticketing (tickets, SLA, interactions) — no lead/opportunity/pipeline |
| — | Material reservation primitive | 🟡 PARTIAL | `StockBalance.reserved_quantity` column + `available` accessor exist, but **no code writes them** |
| — | Per-stage production transitions (basis for the per-stage screens) | 🟡 PARTIAL | `ProductionOrderController` has confirm/start/complete/consume/recordOutput; no customer/trial/deposit/reserve stages |

---

## 1. Sales quotations & quotation→order conversion — ✅ EXISTS

`Modules/Sales` already implements the exact spine CYCLE 1 (the quote) and CYCLE 2's "formal order"
need.

- **Model**: `Modules/Sales/app/Models/SalesQuotation.php` — header with `customer_id`, items,
  totals, `valid_until`, and crucially a `sales_order_id` back-link (fillable L56).
- **Lifecycle** (`Modules/Sales/app/Enums/QuotationStatus.php`,
  controller `Modules/Sales/app/Http/Controllers/SalesQuotationController.php`):
  `Draft → Sent → Accepted / Rejected / Expired → Converted / Cancelled`, plus `duplicate`
  (re-quote). Routes in `Modules/Sales/routes/api.php` L21-26.
- **Conversion**: `SalesOrderController::convertFromQuotation()`
  (`Modules/Sales/app/Http/Controllers/SalesOrderController.php` L368) — guarded by
  `status === Accepted` (L372), copies header + items, sets `quotation_id` on the order, and marks
  the quotation `Converted` (L426). Route: `POST quotations/{quotation}/convert-to-order`
  (api.php L33).

**Fit for the client cycle:**
- CYCLE 1 quote = a `SalesQuotation` priced from the R&D draft BOM. The preliminary cost feeding
  the quote is the gap addressed by the Production costing work in the main plan (md/04, md/20).
- CYCLE 2 "formal order on agreement" = `convertFromQuotation` → `SalesOrder`. **Everything in
  CYCLE 2 must hang off that `SalesOrder` id** — which means the new `mfg_*` production-order rows
  need a `sales_order_id` FK (see §7 amendments).

**Missing**: there is no field carrying the *exploratory vs. trial vs. final* intent on the
quotation, and no link from the quotation/order to a Production draft BOM. Both are small additive
fields, not new subsystems.

---

## 2. Customer deposit / down-payment & its GL — 🟡 PARTIAL (primitives exist, not wired)

This is the financially most delicate requirement: a **down-payment (دفعة تحت حساب)** is an
**advance liability**, not a payment against a receivable. Booking it as a normal sales payment
would mis-state both AR and revenue.

What EXISTS:
- **Chart of accounts already has the right liability**:
  `Modules/Accounting/database/seeders/DefaultChartOfAccountsSeeder.php` L44 →
  `2104 Unearned Revenue / إيرادات مقدمة` (classification `liabilities`, nature `credit`).
  Also `1106 Prepaid Expenses` for the mirror case.
- **A generic cash-in voucher**: `Modules/Accounting/app/Models/ReceiptVoucher.php` (سند قبض).
  It has `partner_id`, `receiving_account_id`/`bank_account_id`, **polymorphic
  `reference_type`/`reference_id`/`reference_number`** (fillable L37-39), and child
  `ReceiptVoucherLine` rows each with their own `account_id` + `partner_account_id`.
- **Its posting is account-agnostic**:
  `Modules/Accounting/app/Actions/ApproveReceiptVoucher.php::buildJournalLines()` (L76) DRs the
  receiving (cash/bank) account and CRs *whatever account the line carries* — so a deposit can be
  booked **DR Cash/Bank · CR 2104 Unearned Revenue** by setting the line account, without any code
  change. When no line account is given it falls back to the partner's AR/AP account
  (`resolvePartnerAccount` L128) — which is the wrong default for a deposit.

What is MISSING:
- **No order linkage / deposit semantics.** `ReceiptVoucher.reference_type` is free-form; nothing
  ties a voucher to a `SalesOrder` (or the new `mfg` production order) as a *required deposit before
  release*. There is no "deposit covered?" gate anywhere.
- **`SalesPayment` is the wrong tool** for this. `Modules/Sales/app/Models/SalesPayment.php` is
  bound to `invoice_id` (fillable L31), and `Modules/Sales/app/Actions/PostSalesPayment.php` is
  hard-wired **DR Cash · CR Accounts Receivable** against an invoice (L30-56) and allocates to
  invoice payment schedules (L106). There is no advance/unearned path; it cannot represent a deposit
  taken before any invoice exists.
- **No precedent in LIS.** The LIS "advance" hits (`CourierPickupService::advanceRequestStatus`,
  L680) are *status* progression, not financial advances — no customer-wallet/credit-balance
  precedent to copy.

**Amendment**: add a deposit step on the production/sales-order flow that issues a `ReceiptVoucher`
with `reference_type = sales_order` (or `mfg_production_order`) and a line crediting a
**Customer Advances** liability (use `2104` or a new `2105 Customer Advances / دفعات عملاء تحت
الحساب` sub-account). On final invoicing, the advance is drawn down (DR Unearned Revenue · CR AR or
revenue). The "finances OK → proceed" gate = `sum(approved deposit vouchers for this order) ≥
required_deposit`.

---

## 3. Customer-owned / consignment (toll) raw materials — ❌ MISSING

The owner explicitly said the customer **may supply his own raw materials which remain HIS
property**. Moon ERP today has **no concept of stock ownership**.

- `Modules/Inventory/app/Models/Warehouse.php` fillable (L19-34) has `type`, `account_id`,
  `allow_negative_stock` — **no owner/partner field**.
- `Modules/Inventory/app/Enums/WarehouseType.php` = `Main, Sub, Transit, Damaged, Returns` — **no
  `Consignment` / customer-owned type**.
- `grep -rin "consign|owned_by|ownership|customer_owned"` across `Modules/Inventory/app` and its
  migrations → **0 hits**. Stock movements and `StockBalance` carry no owner.

**Consequence**: customer-supplied materials would otherwise be valued and posted to the company's
inventory GL (overstating assets) and could be consumed by other orders. The factory holds them in
**bailment**, not ownership.

**Amendment options (pick one, documented for the plan):**
1. **Warehouse flag** — per the **already-ratified D-15** (`md/20` row 17, `md/22` §1) this is the
   `warehouses.is_consignment` boolean flag, NOT a new `WarehouseType::Consignment` enum case —
   plus `owner_partner_id` on `warehouses`; consignment warehouses post to an **off-book / memo**
   valuation (no GL asset), and issuing from them does NOT create a material-cost GL line (cost
   stays with the customer). *(Correction: this option originally proposed a new enum case,
   overlooking that D-15 had already ratified the flag form — `md/33`/`md/36` M-11 follow D-15.)*
2. **Per-balance ownership** — add `owner_partner_id` (nullable; null = company-owned) on
   `stock_balances` + stock movements, with available-stock and valuation filtered by owner.

Either way this is a genuine **new build** (a `mfg_*`-adjacent inventory extension), not a reuse.

---

## 4. R&D / trial-batch / project workflow — 🟡 PARTIAL

The cycle needs two R&D touchpoints: (a) draft a BOM (CYCLE 1 + CYCLE 2 finalize), and (b) run a
**real trial/sample batch with real cost** (CYCLE 2a).

What EXISTS / is reusable:
- **Draft BOM** is already a first-class state: `Modules/Production/app/Enums/BomStatus.php` =
  `Active, Inactive, Draft`. R&D's exploratory BOM = a `BillOfMaterials` in `Draft`. (The published
  plan, md/20, already calls for activating versioning/lifecycle on `bill_of_materials`.)
- **A real production order with real cost** is exactly what `ProductionOrder` is
  (`Modules/Production/app/Models/ProductionOrder.php` carries planned/actual
  material/labor/overhead cost, L41-46). A trial batch is a `ProductionOrder` flagged as trial.

What is MISSING:
- **No trial-batch concept** — `ProductionOrder` has no `kind`/`is_trial` field and no
  customer/sales-order link (fillable L21-48 has neither `customer_id` nor `sales_order_id`).
- **No R&D/project module.** `Modules/CMMS` is **asset maintenance** (`CmmsAsset`,
  `CmmsWorkOrder`, `CmmsPmSchedule`) — work orders for *fixing equipment*, not R&D projects, so it
  is **not** a fit for the R&D trial workflow despite the "work order" name.
- No task/project entity anywhere to model the R&D request intake before a BOM exists.

**Amendment**: model the trial as a `ProductionOrder` with `kind = trial` (vs `production`) +
`sales_order_id`, so the trial inherits all costing/stock plumbing and stays tied to the customer
order. The R&D "exploratory request" intake (CYCLE 1 entry) is the same record-class as a
quotation request (§6).

---

## 5. Approval workflows for gating — 🟡 PARTIAL (engine exists, Production not registered)

A reusable approval engine already lives in Core — ideal for the stage gates the client wants
(BOM sign-off, deposit-cleared, order release, trial acceptance).

- `Modules/Core/app/Models/ApprovalWorkflow.php` + `ApprovalLog.php`;
  controllers `ApprovalWorkflowController` / `ApprovalLogController`;
  statuses `Modules/Core/app/Enums/ApprovalLogStatus.php`.
- **But scope is limited**: `Modules/Core/app/Enums/ApprovalModule.php` = `Sales, Purchases`
  only; `Modules/Core/app/Enums/ApprovalDocumentType.php` covers quotation/order/invoice/return/
  delivery_note/PR/PO/bill/return/GRN — **no Production / BOM / production-order / trial types.**

**Amendment**: add `ApprovalModule::Production` and document types
(`bom`, `production_order`, `trial_batch`, optionally `deposit`) to reuse the existing engine for
stage gating instead of building bespoke approval logic.

---

## 6. CRM pipeline for RFQ intake — ❌ MISSING (wrong CRM shape)

`Modules/CRM` is a **customer-support / ticketing** module, not a sales pipeline:
- Models: `CrmTicket`, `CrmTicketComment`, `CrmSlaPolicy`, `CrmCustomerInteraction`,
  `CrmCustomerExt`, `CrmCustomerTag`. No `Lead`, `Opportunity`, `Pipeline`, or `Stage`.
- `grep -rin "lead|opportunity|pipeline|rfq"` over `Modules/CRM/app/Models` → only `CrmCustomerExt`
  matches incidentally (no pipeline entity).

**Consequence**: there is no opportunity-pipeline to host the "customer brings a product → RFQ"
intake. Two pragmatic options:
1. **Reuse `SalesQuotation` as the RFQ record** (Draft status = open RFQ) — cheapest; the
   exploratory request and the quote are one record, statuses carry the funnel. Recommended.
2. **Reuse `CrmCustomerInteraction`** to log the inbound enquiry, then spawn a quotation.

A full CRM pipeline is **not** required to satisfy the client; the quotation lifecycle already is a
mini-funnel.

---

## 7. Production stage transitions & the "screens per stage" request — 🟡 PARTIAL

The client wants **a dedicated screen per production stage** to push the order stage-to-stage.
The transition primitives partly exist.

- `Modules/Production/app/Http/Controllers/ProductionOrderController.php` exposes
  `confirm` (L204), `start` (L226), `complete` (L251), `consume` (materials issue, L311),
  `recordOutput` (L354), `cancel` (L286) — each permission-guarded. This is the skeleton for
  stage screens.
- `Modules/Production/app/Enums/ProductionOrderStatus.php` = `Draft, Confirmed, InProgress,
  Completed, Cancelled` with `canConfirm/canStart/canComplete/canCancel` guards — a small state
  machine.
- **Material reservation primitive exists but is dormant**:
  `Modules/Inventory/app/Models/StockBalance.php` has `reserved_quantity` (L25) and an
  `available = quantity - reserved_quantity` accessor (L47) — but `grep` shows **no code writes
  `reserved_quantity`** outside resources/factory. The "RESERVE materials for this order" step has a
  column to land in, but no reserve action yet.

What is MISSING for the client's specific flow (the stages between order and manufacture):
- No `customer_id` / `sales_order_id` on `ProductionOrder`.
- No **trial** stage, no **deposit-cleared** gate, no **reserve** stage, no **readiness/planning
  check** stage. The existing enum jumps straight Draft→Confirmed→InProgress.
- No per-stage screen metadata; FE screens must be added per the main plan's FE work (md/13, md/23).

**Amendment**: add a parallel commercial stage machine (NOT a widening of `ProductionOrderStatus`
— resolved as the `mfg_order_cases` umbrella, `md/35` §0) covering the client's chain — e.g.
`Requested → TrialInProgress → TrialAccepted → AwaitingDeposit → Released(+Reserved) → InProgress
→ Completed → ReceivedToFG` — and wire a reserve action that increments
`StockBalance.reserved_quantity`. **Ordering correction:** per the ratified `md/20` row 13,
reservation is a *side-effect of* `ReleaseProductionOrder`, so a `MaterialsReserved` stage is
*entered by* Release — it never precedes it (this file's original chain had reserve before
Release, which was circular against the ratified design). Each stage maps 1:1 to a stage screen.

---

## Net new vs. reuse (summary for the roadmap addendum)

**Reuse as-is (✅):** `SalesQuotation` lifecycle + `convertFromQuotation`; `ReceiptVoucher` posting
engine; COA `2104 Unearned Revenue`; `BomStatus::Draft`; Core `ApprovalWorkflow` engine;
`ProductionOrder` cost plumbing; `StockBalance.reserved_quantity` column.

**Extend (🟡):** add `sales_order_id` + `customer_id` + `kind(trial|production)` to the production
order; add `ApprovalModule::Production` + production document types; default a deposit
`ReceiptVoucher` to credit a Customer-Advances liability + link `reference_type=sales_order`; wire a
reserve action onto `reserved_quantity`; extend the production status machine to the client's stage
chain.

**Build new (❌):** customer-owned / consignment stock ownership in Inventory
(`WarehouseType::Consignment` + `owner_partner_id`, off-book valuation); trial-batch entity/flag;
RFQ intake (recommended: reuse quotation Draft rather than a new CRM pipeline).
