---
name: evolution_invoice_stock_reversal_asymmetry
description: Invoice uncommit stock reversal must mirror the commit stock-movement guards (asset account + non-job)
metadata: 
  node_type: memory
  type: project
  originSessionId: a85d5f39-f7cd-4298-88ea-653696a64e47
---

Invoice COMMIT moves stock `"out"` for a line only when BOTH: (A) `inventory.assaccount > 0` (stocked item, not a service — `assaccount` is `int(6)`, 0 = no asset account) AND (B) `dispatch_check == "1"`, which the form sets only when `invoices.jobid == 0` (jobs dispatch stock on the job side). Guard in `commitInvoice()` — both `library/accounting.php` (~L270, live path) and web-root `accounting.php` (~L153).

Bug fixed 2026-07-16: the UNCOMMIT reversal in `invoiceaddsave.php` `unapprove` handler (~L380) mirrored only guard B (`invoices.jobid = 0`) plus `invoiceitems.status = '1'`, but NOT guard A. So service lines (no asset account) that never got an outgoing movement on commit still got a spurious `stockMovement("in")` on uncommit, inflating SOH. DELETE moves no stock itself — it requires `commited == "0"` first, so drift originates entirely in uncommit.

**Why:** commit and reversal guards drifted out of sync; reversal was over-broad.
**How to apply:** any invoice stock reversal must re-derive the SAME guards as commit — join `inventory` and require `assaccount > 0`, and keep `jobid = 0`. Credit notes already do this carefully in `uncommitCreditNote()`. `library/invoice.php::unApprove()` is an empty stub, unused. See [[evolution_two_accounting_files]], [[evolution_picking_staging_ledger]], [[evolution_creditnote_type_restock]].
