Files
accounted/supabase/migrations/20240101000025_supplier_invoices.sql
Jakob Wennberg 0c422fcd25 chore: clean up migration files and update CLAUDE.md documentation
- Consolidate migration numbering (move full_bas_2026 to slot 044)
- Remove dead/superseded migrations (document_matching, reversal columns)
- Update CLAUDE.md with placeholder migration notes and corrected descriptions
- Fix migration SQL for invoice_inbox, extension_data, supplier_invoices

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 20:53:57 +01:00

350 lines
12 KiB
PL/PgSQL

-- Migration 25: Leverantorsreskontra (Accounts Payable)
-- Suppliers, supplier invoices, line items, partial payments
-- BFL 5:6-9 verifikationskrav, ML 17:24 inkommande fakturor
-- =============================================================================
-- 1. suppliers table
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.suppliers (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
-- Basic info
name text NOT NULL,
supplier_type text NOT NULL DEFAULT 'swedish_business'
CHECK (supplier_type IN ('swedish_business', 'eu_business', 'non_eu_business')),
org_number text,
vat_number text,
-- Contact
email text,
phone text,
-- Address
address_line1 text,
address_line2 text,
postal_code text,
city text,
country text DEFAULT 'SE',
-- Payment details
bankgiro text,
plusgiro text,
bank_account text,
iban text,
bic text,
clearing_number text,
account_number text,
-- Defaults
default_expense_account text, -- e.g. '5010'
default_payment_terms integer DEFAULT 30,
default_currency text NOT NULL DEFAULT 'SEK',
-- Classification
category text,
is_active boolean DEFAULT true,
-- Notes
notes text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.suppliers ENABLE ROW LEVEL SECURITY;
CREATE POLICY "suppliers_select"
ON public.suppliers FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "suppliers_insert"
ON public.suppliers FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "suppliers_update"
ON public.suppliers FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "suppliers_delete"
ON public.suppliers FOR DELETE
USING (auth.uid() = user_id);
CREATE INDEX idx_suppliers_user_id ON public.suppliers (user_id);
CREATE INDEX idx_suppliers_name ON public.suppliers (user_id, name);
CREATE INDEX idx_suppliers_is_active ON public.suppliers (user_id, is_active);
-- =============================================================================
-- 2. supplier_invoices table
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.supplier_invoices (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
supplier_id uuid NOT NULL REFERENCES public.suppliers(id) ON DELETE RESTRICT,
-- Ankomstnummer (BFL running number for incoming invoices)
arrival_number integer NOT NULL,
-- Supplier's reference
supplier_invoice_number text NOT NULL,
-- Dates (ML 17:24 requirements)
invoice_date date NOT NULL,
due_date date NOT NULL,
received_date date NOT NULL DEFAULT CURRENT_DATE,
delivery_date date, -- ML krav
-- Status
status text NOT NULL DEFAULT 'registered'
CHECK (status IN ('registered', 'approved', 'paid', 'partially_paid', 'overdue', 'disputed', 'credited')),
-- Currency
currency text NOT NULL DEFAULT 'SEK',
exchange_rate numeric,
exchange_rate_date date,
-- Amounts
subtotal numeric NOT NULL DEFAULT 0,
subtotal_sek numeric,
vat_amount numeric NOT NULL DEFAULT 0,
vat_amount_sek numeric,
total numeric NOT NULL DEFAULT 0,
total_sek numeric,
-- VAT
vat_treatment text NOT NULL DEFAULT 'standard_25',
reverse_charge boolean NOT NULL DEFAULT false,
-- Payment
payment_reference text, -- OCR-nummer
paid_at timestamptz,
paid_amount numeric NOT NULL DEFAULT 0,
remaining_amount numeric NOT NULL DEFAULT 0,
-- Credit note
is_credit_note boolean NOT NULL DEFAULT false,
credited_invoice_id uuid REFERENCES public.supplier_invoices(id) ON DELETE SET NULL,
-- Bookkeeping links
registration_journal_entry_id uuid REFERENCES public.journal_entries(id) ON DELETE SET NULL,
payment_journal_entry_id uuid REFERENCES public.journal_entries(id) ON DELETE SET NULL,
-- Transaction link
transaction_id uuid REFERENCES public.transactions(id) ON DELETE SET NULL,
-- Document (verifikationsunderlag)
document_id uuid REFERENCES public.document_attachments(id) ON DELETE SET NULL,
-- Notes
notes text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
-- Constraints
CONSTRAINT uq_supplier_invoices_arrival UNIQUE (user_id, arrival_number),
CONSTRAINT uq_supplier_invoices_ref UNIQUE (user_id, supplier_id, supplier_invoice_number)
);
ALTER TABLE public.supplier_invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own supplier invoices"
ON public.supplier_invoices FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own supplier invoices"
ON public.supplier_invoices FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own supplier invoices"
ON public.supplier_invoices FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own supplier invoices"
ON public.supplier_invoices FOR DELETE
USING (auth.uid() = user_id);
CREATE INDEX idx_supplier_invoices_user_id ON public.supplier_invoices (user_id);
CREATE INDEX idx_supplier_invoices_supplier ON public.supplier_invoices (supplier_id);
CREATE INDEX idx_supplier_invoices_status ON public.supplier_invoices (user_id, status);
CREATE INDEX idx_supplier_invoices_due_date ON public.supplier_invoices (user_id, due_date);
-- =============================================================================
-- 3. Per-user arrival number generation (ankomstnummer)
-- =============================================================================
CREATE OR REPLACE FUNCTION public.get_next_arrival_number(p_user_id uuid)
RETURNS integer AS $$
DECLARE
next_num integer;
BEGIN
SELECT COALESCE(MAX(arrival_number), 0) + 1 INTO next_num
FROM public.supplier_invoices
WHERE user_id = p_user_id;
RETURN next_num;
END;
$$ LANGUAGE plpgsql;
-- =============================================================================
-- 4. supplier_invoice_items table
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.supplier_invoice_items (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
supplier_invoice_id uuid NOT NULL REFERENCES public.supplier_invoices(id) ON DELETE CASCADE,
sort_order integer NOT NULL DEFAULT 0,
description text NOT NULL,
quantity numeric NOT NULL DEFAULT 1,
unit text NOT NULL DEFAULT 'st',
unit_price numeric NOT NULL DEFAULT 0, -- exkl. moms (ML krav)
line_total numeric NOT NULL DEFAULT 0,
-- Bookkeeping
account_number text NOT NULL, -- BAS expense account (4xxx-6xxx)
vat_code text,
vat_rate numeric NOT NULL DEFAULT 0.25,
vat_amount numeric NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.supplier_invoice_items ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own supplier invoice items"
ON public.supplier_invoice_items FOR SELECT
USING (
EXISTS (
SELECT 1 FROM public.supplier_invoices si
WHERE si.id = supplier_invoice_id AND si.user_id = auth.uid()
)
);
CREATE POLICY "Users can insert own supplier invoice items"
ON public.supplier_invoice_items FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM public.supplier_invoices si
WHERE si.id = supplier_invoice_id AND si.user_id = auth.uid()
)
);
CREATE POLICY "Users can update own supplier invoice items"
ON public.supplier_invoice_items FOR UPDATE
USING (
EXISTS (
SELECT 1 FROM public.supplier_invoices si
WHERE si.id = supplier_invoice_id AND si.user_id = auth.uid()
)
);
CREATE POLICY "Users can delete own supplier invoice items"
ON public.supplier_invoice_items FOR DELETE
USING (
EXISTS (
SELECT 1 FROM public.supplier_invoices si
WHERE si.id = supplier_invoice_id AND si.user_id = auth.uid()
)
);
CREATE INDEX idx_supplier_invoice_items_invoice ON public.supplier_invoice_items (supplier_invoice_id);
-- =============================================================================
-- 5. supplier_invoice_payments table (partial payments)
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.supplier_invoice_payments (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
supplier_invoice_id uuid NOT NULL REFERENCES public.supplier_invoices(id) ON DELETE CASCADE,
payment_date date NOT NULL,
amount numeric NOT NULL,
currency text NOT NULL DEFAULT 'SEK',
exchange_rate numeric,
exchange_rate_difference numeric DEFAULT 0, -- kursdifferens
journal_entry_id uuid REFERENCES public.journal_entries(id) ON DELETE SET NULL,
transaction_id uuid REFERENCES public.transactions(id) ON DELETE SET NULL,
notes text,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE public.supplier_invoice_payments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own supplier invoice payments"
ON public.supplier_invoice_payments FOR SELECT
USING (
EXISTS (
SELECT 1 FROM public.supplier_invoices si
WHERE si.id = supplier_invoice_id AND si.user_id = auth.uid()
)
);
CREATE POLICY "Users can insert own supplier invoice payments"
ON public.supplier_invoice_payments FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM public.supplier_invoices si
WHERE si.id = supplier_invoice_id AND si.user_id = auth.uid()
)
);
CREATE INDEX idx_supplier_invoice_payments_invoice ON public.supplier_invoice_payments (supplier_invoice_id);
-- =============================================================================
-- 6. Add supplier_invoice_id to transactions
-- =============================================================================
ALTER TABLE public.transactions
ADD COLUMN IF NOT EXISTS supplier_invoice_id uuid REFERENCES public.supplier_invoices(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_transactions_supplier_invoice ON public.transactions (supplier_invoice_id);
-- =============================================================================
-- 7. Expand journal_entries source_type constraint
-- =============================================================================
ALTER TABLE public.journal_entries
DROP CONSTRAINT IF EXISTS journal_entries_source_type_check;
ALTER TABLE public.journal_entries
ADD CONSTRAINT journal_entries_source_type_check
CHECK (source_type IN (
'manual', 'bank_transaction', 'invoice_created',
'invoice_paid', 'invoice_cash_payment', 'credit_note', 'salary_payment',
'opening_balance', 'year_end',
'storno', 'correction', 'import', 'system',
'supplier_invoice_registered', 'supplier_invoice_paid',
'supplier_invoice_cash_payment', 'supplier_credit_note'
));
-- =============================================================================
-- 8. Audit triggers for new tables
-- =============================================================================
CREATE TRIGGER audit_suppliers
AFTER INSERT OR UPDATE OR DELETE ON public.suppliers
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
CREATE TRIGGER audit_supplier_invoices
AFTER INSERT OR UPDATE OR DELETE ON public.supplier_invoices
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
CREATE TRIGGER audit_supplier_invoice_items
AFTER INSERT OR UPDATE OR DELETE ON public.supplier_invoice_items
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
-- =============================================================================
-- 9. Updated_at triggers
-- =============================================================================
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS trigger AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_updated_at_suppliers
BEFORE UPDATE ON public.suppliers
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER set_updated_at_supplier_invoices
BEFORE UPDATE ON public.supplier_invoices
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();