Skip to content

v1.6.100 — FIFO foundations + return-wizard rebuild

The largest release we’ve shipped. The headline is a structural change to how Fexl Lite values inventory — per-layer FIFO via inventory_transactions.qty_remaining replaces the weighted-average that bled cost detail across the books. The return wizard was rebuilt around it; cancel and refund flows now share the same cost-resolution priority chain. Twenty-five-plus retest fixes ship alongside, mostly small but several with material accounting impact.

Released 4 May 2026·Tag fexl-v2.1.0-staging-windows·27 fixes · 7 migrations

Headline change — FIFO costing

breaking Inventory is now FIFO-costed per layer. Every restock writes a new ingress row in inventory_transactions with qty_remaining = quantity — that row is the layer. Sales walk the open layers oldest-first and decrement qty_remaining until the line quantity is satisfied. Reports read frozen costs from the egress audit rows; nothing reads products.cost_price for accounting any more.

What this means in practice:

  • COGS reflects the actual price you paid for the units that left, not a rolling average that smooths the answer into something nobody can audit.
  • The Inventory Valuation report is exact: open layers × cost, summed across products. No more drift between the report and the physical stockroom value.
  • Cancel and refund inherit the same costs. When you cancel a sale, the units flow back at the original sale’s per-unit cost — not the live WAC of the day, which on a price-drift product would be wrong.

Four migrations run the first time the upgraded server boots:

  • v70 — adds qty_remaining to inventory_transactions and stamps existing ingress rows with their original quantity (or remainder if later egresses already drew from them).
  • v71 — replays FIFO chronologically per (tenant, product, variant) and rewrites cost on historical egress rows that had NULL or wrong values.
  • v72 — backfills items[i].unitCost on historical invoices from v71’s corrected egress audit rows.
  • v73 — final fixups for cancel/return ingresses that v71 couldn’t reach (no layer history).

All idempotent — safe to re-run, won’t double-count.

See FIFO costing for the full explainer.

Return wizard rebuild

feature The five-step return wizard is the new canonical refund flow. Invoice → products → conditions → defects → confirm. One disposition picker per returning unit (resaleable / defect / dispose), with bonus-vs-non-bonus cost split correctly across 5010 and 5020 regardless of disposition. Card / cash / store-credit settlement all post the same cleanly-shaped JE.

See The 5-step return wizard.

POS / Invoices

  • fix COD checkout no longer trips the inner payment-method guard #9 v1.6.100 . The guard was rejecting the paid=0 partial as “no payment method” when COD is precisely the no-payment-now case. Implicit-COD fallback also surfaces a status badge on the invoice list.
  • fix Stamp custom invoice titles on receipts #14 v1.6.100 . Manual invoices now carry their user-supplied title onto thermal and A4 prints, centered and bold.
  • fix Service revenue routes to 4015 #20 v1.6.100 instead of 4010. Service-only invoices no longer inflate the Sales Revenue line. v68 backfill migration corrects historical service invoices.
  • fix Cancel-invoice errors surface the real cause #29 v1.6.100 . Pre-fix every sub-op was slog.Error-and-continue, so a poisoned PG transaction surfaced as the misleading “failed to fetch updated invoice” generic. Now every wrap returns context.
  • fix Pin Complete Sale button when Checkout dialog overflows #58 v1.6.100 . Long item lists no longer push the action button below the fold.
  • fix Suppress Change Due on customer print #58 v1.6.100 . Card-paid receipts don’t show a zero change line that confuses customers.
  • fix Print split-payment net amounts #59 v1.6.100 . Per-method rows now sum to the invoice total exactly; pre-fix they were pro-rated and drifted.
  • fix Refetch by id before printing serial-numbered items #62 v1.6.100 . Receipts now render the real IMEI / serial, not a stringified database primary key.
  • fix Dedicated Status column + implicit-COD fallback on the invoice list #65 v1.6.100 . Filter and sort now cohere with what the row actually represents.
  • fix Label refunded totals with a “Refunded” badge #66 v1.6.100 on the list and detail views, so a partially-refunded invoice doesn’t look like a regular sale at a glance.

Inventory FIFO (architectural)

  • breaking Per-layer FIFO via inventory_transactions.qty_remaining #41 v1.6.100 #44 v1.6.100 #46 v1.6.100 #49 v1.6.100 . The four bugs collapsed into one architectural fix: cancel-restore cost mismatch, defect-egress cost on bonus split, return-cost source priority, and stock-level drift on void-and-restate all root-caused to “no layer accounting.”
  • perf Migrations v70 / v71 / v72 / v73 + v75 — the FIFO foundation + historical cost backfill. Reports for any pre-upgrade period now reconstruct from the layer walk, not the live WAC of today.

