---
title: Setup Wizard — module operational settings (Sales/Purchases/Inventory/Accounting)
slug: setup-wizard-settings
status: study-complete-awaiting-owner
owner: hazem
updated: 2026-06-19
related:
  - lis-setup-screen
  - default-accounts
plans:
  - https://moonui.elbaset.com/setup-wizard-settings-study.html
  - https://moonui.elbaset.com/purchase-bill-no-stock-report.html
---

# Setup Wizard — module operational settings (study + plan)

**Goal (owner, 2026-06-19):** the first-run `/setup` wizard must let the admin **choose the operational MODE settings** of Sales, Purchases, Inventory, Accounting — e.g. "receive stock in one step or two?", "does a sale auto-create a stock-issue note?", "auto-approve receipts/issues?", valuation method, auto-posting — and **re-open `/setup` anytime to edit them**. Deliverable: a full **study** of these settings (effect + recommended default), placed into new wizard screens. **plan-first:** study (this topic + an HTML doc) → align → implement.

**What triggered this:** the [purchase-bill "no stock" report](https://moonui.elbaset.com/purchase-bill-no-stock-report.html) — moontest silently ran in two-step GRN mode because `purchases.grn_mode` defaulted to `grn` (and the code/docs assume `direct`). The owner realised these mode choices must be **explicit at setup**, not silent defaults.

## 🔴 KEY cross-cutting finding — LIVE vs SCAFFOLDED settings
Many setting definitions exist in the DB but are **NOT consumed by any code** (toggling them does nothing yet). The wizard must only expose **LIVE** settings (or clearly mark scaffolded ones "coming soon") — otherwise we mislead the admin.
- **Purchases — NOT consumed:** `approval_workflow`, `price_includes_tax`, `enable_purchase_requests` (defined + validated, zero enforcement). `vat_inclusive` **does not exist** (only `price_includes_tax`) — the original ask was slightly off.
- **Inventory — NOT YET consumed:** `auto_approve_receipts` (no caller in Inventory; planned for Purchases GRN), `require_batch_tracking` (batch fields exist but optional, not enforced).
- **LIVE & impactful (verified in code):** `purchases.grn_mode`, `purchases.use_supplier_ap_account`, `purchases.enable_supplier_price_lists`/`enable_supplier_comparison`; `inventory.valuation_method`, `inventory.allow_negative`, `inventory.auto_approve_issues`; `sales.stock_deduction_point` + the auto-issue chain (pending Sales agent). **The study must label each setting LIVE / SCAFFOLDED.**

## Settings catalog (setting_definitions, moonui_dev_be — read-only)
Counts: **Sales 31** (10 bool, 3 enum) · **Purchases 23** (6 bool, 2 enum) · **Accounting 14** (3 bool) · **Inventory 11** (4 bool, 1 enum) · Core 9.

The **decision settings** (boolean + enum — the wizard-relevant choices):

| Module | Key | Type | Seeded default | Choices |
|---|---|---|---|---|
| Purchases | `grn_mode` | enum | `grn` ⚠️ | direct / grn / grn_quality |
| Purchases | `approval_workflow` | enum (scaffolded) | none | none/simple/multi_level |
| Purchases | `use_supplier_ap_account` | bool (LIVE) | false | — |
| Purchases | `enable_supplier_price_lists` / `_comparison` | bool (LIVE) | false | — |
| Purchases | `price_includes_tax`, `enable_purchase_requests` | bool (scaffolded) | false / true | — |
| Sales | `stock_deduction_point` | enum (LIVE) | invoice | invoice / delivery |
| Sales | `default_price_type` | enum | wholesale | sale/wholesale/half_wholesale/retail |
| Sales | `auto_create_stock_issue_on_invoice` / `auto_approve_stock_issue_on_invoice` | bool | 0 / 1 | — |
| Sales | `approval_workflow` | enum | none | none/simple/multi_level |
| Sales | (+ allow_negative_stock, vat_inclusive, auto_generate_delivery, commission_enabled, price_includes_tax, allow_below_min_price, enable_delivery_notes=true, use_customer_ar_account) | bool | — | — |
| Inventory | `valuation_method` | enum (LIVE) | weighted_avg | weighted_avg/fifo/lifo (LIFO=stub) |
| Inventory | `allow_negative` (LIVE), `auto_approve_issues` (LIVE), `auto_approve_receipts` (scaffolded), `require_batch_tracking` (scaffolded) | bool | false | — |
| Accounting | `auto_create_partner_account` | bool | true | — |
| Accounting | `cost_center_enabled` / `cost_center_required` | bool | false | — |

### ⚠️ The `grn_mode` default mismatch (real latent defect — owner decision pending)
Seeder ships `default_value='grn'` (`SettingDefinitionSeeder.php:911`) but the code's inline fallback + docs + tests assume `'direct'` (`PostPurchaseBill.php:187`, `CancelPurchaseBill.php:64`, `PurchasesSettingController.php:37,76`). So a fresh install silently runs two-step GRN. **Fix = align them** — owner picks: (a) default→`direct` (one-step, matches code/docs), or (b) keep `grn` + fix the fallbacks/docs/tests. Until aligned, every new client hits the same confusion.

## Per-module effect notes (from code, agents)
### Purchases
- `grn_mode`: `direct`=posted bill auto-creates stock receipt+movement; `grn`=bill financial-only, stock via separate GRN; `grn_quality`=GRN with quality gate (`PostPurchaseBill.php:185`).
- `use_supplier_ap_account` (LIVE): if true, posting uses the supplier's own AP sub-account instead of the global payable (`PostPurchaseBill.php:171`, `PostPurchasePayment.php:92`, `PostPurchaseReturn.php:162`).
- `enable_supplier_price_lists` (LIVE): PO auto-fills unit_cost from the supplier price list (`PurchaseOrderController.php:530`); `enable_supplier_comparison` gates the comparison endpoints (`SupplierPriceListController.php:444`).
- **Required non-toggle (or bill-post fails):** `inventory_account_id`, `expense_account_id`, `payable_account_id` (PostPurchaseBill.php:65-66, payments :103), `tax_receivable_account_id`, `default_warehouse_id`; optional: `discount_account_id`, `default_tax_id`, `default_cost_center_id`, `default_payment_terms_days`, scorecard weights (40/30/30).

### Inventory
- `valuation_method` (LIVE): cost on issue — weighted_avg (avg cost) vs FIFO (consumes `InventoryCostLayer` oldest-first); LIFO is a stub (`StockService.php:238-241`, `getIssueCost():180-197`).
- `allow_negative` (LIVE, **per-warehouse** `warehouses.allow_negative_stock`): if false, `ApproveIssue.php:98-129` + `ShipTransfer.php:27-53` block issuing below available.
- `auto_approve_issues` (LIVE): with sales `auto_approve_stock_issue_on_invoice`, the auto-created GDN is approved immediately (`PostSalesInvoice.php:382-386`), deducting stock + COGS.
- `auto_approve_receipts` (SCAFFOLDED — no caller yet); `require_batch_tracking` (SCAFFOLDED).
- Non-toggle: `default_warehouse_id` (branch), `stock_account`(1105)/`cogs_account`(5101) GL codes, cost-analytics `carrying_cost_percent`(20)/`ordering_cost`(0)/`stockout_cost`(0) for EOQ/KPIs.

### Sales
- `stock_deduction_point` (LIVE): **invoice** = COGS + stock deducted on invoice post (`PostSalesInvoice.php:52-56`); **delivery** = stock leaves only on delivery-note confirm (`ConfirmDeliveryNote.php:31-35`).
- `auto_create_stock_issue_on_invoice` (LIVE, def `false`): posting an invoice auto-creates a GDN; direct stock decrease is then **deferred** to GDN approval (`PostSalesInvoice.php:175,219,330,381`).
- `auto_approve_stock_issue_on_invoice` (LIVE, def **`true`** ⚠️): auto-approves that GDN **only if** `inventory.auto_approve_issues` is also true (`PostSalesInvoice.php:381-386`). **INCONSISTENCY:** default `true` while `auto_create`=`false` → it does nothing until auto_create is on; recommend default → `false`.
- `commission_enabled` (LIVE, `CommissionService.php:25`); `use_customer_ar_account` (LIVE, `PostSalesInvoice.php:412-421`, payments :92, returns :144) = post to the customer's own AR sub-account vs global.
- **SCAFFOLDED / UI-only (not enforced in actions):** `approval_workflow`, `auto_generate_delivery`, `price_includes_tax`, `allow_below_min_price`, `enable_delivery_notes`.
- **Required GL (or invoice-post fails):** `revenue_account_id`, `cogs_account_id`, `receivable_account_id`, `tax_payable_account_id` (+ optional discount/inventory/default_tax/cost_center/warehouse/payment-terms/quotation-validity/commission-rate).
- **Recommended first-run:** `stock_deduction_point=invoice` + `auto_create=false` + `auto_approve=false` (simplest: stock leaves on invoice post, no async GDN).

### Accounting
- `auto_create_partner_account` (LIVE, def `true`, `Listeners/CreatePartnerAccounts.php:32`): on partner create/update, auto-creates a **leaf** AR/AP sub-account under the parent → avoids the **AR-header trap** ([[lis-setup-screen]]: postings must hit detail leaves, not control headers). **Keep true.**
- `cost_center_required` (LIVE, `StoreJournalEntryRequest.php:29`, def `false`): forces `cost_center_id` on every JE line. **Keep false** at first run (else blocks manual JEs).
- `cost_center_enabled` (SCAFFOLDED — no consuming code, def `false`): UI visibility only. **Keep false.**
- `auto_post_entries` was **REMOVED** (migration `2026_05_12_000011_…`) → posting is now explicit Approve→Post (separation of duties). Don't re-expose it.
- **GL parents (account-code, belong in the existing Default-Accounts step):** `ar_parent_account`(1103) / `ap_parent_account`(2101) / `bank_parent_account`(1102) / `cash_parent_account`(1101) — anchors for auto-created sub-accounts. Plus optional `checks_issued/received_account_id`, `wht_payable_account_id`, `zakat.*` (KSA), `default_journal_type`(general), `fiscal_year_start`(01-01).
- **New behavior step = just the 3 toggles** (`auto_create_partner_account`, `cost_center_enabled`, `cost_center_required`); the rest are GL-pickers for the existing accounts step.

## Existing `/setup` wizard — how to extend (FE map)
- **Files:** `src/app/features/setup/setup-wizard.component.ts` (~963 ln) + `.html` (~609) + `default-accounts.config.ts` (config-driven steps).
- **Steps array** (ts:88-99): company · currency · fiscal-year · COA · **default accounts** · warehouse · treasury · bank · finish. Add a step = push to `steps[]` + a `@if(currentStep()===N)` panel + signals + a save method, **renumber downstream**.
- **Settings load:** `forkJoin(modules.map(m => settingService.list(m)))` → map `setting_key → current_value` (setup-wizard.ts:680-727). **Save:** `settingService.update({ setting_key:'module.key', value:String(v) })` → `PUT /core/settings`, chained sequentially (ts:756-776). `UpdateSetting { setting_key, value, branch_id?, user_id? }`.
- **♻️ REUSE:** a generic **definition-driven renderer** already exists at `src/app/features/settings/settings.component.ts` — fetches `listDefinitions(module)`, groups by `display_group`, renders by `value_type` (boolean→toggle, enum→select). The new wizard steps should reuse this so they stay in sync with the DB definitions (no hardcoded field lists).
- **First-run flag:** `setup.completed='1'` set at finish (ts:903). **No guard** currently blocks re-opening `/setup` — it's idempotent (re-edit + re-save). Route `/setup` needs `core.settings` permission; no sidebar entry. (Lab wizard `/lab-setup` is separate, `lis.settings`.)
- **Insertion point:** new steps **after "Default Accounts" (step 4), before "Warehouse"** → Sales/Purchases/Inventory/Accounting-behavior as steps 5-8, renumber 9-12.

## Plan (proposed — pending owner sign-off)
1. 4 new `/setup` steps (Sales · Purchases · Inventory · Accounting-behavior), **definition-driven** (reuse the settings renderer; group by `display_group`), exposing only **LIVE** decision settings + the required GL/warehouse defaults; scaffolded ones hidden or "coming soon".
2. Each step: clear plain-language help per mode (e.g. grn_mode "one-step: bill adds stock / two-step: separate receipt"), sensible pre-selected defaults, save via `PUT /core/settings`.
3. Fix the `grn_mode` default mismatch (owner picks direct vs grn) + audit other seeded-default vs code-default mismatches.
4. Re-openable from a Settings/"Setup" entry to edit later.

## Cross-cutting recommendations
1. **Only expose LIVE settings** in the wizard; hide or mark "coming soon" the SCAFFOLDED ones (Purchases: approval_workflow/price_includes_tax/enable_purchase_requests; Sales: approval_workflow/auto_generate_delivery/price_includes_tax/allow_below_min_price/enable_delivery_notes; Inventory: auto_approve_receipts/require_batch_tracking; Accounting: cost_center_enabled). Exposing dead toggles misleads the admin.
2. **Fix seeded-default vs code-default mismatches** before/with the wizard: `purchases.grn_mode` (`grn` seeded vs `direct` assumed) and `sales.auto_approve_stock_issue_on_invoice` (`true` while auto_create=`false`). Owner picks the intended behavior; align seeder + fallback + docs + tests.
3. **Definition-driven UI** (reuse `features/settings/settings.component.ts` renderer, group by `display_group`) so the wizard stays in sync with the DB definitions — no hardcoded field lists.
4. Plain-language mode help on every operational toggle (the user shouldn't need to know "grn_mode").

## Implementation (owner 2026-06-19: "use your recommendations, fix every conflict, make every option real")
**Phase 1 — conflict fixes ✅ DONE + verified (BE, committed):**
- `purchases.grn_mode` seeder default `grn`→**`direct`** (`SettingDefinitionSeeder.php:911`). Propagates to the fleet via `moonstack:sync-reference` (the seeder is `updateOrCreate` on `setting_key`, and is in BOTH `installer.seeders` + `updater.seeders`) → existing installs with NO explicit row (e.g. moontest) now resolve `direct` on next update. **Verified:** no-row company resolves `direct`; moonui company 4 keeps its explicit `grn` row (deliberate). Test fixture already expected `direct`.
- `sales.auto_approve_stock_issue_on_invoice` seeder default `true`→**`false`** (`:442`) — it's inert unless `auto_create…` is on. Verified default now `0`.
- 🐞 **Side-bug found (not yet fixed):** `LabSectionSeeder` + `LabSpecimenTypeSeeder` `run()` are NOT idempotent against the **global-unique `code`** (hardcode company_id=1, plain insert) → they **fail every `sync-reference`** ("Duplicate entry 'SEC-HEM'", non-fatal). They were added to `updater.seeders` in the lis-setup work. Fix later: make their `run()` use `firstOrCreate(['code'=>…])` (or drop them from `updater.seeders`, keep only `seedForCompany`). Relates to [[lis-setup-screen]] global-unique-code limitation.

**Phase 2 — make SCAFFOLDED settings real ✅ DONE + tested (BE, committed `49ae5cb17`, tests `cff611265`):** 6 settings wired as real gates, company-scoped via `Modules\Core\Services\SettingsService`, fail-open (default = prior behavior).
- Inventory: `auto_approve_receipts` (`InventoryReceiptController::store` → `ApproveReceipt` when on), `require_batch_tracking` (4 FormRequests; `batch_number` required when on).
- Purchases: `enable_purchase_requests` (`PurchaseRequestController::store`, blocks create when `=== false`).
- Sales: `enable_delivery_notes` (`SalesDeliveryNoteController` store/from-order/confirm), `allow_below_min_price` (`Store{SalesInvoice,SalesOrder}Request` vs `products.min_sale_price`; min 0 = no floor), `auto_generate_delivery` (`PostSalesInvoice::handleAutoGenerateDelivery` — DRAFT DN, **isolated nested transaction + `report()` so it NEVER rolls back the invoice posting**).
- **23 Pest gate tests + 61 existing Sales tests green.** Reviewed manually (Codex sandbox broken — Linux namespace exhaustion from concurrent agents).
- ⚠️ **Behavior change:** `allow_below_min_price` now enforces the min price by default (only affects products with `min_sale_price>0`) — flagged in CHANGELOG. Existing Sales tests pinned `min_sale_price=0` (factory was random) to stay deterministic.
- **DEFERRED — real features, NOT faked:** `approval_workflow` (Core `ApprovalWorkflow` exists but UNWIRED — a subsystem) + `price_includes_tax` (tax-inclusive math). Left scaffolded; separate build. Also `accounting.cost_center_enabled` stays a FE-visibility flag (no consumer either side yet) — exposed in the wizard but currently inert.

**Phase 3 — the 4 wizard steps ✅ DONE (FE, builds clean):** 4 new `/setup` steps (Sales/Purchases/Inventory/Accounting) inserted after Default-Accounts (indices 5-8; warehouse/treasury/bank/finish renumbered 9-12), driven by a new `OPERATIONAL_STEPS` config in `default-accounts.config.ts`, rendered by ONE DRY `@for` block (boolean→`p-toggleSwitch`, enum→`p-select`, with plain-language help per setting, EN+AR). Load via `forkJoin(settingService.list(module))`, save via chained `PUT /core/settings` per step. Re-openable (idempotent, no guard). Exposes the LIVE operational decisions + `grn_mode`/`stock_deduction_point`/`valuation_method` enums.

## Setup wizard — accounts + currency + navigation (3 owner asks, 2026-06-20) ✅ DONE + verified live
1. **Auto-fill missing default accounts** (BE `7193f7e2b`, FE `56fab6a`): new **`POST /core/settings/default-accounts/ensure`** (`SettingController::ensureDefaultAccounts`, perm `core.settings.manage`) wraps the existing idempotent `SettingDefinitionSeeder::seedDefaultAccountSettings($companyId)` (`:2118`) — for any unwired default-account slot it **creates a postable DETAIL account under the right parent** (AR/AP-header-safe via `AutoAccountService::createChildAccount`) and wires the setting; repairs header-wired ones. FE: "Auto-fill missing accounts" button in the Default-Accounts step → calls it → `loadDetailAccounts()` reloads the pickers. 45 slots / 7 modules (`default-accounts.config.ts`). Verified live (200; slots resolve to detail leaves, `wired:0` on already-backfilled co 4).
2. **Free step navigation** (FE): `setup-wizard.component.ts goToStep()` now allows jumping to ANY step (was `index <= currentStep()` backward-only); all step tabs `clickable`. No more one-by-one.
   - ✅ **FIXED (2026-06-20) — Default Accounts tab shows blank on revisit ("settings reverted").** Owner confirmed: **data IS saved, just not displayed** ("الداتا محفوظة بس مش بتتعرض"). **Root cause (NOT a save bug, NOT the `s.setting_key`/`current_value` mapping — `listByModule` flattens those correctly):** `loadDetailAccounts()` → `seedAccountValues()` (the LOAD that fills the account pickers) is called ONLY from `importCOA()` (`:663`), `skipCOA()` (`:738`), and the ensure button (`:790`) — **NOT in `ngOnInit`, and NOT on step entry.** `ngOnInit` loads everything else (`loadOpSettings`/warehouses/treasury/bank) but only `loadAccountsCount()` for accounts (count, not the pickers). So **opening the Accounts step directly — esp. via the new free-nav tab jump — never seeds `accountValues` → all pickers blank.** (Op-steps 5-8 were fine: `loadOpSettings()` is in `ngOnInit`.) **Fix:** added `enterStep(index)` — routes `nextStep`/`prevStep`/`goToStep` through it; when entering step **4** it calls `loadDetailAccounts()`, **guarded by `Object.keys(accountValues()).length === 0`** so it seeds once and never clobbers in-progress edits or the COA-flow's load. FE `setup-wizard.component.ts`. (Save-on-leave for the free-nav was the earlier hypothesis but the owner clarified data is saved; a true save-on-leave is risky — could persist an un-loaded blank step over saved values — so NOT done.)
   - ✅ **FIXED (2026-06-20) — operational-settings TOGGLES (Sales/Purchases/Inventory/Accounting ops) revert to OFF on revisit.** Owner: "كل الشاشات كده معدا الحسابات" (all op-tabs fail, accounts works). **Root cause:** the API casts a boolean setting to a **real JSON boolean** (`current_value: true`) — verified by curl (PUT `"true"` → GET `true`). But `loadOpSettings()` did `values[key] = cur === 'true' || cur === '1'` (**string** compare) → a real boolean `true` never matches → **every op toggle loads OFF regardless of saved value.** Enums (string) were fine; the Accounts step has NO booleans → unaffected (exactly why it worked). **Fix:** `cur === true || cur === 'true' || cur === '1' || cur === 1` + widened the `stored` map type `string|null`→`unknown` (the `=== true`/`=== 1` triggered TS2367 against the old type — **this TS error failed the build and the deploy script had already `rm -f`'d `/app` before the copy → moonui app went blank; restore = fix TS + rebuild + redeploy. Lesson: make the deploy `cp` CONDITIONAL on `dist/.../index.html` existing**). Verified via headless Playwright repro (`/tmp/shots/op-persist.js`): saved `true` now loads ON; flip+Next+reload round-trips. BE save/load was always correct — pure FE load bug. ⚠️ Same string-boolean pattern likely lurks in other FE settings readers.
3. **Currency follows the company currency** (FE): sales invoices/orders + purchase bills/orders defaulted to a hardcoded `'SAR'` or the `is_base` flag and **reverted every form-open**. Root cause: `CompanyCurrencyService.currency()` (= `company.currency`, fallback `EGP`) existed but was **never used** by any form. Fixed all 4 to default from it (bills resolve the numeric `currency_id` by matching `company.currency` code). Inventory has no currency field (implicit). Files: `features/sales/{invoices,orders}`, `features/purchases/{orders,bills}`. Relates to the is_base cleanup ([[lis-receivable-header-account-trap]]).

## 🐞 FIXED (2026-06-20) — sales invoice posts COGS but never deducts stock (moontest ORD-2026-00001)
Owner: made a sales invoice from an order + paid it → **stock not deducted**; "delivery note" says success but nothing visible. Root-cause agent (moontest, read-only) confirmed:
- **INV-2026-00002 (from ORD-2026-00001):** order has `warehouse_id=NULL`, so the invoice **header** warehouse is NULL while the **line items** carry warehouse 1. `PostSalesInvoice::handleCogsAndStock` resolves the warehouse as `item ?? header` → posts the **COGS JE** but **defers** the physical decrease to the auto-GDN (because `auto_create_stock_issue_on_invoice=true`). Then `handleAutoStockIssue` read **only the header** (`$invoice->warehouse_id`) → NULL → **early-return → no GDN, no deduction** = COGS-without-stock (GL vs inventory mismatch). INV-2026-00001 worked only because its header warehouse was set.
- Two **secondary** truths (not bugs): the "delivery note" action **does** persist a Draft DN (returns 201) — owner perceives "nothing" because it's a hidden draft, and confirming it would deduct 0 anyway since `stock_deduction_point=invoice` (DN-confirm only deducts when `=delivery`). And the order's "invoice" icon stays because the invoice items have `sales_order_item_id=NULL` → `updateOrderInvoicedQuantities` never marks the order invoiced.

**FIX (BE, `hazemdev`, Pest-verified 14/14 in `CogsJournalEntryTest`):**
1. `PostSalesInvoice::handleAutoStockIssue` now resolves the warehouse the SAME way COGS does — header **else** the first tracked item's warehouse (`$invoice->warehouse_id ?? $inventoryItems->first(fn($i)=>$i->warehouse_id)?->warehouse_id`). ⚠️ used `->first(closure)` NOT `->firstWhere(closure)` (Collection::firstWhere takes a key, not a callback — silent null → the bug-in-the-fix I hit first).
2. `inventory.auto_approve_issues` seeded **default flipped `false`→`true`** in Core `SettingDefinitionSeeder` (~:861). It is the **sole** consumer = PostSalesInvoice's auto-approve veto `$salesAutoApprove && $inventoryAutoApprove !== false`. The code's `!== false` intends *unset→approve, only explicit-false vetoes*, but the seeded default `false` made the default itself veto → every auto-GDN stayed **draft → silent no-deduction on any fresh install that enables auto_create**. Default-true aligns the seeded default with the code's intent. Explicit `false` still vetoes (test 748). moontest already had an explicit `true` row → unaffected by the flip; its bug was purely #1 (the NULL-header warehouse).
- Test-infra fixes exposed by #2 (auto-GDN tests had been red since `inventory.auto_approve_issues` got seeded false via the sync-reference bridge migration): `SettingDefinition::create`→`updateOrCreate` for the now-seeded key; the cancel test `travelTo(2026-02-15)` so the cancel's reversal JE lands in the seeded Feb fiscal period.
- **Not retroactive:** INV-2026-00002 already posted COGS-without-stock → must be cancelled+re-posted after the release (or manually issued). New orders should carry a warehouse; the fix makes the invoice resilient regardless.
- ⏳ deferred (separate, lower-pri): order→invoice item `sales_order_item_id` linkage (icon/`invoice_status`); the hidden-draft DN UX when `stock_deduction_point=invoice`.

## Status
- ✅ Study published ([HTML](https://moonui.elbaset.com/setup-wizard-settings-study.html)) + topic + INDEX. ✅ Phase 1 (conflicts) + Phase 2 (6 gates + tests) committed on `hazemdev` (`07deb8a7a`, `49ae5cb17`, `cff611265`). ✅ Phase 3 wizard built (deploying to `/app` + FE commit). ⏳ owner tests on `moonui.elbaset.com/app/setup` + cuts a release (fleet gets it via update). DEFERRED: `approval_workflow` + `price_includes_tax` (real features, separate build).
- Every change → [[feedback_whats_new_log]] CHANGELOG; this topic updated as we go ([[feedback_kb_update_workflow]]).
