Files
accounted/supabase/migrations/20260613100000_self_billing_received_invoices.sql
Mattsson 3e42fc6f32 Feat/voucher docs (#664)
* feat: implement inbox document picker and linking functionality

* feat: implement self-billing invoice functionality

- Added support for registering self-billed invoices received from customers.
- Updated the invoice schema to include fields for self-billing metadata such as `is_self_billed`, `external_invoice_number`, `self_billing_agreement_ref`, and `received_date`.
- Created API route for handling self-billed invoice submissions, including validation and error handling.
- Implemented database migrations to add necessary columns and constraints for self-billing invoices.
- Developed tests to ensure correct behavior of self-billing invoice creation and validation rules.
- Updated Swedish localization files to include new terms related to self-billing.

* feat: enforce SIE import requirement for non-Fortnox providers in migration process

* feat: streamline invoice processing and enhance error logging across APIs
2026-06-04 13:14:57 +02:00

80 lines
4.4 KiB
SQL

-- Self-billing received (mottagna självfakturor) — ML 17 kap 15§
--
-- A self-billing invoice we RECEIVE is a SALE for us (we are the seller); our
-- customer issues the invoice document on our behalf. For our books it is an
-- ordinary customer invoice: it books revenue + OUTPUT VAT and we remain
-- responsible for reporting that VAT. We model it as a flag on `invoices` so we
-- reuse the whole customer-invoice stack (booking, AR ledger, VAT declaration,
-- payment matching) instead of duplicating it on the supplier side (which would
-- book the VAT on the wrong side entirely).
--
-- Two things must differ from a normal customer invoice:
-- 1. The invoice number belongs to the CUSTOMER's series, not ours. We must
-- not consume our own löpnummerserie (BFL 5 kap 6§). The counterparty's
-- number lives in external_invoice_number; invoice_number stays NULL.
-- 2. There is no send step — the document is received, so it is booked on
-- registration.
--
-- Idempotent (IF NOT EXISTS / DROP-then-ADD) so it is safe to apply to a
-- preview/staging branch ahead of the repo sync without colliding.
-- 1. Self-billing metadata --------------------------------------------------
ALTER TABLE public.invoices
ADD COLUMN IF NOT EXISTS is_self_billed boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS external_invoice_number text,
ADD COLUMN IF NOT EXISTS self_billing_agreement_ref text,
ADD COLUMN IF NOT EXISTS received_date date;
COMMENT ON COLUMN public.invoices.is_self_billed IS
'True when this row is a self-billing invoice we received (ML 17 kap 15§). The counterparty issued it; for us it is a sale booked with output VAT.';
COMMENT ON COLUMN public.invoices.external_invoice_number IS
'The invoice number assigned by the customer (issuer) on a received self-billing invoice. Our own invoice_number stays NULL so we never touch our löpnummerserie.';
COMMENT ON COLUMN public.invoices.self_billing_agreement_ref IS
'Reference to the self-billing agreement (avtal i förväg) required by ML 17 kap 15§ p.1.';
-- 2. journal_entry_id -------------------------------------------------------
-- Referenced by the send / mark-sent / mark-paid routes
-- (invoices.update({ journal_entry_id }) and mark-paid's "already booked"
-- detection) but never created by any migration, so those writes silently
-- no-op on every database. Adding it here (IF NOT EXISTS — no-op where it was
-- patched in by hand) makes the linkage real, and lets mark-paid recognise an
-- already-booked self-billing sale and clear 1510 instead of re-recognising
-- revenue (which would double-count under kontantmetoden).
ALTER TABLE public.invoices
ADD COLUMN IF NOT EXISTS journal_entry_id uuid
REFERENCES public.journal_entries(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_invoices_journal_entry_id
ON public.invoices (journal_entry_id) WHERE journal_entry_id IS NOT NULL;
-- 3. Numbering integrity ----------------------------------------------------
-- A self-billed row carries the counterparty's number in external_invoice_number
-- and never one from our own series in invoice_number.
ALTER TABLE public.invoices DROP CONSTRAINT IF EXISTS invoices_self_billed_numbering;
ALTER TABLE public.invoices
ADD CONSTRAINT invoices_self_billed_numbering CHECK (
NOT is_self_billed
OR (external_invoice_number IS NOT NULL AND invoice_number IS NULL)
);
-- 4. Loosen the sent-requires-number rule -----------------------------------
-- 20260427150000 added: status IN ('draft','cancelled') OR invoice_number IS NOT NULL.
-- A received self-billing invoice is 'sent' (booked, awaiting/with payment) yet
-- legitimately has a NULL own number — its löpnummer is the customer's
-- external_invoice_number, guaranteed present by invoices_self_billed_numbering.
-- This preserves the ML 17 kap 24§ intent (every non-draft invoice carries a
-- number, ours or the counterparty's).
ALTER TABLE public.invoices DROP CONSTRAINT IF EXISTS invoices_sent_requires_number;
ALTER TABLE public.invoices
ADD CONSTRAINT invoices_sent_requires_number CHECK (
status IN ('draft', 'cancelled')
OR invoice_number IS NOT NULL
OR is_self_billed
);
-- 5. Reporting / list filter ------------------------------------------------
CREATE INDEX IF NOT EXISTS idx_invoices_self_billed
ON public.invoices (company_id, is_self_billed) WHERE is_self_billed;
NOTIFY pgrst, 'reload schema';