See FIFO costing and Restock.

Returns / Cancellations

  • fix Defect-egress writes resolved cost (not NULL) #40 v1.6.100 . The defect leg of a refund now honours the bonus / non-bonus split that the regular sale leg honours. Migration v75 backfills historical defect rows that posted with NULL cost.
  • fix Cancel restore uses resolveReturnUnitCost (not item.price fallback) #41 v1.6.100 . The cancel-time ingress now mirrors the original sale’s per-unit cost via the same priority chain refunds use.

Reports

  • fix Suppress repeat date+entry# in GL when consecutive lines share a JE #55 v1.6.100 . Multi-line entries no longer redundantly stamp the header on every row.
  • fix Per-line description on JournalLine #55 v1.6.100 . Sale-with-bonus invoices now carry distinct labels on the two CR 1200 Inventory lines so the GL can tell them apart.
  • fix Derive Balance Sheet cash from JE ledger so children sum to parent #56 v1.6.100 . The Cash row on the BS now sums across 1010-* sub-accounts; pre-fix it read the parent code directly and missed sub-account legs.
  • fix Render Investing + Financing sections + label cleanup on Cash Flow #50 v1.6.100 . The 3-section structure now displays in full, not just Operating.
  • fix Readable source labels for Inventory Moves #67 v1.6.100 . The source column emits “Sale”, “Restock”, “Refund — restock”, etc. instead of raw enum strings.

Shipping

  • fix Persist carrier cost when input matches customer-price fallback #30 v1.6.100 . The form no longer drops the typed carrier cost when it equals the customer-charged price.
  • fix Expose customer JOIN + tenant-formatted displayAt + clickable invoice #32 v1.6.100 on the shipping list. Operators can navigate from a tracking row to the originating invoice without leaving keyboard hands.
  • fix Normalize company history table layout + relabel “Balance” #63 v1.6.100 #64 v1.6.100 . Per-carrier ledger view now matches the supplier/customer pattern.

Procurement / Suppliers

  • fix Use core formatCurrency to avoid NaN on legacy PO totals #36 v1.6.100 . Pre-fix, legacy quick-PO rows with a cost field (where formal POs use unitCost) rendered NaN on the supplier balance display.
  • fix Default omitted PO status to draft + restock before mark-received #38 v1.6.100 . Two parts: handler coerces empty status to 'draft' so the NOT NULL constraint doesn’t trip, and the receive flow now runs ingress before the PATCH so a failed ingress doesn’t leave behind a “received” PO with zero stock.
  • fix Default PO line cost to last purchase, not WAC #60 v1.6.100 . New PO lines pre-fill from the most recent ingress cost via /api/products/:id/cost-info?variantId=N rather than the rolling-average that re-blends on receive.

See Purchase orders.

Settings / Roles

  • fix Translate every role permission label, drop raw-key fallback #61 v1.6.100 . The permission matrix in the role editor now shows human labels for all 33 permission categories; previously a handful (the recent additions) leaked the raw users:roles:read keys.

Expenses

  • feature Real prepaid amortization — capitalize, schedule, run-due #70 v1.6.100 . Prepaid expenses (12-month rent, annual insurance) now post to 1300 Advanced Payments on capture, write a per-month amortisation schedule, and the run-due endpoint posts the 1/N-share monthly entry from 1300 to 6xxx for whichever expense category. v76 + v77 migrations install the schedule table and backfill historical prepaid entries.

See Prepaid expenses.

Database migrations

#What it doesIdempotent?
v68Backfill service-invoice revenue from 4010 to 4015Yes
v70Add qty_remaining to inventory_transactions; stamp existing ingressesYes
v71Replay FIFO chronologically; rewrite egress costs that were NULL or wrongYes
v72Backfill items[i].unitCost on historical invoices from corrected egressesYes
v73Fix-ups for cancel/return ingresses with no layer historyYes
v75Backfill historical defect-egress rows that posted with NULL costYes
v76Create prepaid_expense_schedules tableYes
v77Backfill historical prepaid expenses with their monthly scheduleYes

Upgrading

The desktop app auto-updates on next launch. The cloud server picks up the new build on the next deploy. Either way, migrations run on first boot of the new binary; a typical store sees them complete in seconds, larger ledgers (50k+ invoices) in a minute or two.

There’s no manual step. After the upgrade, your reports will recompute the next time they’re opened — that’s normal.