# Sales: cash quick-sale + line-entry UX (stock-in-search, auto-row)

> updated: 2026-06-23 · status: **built + deployed to `/app`, awaiting user test, NOT yet pushed to `main`.**
> Three FE features + one BE settings change, built subagent-driven (implementer → code-review → fix per feature).

This topic covers three related sales-UX features shipped together. The shared line-items grid is `TransactionLineItemsComponent` — see the [grid unify plan](../plans/line-items-unify-column-control-plan.html) for the descriptor model.

---

## A) Stock balance inside the product-search dropdown (sales invoice)

**What:** while searching a product in the invoice, each dropdown option shows its on-hand qty **in that line's warehouse** (green; red when 0; `…` while loading).

**How:**
- `ProductSearchComponent` gained two **opt-in** signal inputs: `showStock` + `warehouseId`. Off by default → the other ~18 screens are untouched (no extra calls, no stock span). Enabled only on the invoice product column via new `TxColumn` fields `productShowStock` + `productStockWarehouseId(index)` (resolves row warehouse → header default).
- Stock source: **`WarehouseStockCacheService`** (`core/services/warehouse-stock-cache.service.ts`) — `forWarehouse(id)` does ONE `stockBalanceService.listFiltered({warehouse_id})` per warehouse, builds `Map<productId,qty>`, **`shareReplay(1)`** so all rows/searches reuse it. The component loads it in an `effect()` on `warehouseId`/`showStock`; the dropdown reads `stockMap()`.
- **Decision:** cache-per-warehouse (not per-keystroke `search`). The first design queried `listFiltered({warehouse_id, search:q})` — review flagged it could show a false **0** when the stock-balances `search` matched different fields than the product search, and could fan out unbounded with no warehouse. Cache fixes both: accurate (whole warehouse) + 1 fetch. Stock span is guarded by `showStock() && warehouseId()`.

## B) Auto-append row + focus next product on pick (ALL documents)

**What:** picking a product in the **last** row auto-adds a blank row and moves the cursor to the new row's product search → rapid sequential entry. Picking in a **middle** row keeps the normal in-row advance (to quantity).

**How:**
- `TxLineDescriptor.appendRow?: () => void` — each of the 12 host docs wires `appendRow: () => this.addItem()` (returns docs use `addStandaloneItem`). Grid `onProductPicked(i, product)` → calls host `onProductPicked` (fills unit/price), then **only if it was the last row** appends + `setTimeout(0)` focuses `@ViewChildren(ProductSearchComponent)[i+1].focusInput()`.
- **Decision (review fix):** focus jump is gated on **isLast** (a row was actually appended). The first version jumped on every pick (even middle rows), bypassing qty/price — corrected so middle-row edits advance normally via `ProductSearchComponent.focusNext()`. The `autoAdvance` input added in the first cut was removed.
- The invoice **barcode-scan box** (host-level, pre-existing) is a separate path (`addProductByBarcode` keeps focus on the box for repeated scanning) — untouched and unaffected.

## C) Cash quick-sale (POS-style flow in the sales invoice)

**Setting (company-wide, key/value — no migration):**
- `sales.enable_cash_customer` (boolean) + `sales.default_cash_customer_id` (partner id).
- BE: validation in `Modules/Sales/.../UpdateSalesSettingsRequest.php`; definition in `Modules/Sales/database/seeders/SalesSettingDefinitionSeeder.php` (value_type `boolean`/`integer`, group `general`). Partners table is **`business_partners`**.
- FE settings screen renders the toggle (boolean case) + a **customer picker** for the `_customer_id` key (`isCustomerSetting` = exact match `sales.default_cash_customer_id`; customers via `partnerService.listAll().filter(is_customer)` — **listAll**, not the 25-cap dropdown search).

**Cashbox = `PettyCash` (الصندوق النثري).** It has `branch_id` + `account_id` (GL). 🔴 A sales cash payment links to a **GL account only** (`receiving_account_id`); `PostSalesPayment` posts `Dr receiving_account / Cr AR` and does **NOT** auto-update `PettyCash.current_balance` or create a `PettyCashTransaction` (pre-existing behaviour — possible follow-up: call `PettyCashBalanceService` on post). There is **no per-user default cashbox** in the system.

**Invoice cash mode (when `enable_cash_customer` on):**
- `openNew()` pre-selects the cash customer, enables the existing **inline-payment** (so Save = create → auto-post → record+post payment), sets the cashbox destination, and `onSave()` forces `inlinePayAmount = this.total` (paid in full).
- Cashbox selection: branch cashboxes = `pettyCashService.listAll()` filtered by `branchContext.primaryBranchId()` + `is_active`. Default = **first**, or the **preferred** one remembered per branch in `localStorage['moon.cashSale.cashbox.b<branchId>']` (changing the in-form cashbox dropdown persists it). The cash payment's `receiving_account_id = cashbox.account_id`.
- New buttons **"Save & New"** + **"Print, Save & New"** (`pendingNewAfterSave` → success handler re-opens `openNew()` instead of closing; print-after-save fires first).

**🔴 Key correctness fix (review):** `buildPaymentDestinations()` used to early-return when no GL default accounts were configured — which dropped the cashbox destinations too, so the invoice posted but the payment was **silently skipped** (false "paid" toast). Fixed: cashbox destinations build **unconditionally**; and `onSave()` now **aborts with a `SALES.NO_CASHBOX` warning** (no silent skip) when a cash payment is expected but no destination resolves.

### Open / design notes
- **Approval bypass:** cash mode forces post (`shouldPost` via `withPay`) even when an approval workflow is set — intended for cash sales, but the **cashier role must have post/approve permission** or the save errors. (Pre-existing inline-pay behaviour, not new.)
- **PettyCash balance** not auto-synced on sales cash payment (see above) — revisit if the cashbox screen must show live cash-sale balances.
- Preferred cashbox is **localStorage** (per-browser/terminal), not a server user-setting.

---

## Commits (branch `hazemdev2`, NOT pushed to main yet)
- FE: `d79f0e3bf` + `b250cb1a2` (stock-in-search + cache) · `08f091d72` + `570c413ea` (auto-row + isLast fix) · `0096c27a0` + `46ee44ef5` + `e0bf64033` (cash setting/invoice/picker) · `154c19b4a` (cash review fixes).
- BE: `be629e391` (settings validation + definition seeder; ran the seeder manually — a pre-existing LIS migration fails on this parallel-dev DB, unrelated).
- Deployed to `/app` (534 bundles). Changelog: two `[Unreleased]` bullets added.

## Resume point
User to test on `/app`: (1) stock shows in invoice product search per warehouse; (2) pick in last row → new row + cursor; (3) cash mode pre-selects customer + Save&New/Print&Save&New post a full cash payment to the branch cashbox. **After the user confirms → push FE `hazemdev2`→`main` and BE `hazemdev2`→`main`** (fetch + FF; resolve only CHANGELOG conflicts by keeping both bullets, per [parallel-dev](parallel-dev.md)).
