* fix: make new user checklist mobile responsive Adjust spacing, padding, typography, and indentation for small screens so the onboarding welcome screen works well on mobile devices. * fix: make expenses and bookkeeping verification views mobile responsive - Expenses/new: stack form grids on mobile, replace line items table with card layout, full-width action buttons - Expenses/[id]: stack header and actions, card layout for line items and payments, single-column info grid on mobile - JournalEntryList: unify verification lines into card layout for all screen sizes, hide redundant line descriptions that duplicate account name or entry description, stack filter bar and action buttons on mobile * fix: remove stray )); in provider_consents migration Syntax error on line 35 caused MIGRATIONS_FAILED on the Supabase staging branch. * fix: enhance mobile responsiveness for invoices page and update dialog styles
540 lines
20 KiB
PL/PgSQL
540 lines
20 KiB
PL/PgSQL
-- Sync schema: add all columns and tables that were applied directly to production
|
|
-- but never captured in migration files. Uses IF NOT EXISTS throughout for idempotency.
|
|
|
|
-- =============================================================================
|
|
-- Missing columns on existing tables
|
|
-- =============================================================================
|
|
|
|
ALTER TABLE public.calendar_feeds
|
|
ADD COLUMN IF NOT EXISTS token_version integer DEFAULT 1;
|
|
|
|
ALTER TABLE public.cost_centers
|
|
ADD COLUMN IF NOT EXISTS description text,
|
|
ADD COLUMN IF NOT EXISTS manager_name text,
|
|
ADD COLUMN IF NOT EXISTS parent_id uuid,
|
|
ADD COLUMN IF NOT EXISTS sort_order integer DEFAULT 0;
|
|
|
|
ALTER TABLE public.invoice_inbox_items
|
|
ADD COLUMN IF NOT EXISTS raw_llm_response jsonb;
|
|
|
|
ALTER TABLE public.invoice_items
|
|
ADD COLUMN IF NOT EXISTS vat_amount numeric NOT NULL DEFAULT 0,
|
|
ADD COLUMN IF NOT EXISTS vat_rate numeric NOT NULL DEFAULT 25;
|
|
|
|
ALTER TABLE public.invoices
|
|
ADD COLUMN IF NOT EXISTS bankgiro_number text,
|
|
ADD COLUMN IF NOT EXISTS is_recurring boolean DEFAULT false,
|
|
ADD COLUMN IF NOT EXISTS ocr_number text,
|
|
ADD COLUMN IF NOT EXISTS payment_type text,
|
|
ADD COLUMN IF NOT EXISTS plusgiro_number text,
|
|
ADD COLUMN IF NOT EXISTS recurring_invoice_id uuid;
|
|
|
|
ALTER TABLE public.journal_entry_lines
|
|
ADD COLUMN IF NOT EXISTS cost_center_id uuid,
|
|
ADD COLUMN IF NOT EXISTS project_id uuid;
|
|
|
|
ALTER TABLE public.projects
|
|
ADD COLUMN IF NOT EXISTS budget_amount numeric DEFAULT 0,
|
|
ADD COLUMN IF NOT EXISTS customer_id uuid,
|
|
ADD COLUMN IF NOT EXISTS description text,
|
|
ADD COLUMN IF NOT EXISTS project_number text,
|
|
ADD COLUMN IF NOT EXISTS status text DEFAULT 'planning';
|
|
|
|
ALTER TABLE public.receipts
|
|
ADD COLUMN IF NOT EXISTS email_from text,
|
|
ADD COLUMN IF NOT EXISTS representation_business_connection text,
|
|
ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'upload';
|
|
|
|
-- =============================================================================
|
|
-- Missing tables
|
|
-- =============================================================================
|
|
|
|
-- voucher_gap_explanations (BFNAR 2013:2 compliance)
|
|
CREATE TABLE IF NOT EXISTS public.voucher_gap_explanations (
|
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
company_id uuid NOT NULL REFERENCES public.companies ON DELETE CASCADE,
|
|
user_id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
|
|
fiscal_period_id uuid NOT NULL REFERENCES public.fiscal_periods ON DELETE CASCADE,
|
|
voucher_series text NOT NULL DEFAULT 'A',
|
|
gap_start integer NOT NULL,
|
|
gap_end integer NOT NULL,
|
|
explanation text NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
ALTER TABLE public.voucher_gap_explanations ENABLE ROW LEVEL SECURITY;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "voucher_gap_explanations_select" ON public.voucher_gap_explanations
|
|
FOR SELECT USING (company_id IN (SELECT user_company_ids()));
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "voucher_gap_explanations_insert" ON public.voucher_gap_explanations
|
|
FOR INSERT WITH CHECK (company_id IN (SELECT user_company_ids()));
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "voucher_gap_explanations_update" ON public.voucher_gap_explanations
|
|
FOR UPDATE USING (company_id IN (SELECT user_company_ids()));
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "voucher_gap_explanations_delete" ON public.voucher_gap_explanations
|
|
FOR DELETE USING (company_id IN (SELECT user_company_ids()));
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DROP TRIGGER IF EXISTS voucher_gap_explanations_updated_at ON public.voucher_gap_explanations;
|
|
CREATE TRIGGER voucher_gap_explanations_updated_at
|
|
BEFORE UPDATE ON public.voucher_gap_explanations
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- automation_webhooks
|
|
CREATE TABLE IF NOT EXISTS public.automation_webhooks (
|
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
company_id uuid NOT NULL REFERENCES public.companies ON DELETE CASCADE,
|
|
event_type text NOT NULL,
|
|
webhook_url text NOT NULL,
|
|
active boolean NOT NULL DEFAULT true,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
ALTER TABLE public.automation_webhooks ENABLE ROW LEVEL SECURITY;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "automation_webhooks_select" ON public.automation_webhooks
|
|
FOR SELECT USING (company_id IN (SELECT user_company_ids()));
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "automation_webhooks_insert" ON public.automation_webhooks
|
|
FOR INSERT WITH CHECK (company_id IN (SELECT user_company_ids()));
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "automation_webhooks_update" ON public.automation_webhooks
|
|
FOR UPDATE USING (company_id IN (SELECT user_company_ids()));
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "automation_webhooks_delete" ON public.automation_webhooks
|
|
FOR DELETE USING (company_id IN (SELECT user_company_ids()));
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DROP TRIGGER IF EXISTS automation_webhooks_updated_at ON public.automation_webhooks;
|
|
CREATE TRIGGER automation_webhooks_updated_at
|
|
BEFORE UPDATE ON public.automation_webhooks
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- bankid_identities
|
|
CREATE TABLE IF NOT EXISTS public.bankid_identities (
|
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
|
|
personal_number_hash text NOT NULL,
|
|
personal_number_enc bytea NOT NULL,
|
|
given_name text,
|
|
surname text,
|
|
linked_at timestamptz NOT NULL DEFAULT now(),
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
ALTER TABLE public.bankid_identities ENABLE ROW LEVEL SECURITY;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "bankid_identities_select" ON public.bankid_identities
|
|
FOR SELECT USING (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "bankid_identities_insert" ON public.bankid_identities
|
|
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "bankid_identities_update" ON public.bankid_identities
|
|
FOR UPDATE USING (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DROP TRIGGER IF EXISTS bankid_identities_updated_at ON public.bankid_identities;
|
|
CREATE TRIGGER bankid_identities_updated_at
|
|
BEFORE UPDATE ON public.bankid_identities
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- provider_connections
|
|
CREATE TABLE IF NOT EXISTS public.provider_connections (
|
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
|
|
provider text NOT NULL,
|
|
status text NOT NULL DEFAULT 'pending',
|
|
provider_company_name text,
|
|
error_message text,
|
|
connected_at timestamptz,
|
|
last_synced_at timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
ALTER TABLE public.provider_connections ENABLE ROW LEVEL SECURITY;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "provider_connections_select" ON public.provider_connections
|
|
FOR SELECT USING (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "provider_connections_insert" ON public.provider_connections
|
|
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "provider_connections_update" ON public.provider_connections
|
|
FOR UPDATE USING (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "provider_connections_delete" ON public.provider_connections
|
|
FOR DELETE USING (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DROP TRIGGER IF EXISTS provider_connections_updated_at ON public.provider_connections;
|
|
CREATE TRIGGER provider_connections_updated_at
|
|
BEFORE UPDATE ON public.provider_connections
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- provider_connection_tokens
|
|
CREATE TABLE IF NOT EXISTS public.provider_connection_tokens (
|
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
connection_id uuid NOT NULL REFERENCES public.provider_connections ON DELETE CASCADE,
|
|
access_token text NOT NULL,
|
|
refresh_token text,
|
|
token_expires_at timestamptz,
|
|
provider_company_id text,
|
|
extra_data jsonb DEFAULT '{}'::jsonb,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
ALTER TABLE public.provider_connection_tokens ENABLE ROW LEVEL SECURITY;
|
|
DROP TRIGGER IF EXISTS provider_connection_tokens_updated_at ON public.provider_connection_tokens;
|
|
CREATE TRIGGER provider_connection_tokens_updated_at
|
|
BEFORE UPDATE ON public.provider_connection_tokens
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- provider_oauth_states
|
|
CREATE TABLE IF NOT EXISTS public.provider_oauth_states (
|
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
|
|
provider text NOT NULL,
|
|
csrf_token text NOT NULL,
|
|
connection_id uuid NOT NULL REFERENCES public.provider_connections ON DELETE CASCADE,
|
|
expires_at timestamptz NOT NULL DEFAULT (now() + interval '10 minutes'),
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
ALTER TABLE public.provider_oauth_states ENABLE ROW LEVEL SECURITY;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "provider_oauth_states_select" ON public.provider_oauth_states
|
|
FOR SELECT USING (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "provider_oauth_states_insert" ON public.provider_oauth_states
|
|
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "provider_oauth_states_delete" ON public.provider_oauth_states
|
|
FOR DELETE USING (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
|
|
-- provider_sync_data
|
|
CREATE TABLE IF NOT EXISTS public.provider_sync_data (
|
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
|
|
connection_id uuid NOT NULL REFERENCES public.provider_connections ON DELETE CASCADE,
|
|
provider text NOT NULL,
|
|
resource_type text NOT NULL,
|
|
data jsonb NOT NULL DEFAULT '[]'::jsonb,
|
|
record_count integer NOT NULL DEFAULT 0,
|
|
synced_at timestamptz NOT NULL DEFAULT now(),
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
ALTER TABLE public.provider_sync_data ENABLE ROW LEVEL SECURITY;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "provider_sync_data_select" ON public.provider_sync_data
|
|
FOR SELECT USING (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "provider_sync_data_insert" ON public.provider_sync_data
|
|
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "provider_sync_data_update" ON public.provider_sync_data
|
|
FOR UPDATE USING (auth.uid() = user_id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DROP TRIGGER IF EXISTS provider_sync_data_updated_at ON public.provider_sync_data;
|
|
CREATE TRIGGER provider_sync_data_updated_at
|
|
BEFORE UPDATE ON public.provider_sync_data
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- email_connections
|
|
CREATE TABLE IF NOT EXISTS public.email_connections (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
company_id uuid NOT NULL REFERENCES public.companies ON DELETE CASCADE,
|
|
user_id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
|
|
provider text NOT NULL DEFAULT 'gmail',
|
|
email_address text NOT NULL,
|
|
encrypted_token text NOT NULL,
|
|
last_sync_at timestamptz,
|
|
gmail_label_id text,
|
|
status text NOT NULL DEFAULT 'active',
|
|
error_message text,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
ALTER TABLE public.email_connections ENABLE ROW LEVEL SECURITY;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "email_connections_select" ON public.email_connections
|
|
FOR SELECT USING (company_id IN (SELECT user_company_ids()));
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "email_connections_insert" ON public.email_connections
|
|
FOR INSERT WITH CHECK (company_id IN (SELECT user_company_ids()));
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "email_connections_update" ON public.email_connections
|
|
FOR UPDATE USING (company_id IN (SELECT user_company_ids()));
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DO $$ BEGIN
|
|
CREATE POLICY "email_connections_delete" ON public.email_connections
|
|
FOR DELETE USING (company_id IN (SELECT user_company_ids()));
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$;
|
|
DROP TRIGGER IF EXISTS email_connections_updated_at ON public.email_connections;
|
|
CREATE TRIGGER email_connections_updated_at
|
|
BEFORE UPDATE ON public.email_connections
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- =============================================================================
|
|
-- Missing functions
|
|
-- =============================================================================
|
|
|
|
-- commit_journal_entry — atomic voucher assignment (critical for bookkeeping)
|
|
CREATE OR REPLACE FUNCTION public.commit_journal_entry(p_company_id uuid, p_entry_id uuid)
|
|
RETURNS TABLE(voucher_number integer)
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
AS $function$
|
|
DECLARE
|
|
v_next integer;
|
|
v_fiscal_period_id uuid;
|
|
v_series text;
|
|
BEGIN
|
|
SELECT je.fiscal_period_id, COALESCE(je.voucher_series, 'A')
|
|
INTO v_fiscal_period_id, v_series
|
|
FROM public.journal_entries je
|
|
WHERE je.id = p_entry_id
|
|
AND je.company_id = p_company_id
|
|
AND je.status = 'draft'
|
|
FOR UPDATE;
|
|
|
|
IF NOT FOUND THEN
|
|
RAISE EXCEPTION 'Draft journal entry not found: %', p_entry_id;
|
|
END IF;
|
|
|
|
INSERT INTO public.voucher_sequences (company_id, user_id, fiscal_period_id, voucher_series, last_number)
|
|
VALUES (p_company_id, auth.uid(), v_fiscal_period_id, v_series, 1)
|
|
ON CONFLICT (company_id, fiscal_period_id, voucher_series)
|
|
DO UPDATE SET
|
|
last_number = public.voucher_sequences.last_number + 1,
|
|
updated_at = now()
|
|
RETURNING last_number INTO v_next;
|
|
|
|
UPDATE public.journal_entries
|
|
SET voucher_number = v_next,
|
|
status = 'posted'
|
|
WHERE id = p_entry_id
|
|
AND company_id = p_company_id;
|
|
|
|
RETURN QUERY SELECT v_next;
|
|
END;
|
|
$function$;
|
|
|
|
-- release_voucher_range
|
|
CREATE OR REPLACE FUNCTION public.release_voucher_range(p_company_id uuid, p_fiscal_period_id uuid, p_series text, p_actual_last integer, p_reserved_highest integer)
|
|
RETURNS void
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path TO 'public'
|
|
AS $function$
|
|
BEGIN
|
|
UPDATE public.voucher_sequences
|
|
SET last_number = p_actual_last,
|
|
updated_at = now()
|
|
WHERE company_id = p_company_id
|
|
AND fiscal_period_id = p_fiscal_period_id
|
|
AND voucher_series = p_series
|
|
AND last_number > p_actual_last
|
|
AND last_number <= p_reserved_highest;
|
|
END;
|
|
$function$;
|
|
|
|
-- create_invoice_with_items
|
|
CREATE OR REPLACE FUNCTION public.create_invoice_with_items(p_invoice jsonb, p_items jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path TO 'public'
|
|
AS $function$
|
|
DECLARE
|
|
v_invoice_id uuid;
|
|
v_invoice_number integer;
|
|
v_result jsonb;
|
|
BEGIN
|
|
SELECT COALESCE(MAX(invoice_number::integer), 0) + 1
|
|
INTO v_invoice_number
|
|
FROM invoices
|
|
WHERE user_id = (p_invoice->>'user_id')::uuid;
|
|
|
|
INSERT INTO invoices (
|
|
user_id, customer_id, invoice_number, invoice_date, due_date,
|
|
status, currency, exchange_rate, exchange_rate_date,
|
|
subtotal, vat_amount, total,
|
|
subtotal_sek, vat_amount_sek, total_sek,
|
|
vat_treatment, vat_rate, moms_ruta,
|
|
your_reference, our_reference, notes,
|
|
reverse_charge_text
|
|
) VALUES (
|
|
(p_invoice->>'user_id')::uuid,
|
|
(p_invoice->>'customer_id')::uuid,
|
|
v_invoice_number::text,
|
|
(p_invoice->>'invoice_date')::date,
|
|
(p_invoice->>'due_date')::date,
|
|
COALESCE(p_invoice->>'status', 'draft'),
|
|
COALESCE(p_invoice->>'currency', 'SEK'),
|
|
(p_invoice->>'exchange_rate')::numeric,
|
|
(p_invoice->>'exchange_rate_date')::date,
|
|
(p_invoice->>'subtotal')::numeric,
|
|
(p_invoice->>'vat_amount')::numeric,
|
|
(p_invoice->>'total')::numeric,
|
|
(p_invoice->>'subtotal_sek')::numeric,
|
|
(p_invoice->>'vat_amount_sek')::numeric,
|
|
(p_invoice->>'total_sek')::numeric,
|
|
p_invoice->>'vat_treatment',
|
|
(p_invoice->>'vat_rate')::numeric,
|
|
p_invoice->>'moms_ruta',
|
|
p_invoice->>'your_reference',
|
|
p_invoice->>'our_reference',
|
|
p_invoice->>'notes',
|
|
p_invoice->>'reverse_charge_text'
|
|
) RETURNING id INTO v_invoice_id;
|
|
|
|
INSERT INTO invoice_items (invoice_id, sort_order, description, quantity, unit, unit_price, line_total)
|
|
SELECT
|
|
v_invoice_id,
|
|
(item->>'sort_order')::integer,
|
|
item->>'description',
|
|
(item->>'quantity')::numeric,
|
|
item->>'unit',
|
|
(item->>'unit_price')::numeric,
|
|
(item->>'line_total')::numeric
|
|
FROM jsonb_array_elements(p_items) AS item;
|
|
|
|
SELECT jsonb_build_object(
|
|
'id', i.id,
|
|
'invoice_number', i.invoice_number,
|
|
'invoice_date', i.invoice_date,
|
|
'due_date', i.due_date,
|
|
'status', i.status,
|
|
'currency', i.currency,
|
|
'exchange_rate', i.exchange_rate,
|
|
'subtotal', i.subtotal,
|
|
'vat_amount', i.vat_amount,
|
|
'total', i.total,
|
|
'subtotal_sek', i.subtotal_sek,
|
|
'vat_amount_sek', i.vat_amount_sek,
|
|
'total_sek', i.total_sek,
|
|
'vat_treatment', i.vat_treatment,
|
|
'vat_rate', i.vat_rate,
|
|
'moms_ruta', i.moms_ruta,
|
|
'your_reference', i.your_reference,
|
|
'our_reference', i.our_reference,
|
|
'notes', i.notes,
|
|
'reverse_charge_text', i.reverse_charge_text,
|
|
'customer', jsonb_build_object('id', c.id, 'name', c.name),
|
|
'items', (
|
|
SELECT jsonb_agg(jsonb_build_object(
|
|
'id', ii.id,
|
|
'sort_order', ii.sort_order,
|
|
'description', ii.description,
|
|
'quantity', ii.quantity,
|
|
'unit', ii.unit,
|
|
'unit_price', ii.unit_price,
|
|
'line_total', ii.line_total
|
|
) ORDER BY ii.sort_order)
|
|
FROM invoice_items ii WHERE ii.invoice_id = v_invoice_id
|
|
)
|
|
)
|
|
INTO v_result
|
|
FROM invoices i
|
|
LEFT JOIN customers c ON c.id = i.customer_id
|
|
WHERE i.id = v_invoice_id;
|
|
|
|
RETURN v_result;
|
|
END;
|
|
$function$;
|
|
|
|
-- seed_asset_categories
|
|
CREATE OR REPLACE FUNCTION public.seed_asset_categories(p_user_id uuid)
|
|
RETURNS void
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path TO 'public'
|
|
AS $function$
|
|
begin
|
|
if exists (select 1 from public.asset_categories where user_id = p_user_id) then
|
|
return;
|
|
end if;
|
|
|
|
insert into public.asset_categories (user_id, code, name, asset_account, depreciation_account, expense_account, default_useful_life_months, default_depreciation_method, is_system)
|
|
values
|
|
(p_user_id, 'BYGGNADER', 'Byggnader', '1110', '1119', '7820', 600, 'straight_line', true),
|
|
(p_user_id, 'MASKINER', 'Maskiner och tekniska anläggningar', '1210', '1219', '7831', 60, 'straight_line', true),
|
|
(p_user_id, 'INVENTARIER', 'Inventarier', '1220', '1229', '7832', 60, 'straight_line', true),
|
|
(p_user_id, 'FORDON', 'Fordon', '1240', '1249', '7834', 60, 'straight_line', true),
|
|
(p_user_id, 'DATORER', 'Datorer och IT-utrustning','1250', '1259', '7833', 36, 'straight_line', true),
|
|
(p_user_id, 'IMMATERIELLA', 'Immateriella tillgångar', '1010', '1019', '7810', 60, 'straight_line', true);
|
|
end;
|
|
$function$;
|
|
|
|
-- update_reconciliation_session_counts (trigger function)
|
|
CREATE OR REPLACE FUNCTION public.update_reconciliation_session_counts()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path TO 'public'
|
|
AS $function$
|
|
begin
|
|
update public.bank_reconciliation_sessions
|
|
set
|
|
matched_count = (
|
|
select count(*) from public.bank_reconciliation_items
|
|
where session_id = coalesce(new.session_id, old.session_id)
|
|
and is_reconciled = true
|
|
),
|
|
unmatched_count = (
|
|
select count(*) from public.bank_reconciliation_items
|
|
where session_id = coalesce(new.session_id, old.session_id)
|
|
and is_reconciled = false
|
|
),
|
|
total_transactions = (
|
|
select count(*) from public.bank_reconciliation_items
|
|
where session_id = coalesce(new.session_id, old.session_id)
|
|
)
|
|
where id = coalesce(new.session_id, old.session_id);
|
|
|
|
return coalesce(new, old);
|
|
end;
|
|
$function$;
|