# PO "Deliver" screen never logged to the In/Out movement log

Fixed 2026-10-01, then found broken in two further ways the same day — see "Second bug in the fix
itself" (null uom_id) and "Third bug in the fix itself" (Valuation double-counted) below; the
version at the bottom of this doc is the one actually confirmed working end-to-end. Real bug,
found via a real report: FRC6872 was booked in via a purchase order, stock correctly increased,
but the receipt never appeared on the product's own In/Out movement log
(`/admin/inventory/products/{id}/moves`) — only a later manual adjustment and a sale showed.

## Root cause

There are **two separate ways** a purchase order's receipt gets recorded in Cydekick, and only one
of them was ever creating a proper stock-movement audit trail:

1. **The full Inventory Operations flow** — `PurchaseOrder::createInventoryReceipt()` creates a
   `Receipt` (`Operation`) and a `Move` (draft), then a person opens that Receipt under
   Inventory → Operations and clicks "Validate". That click runs
   `Inventory::validateTransfer()` (core `inventories` plugin), which **does** create a proper
   `MoveLine` (state `done`) — confirmed by reproducing this exact flow locally end-to-end.

2. **The Purchase Order's own "Deliver" screen** (`DeliverOrder.php`,
   `commitPendingDeliveries()` → `performDeliverLine()` → `applyInventoryDelta()`) — a simpler,
   faster screen built directly into the PO itself, letting someone type in a received quantity
   without touching the Operations module at all. This is almost certainly what was used for
   FRC6872 (its `qty_received_method` ends up `MANUAL`, not `STOCK_MOVE`, confirming it went
   through this screen). `applyInventoryDelta()` updated `inventories_product_quantities` (real
   stock) and `inventory_valuations` (average cost) directly — correctly — but **never created an
   `inventories_moves`/`inventories_move_lines` row at all**. No audit trail, nothing for the
   product's own In/Out log to show, even though the stock change was completely real.

