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.
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_remainingtoinventory_transactionsand 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].unitCoston 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.
POS / Invoices
- fix COD checkout no longer trips the inner payment-method guard #9 v1.6.100 . The guard was rejecting the
paid=0partial 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 of4010. 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 Inventorylines 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
costfield (where formal POs useunitCost) 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=Nrather 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:readkeys.
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 Paymentson capture, write a per-month amortisation schedule, and the run-due endpoint posts the1/N-share monthly entry from1300to6xxxfor whichever expense category. v76 + v77 migrations install the schedule table and backfill historical prepaid entries.
See Prepaid expenses.
Database migrations
| # | What it does | Idempotent? |
|---|---|---|
v68 | Backfill service-invoice revenue from 4010 to 4015 | Yes |
v70 | Add qty_remaining to inventory_transactions; stamp existing ingresses | Yes |
v71 | Replay FIFO chronologically; rewrite egress costs that were NULL or wrong | Yes |
v72 | Backfill items[i].unitCost on historical invoices from corrected egresses | Yes |
v73 | Fix-ups for cancel/return ingresses with no layer history | Yes |
v75 | Backfill historical defect-egress rows that posted with NULL cost | Yes |
v76 | Create prepaid_expense_schedules table | Yes |
v77 | Backfill historical prepaid expenses with their monthly schedule | Yes |
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.
Related
- FIFO costing — the architectural change in detail
- The 5-step return wizard — the rebuilt refund flow
- Chart of accounts —
4015Service Revenue and1300Advanced Payments - Prepaid expenses — the new amortisation flow
- Purchase orders — last-cost defaults and the receive-before-PATCH guard