* fix: article number ordering, free-text rows, invoice back-nav, onboarding resilience (#1053) Four fixes from Discord feedback in issue #1053: - Articles now order by article number with numeric-aware comparison ('2' before '10', unnumbered last, name tiebreak) in the invoice editor's article picker and as the register's default sort, via a shared lib/articles/sort.ts. Name order put article "1" last. - Invoice rows with no amounts (quantity 0, unit price 0) render as pure text rows on the PDF, the invoice detail page, and the review step via shared isTextLikeLine(), instead of printing "0 / 0,00 SEK / 0,00 SEK". Display-only; booking untouched. - The invoice editor navigates with router.replace after saving, so the detail page's back arrow returns to the list instead of reopening a fresh editor from history. - A transient query failure no longer reads as "no companies" / "onboarding not done": getActiveCompanyId throws CompanyContextError('resolution_failed') instead of returning null, the Edge middleware fails open on a degraded resolution (no onboarding redirect, no cookie clearing, no locale overwrite), and the dashboard page only redirects to /onboarding on a positively read incomplete/missing settings row. This is the likely cause of the completed onboarding wizard reappearing. Fixes #1053 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: CLAUDE.md tenancy line matches actual resolution order (prefs-first, cookie not read) The middleware stopped reading the gnubok-company-id cookie when user_preferences became authoritative (RLS parity); the stale doc line still described cookie-first order and misled review tooling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0e9cca2750
commit
97907a5a5c
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { isTextLikeLine } from '@/lib/invoices/display'
|
||||
|
||||
describe('isTextLikeLine', () => {
|
||||
it('is true for explicit text rows regardless of amounts', () => {
|
||||
expect(isTextLikeLine({ line_type: 'text', quantity: 0, unit_price: 0 })).toBe(true)
|
||||
expect(isTextLikeLine({ line_type: 'text', quantity: 2, unit_price: 100 })).toBe(true)
|
||||
})
|
||||
|
||||
it('is true for product rows with no amounts (issue #1053 free-text line)', () => {
|
||||
expect(isTextLikeLine({ line_type: 'product', quantity: 0, unit_price: 0 })).toBe(true)
|
||||
expect(isTextLikeLine({ quantity: 0, unit_price: 0 })).toBe(true)
|
||||
expect(isTextLikeLine({ quantity: null, unit_price: null })).toBe(true)
|
||||
expect(isTextLikeLine({})).toBe(true)
|
||||
})
|
||||
|
||||
it('is false as soon as the row carries a quantity or a price', () => {
|
||||
expect(isTextLikeLine({ line_type: 'product', quantity: 1, unit_price: 0 })).toBe(false)
|
||||
expect(isTextLikeLine({ line_type: 'product', quantity: 0, unit_price: 250 })).toBe(false)
|
||||
expect(isTextLikeLine({ line_type: 'product', quantity: 0, unit_price: -250 })).toBe(false)
|
||||
expect(isTextLikeLine({ quantity: 3, unit_price: 99.5 })).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -16,3 +16,19 @@ export function invoiceDisplayNumber(invoice: {
|
||||
}): string {
|
||||
return invoice.invoice_number ?? invoice.external_invoice_number ?? INVOICE_NUMBER_DRAFT_LABEL
|
||||
}
|
||||
|
||||
/**
|
||||
* True when an invoice line should render as a pure text row: description
|
||||
* only, no quantity/unit/price/amount columns. Explicit text rows
|
||||
* (line_type 'text') always qualify; so do product rows carrying no amounts
|
||||
* at all (quantity and unit price both zero/absent). Users write free-text
|
||||
* lines via the article picker's "Egen rad (fri text)" and leave antal/pris
|
||||
* at zero; printing "0 / 0,00 SEK / 0,00 SEK" on those is noise (issue #1053).
|
||||
*/
|
||||
export function isTextLikeLine(item: {
|
||||
line_type?: 'product' | 'text' | null
|
||||
quantity?: number | null
|
||||
unit_price?: number | null
|
||||
}): boolean {
|
||||
return item.line_type === 'text' || (!item.quantity && !item.unit_price)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import type { Invoice, InvoiceItem, Customer, CompanySettings, InvoiceDocumentType } from '@/types'
|
||||
import { generateOcrReference } from '@/lib/bankgiro/luhn'
|
||||
import { getAmountToPay } from '@/lib/invoices/rounding'
|
||||
import { isTextLikeLine } from '@/lib/invoices/display'
|
||||
|
||||
type PdfLang = 'sv' | 'en'
|
||||
|
||||
@@ -657,7 +658,9 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
|
||||
|
||||
// Free-text / blank rows carry no amounts: exclude them from every VAT
|
||||
// calculation. They still render as their own row in the line-items table.
|
||||
const billableItems = items.filter((item) => item.line_type !== 'text')
|
||||
// Amount-less product rows count as text too (isTextLikeLine), so they
|
||||
// neither print zeros nor seed an empty per-rate VAT group.
|
||||
const billableItems = items.filter((item) => !isTextLikeLine(item))
|
||||
|
||||
// Check if items have mixed VAT rates
|
||||
const hasPerLineVat = billableItems.some((item) => item.vat_rate !== undefined && item.vat_rate !== null)
|
||||
@@ -857,7 +860,7 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
|
||||
|
||||
{/* Table rows */}
|
||||
{items.map((item, index) =>
|
||||
item.line_type === 'text' ? (
|
||||
isTextLikeLine(item) ? (
|
||||
// Free-text / blank row: description spans the full width, no
|
||||
// numeric columns. An empty description renders as a spacer.
|
||||
<View key={index} style={styles.tableRow}>
|
||||
|
||||
Reference in New Issue
Block a user