This is the same root cause already noted in passing during the test-order-cleanup investigation
earlier this session ("this specific local dataset never created real Move rows despite
receipt_status=full") — not root-caused at the time, now explained and fixed.

## Fix

`DeliverOrder::applyInventoryDelta()` now also calls a new private method,
`createReceiptMove()`, right after the existing `ProductQuantity`/`Valuation` updates. It creates a
standalone `Move` + `MoveLine` (no `operation_id` — this screen has no formal Operation/Receipt
workflow object the way the full Inventory flow does), state `done` directly (this method only
ever runs at the moment stock is actually received, never a pending/reserved state). Mirrors
`OrderObserver::createSaleMove()` (channel-orders plugin) exactly — the same pattern already
proven correct for sales — source location is the same `Supplier` virtual location
`PurchaseOrder::createInventoryReceipt()` already uses for a real PO receipt, so a receipt reads
identically in the log regardless of which of the two paths above produced it.

Files: `plugins/webkul/purchases/src/Filament/Admin/Clusters/Orders/Resources/OrderResource/Pages/DeliverOrder.php`.

## Verification performed

- `php -l` + Pint — clean.
- Standalone reproduction: created the exact same `Move`/`MoveLine` records by hand against real
  local data (real Supplier location id, real warehouse location, real product) — succeeded,
  confirming the field values/shape are valid against the real schema.
- Full reproduction via reflection on the real `DeliverOrder` page class, calling
  `applyInventoryDelta()` with a real Purchase Order (its name is auto-generated on save —
  `Order::updateName()` — a first verification attempt searched for the wrong, manually-supplied
  name and wrongly looked like nothing was created; redone against the real auto-assigned name):
  - `Move` created, `state = done`.
  - `MoveLine` created, `state = done`, `qty = 1`, `unit_cost = 12.29` (exactly the PO line's
    price), `source_location_id` = the real Supplier location, `destination_location_id` = the
    order's real destination warehouse location.
  - `inventories_product_quantities.quantity` correctly incremented (25 → 26).
  - **Confirmed visible via `Webkul\Inventory\Models\Product::moveLines()` filtered to
    `state = done`** — the exact relation/filter `ManageMoves.php` (the real In/Out log page) uses.
- A full `Livewire::test(...)` drive of the real component hit an unrelated local-environment
  routing limitation (`OrderResource` has no registered index page in this dev setup, surfaced
  during the post-action re-render/redirect, not during the action itself) — not a flaw in this
  fix; the reflection-based test above exercises the exact same `applyInventoryDelta()` call with
  real data and confirmed the fix works.

## Second bug in the fix itself — null uom_id (found 2026-10-01)

The fix above shipped but didn't actually work on production — reported back via a real test: a
product was delivered through the Deliver screen, stock/valuation correctly updated (visible on
the product's own Stock/Valuation tab), but the In/Out log still showed nothing for it, only an
older unrelated sale.

Root cause: `$line->uom_id` can genuinely be `null` — not every PO line has one set — and
`Move::create()` has a DB `NOT NULL` constraint on `uom_id`. The original fix passed
`$line->uom_id` straight through with no fallback, so `Move::create()` threw a
`QueryException` ("Column 'uom_id' cannot be null") — but only *after*
`applyInventoryDelta()`'s `ProductQuantity`/`Valuation` updates had already committed, and nothing
caught the exception, so the delivery looked completely successful (because the stock change was
real) while the receipt move silently never got created. No error was ever surfaced to the user.

This exact null-`uom_id` case wasn't exercised by this fix's own original verification — the real
PO line used in that reflection test happened to already have a `uom_id` set, so the gap went
unnoticed until a different real PO (with a line that had no `uom_id`) was tested.

Fix: `applyInventoryDelta()` now resolves `$uomId = $line->uom_id ?? $inventoryProduct?->uom_id ??
1` before calling `createReceiptMove()` — the exact same fallback chain
`OrderObserver::createSaleMove()` already uses for sales (`$line->product?->uom_id ?? 1`), just
applied here too. `createReceiptMove()` itself is now also wrapped in try/catch (mirroring
`createSaleMove()`'s own try/catch), logging via `Log::error()` instead of throwing, so any
*future* cause of this same failure mode degrades to "stock updated, receipt not logged, but
visible in the Laravel log" rather than a silent, invisible gap again.

Verified by reproducing the exact real failure: same real PO line (confirmed `uom_id = null`)
that previously threw `QueryException` on `Move::create()` now creates both `Move` and `MoveLine`
successfully (`state = done`, `uom_id = 1` via the fallback, correct `product_id`/`qty`).

## Third bug in the fix itself — Valuation double-counted (found 2026-10-01)

Reported straight after the uom_id fix above, via a real delivery: AJ85676-R, 1 unit booked in at
£17.75 via the Deliver screen (clean product, zero prior stock at that location). Available
Quantity correctly showed 1 and Avg Cost correctly showed £17.75, but **Total Value showed
£35.50** — exactly double.

Root cause: `applyInventoryDelta()` has always manually computed and written `Valuation`
(`quantity`/`average_cost`/`total_value`) directly — that was the ONLY place Valuation ever got
updated for a delivery through this screen, because before the first fix above, this screen never
created a real Move at all. Once `createReceiptMove()` started creating a real `state = done`
MoveLine, it unknowingly started also triggering
`Webkul\InventoryValuation\Observers\MoveLineObserver::created()` — which independently
recomputes that exact same `Valuation` row from scratch whenever an incoming MoveLine lands
(confirmed this is the ONLY mechanism the full Inventory Operations "Validate" flow relies on —
`PurchaseOrder::createInventoryReceipt()` has no manual `Valuation::` write anywhere). So every
delivery through this screen started running BOTH: `applyInventoryDelta()`'s own manual
calculation, then the observer's separate calculation on top of that — doubling `quantity` and
`total_value` within `inventory_valuations` specifically. `inventories_product_quantities` (the
real stock table) was never touched by the observer, so Available Quantity stayed correct
throughout, and Avg Cost happened to still look right too (coincidence: both "additions" used the
identical £17.75 unit cost, so `total_value ÷ quantity` still landed on the correct per-unit figure
even with the internal Valuation-table quantity silently doubled) — which is exactly why only
Total Value looked wrong and nothing else did.

Fix: removed `applyInventoryDelta()`'s manual `Valuation::updateOrCreate()` block entirely.
`$unitCost` is still computed and still passed into `createReceiptMove()` (so the MoveLine itself
carries the right cost for the observer's own `resolveCost()`), but Valuation is now written
exactly once, by the observer, exactly like the full Inventory Operations flow already does.
`ProductQuantity` (real stock) is unaffected — that was never double-counted and is still updated
directly by this method, same as before.

Verified two ways against real local data: (1) a clean product with zero prior stock, delivered 1
unit at £17.75 — `total_value` now lands on exactly £17.75, not £35.50; (2) a product with 2 units
already in stock at £10 avg, delivered 2 more at £20 — correctly lands on quantity 4,
weighted-average cost £15, total value £60, confirming the observer's own averaging logic (not
reproduced manually) is what runs now.

## Not touched

- The full Inventory Operations "Validate" flow (`PurchaseOrder::createInventoryReceipt()` +
  `Inventory::validateTransfer()`) was already correct — confirmed by reproducing it too, same
  session, before finding the real bug was specifically in the "Deliver" screen.
- No backfill for historical receipts booked in via this screen before the fix — their stock
  levels/average costs are already correct (that part never had a bug), only their *movement log
  entry* is permanently missing. Retroactively fabricating a Move/MoveLine for a past receipt
  wasn't attempted — ask first if this is wanted, same standing principle as every other
  historical-data question this session.
