feat: booking template library with system templates and cross-company sharing (#235)
* feat: implement viewer role permissions for bank transaction imports and connections * feat: add booking_template_library table with 30 system templates Three-level scoping (system/team/company), RLS policies for read/write/delete, and pre-seeded templates for EU reverse charge, tax account, private transfers, salary, representation, year-end, VAT netting, and bank/finance scenarios. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add template library types, helpers, and tests - BookingTemplateLibrary/Line/Category types in types/index.ts - applyTemplate() converts template lines + amount into form lines - Category labels, scope helpers for UI display - 8 unit tests for amount calculation, VAT, rounding, and scope detection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add booking template CRUD, export, and import API routes - GET/POST/DELETE /api/settings/booking-templates (list, create, soft-delete) - PUT /api/settings/booking-templates/[id] (update non-system templates) - GET /api/settings/booking-templates/export (JSON download) - POST /api/settings/booking-templates/import (bulk import from JSON) All routes enforce auth, write permissions, and Zod validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add template picker UI and settings management panel - BookingTemplatePicker: dialog with search, category/entity-type filter, line preview, and amount input — integrated into JournalEntryForm - BookingTemplatesPanel: settings page with grouped templates (system/team/company), create dialog, export/import, soft-delete - Settings templates page now shows both booking and counterparty templates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update supabase/migrations/20260413160000_booking_template_library.sql Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
greptile-apps[bot]
parent
cd376e1cad
commit
bf36ebfd88
@@ -0,0 +1,386 @@
|
||||
-- =============================================================================
|
||||
-- Booking Template Library
|
||||
-- =============================================================================
|
||||
--
|
||||
-- Reusable journal entry templates (bokföringsmallar) for common scenarios
|
||||
-- like EU reverse charge purchases, tax account bookings, private transfers.
|
||||
--
|
||||
-- Three scoping levels:
|
||||
-- 1. System templates (company_id IS NULL, team_id IS NULL, is_system = TRUE)
|
||||
-- Pre-seeded, visible to all authenticated users. Read-only.
|
||||
-- 2. Team templates (company_id IS NULL, team_id IS NOT NULL)
|
||||
-- Shared across all companies in a team. Created by team admins.
|
||||
-- 3. Company templates (company_id IS NOT NULL)
|
||||
-- Private to one company. Created by any non-viewer member.
|
||||
--
|
||||
-- Template lines use the same ratio-based pattern as categorization_templates
|
||||
-- line_pattern: user enters total amount, system calculates each line.
|
||||
|
||||
CREATE TABLE public.booking_template_library (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
company_id UUID REFERENCES public.companies(id) ON DELETE CASCADE,
|
||||
team_id UUID REFERENCES public.teams(id) ON DELETE CASCADE,
|
||||
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
|
||||
-- Template identity
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
category TEXT NOT NULL DEFAULT 'other'
|
||||
CHECK (category IN (
|
||||
'eu_trade', 'tax_account', 'private_transfer',
|
||||
'salary', 'representation', 'year_end',
|
||||
'vat', 'financial', 'other'
|
||||
)),
|
||||
entity_type TEXT NOT NULL DEFAULT 'all'
|
||||
CHECK (entity_type IN ('all', 'enskild_firma', 'aktiebolag')),
|
||||
|
||||
-- Template lines (ratio-based pattern)
|
||||
-- Array of: { account, label, side, type, ratio?, vat_rate? }
|
||||
lines JSONB NOT NULL DEFAULT '[]',
|
||||
|
||||
-- Flags
|
||||
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
-- Metadata
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
-- Constraints
|
||||
-- System templates: no company, no team
|
||||
CHECK (NOT is_system OR (company_id IS NULL AND team_id IS NULL)),
|
||||
-- Team templates: no company
|
||||
CHECK (team_id IS NULL OR company_id IS NULL),
|
||||
-- Company or team or system — at least one scope
|
||||
CHECK (company_id IS NOT NULL OR team_id IS NOT NULL OR is_system)
|
||||
);
|
||||
|
||||
-- RLS
|
||||
ALTER TABLE public.booking_template_library ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- SELECT: system templates + own company + own team templates
|
||||
CREATE POLICY "btl_select" ON public.booking_template_library
|
||||
FOR SELECT USING (
|
||||
is_system
|
||||
OR company_id IN (SELECT public.user_company_ids())
|
||||
OR team_id IN (SELECT public.user_team_ids())
|
||||
);
|
||||
|
||||
-- INSERT: company templates (non-viewers) or team templates (team members)
|
||||
CREATE POLICY "btl_insert" ON public.booking_template_library
|
||||
FOR INSERT WITH CHECK (
|
||||
NOT is_system
|
||||
AND public.current_user_can_write()
|
||||
AND (
|
||||
company_id = public.current_active_company_id()
|
||||
OR (company_id IS NULL AND team_id IN (SELECT public.user_team_ids()))
|
||||
)
|
||||
);
|
||||
|
||||
-- UPDATE: own company or own team templates (non-viewers), never system
|
||||
CREATE POLICY "btl_update" ON public.booking_template_library
|
||||
FOR UPDATE USING (
|
||||
NOT is_system
|
||||
AND public.current_user_can_write()
|
||||
AND (
|
||||
company_id IN (SELECT public.user_company_ids())
|
||||
OR (company_id IS NULL AND team_id IN (SELECT public.user_team_ids()))
|
||||
)
|
||||
);
|
||||
|
||||
-- DELETE: own company or own team templates (non-viewers), never system
|
||||
CREATE POLICY "btl_delete" ON public.booking_template_library
|
||||
FOR DELETE USING (
|
||||
NOT is_system
|
||||
AND public.current_user_can_write()
|
||||
AND (
|
||||
company_id IN (SELECT public.user_company_ids())
|
||||
OR (company_id IS NULL AND team_id IN (SELECT public.user_team_ids()))
|
||||
)
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_btl_company ON public.booking_template_library (company_id)
|
||||
WHERE company_id IS NOT NULL;
|
||||
CREATE INDEX idx_btl_team ON public.booking_template_library (team_id)
|
||||
WHERE team_id IS NOT NULL;
|
||||
CREATE INDEX idx_btl_system ON public.booking_template_library (is_system)
|
||||
WHERE is_system = TRUE;
|
||||
CREATE INDEX idx_btl_category ON public.booking_template_library (category);
|
||||
CREATE INDEX idx_btl_active ON public.booking_template_library (is_active)
|
||||
WHERE is_active = TRUE;
|
||||
|
||||
-- updated_at trigger
|
||||
CREATE TRIGGER btl_updated_at
|
||||
BEFORE UPDATE ON public.booking_template_library
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
||||
|
||||
-- =============================================================================
|
||||
-- Seed system templates
|
||||
-- =============================================================================
|
||||
-- Lines format: [{ account, label, side, type, ratio?, vat_rate? }]
|
||||
-- type: 'business' (main account), 'vat' (VAT line), 'settlement' (bank/cash)
|
||||
-- ratio: proportion of total amount (business lines should sum to 1.0)
|
||||
-- vat_rate: decimal (0.25, 0.12, 0.06) — applied via rate/(1+rate) for inclusive
|
||||
|
||||
-- EU TRADE
|
||||
INSERT INTO public.booking_template_library (name, description, category, entity_type, is_system, lines) VALUES
|
||||
(
|
||||
'Inköp EU-varor, omvänd moms 25%',
|
||||
'Köp av varor från annat EU-land. Omvänd skattskyldighet — du redovisar både utgående och ingående moms.',
|
||||
'eu_trade', 'all', TRUE,
|
||||
'[
|
||||
{"account": "4010", "label": "Varuinköp", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "2614", "label": "Utgående moms omvänd skattskyldighet 25%", "side": "credit", "type": "vat", "vat_rate": 0.25},
|
||||
{"account": "2645", "label": "Beräknad ingående moms 25%", "side": "debit", "type": "vat", "vat_rate": 0.25},
|
||||
{"account": "1930", "label": "Företagskonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Inköp EU-tjänster, omvänd moms 25%',
|
||||
'Köp av tjänster från annat EU-land. Omvänd skattskyldighet — du redovisar både utgående och ingående moms.',
|
||||
'eu_trade', 'all', TRUE,
|
||||
'[
|
||||
{"account": "6540", "label": "IT-tjänster", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "2614", "label": "Utgående moms omvänd skattskyldighet 25%", "side": "credit", "type": "vat", "vat_rate": 0.25},
|
||||
{"account": "2645", "label": "Beräknad ingående moms 25%", "side": "debit", "type": "vat", "vat_rate": 0.25},
|
||||
{"account": "1930", "label": "Företagskonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Försäljning EU-tjänster (B2B)',
|
||||
'Tjänsteförsäljning till annat EU-land (B2B). Ingen moms — kunden redovisar omvänd skattskyldighet.',
|
||||
'eu_trade', 'all', TRUE,
|
||||
'[
|
||||
{"account": "1510", "label": "Kundfordringar", "side": "debit", "type": "settlement", "ratio": 1.0},
|
||||
{"account": "3308", "label": "Försäljning tjänster EU", "side": "credit", "type": "business", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Försäljning export (utanför EU)',
|
||||
'Försäljning till land utanför EU. Momsfritt.',
|
||||
'eu_trade', 'all', TRUE,
|
||||
'[
|
||||
{"account": "1510", "label": "Kundfordringar", "side": "debit", "type": "settlement", "ratio": 1.0},
|
||||
{"account": "3305", "label": "Försäljning export", "side": "credit", "type": "business", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
);
|
||||
|
||||
-- TAX ACCOUNT
|
||||
INSERT INTO public.booking_template_library (name, description, category, entity_type, is_system, lines) VALUES
|
||||
(
|
||||
'Insättning skattekonto',
|
||||
'Betalning från företagskonto till skattekontot hos Skatteverket.',
|
||||
'tax_account', 'all', TRUE,
|
||||
'[
|
||||
{"account": "1630", "label": "Skattekonto", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "1930", "label": "Företagskonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Skatteåterbäring',
|
||||
'Återbetalning från skattekontot till företagskonto.',
|
||||
'tax_account', 'all', TRUE,
|
||||
'[
|
||||
{"account": "1930", "label": "Företagskonto", "side": "debit", "type": "settlement", "ratio": 1.0},
|
||||
{"account": "1630", "label": "Skattekonto", "side": "credit", "type": "business", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Preliminär F-skatt (EF)',
|
||||
'Betalning av preliminär F-skatt från skattekontot (enskild firma).',
|
||||
'tax_account', 'enskild_firma', TRUE,
|
||||
'[
|
||||
{"account": "2012", "label": "Egna skatter", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "1630", "label": "Skattekonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Preliminär F-skatt (AB)',
|
||||
'Betalning av preliminär bolagsskatt från skattekontot.',
|
||||
'tax_account', 'aktiebolag', TRUE,
|
||||
'[
|
||||
{"account": "2518", "label": "Betald F-skatt", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "1630", "label": "Skattekonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Momsbetalning via skattekonto',
|
||||
'Moms som dras från skattekontot efter momsdeklaration.',
|
||||
'tax_account', 'all', TRUE,
|
||||
'[
|
||||
{"account": "2650", "label": "Redovisningskonto moms", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "1630", "label": "Skattekonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Arbetsgivaravgifter via skattekonto',
|
||||
'Arbetsgivaravgifter som dras från skattekontot.',
|
||||
'tax_account', 'all', TRUE,
|
||||
'[
|
||||
{"account": "2731", "label": "Avräkning sociala avgifter", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "1630", "label": "Skattekonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
);
|
||||
|
||||
-- PRIVATE TRANSFERS
|
||||
INSERT INTO public.booking_template_library (name, description, category, entity_type, is_system, lines) VALUES
|
||||
(
|
||||
'Eget uttag',
|
||||
'Privat uttag från företagskontot (enskild firma).',
|
||||
'private_transfer', 'enskild_firma', TRUE,
|
||||
'[
|
||||
{"account": "2013", "label": "Egna uttag", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "1930", "label": "Företagskonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Eget insättning',
|
||||
'Privat insättning till företagskontot (enskild firma).',
|
||||
'private_transfer', 'enskild_firma', TRUE,
|
||||
'[
|
||||
{"account": "1930", "label": "Företagskonto", "side": "debit", "type": "settlement", "ratio": 1.0},
|
||||
{"account": "2018", "label": "Egna insättningar", "side": "credit", "type": "business", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Aktieägarlån — insättning',
|
||||
'Ägaren sätter in pengar som lån till bolaget.',
|
||||
'private_transfer', 'aktiebolag', TRUE,
|
||||
'[
|
||||
{"account": "1930", "label": "Företagskonto", "side": "debit", "type": "settlement", "ratio": 1.0},
|
||||
{"account": "2893", "label": "Skuld till aktieägare", "side": "credit", "type": "business", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Aktieägarlån — återbetalning',
|
||||
'Bolaget betalar tillbaka lån till ägaren.',
|
||||
'private_transfer', 'aktiebolag', TRUE,
|
||||
'[
|
||||
{"account": "2893", "label": "Skuld till aktieägare", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "1930", "label": "Företagskonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Utdelning till aktieägare',
|
||||
'Utbetalning av beslutad utdelning till aktieägare.',
|
||||
'private_transfer', 'aktiebolag', TRUE,
|
||||
'[
|
||||
{"account": "2898", "label": "Outtagen utdelning", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "1930", "label": "Företagskonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
);
|
||||
|
||||
-- SALARY
|
||||
INSERT INTO public.booking_template_library (name, description, category, entity_type, is_system, lines) VALUES
|
||||
(
|
||||
'Löneutbetalning',
|
||||
'Utbetalning av nettolön till anställd.',
|
||||
'salary', 'aktiebolag', TRUE,
|
||||
'[
|
||||
{"account": "2710", "label": "Personalskatt", "side": "debit", "type": "business", "ratio": 0.3},
|
||||
{"account": "2920", "label": "Upplupna semesterlöner", "side": "debit", "type": "business", "ratio": 0.12},
|
||||
{"account": "7010", "label": "Löner", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "1930", "label": "Företagskonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Arbetsgivaravgifter',
|
||||
'Bokföring av arbetsgivaravgifter (31,42% av bruttolön).',
|
||||
'salary', 'aktiebolag', TRUE,
|
||||
'[
|
||||
{"account": "7510", "label": "Arbetsgivaravgifter", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "2731", "label": "Avräkning sociala avgifter", "side": "credit", "type": "business", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
);
|
||||
|
||||
'[
|
||||
{"account": "6072", "label": "Representation avdragsgill", "side": "debit", "type": "business", "ratio": 0.8},
|
||||
{"account": "2641", "label": "Ingående moms", "side": "debit", "type": "vat", "vat_rate": 0.25},
|
||||
{"account": "1930", "label": "Företagskonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
|
||||
-- YEAR-END / FINANCIAL
|
||||
INSERT INTO public.booking_template_library (name, description, category, entity_type, is_system, lines) VALUES
|
||||
(
|
||||
'Periodiseringsfond avsättning (AB)',
|
||||
'Avsättning till periodiseringsfond vid bokslut. Max 25% av överskottet.',
|
||||
'year_end', 'aktiebolag', TRUE,
|
||||
'[
|
||||
{"account": "8811", "label": "Avsättning periodiseringsfond", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "2113", "label": "Periodiseringsfond", "side": "credit", "type": "business", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Periodiseringsfond återföring (AB)',
|
||||
'Återföring av periodiseringsfond (senast efter 6 år).',
|
||||
'year_end', 'aktiebolag', TRUE,
|
||||
'[
|
||||
{"account": "2113", "label": "Periodiseringsfond", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "8819", "label": "Återföring periodiseringsfond", "side": "credit", "type": "business", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Beräknad bolagsskatt',
|
||||
'Bokföring av beräknad inkomstskatt vid bokslut.',
|
||||
'year_end', 'aktiebolag', TRUE,
|
||||
'[
|
||||
{"account": "8910", "label": "Skatt på årets resultat", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "2512", "label": "Beräknad inkomstskatt", "side": "credit", "type": "business", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Överavskrivning inventarier',
|
||||
'Bokföring av överavskrivning (skillnad räkenskapsenlig vs planenlig).',
|
||||
'year_end', 'aktiebolag', TRUE,
|
||||
'[
|
||||
{"account": "8850", "label": "Förändring överavskrivning", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "2150", "label": "Ackumulerade överavskrivningar", "side": "credit", "type": "business", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
);
|
||||
|
||||
-- VAT
|
||||
INSERT INTO public.booking_template_library (name, description, category, entity_type, is_system, lines) VALUES
|
||||
(
|
||||
'Momsredovisning (nettning)',
|
||||
'Nettning av momskonton vid momsdeklaration. Justera konton och belopp efter din deklaration.',
|
||||
'vat', 'all', TRUE,
|
||||
'[
|
||||
{"account": "2611", "label": "Utgående moms 25%", "side": "debit", "type": "business", "ratio": 0.5},
|
||||
{"account": "2641", "label": "Ingående moms", "side": "credit", "type": "business", "ratio": 0.3},
|
||||
{"account": "2650", "label": "Redovisningskonto moms", "side": "credit", "type": "business", "ratio": 0.2}
|
||||
]'::jsonb
|
||||
);
|
||||
|
||||
-- FINANCIAL
|
||||
INSERT INTO public.booking_template_library (name, description, category, entity_type, is_system, lines) VALUES
|
||||
(
|
||||
'Bankavgift',
|
||||
'Månadsavgift eller transaktionsavgift från banken.',
|
||||
'financial', 'all', TRUE,
|
||||
'[
|
||||
{"account": "6570", "label": "Bankkostnader", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "1930", "label": "Företagskonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Ränteintäkt',
|
||||
'Ränta från sparkonto eller bank.',
|
||||
'financial', 'all', TRUE,
|
||||
'[
|
||||
{"account": "1930", "label": "Företagskonto", "side": "debit", "type": "settlement", "ratio": 1.0},
|
||||
{"account": "8311", "label": "Ränteintäkter", "side": "credit", "type": "business", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
),
|
||||
(
|
||||
'Räntekostnad',
|
||||
'Ränta på lån eller kredit.',
|
||||
'financial', 'all', TRUE,
|
||||
'[
|
||||
{"account": "8410", "label": "Räntekostnader", "side": "debit", "type": "business", "ratio": 1.0},
|
||||
{"account": "1930", "label": "Företagskonto", "side": "credit", "type": "settlement", "ratio": 1.0}
|
||||
]'::jsonb
|
||||
);
|
||||
|
||||
-- Schema reload for PostgREST
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user