diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index 14127a67..79b92916 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -68,9 +68,10 @@ export async function PUT(request: Request) { if (!validation.success) return validation.response const body = validation.data - // Lock company_name and org_number after onboarding is complete + // Lock org_number after onboarding is complete (legal identifier — changing it + // would orphan vouchers, SIE history, and tax filings). company_name remains + // editable so users can update their display/brand name (e.g. särskilt företagsnamn). if (oldSettings && (oldSettings as Record).onboarding_complete === true) { - delete (body as Record).company_name delete (body as Record).org_number } diff --git a/components/settings/PdfPrintSettings.tsx b/components/settings/PdfPrintSettings.tsx index 5569f7e8..e48f40d7 100644 --- a/components/settings/PdfPrintSettings.tsx +++ b/components/settings/PdfPrintSettings.tsx @@ -106,6 +106,17 @@ export function PdfPrintSettings({ settings, onUpdate }: PdfPrintSettingsProps) onCheckedChange={(v) => saveToggle('invoice_show_logo', v)} /> + +
+
+ +

Visa företagsnamn under loggan i fakturahuvudet

+
+ saveToggle('invoice_show_company_name', v)} + /> +
diff --git a/lib/api/schemas.ts b/lib/api/schemas.ts index a5106e29..bb879f55 100644 --- a/lib/api/schemas.ts +++ b/lib/api/schemas.ts @@ -399,6 +399,7 @@ export const UpdateSettingsSchema = z.object({ invoice_show_bankgiro: z.boolean().optional(), invoice_show_plusgiro: z.boolean().optional(), invoice_show_logo: z.boolean().optional(), + invoice_show_company_name: z.boolean().optional(), invoice_late_fee_text: z.string().nullable().optional(), invoice_credit_terms_text: z.string().nullable().optional(), // AI agent flow diff --git a/lib/invoices/pdf-template.tsx b/lib/invoices/pdf-template.tsx index 5e228859..7a966b92 100644 --- a/lib/invoices/pdf-template.tsx +++ b/lib/invoices/pdf-template.tsx @@ -377,7 +377,9 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN {company.logo_url && (company.invoice_show_logo ?? true) && ( )} - {company.company_name} + {(company.invoice_show_company_name ?? true) && ( + {company.company_name} + )} diff --git a/supabase/migrations/20260513120000_invoice_show_company_name.sql b/supabase/migrations/20260513120000_invoice_show_company_name.sql new file mode 100644 index 00000000..8d7a4342 --- /dev/null +++ b/supabase/migrations/20260513120000_invoice_show_company_name.sql @@ -0,0 +1,8 @@ +-- Add invoice_show_company_name toggle for invoice PDF header. +-- When false, the company name is hidden under the logo in the invoice PDF. +-- Default true preserves existing invoice layout for all current users. + +ALTER TABLE public.company_settings + ADD COLUMN IF NOT EXISTS invoice_show_company_name boolean DEFAULT true; + +NOTIFY pgrst, 'reload schema'; diff --git a/supabase/migrations/20260513120100_fix_vat_seed_chart_of_accounts.sql b/supabase/migrations/20260513120100_fix_vat_seed_chart_of_accounts.sql new file mode 100644 index 00000000..a767e855 --- /dev/null +++ b/supabase/migrations/20260513120100_fix_vat_seed_chart_of_accounts.sql @@ -0,0 +1,138 @@ +-- Fix seed_chart_of_accounts: VAT account labels did not match BAS 2026. +-- +-- The previous seed (introduced in 20260330130000_multi_tenant_company_refactor.sql) +-- created the 26xx output VAT accounts with the wrong account numbers: +-- +-- 2611 labelled "Utgaende moms 12%" ← per BAS, 2611 is 25% +-- 2612 labelled "Utgaende moms 6%" ← per BAS, 2612 is "egna uttag 25%"; 6% is 2631 +-- 2610 was seeded as plain "25%" ← 2610 is a collective parent in BAS, the engine +-- does not route to it +-- +-- Per BAS 2026 (BAS-intressenternas Förening) and the rest of this codebase +-- (lib/bookkeeping/bas-data, lib/bookkeeping/account-descriptions.ts, the VAT +-- declaration mapping, and the engine itself), the correct accounts are: +-- +-- 2611 Utgaende moms forsaljning inom Sverige, 25% +-- 2621 Utgaende moms forsaljning inom Sverige, 12% +-- 2631 Utgaende moms forsaljning inom Sverige, 6% +-- +-- This migration only fixes the seed function. Existing companies with the bad +-- seed are intentionally NOT backfilled here. + +DROP FUNCTION IF EXISTS public.seed_chart_of_accounts(uuid, text); + +CREATE OR REPLACE FUNCTION public.seed_chart_of_accounts(p_company_id uuid, p_entity_type text) +RETURNS void +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + v_account_count integer; + v_user_id uuid; +BEGIN + SELECT created_by INTO v_user_id FROM public.companies WHERE id = p_company_id; + + SELECT count(*) INTO v_account_count + FROM public.chart_of_accounts + WHERE company_id = p_company_id; + + IF v_account_count > 0 THEN + RETURN; + END IF; + + -- Assets (1xxx) + INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account) + VALUES + (v_user_id, p_company_id, '1510', 'Kundfordringar', 1, '15', 'asset', 'debit', 'k1', true), + (v_user_id, p_company_id, '1910', 'Kassa', 1, '19', 'asset', 'debit', 'k1', true), + (v_user_id, p_company_id, '1930', 'Foretagskonto / checkkonto', 1, '19', 'asset', 'debit', 'k1', true), + (v_user_id, p_company_id, '1940', 'Ovriga bankkonton', 1, '19', 'asset', 'debit', 'k1', true); + + -- Equity (2xxx) + IF p_entity_type = 'enskild_firma' THEN + INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account) + VALUES + (v_user_id, p_company_id, '2010', 'Eget kapital', 2, '20', 'equity', 'credit', 'k1', true), + (v_user_id, p_company_id, '2013', 'Ovriga egna uttag', 2, '20', 'equity', 'credit', 'k1', true), + (v_user_id, p_company_id, '2018', 'Ovriga egna insattningar', 2, '20', 'equity', 'credit', 'k1', true); + END IF; + + IF p_entity_type = 'aktiebolag' THEN + INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account) + VALUES + (v_user_id, p_company_id, '2081', 'Aktiekapital', 2, '20', 'equity', 'credit', 'k1', true), + (v_user_id, p_company_id, '2091', 'Balanserat resultat', 2, '20', 'equity', 'credit', 'k1', true), + (v_user_id, p_company_id, '2099', 'Arets resultat', 2, '20', 'equity', 'credit', 'k1', true); + END IF; + + -- Liabilities (2xxx) — corrected VAT account labels per BAS 2026 + INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account) + VALUES + (v_user_id, p_company_id, '2440', 'Leverantorsskulder', 2, '24', 'liability', 'credit', 'k1', true), + (v_user_id, p_company_id, '2611', 'Utgaende moms forsaljning inom Sverige, 25%', 2, '26', 'liability', 'credit', 'k1', true), + (v_user_id, p_company_id, '2621', 'Utgaende moms forsaljning inom Sverige, 12%', 2, '26', 'liability', 'credit', 'k1', true), + (v_user_id, p_company_id, '2631', 'Utgaende moms forsaljning inom Sverige, 6%', 2, '26', 'liability', 'credit', 'k1', true), + (v_user_id, p_company_id, '2641', 'Debiterad ingaende moms', 2, '26', 'liability', 'credit', 'k1', true), + (v_user_id, p_company_id, '2650', 'Redovisningskonto for moms', 2, '26', 'liability', 'credit', 'k1', true), + (v_user_id, p_company_id, '2710', 'Personalskatt', 2, '27', 'liability', 'credit', 'k1', true), + (v_user_id, p_company_id, '2731', 'Avrakning socialavgifter', 2, '27', 'liability', 'credit', 'k1', true); + + IF p_entity_type = 'aktiebolag' THEN + INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account) + VALUES + (v_user_id, p_company_id, '2893', 'Skuld till aktieagare', 2, '28', 'liability', 'credit', 'k1', true); + END IF; + + -- Revenue (3xxx) + INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account) + VALUES + (v_user_id, p_company_id, '3001', 'Forsaljning tjanster 25%', 3, '30', 'revenue', 'credit', 'k1', true), + (v_user_id, p_company_id, '3002', 'Forsaljning varor 25%', 3, '30', 'revenue', 'credit', 'k1', true), + (v_user_id, p_company_id, '3100', 'Momsfri forsaljning', 3, '31', 'revenue', 'credit', 'k1', true), + (v_user_id, p_company_id, '3900', 'Ovriga rorelseintakter', 3, '39', 'revenue', 'credit', 'k1', true), + (v_user_id, p_company_id, '3960', 'Valutakursvinster', 3, '39', 'revenue', 'credit', 'k1', true); + + -- COGS (4xxx) + INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account) + VALUES + (v_user_id, p_company_id, '4000', 'Varuinkop', 4, '40', 'expense', 'debit', 'k1', true); + + -- External expenses (5xxx-6xxx) + INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account) + VALUES + (v_user_id, p_company_id, '5010', 'Lokalhyra', 5, '50', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '5410', 'Forbrukningsinventarier', 5, '54', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '5420', 'Programvaror', 5, '54', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '5460', 'Forbrukningsmaterial', 5, '54', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '5800', 'Resekostnader', 5, '58', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '5910', 'Annonsering', 5, '59', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '6071', 'Representation avdragsgill', 6, '60', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '6110', 'Kontorsmateriel', 6, '61', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '6212', 'Mobiltelefon', 6, '62', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '6230', 'Datakommunikation', 6, '62', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '6530', 'Redovisningstjanster', 6, '65', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '6570', 'Bankavgifter', 6, '65', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '6991', 'Ovriga avdragsgilla kostnader', 6, '69', 'expense', 'debit', 'k1', true); + + -- Personnel (7xxx) + IF p_entity_type = 'aktiebolag' THEN + INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account) + VALUES + (v_user_id, p_company_id, '7010', 'Loner', 7, '70', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '7210', 'Semesterloner', 7, '72', 'expense', 'debit', 'k1', true), + (v_user_id, p_company_id, '7510', 'Arbetsgivaravgifter', 7, '75', 'expense', 'debit', 'k1', true); + END IF; + + INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account) + VALUES + (v_user_id, p_company_id, '7960', 'Valutakursforluster', 7, '79', 'expense', 'debit', 'k1', true); + + -- Financial (8xxx) + INSERT INTO public.chart_of_accounts (user_id, company_id, account_number, account_name, account_class, account_group, account_type, normal_balance, plan_type, is_system_account) + VALUES + (v_user_id, p_company_id, '8310', 'Ranteintakter', 8, '83', 'revenue', 'credit', 'k1', true), + (v_user_id, p_company_id, '8410', 'Rantekostnader', 8, '84', 'expense', 'debit', 'k1', true); +END; +$$; + +NOTIFY pgrst, 'reload schema'; diff --git a/supabase/migrations/__tests__/seed-chart-of-accounts.pg.test.ts b/supabase/migrations/__tests__/seed-chart-of-accounts.pg.test.ts new file mode 100644 index 00000000..5b608f23 --- /dev/null +++ b/supabase/migrations/__tests__/seed-chart-of-accounts.pg.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest' +import { seedCompany } from '@/tests/pg/fixtures' +import { getPool } from '@/tests/pg/setup' + +/** + * Locks in the labels seed_chart_of_accounts() writes for the 26xx output VAT + * accounts. Per BAS 2026: 2611=25%, 2621=12%, 2631=6%. + * + * Background: this mapping has regressed once already (2026-03-30 multi-tenant + * refactor copy-pasted the original 2024 buggy seed back in). The engine and + * the VAT-rutor mapping both route by account number, so a mislabel in this + * function never breaks any other test — only the chart-of-accounts UI shows + * the wrong text. This test is the canary. + */ + +describe('seed_chart_of_accounts', () => { + it('seeds 26xx VAT accounts with BAS-correct labels', async () => { + const { companyId } = await seedCompany() + const pool = getPool() + + await pool.query(`SELECT public.seed_chart_of_accounts($1::uuid, $2::text)`, [ + companyId, + 'aktiebolag', + ]) + + const { rows } = await pool.query<{ account_number: string; account_name: string }>( + `SELECT account_number, account_name + FROM public.chart_of_accounts + WHERE company_id = $1 + AND account_number IN ('2611', '2621', '2631') + ORDER BY account_number`, + [companyId], + ) + + const byNumber = Object.fromEntries(rows.map((r) => [r.account_number, r.account_name])) + + expect(byNumber['2611']).toBeDefined() + expect(byNumber['2611']).toMatch(/25\s*%/) + expect(byNumber['2611']).toMatch(/[Uu]tg.*moms/) + + expect(byNumber['2621']).toBeDefined() + expect(byNumber['2621']).toMatch(/12\s*%/) + + expect(byNumber['2631']).toBeDefined() + expect(byNumber['2631']).toMatch(/6\s*%/) + }) + + it('seeds the input VAT account 2641', async () => { + const { companyId } = await seedCompany() + const pool = getPool() + + await pool.query(`SELECT public.seed_chart_of_accounts($1::uuid, $2::text)`, [ + companyId, + 'aktiebolag', + ]) + + const { rows } = await pool.query<{ account_name: string }>( + `SELECT account_name FROM public.chart_of_accounts + WHERE company_id = $1 AND account_number = '2641'`, + [companyId], + ) + + expect(rows).toHaveLength(1) + expect(rows[0].account_name).toMatch(/[Ii]ng.*moms/) + }) +}) diff --git a/tests/helpers.ts b/tests/helpers.ts index 6f1ceedf..d0df1376 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -531,6 +531,7 @@ export function makeCompanySettings( invoice_show_bankgiro: true, invoice_show_plusgiro: true, invoice_show_logo: true, + invoice_show_company_name: true, invoice_late_fee_text: null, invoice_credit_terms_text: null, logo_url: null, diff --git a/types/index.ts b/types/index.ts index 8c2ad43b..511bfc4d 100644 --- a/types/index.ts +++ b/types/index.ts @@ -240,6 +240,7 @@ export interface CompanySettings { invoice_show_bankgiro: boolean invoice_show_plusgiro: boolean invoice_show_logo: boolean + invoice_show_company_name: boolean invoice_late_fee_text: string | null invoice_credit_terms_text: string | null