---
name: evolution_creditnote_allocations
description: "Credit-note allocation lifecycle: creditallocations is the real link (sourcedoc_* is provenance only); auto-allocate-on-commit; bill commit now stores tax; allocations shown on invoiceadd/billadd"
metadata:
  node_type: memory
  type: project
  originSessionId: 23a696f2-8dea-45cd-b183-8235b37f671d
---

Credit-note allocation work done 2026-07-15 (AR + AP). The data model has a trap that caused "why wasn't the credit allocated automatically?":

**`creditallocations` is the ONLY real link.** A row (`source` AP/AR, `targetid`, `amount`, `status=1` active, `creditnoteid`) links a credit note to the bill/invoice it offsets. The `sourcedoc_type`/`sourcedoc_id` columns on `creditnotes` are **provenance only** — they record which doc the note was created *from*, NOT an allocation. `importFromBill()`/`importFromInvoice()` (`library/creditnote.php`) copy header + lines and stamp `sourcedoc_*` but write **no `creditallocations` row** — so a credit note made from a bill shows committed with zero allocation until something allocates it.

**Auto-allocate-on-commit (new this build).** `creditnote::autoAllocateToSource($id,$date)` allocates `min(credit remaining = total+tax − existing allocs, target open balance)` through the existing guarded `allocate()` (advisory lock, balance caps, Xero push all reused); safe no-op if source not committed / no open balance. Wired in `creditnotesave.php` commit block behind the posted `autoAllocate` flag, **non-fatal** (commit stands even if allocation can't complete; reason returned in JSON). UI: default-checked box `#cnAutoAllocate` in `creditnoteadd.inc` (only when the note has a committed source doc), passed by `cnCommit()` in `creditnoteadd.js`, which alerts if allocation was skipped.

**ORDERING CAVEAT:** `targetOpenBalance()` reads `bills.tax` directly, so the bill's stored tax must be correct *before* auto-allocation runs, or it under-allocates by the tax amount.

**Bill commit now stores tax (root cause of tax=0).** `commitBill()` computed grouped tax for the GL journal but its final `bills` UPDATE wrote only status/balance/total — never `tax`. The form save only recomputes tax when posted `total` is empty, so a bill committed with a JS-populated total but stale taxTotal landed `bills.tax=0` (uncommit/recommit "fixed" it by re-posting lines). Now writes `tax=round($billTax,2)` from the same per-group amounts posted to the GL, in **both** copies (`accounting.php` + `library/accounting.php` — see [[evolution_two_accounting_files]]). Also fixed a latent multi-rate bug there: the loop reused `$tax` (the rates array) as the per-group amount → renamed to `$lineTax` (any bill with >1 tax rate was miscomputing group 2+).

**Allocations now visible + click-through.** `invoiceadd.inc` (AR) and `billadd.inc` (AP) render a "Credit Notes" panel beside Payments, shown only when allocations exist, each row linking to `index.php?page=creditnoteadd&creditnoteid=X&source=AR|AP`. billadd derives its balance `$creditTotal` from the same detail fetch (was a separate sum query). Removed a pre-existing undefined `$warning` in billadd's payment row (leftover from invoiceadd copy; billadd uses an `$uncommitted` badge instead).

**RESOLVED 2026-07-15 (was OPEN):** the AR balance gap was in the payment modal `invoicePayment.inc`, not the invoiceadd header (its header shows Total/Payments/Credits separately, same as billadd). The modal computed the current invoice's outstanding as `total+tax-paid` (line ~78) and *overwrote* every other outstanding invoice's stored balance with the same `total+tax-paid` (line ~97) — both ignoring active AR credit allocations, so it would offer to collect more than owed. Fixed: current invoice now subtracts `Σcreditallocations(AR,active)` (paid is cash-only); other invoices now trust the stored `invoices.balance` (already credit-net via `invoice::recomputeBalance`) instead of re-deriving. `billPayment.inc` never had the bug (used credit-net figures).

**Manual allocation → Xero sync (added 2026-07-16).** `creditnotesave.php?call=syncAllocations` (write-gated) pushes every active `creditallocations` row for a CN and returns per-row JSON `{success,synced,skipped,failed,message,rows:[{id,targetid,amount,status,ok,message}]}`. Preconditions: CN committed + `creditnotes.externalRef` set (allocations hang off the CN's Xero GUID); tenant defaults to `externalTenant`. UI: "Sync Allocations to Xero" button + `#xeroAllocResponse` in the `#cnToolbox` Xero panel (`creditnoteadd.inc`), plus a per-row Xero status badge (Synced/Not synced from `creditallocations.externalRef`) in the Current Allocations table; JS `cnSyncAllocations()`/`cnRenderAllocResult()` in `creditnoteadd.js`. **Contract change:** `evoXero::allocateCreditNoteToXero()` now RETURNS a structured array `['ok','status'(synced|skipped|blocked|failed),'message','externalRef',...]` and NEVER echoes (was echo + bool/string) — this also fixed a latent bug where a Xero failure during the live `allocate()` push echoed `showErrors()` HTML into the middle of the allocate JSON body. New helper `evoXero::xeroErrorMessage($e)` returns Xero's parsed error text (echo-free `showErrors`). **Still OPEN:** no retry cron — a `blocked`/`failed` allocation only re-syncs when a user clicks the button; nothing sweeps `creditallocations` with empty `externalRef` automatically. Note the Current Allocations status column only refreshes on page reload after a sync (the result list is the live feedback).

**UI consistency:** `creditnotereg.inc` uses the standard `pageheader`/`pagetitle`/`pagebutton` like invoicereg/billreg — registers are NOT report pages, so the [[feedback_reports_no_pageheader]] "no 2nd pageheader" rule does not apply to them.

Related: [[evolution-creditnote-commit-xero]] (commit JSON corruption + Xero sync), [[evolution_creditnote_party_picker]] (party picker + manual allocation panel), [[evolution_float_money_columns]], DB style [[evolution_getfieldarray_deprecated]].
