# Inventory count: counted product "disappears" from Stock Balances (zero-balance row gap)

> updated: 2026-06-27 · status: **FIXED + MERGED TO `main`** (BE `d247a63fa`). Portal ticket **ISS-2026-0004** (مون/project 283) → `client_review`.
> Code: BE `/home/moonui2/moon-erp-be` (`Modules/Inventory`). Reported on the `smart` deployment (CNT-000001 / CNT-000002).

## Symptom (client)
In `…/app/core/inventory-counts`, counting a product and setting it to **zero** makes it **disappear from "أرصدة المخزون" (Stock Balances)**. **Selective** — one counted product vanished (`P-000256`), another stayed (`P-000263`). Client noticed: an auto-adjustment `ADJ-000001` was created for the one that **stayed**, but **not** for the one that vanished.

## Root cause (code-verified)
The count cycle is otherwise correct — **nothing deletes or filters stock-balance rows** (grepped the whole BE: no `StockBalance` delete anywhere; `StockBalanceController::index` shows zeros by default; the FE screen even highlights zero-stock rows). The product wasn't deleted — **it never had a stock-balance row.**

`FinalizeCount::execute` (`Modules/Inventory/app/Actions/FinalizeCount.php`):
1. Per item: reads system qty from the existing `StockBalance` (or 0 if none), computes `difference = counted − system`.
2. Auto-creates an `InventoryAdjustment` **only for items with `difference != 0`**.

So a product whose **system qty is 0 (no balance row) counted to 0** → `difference = 0` → **no adjustment, and no balance row is ever created** → it's absent from "Stock Balances" (which lists existing rows) → looks like it "vanished". (`P-000256` = no diff → no ADJ → no row; `P-000263` = had a diff → `ADJ-000001` → got a row → stayed.)

## Fix
In `FinalizeCount::execute`, the per-item balance lookup now **`firstOrCreate`s** the row (matching the canonical `StockService::getOrCreateBalance` pattern — keys `product_id/product_variant_id/warehouse_id`, defaults `company_id, quantity 0, average_cost 0, total_value 0`) instead of `first()`. So **every counted product gets a stock-balance row** (0-qty if never stocked) and stays visible in "أرصدة المخزون". The difference/adjustment logic is unchanged (diff-0 items still create no adjustment).

- Test added (Pest): *"finalize creates a zero stock balance row for a counted product that had none"* → `InventoryCountApiTest.php`. **18 count tests green.**
- Changelog: `[Unreleased]` bullet added. `php -l`/`pint` clean.
- ⚠️ Note: client's `smart` copy needs the **next MoonStack update** (release/ship) before the fix is live there.

## Decision
Ensure-the-row at the **count** (where the user expects the product to appear), not by relaxing the report filters — keeps valuation/value reports correct (they still legitimately exclude zero-value rows) while the raw "Stock Balances" screen shows everything counted.

---

## Related feature — Enhanced Count (ISS-2026-0186, 2026-07-14)
> status: **IMPLEMENTED on hazemdev2, deployed to /app, `client_review`.** Not merged to main (owner runs /fullpush).

A second "New Count" mode («جرد جديد محسّن») that preloads **all** warehouse products at once (the same "show every product incl. zero-balance" theme as this fix). Design decisions (client-approved, scope-frozen):
- **New BE endpoint** `GET inventory/counts/products-for-warehouse/{warehouse}` — `Product` LEFT JOIN `inventory_stock_balances` (variant NULL, warehouse), company-scoped, `is_active`+`track_inventory`+`type=product`, paginated (per_page cap 5000/default 2000), `COALESCE(balance,0)` → `before_quantity`/`average_cost`. Gated `inventory.counts.create`; 404s a foreign-company warehouse. `InventoryCountController::productsForWarehouse`.
- **FE** = LIS price-editor pattern (virtual-scroll `p-table` over a plain `signal<Row[]>`, NOT the FormArray grid — a FormGroup per line won't virtualize at ~17k products). `InventoryCountService.loadProductsForWarehouse` auto-paginates. All new state/methods live in the existing `inventory-counts.component.ts` (a second dialog).
- **Safety (critical):** counted defaults EQUAL to "before", and save sends ONLY rows where counted≠before → an untouched product never produces an adjustment (no accidental warehouse write-off). BE invariant diff=0→no adjustment already tested.
- **Product-grain only** (variant NULL) — this install has ZERO variant products; per-variant is a documented follow-up.
- **Review caught a CRITICAL** (later fixed): `[(ngModel)]` mutating a row object never invalidates a `computed()` over the array signal → Save button stayed disabled after any edit. Fix: `(ngModelChange)` handler that does `enhancedRows.update(r => [...r])`. **Lesson: signal `computed()` won't recompute on nested in-place mutation — publish a new array reference.**
