* fix(invoices): apply configured voucher series to payments + preview next voucher The booking engine resolves the series from default_voucher_series_per_source_type, but the global "Standardserie" dropdown wrote a separate field the engine ignored, and cash-method invoice payments (invoice_cash_payment) weren't exposed in settings — so configured series were silently dropped to "A". - Expose cash/private payment source types in the per-source-type form - Write the global default through to the map on save, keeping overrides - Resolve voucher-sequences/next by source_type (+date) to match the engine - Show the upcoming voucher (V2) in the payment dialog title - Share resolveInvoicePaymentSourceType so preview and booking can't drift Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(salary): keep AGI panel in sync with Skatteverket signing state The AGI panel mixed run-scoped generation state (agi_generated_at, agi_declarations) with period-scoped submission state (extension_data agi_submission_{period}), so the two could drift and present contradictory UI. Reconcile them: - Auto-detect a Mina Sidor BankID signature: while awaiting_signing, poll /agi/kvittenser on mount and on tab refocus so the panel flips to "signed" (hiding the signing actions) without a manual "Hamta kvittens" click. - Warn instead of offering to sign when the locked granskningsunderlag predates the run's latest AGI generation (draftIsStale) — avoids filing superseded figures. - Self-heal a stale "AGI-XML saknas" error once the run's AGI is (re)generated out-of-band (MCP/API/other tab). - Refetch the salary run on tab focus so agi_generated_at reflects out-of-band generation without a hard reload. - /agi/lasUpp now clears the cached agi_submission_{period} record, so unlocking drops the panel back to the pre-submission state instead of stranding it on a released "redo att signeras" draft. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: Implement VAT registration handling and invoice item line types - Added VAT registration check in commitCreateInvoice to set VAT rate to 0% for non-VAT registered companies. - Updated invoice creation logic to reflect 'exempt' VAT treatment and adjusted related fields accordingly. - Introduced support for free-text and blank spacer rows in invoice items by adding a new line_type field. - Enhanced invoice and credit note handling to accommodate new line types. - Added new localized messages for text rows in English and Swedish. - Created tests for salary run approval logic, ensuring bank details are validated correctly. - Implemented effective net payout calculation for salary runs, considering tax overrides. - Added SQL migrations to support new invoice item line types and accounting method awareness for linking invoices to vouchers. * feat(articles): artikelregister with revenue account + VAT rate per article Article register (non-inventory) with per-article VAT rate and optional BAS class-3 revenue-account override. Includes API routes, UI pages, MCP tools, pending-operation staging, and the activate-or-create account flow (ACCOUNTS_NOT_IN_CHART -> ActivateAccountsDialog, unknown numbers -> AddAccountDialog) reusing the journal entry UX. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): no-doc-required batch + bulk-missing endpoints Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(payments): supplier payment lines + cash-method invoice matching Shared payment-line proposal for supplier invoices, improved match-invoice/match-supplier-invoice flows (kontantmetoden-aware), and voucher-link support without requiring a 151x clearing entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): new journal entry dialog, SIE import tweaks, misc New journal entry dialog component, journal list/page updates, invoice editor updates, SIE import adjustments, transaction ingest and api-key tweaks, pr-agent workflow update. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): implement tax reduction features and localization updates * feat(tests): add VAT registration gate to pending operations commit tests --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
4.5 KiB
TypeScript
124 lines
4.5 KiB
TypeScript
/**
|
||
* Voucher series resolver — pure helpers for mapping journal_entries.source_type
|
||
* to a default voucher_series per company_settings, formatting voucher labels
|
||
* for display, and parsing them back.
|
||
*
|
||
* Source-type → series mapping lives in
|
||
* company_settings.default_voucher_series_per_source_type (JSONB).
|
||
*
|
||
* Defaults to 'A' when:
|
||
* - the settings row is null/undefined
|
||
* - the JSONB is missing the source_type key
|
||
* - the configured value is not a single uppercase letter A–Z
|
||
*
|
||
* These functions are pure; no I/O. Engine call-sites read the settings row
|
||
* once and pass it in.
|
||
*/
|
||
import type { JournalEntrySourceType } from '@/types'
|
||
|
||
export type VoucherSeriesMap = Partial<Record<JournalEntrySourceType, string>> &
|
||
Record<string, string>
|
||
|
||
const SERIES_LETTER_RE = /^[A-Z]$/
|
||
|
||
/**
|
||
* Resolve the default voucher_series letter for a given source_type from a
|
||
* company_settings row. Returns 'A' as a safe fallback when no mapping is
|
||
* configured for that source_type.
|
||
*
|
||
* @param settings - Either a full CompanySettings row or just the per-source
|
||
* map. `null`/`undefined` is allowed (returns 'A').
|
||
* @param sourceType - The journal_entries.source_type value.
|
||
*/
|
||
export function resolveDefaultSeriesForSource(
|
||
settings:
|
||
| { default_voucher_series_per_source_type?: VoucherSeriesMap | null }
|
||
| VoucherSeriesMap
|
||
| null
|
||
| undefined,
|
||
sourceType: JournalEntrySourceType,
|
||
): string {
|
||
if (!settings) return 'A'
|
||
|
||
// Accept both the full settings row and a bare map. Both shapes are
|
||
// narrowed via duck-typing on the column key — when present, treat it as
|
||
// the settings row; otherwise treat the argument itself as the map.
|
||
const raw = settings as {
|
||
default_voucher_series_per_source_type?: VoucherSeriesMap | null
|
||
} & VoucherSeriesMap
|
||
const mapCandidate =
|
||
raw.default_voucher_series_per_source_type !== undefined
|
||
? raw.default_voucher_series_per_source_type
|
||
: (settings as VoucherSeriesMap)
|
||
|
||
if (!mapCandidate || typeof mapCandidate !== 'object') return 'A'
|
||
|
||
const value = (mapCandidate as VoucherSeriesMap)[sourceType]
|
||
if (typeof value === 'string' && SERIES_LETTER_RE.test(value)) {
|
||
return value
|
||
}
|
||
return 'A'
|
||
}
|
||
|
||
/**
|
||
* Propagate a change to the global default voucher series across the
|
||
* per-source-type map. Source types that were still following the previous
|
||
* default move to the new default; explicit overrides (values that differ from
|
||
* the previous default) are preserved untouched.
|
||
*
|
||
* The booking engine resolves series from the per-source-type map, not from the
|
||
* global default, so the bookkeeping settings form calls this when the user
|
||
* changes the "Standardserie" dropdown — otherwise that control would be a
|
||
* no-op for bookkeeping. Pure; returns the next map (input is not mutated).
|
||
*/
|
||
export function applyDefaultSeriesToMap(
|
||
currentMap: VoucherSeriesMap | null | undefined,
|
||
prevDefault: string,
|
||
nextDefault: string,
|
||
): VoucherSeriesMap {
|
||
const out: VoucherSeriesMap = {}
|
||
for (const [key, value] of Object.entries(currentMap || {})) {
|
||
out[key] = value === prevDefault ? nextDefault : value
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Format a voucher (series + number) for UI display. Returns "—" when the
|
||
* voucher number is null (e.g. a draft entry that has not been committed yet).
|
||
*
|
||
* Always lifts the series to uppercase. Falls back to 'A' when the series is
|
||
* null/empty for forward-compat with legacy rows. Accepts partial inputs so
|
||
* callsites can pass through API responses without re-shaping them.
|
||
*/
|
||
export function formatVoucher(entry: {
|
||
voucher_series?: string | null
|
||
voucher_number?: number | null
|
||
}): string {
|
||
if (entry.voucher_number == null || entry.voucher_number === 0) {
|
||
return '—'
|
||
}
|
||
const series =
|
||
entry.voucher_series && typeof entry.voucher_series === 'string'
|
||
? entry.voucher_series.toUpperCase()
|
||
: 'A'
|
||
return `${series}${entry.voucher_number}`
|
||
}
|
||
|
||
/**
|
||
* Parse a formatted voucher label back into its parts. Returns null when the
|
||
* input does not match the expected shape (single uppercase letter followed
|
||
* by a positive integer). Use for filter inputs / search.
|
||
*/
|
||
export function parseVoucher(
|
||
formatted: string,
|
||
): { series: string; number: number } | null {
|
||
if (typeof formatted !== 'string') return null
|
||
const trimmed = formatted.trim().toUpperCase()
|
||
const match = trimmed.match(/^([A-Z])(\d+)$/)
|
||
if (!match) return null
|
||
const number = parseInt(match[2], 10)
|
||
if (!Number.isFinite(number) || number <= 0) return null
|
||
return { series: match[1], number }
|
||
}
|