Files
vermietungs-tool/migrations/001_schema.sql
T
BlackMarkandClaude Opus 5.5 45424e6fd6 Vermietungs-Tool
A local web app for a small room-rental business in Austria: guest register,
room ledger, occupancy plan, invoices, the monthly tourist-tax report and the
quarterly VAT list. Application code only; it ships no data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 16:52:27 +01:00

181 lines
8.1 KiB
PL/PgSQL

-- 001 schema.
-- * text is COLLATE "C" (byte order), dates are ISO text, money is double precision
-- * every foreign key is DEFERRABLE (INITIALLY IMMEDIATE): company and client point at each other
CREATE TABLE unit (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
code text COLLATE "C" NOT NULL UNIQUE, -- short code shown in the lists
name text COLLATE "C" NOT NULL,
subtitle text COLLATE "C",
address text COLLATE "C", -- multi-line issuer address block
place text COLLATE "C", -- where invoices are issued ('<place>, am <date>')
contact text COLLATE "C", -- multi-line contact block of the invoice footer
bank text COLLATE "C",
iban text COLLATE "C",
uid text COLLATE "C",
tvb_number text COLLATE "C", -- Betriebsnummer for the tourist-tax report
invoice_prefix text COLLATE "C", -- '2026-' | '26-': the year is kept current
logo text COLLATE "C", -- file in app/static/invoice/, nullable
photo text COLLATE "C",
theme text COLLATE "C", -- 'green' | 'blue'
is_current integer NOT NULL DEFAULT 1,
succeeded_by_id bigint REFERENCES unit(id) DEFERRABLE -- a former operator reports under its successor
);
CREATE TABLE room (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
number text COLLATE "C" NOT NULL UNIQUE, -- floor-coded, e.g. '21'
category text COLLATE "C", -- EZ | DZ | 3BZ | 4BZ | Garconniere
capacity integer,
unit_id bigint REFERENCES unit(id) DEFERRABLE, -- current owner
long_term integer NOT NULL DEFAULT 0, -- let long-term, left out of the free-bed count
floor text COLLATE "C"
);
CREATE TABLE company (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name text COLLATE "C" NOT NULL, -- canonical display name
kind text COLLATE "C" NOT NULL DEFAULT 'company', -- company | private
uid text COLLATE "C",
email text COLLATE "C",
phone text COLLATE "C",
notes text COLLATE "C",
current_client_id bigint -- the address version used for NEW invoices
);
CREATE TABLE client (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
kind text COLLATE "C" NOT NULL DEFAULT 'company', -- company | private
name text COLLATE "C" NOT NULL,
address text COLLATE "C",
postcode text COLLATE "C",
city text COLLATE "C",
country text COLLATE "C",
company_id bigint REFERENCES company(id) DEFERRABLE
);
ALTER TABLE company ADD FOREIGN KEY (current_client_id) REFERENCES client(id) DEFERRABLE;
CREATE TABLE company_key (
key text COLLATE "C" PRIMARY KEY, -- normalize.company_key(spelling)
company_id bigint NOT NULL REFERENCES company(id) DEFERRABLE
);
CREATE TABLE dismissed_finding (
kind text COLLATE "C" NOT NULL,
key text COLLATE "C" NOT NULL,
note text COLLATE "C",
PRIMARY KEY (kind, key)
);
CREATE TABLE guest (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
last_name text COLLATE "C",
first_name text COLLATE "C",
birth_date text COLLATE "C", -- ISO yyyy-mm-dd
nationality text COLLATE "C",
doc_type text COLLATE "C",
doc_number text COLLATE "C",
profession text COLLATE "C",
home_street text COLLATE "C",
home_postcode text COLLATE "C",
home_city text COLLATE "C",
home_country text COLLATE "C",
phone text COLLATE "C"
);
CREATE TABLE invoice (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
unit_id bigint REFERENCES unit(id) DEFERRABLE,
client_id bigint REFERENCES client(id) DEFERRABLE,
number text COLLATE "C", -- '2026-01'
date text COLLATE "C",
paid_date text COLLATE "C",
payment_method text COLLATE "C",
notes text COLLATE "C",
lines_stale integer NOT NULL DEFAULT 0
);
-- One table for three kinds of rows, told apart by source_sheet: 'guestsheet' (a Gästeblatt in the guest
-- register), 'ledger' (a Zimmerbelegung row; its dates live on the charge) and 'invoice' (an invoice line).
CREATE TABLE stay (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
guest_id bigint REFERENCES guest(id) DEFERRABLE, -- null for ledger and invoice rows
room_id bigint REFERENCES room(id) DEFERRABLE,
unit_id bigint REFERENCES unit(id) DEFERRABLE, -- the operator at the time
employer_client_id bigint REFERENCES client(id) DEFERRABLE,
arrival text COLLATE "C",
departure text COLLATE "C",
sheet_number text COLLATE "C",
label text COLLATE "C", -- occupant text when guest_id is null
notes text COLLATE "C",
source_sheet text COLLATE "C", -- guestsheet | ledger | invoice
hauptwohnsitz integer,
tourism_liable integer
);
CREATE TABLE charge (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
stay_id bigint REFERENCES stay(id) DEFERRABLE,
invoice_id bigint REFERENCES invoice(id) DEFERRABLE,
period_from text COLLATE "C",
period_to text COLLATE "C",
quantity double precision,
unit_type text COLLATE "C", -- week | day | night | month
gross_per_unit double precision,
line_net_override double precision, -- null => computed
source_sheet text COLLATE "C",
persons numeric,
paid_date text COLLATE "C",
paid_status text COLLATE "C",
tourism_tax double precision,
art text COLLATE "C",
gb_ref text COLLATE "C",
vat_rate double precision NOT NULL DEFAULT 0.10,
kind text COLLATE "C" NOT NULL DEFAULT 'stay',
description text COLLATE "C",
rnr text COLLATE "C",
payer text COLLATE "C",
notes text COLLATE "C"
);
CREATE TABLE payment (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
charge_id bigint NOT NULL REFERENCES charge(id) ON DELETE CASCADE DEFERRABLE,
paid_date text COLLATE "C" NOT NULL,
amount double precision NOT NULL
);
CREATE INDEX idx_charge_invoice ON charge(invoice_id);
CREATE INDEX idx_charge_stay ON charge(stay_id);
CREATE INDEX idx_client_company ON client(company_id);
CREATE INDEX idx_stay_guest ON stay(guest_id);
CREATE INDEX idx_stay_room ON stay(room_id);
-- the company pages count stays and invoices per company in correlated subqueries
CREATE INDEX idx_stay_employer ON stay(employer_client_id);
CREATE INDEX idx_invoice_client ON invoice(client_id);
CREATE INDEX payment_charge ON payment(charge_id);
-- "Has anything changed?" for the page cache and the browser's frozen column widths: every statement that
-- writes a data table counts it up.
CREATE TABLE data_version (
id integer PRIMARY KEY DEFAULT 1 CHECK (id = 1),
version bigint NOT NULL DEFAULT 0
);
INSERT INTO data_version (id, version) VALUES (1, 0);
CREATE FUNCTION bump_data_version() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
EXECUTE format('UPDATE %I.data_version SET version = version + 1', TG_TABLE_SCHEMA);
RETURN NULL;
END $$;
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['unit', 'room', 'company', 'client', 'company_key', 'dismissed_finding', 'guest',
'invoice', 'stay', 'charge', 'payment'] LOOP
EXECUTE format('CREATE TRIGGER %I AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON %I '
'FOR EACH STATEMENT EXECUTE FUNCTION bump_data_version()', t || '_data_version', t);
END LOOP;
END $$;