diff --git a/app/api/settings/__tests__/route.test.ts b/app/api/settings/__tests__/route.test.ts index 6b99b643..8eb9039e 100644 --- a/app/api/settings/__tests__/route.test.ts +++ b/app/api/settings/__tests__/route.test.ts @@ -42,6 +42,7 @@ vi.mock('@/lib/tax/deadline-generator', async (importOriginal) => { import { PUT } from '../route' import { regenerateTaxDeadlinesForUser } from '@/lib/tax/deadline-generator' +import { STANDARD_VOUCHER_SERIES_MAP } from '@/lib/bookkeeping/voucher-series-resolver' describe('PUT /api/settings', () => { beforeEach(() => { @@ -123,6 +124,31 @@ describe('PUT /api/settings', () => { expect(findCall('company_settings', 'update')?.[0]).toEqual({ voucher_series_labels: { L: 'Lön' } }) }) + it('stores the standard voucher series set as sent (Använd standarduppsättningen)', async () => { + const standard = { ...STANDARD_VOUCHER_SERIES_MAP } + enqueueMany([ + { data: { entity_type: 'aktiebolag', onboarding_complete: true } }, // fetch oldSettings + { data: { id: 's1', default_voucher_series_per_source_type: standard } }, // update ... returning + { data: null, count: 5 }, // deadlines count + ]) + + const request = createMockRequest('/api/settings', { + method: 'PUT', + body: { default_voucher_series_per_source_type: standard }, + }) + const response = await PUT(request, { params: Promise.resolve({}) }) + const { status, body } = await parseJsonResponse<{ + data: { default_voucher_series_per_source_type: Record } + }>(response) + + expect(status).toBe(200) + expect(body.data.default_voucher_series_per_source_type).toEqual(standard) + // Every source type, storno and correction included, reaches the row unchanged. + expect(findCall('company_settings', 'update')?.[0]).toEqual({ + default_voucher_series_per_source_type: standard, + }) + }) + it('rejects a voucher_series_labels key that is not a single uppercase letter', async () => { const request = createMockRequest('/api/settings', { method: 'PUT', diff --git a/components/settings/VoucherSeriesPerSourceTypeForm.tsx b/components/settings/VoucherSeriesPerSourceTypeForm.tsx index 845bad67..88ff07c8 100644 --- a/components/settings/VoucherSeriesPerSourceTypeForm.tsx +++ b/components/settings/VoucherSeriesPerSourceTypeForm.tsx @@ -13,7 +13,12 @@ import { } from '@/components/settings/SettingsRows' import { cn } from '@/lib/utils' import { getErrorMessage } from '@/lib/errors/get-error-message' -import { buildVoucherSeriesOptions } from '@/lib/bookkeeping/voucher-series-resolver' +import { HelpPopover } from '@/components/ui/help-popover' +import { + buildVoucherSeriesOptions, + isStandardVoucherSeriesMap, + STANDARD_VOUCHER_SERIES_MAP, +} from '@/lib/bookkeeping/voucher-series-resolver' import type { CompanySettings, JournalEntrySourceType } from '@/types' // Subset of source_types presented to the user. The DB column accepts every @@ -43,6 +48,18 @@ const VISIBLE_SOURCE_TYPES: Array<{ key: JournalEntrySourceType; labelKey: strin // Keeps the map's iteration order intact: we only split it, never reorder. const ALWAYS_VISIBLE_COUNT = 3 +// The payment types that belong to one bokföringsmetod. Under the other +// method their rows are dimmed, never hidden (#2184): the choice stays visible +// and editable, so a company that switches method finds it already made and +// nothing in the map goes stale out of sight. Only the payment rows are +// bound to a method; registering an invoice happens under both. +const METHOD_BOUND: Partial> = { + invoice_paid: 'accrual', + supplier_invoice_paid: 'accrual', + invoice_cash_payment: 'cash', + supplier_invoice_cash_payment: 'cash', +} + // Swedish labels. Kept inline so this component is self-contained: these // labels are bookkeeping-domain terms that intentionally stay Swedish across // locales (see CLAUDE.md i18n table). @@ -73,6 +90,7 @@ export function VoucherSeriesPerSourceTypeForm({ settings, onSettingsUpdated }: // Generic fold labels ("Visa alla (n)" / "Visa färre") shared with the // dashboard widgets; the domain labels themselves stay hardcoded Swedish. const tCommon = useTranslations('dashboard') + const t = useTranslations('settings_bookkeeping') const { toast } = useToast() const initialMap = settings.default_voucher_series_per_source_type || {} const [draft, setDraft] = useState>>( @@ -80,6 +98,7 @@ export function VoucherSeriesPerSourceTypeForm({ settings, onSettingsUpdated }: ) const [isSaving, setIsSaving] = useState(false) const [showAll, setShowAll] = useState(false) + const method: 'accrual' | 'cash' = settings.accounting_method === 'cash' ? 'cash' : 'accrual' // Same closed list as the manual verifikat form and the per-bankkonto // picker: the fixed Swedish presets, then any letter already configured in @@ -111,6 +130,15 @@ export function VoucherSeriesPerSourceTypeForm({ settings, onSettingsUpdated }: const hasChanges = JSON.stringify(draft) !== JSON.stringify(initialMap) + // Fill the form with the set a new company starts with. Fills, never + // saves: a series switch mid-year is a deliberate act, so the fold opens to + // show every row it changes and the user commits with Spara serier. + const onStandardSet = isStandardVoucherSeriesMap(draft) + const handleUseStandardSet = () => { + setDraft({ ...STANDARD_VOUCHER_SERIES_MAP }) + setShowAll(true) + } + const handleSave = async () => { setIsSaving(true) try { @@ -151,27 +179,35 @@ export function VoucherSeriesPerSourceTypeForm({ settings, onSettingsUpdated }: const renderRow = ( { key, labelKey }: (typeof VISIBLE_SOURCE_TYPES)[number], borderless = false, - ) => ( - - handleChange(key, e.target.value)} - className="font-mono" + ) => { + const boundTo = METHOD_BOUND[key] + const dimmed = boundTo !== undefined && boundTo !== method + return ( + - {seriesOptions.map((option) => ( - - ))} - - - ) + handleChange(key, e.target.value)} + className="font-mono" + > + {seriesOptions.map((option) => ( + + ))} + + + ) + } const alwaysVisible = VISIBLE_SOURCE_TYPES.slice(0, ALWAYS_VISIBLE_COUNT) const folded = VISIBLE_SOURCE_TYPES.slice(ALWAYS_VISIBLE_COUNT) @@ -204,7 +240,19 @@ export function VoucherSeriesPerSourceTypeForm({ settings, onSettingsUpdated }: {folded.map((entry, i) => renderRow(entry, i === folded.length - 1))} -
+
+
+ + {t('series_standard_set_help')} +