br-office-suite/database/migrations/028_finance_invoice_lines_tax_codes.sql
2026-05-10 10:46:05 +02:00

181 lines
8.9 KiB
SQL

CREATE TABLE IF NOT EXISTS finance_tax_code (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(160) NOT NULL UNIQUE,
label VARCHAR(220) NOT NULL,
tax_rate_percent NUMERIC(6,3) NOT NULL DEFAULT 0 CHECK (tax_rate_percent >= 0 AND tax_rate_percent <= 100),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
sort_order INTEGER NOT NULL DEFAULT 1000,
created_by BIGINT REFERENCES app_user(id),
updated_by BIGINT REFERENCES app_user(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS finance_expense_line (
id BIGSERIAL PRIMARY KEY,
expense_id BIGINT NOT NULL REFERENCES finance_expense(id) ON DELETE CASCADE,
line_no INTEGER NOT NULL DEFAULT 1,
description VARCHAR(255) NOT NULL,
tax_code_id BIGINT REFERENCES finance_tax_code(id) ON DELETE SET NULL,
tax_code_key VARCHAR(160),
net_amount NUMERIC(14,2) NOT NULL DEFAULT 0 CHECK (net_amount >= 0),
tax_rate_percent NUMERIC(6,3) NOT NULL DEFAULT 0 CHECK (tax_rate_percent >= 0 AND tax_rate_percent <= 100),
tax_amount NUMERIC(14,2) NOT NULL DEFAULT 0 CHECK (tax_amount >= 0),
gross_amount NUMERIC(14,2) NOT NULL DEFAULT 0 CHECK (gross_amount >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT finance_expense_line_line_no_unique UNIQUE (expense_id, line_no)
);
CREATE INDEX IF NOT EXISTS idx_finance_expense_line_expense ON finance_expense_line(expense_id);
CREATE INDEX IF NOT EXISTS idx_finance_expense_line_tax_code ON finance_expense_line(tax_code_id);
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_finance_tax_code_updated_at') THEN
CREATE TRIGGER trg_finance_tax_code_updated_at BEFORE UPDATE ON finance_tax_code FOR EACH ROW EXECUTE FUNCTION set_updated_at();
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_finance_expense_line_updated_at') THEN
CREATE TRIGGER trg_finance_expense_line_updated_at BEFORE UPDATE ON finance_expense_line FOR EACH ROW EXECUTE FUNCTION set_updated_at();
END IF;
END $$;
INSERT INTO finance_tax_code(code, label, tax_rate_percent, is_active, sort_order)
VALUES
('Kfz_Betriebskosten_19%', 'Kfz Betriebskosten 19%', 19.000, TRUE, 10),
('Mietwagen_Betanken_19%', 'Mietwagen Betanken 19%', 19.000, TRUE, 11),
('Mietwagen_Ausland', 'Mietwagen Ausland', 0.000, TRUE, 12),
('Bahn_ueber_50_km_19%', 'Bahn ueber 50 km 19%', 19.000, TRUE, 13),
('Bahn_ueber_50_km', 'Bahn ueber 50 km', 0.000, TRUE, 14),
('Bahn_unter_50_km_7%', 'Bahn unter 50 km 7%', 7.000, TRUE, 15),
('Bahn_unter_50_km', 'Bahn unter 50 km', 0.000, TRUE, 16),
('Bewirtungskosten_19%', 'Bewirtungskosten 19%', 19.000, TRUE, 17),
('Bewirtungskosten_7%', 'Bewirtungskosten 7%', 7.000, TRUE, 18),
('Bewirtungskosten', 'Bewirtungskosten', 0.000, TRUE, 19),
('Bewirtungskosten_30_19%', 'Bewirtungskosten 30 19%', 19.000, TRUE, 20),
('Bewirtungskosten_30_7%', 'Bewirtungskosten 30 7%', 7.000, TRUE, 21),
('Bewirtungskosten_30', 'Bewirtungskosten 30', 0.000, TRUE, 22),
('Buerobedarf_19%', 'Buerobedarf 19%', 19.000, TRUE, 23),
('Buerobedarf', 'Buerobedarf', 0.000, TRUE, 24),
('Bus_ueber_50_km_19%', 'Bus ueber 50 km 19%', 19.000, TRUE, 25),
('Bus_ueber_50_km', 'Bus ueber 50 km', 0.000, TRUE, 26),
('Bus_unter_50_km_7%', 'Bus unter 50 km 7%', 7.000, TRUE, 27),
('Bus_unter_50_km', 'Bus unter 50 km', 0.000, TRUE, 28),
('Flug_19%', 'Flug 19%', 19.000, TRUE, 29),
('Flug', 'Flug', 0.000, TRUE, 30),
('Fruehstueck_(Hotel_Pension)_19%', 'Fruehstueck (Hotel Pension) 19%', 19.000, TRUE, 31),
('Fruehstueck_(Hotel_Pension)', 'Fruehstueck (Hotel Pension)', 0.000, TRUE, 32),
('Mittagessen_(Hotel_Pension)_19%', 'Mittagessen (Hotel Pension) 19%', 19.000, TRUE, 33),
('Mittagessen_(Hotel_Pension)', 'Mittagessen (Hotel Pension)', 0.000, TRUE, 34),
('Abendessen_(Hotel_Pension)_19%', 'Abendessen (Hotel Pension) 19%', 19.000, TRUE, 35),
('Abendessen_(Hotel_Pension)', 'Abendessen (Hotel Pension)', 0.000, TRUE, 36),
('Handy_19%', 'Handy 19%', 19.000, TRUE, 37),
('Handy', 'Handy', 0.000, TRUE, 38),
('Hotel_Pension_7%', 'Hotel Pension 7%', 7.000, TRUE, 39),
('Hotel_Pension', 'Hotel Pension', 0.000, TRUE, 40),
('Internet_19%', 'Internet 19%', 19.000, TRUE, 41),
('Internet', 'Internet', 0.000, TRUE, 42),
('Lebensmittel_19%%', 'Lebensmittel 19%%', 19.000, TRUE, 43),
('Lebensmittel_7%', 'Lebensmittel 7%', 7.000, TRUE, 44),
('Lebensmittel', 'Lebensmittel', 0.000, TRUE, 45),
('Mietwagen_19%', 'Mietwagen 19%', 19.000, TRUE, 46),
('Mietwagen', 'Mietwagen', 0.000, TRUE, 47),
('Mietwohnung_Appartement_19%', 'Mietwohnung Appartement 19%', 19.000, TRUE, 48),
('Mietwohnung_Appartement', 'Mietwohnung Appartement', 0.000, TRUE, 49),
('Parkgebuehren_19%', 'Parkgebuehren 19%', 19.000, TRUE, 50),
('Parkgebuehren', 'Parkgebuehren', 0.000, TRUE, 51),
('Porto_19%', 'Porto 19%', 19.000, TRUE, 52),
('Porto', 'Porto', 0.000, TRUE, 53),
('Reservierung_19%', 'Reservierung 19%', 19.000, TRUE, 54),
('Reservierung', 'Reservierung', 0.000, TRUE, 55),
('Sonstiges_(19_%_MwSt.)', 'Sonstiges (19 % MwSt.)', 19.000, TRUE, 56),
('Sonstiges_(7_%_MwSt.)', 'Sonstiges (7 % MwSt.)', 7.000, TRUE, 57),
('Sonstiges', 'Sonstiges', 0.000, TRUE, 58),
('Tankbelege_19%', 'Tankbelege 19%', 19.000, TRUE, 59),
('Tankbelege', 'Tankbelege', 0.000, TRUE, 60),
('Taxi_ueber_50_km_19%', 'Taxi ueber 50 km 19%', 19.000, TRUE, 61),
('Taxi_ueber_50_km', 'Taxi ueber 50 km', 0.000, TRUE, 62),
('Taxi_unter_50_km_7%', 'Taxi unter 50 km 7%', 7.000, TRUE, 63),
('Taxi_unter_50_km', 'Taxi unter 50 km', 0.000, TRUE, 64),
('Telefonkosten_19%', 'Telefonkosten 19%', 19.000, TRUE, 65),
('Telefonkosten', 'Telefonkosten', 0.000, TRUE, 66),
('Abendessen_(Hotel/Pension)_19%', 'Abendessen (Hotel/Pension) 19%', 19.000, TRUE, 67)
ON CONFLICT (code) DO UPDATE
SET label = EXCLUDED.label,
tax_rate_percent = EXCLUDED.tax_rate_percent,
sort_order = EXCLUDED.sort_order,
updated_at = NOW();
INSERT INTO finance_expense_line(
expense_id, line_no, description, tax_code_key,
net_amount, tax_rate_percent, tax_amount, gross_amount
)
SELECT e.id,
1,
COALESCE(NULLIF(e.cost_category, ''), e.title, 'Rechnungsposition'),
e.cost_category,
e.net_amount,
e.tax_rate_percent,
e.tax_amount,
e.gross_amount
FROM finance_expense e
WHERE NOT EXISTS (
SELECT 1 FROM finance_expense_line l WHERE l.expense_id = e.id
);
INSERT INTO app_page(page_key, path, label, nav_group, sort_order, description)
VALUES ('finance-tax-codes', '/finance-tax-codes', 'MwSt.-Sätze Finanzen', 'admin', 55, 'Kostenarten und MwSt.-Sätze für Rechnungspositionen pflegen')
ON CONFLICT (page_key) DO UPDATE
SET path = EXCLUDED.path,
label = EXCLUDED.label,
nav_group = EXCLUDED.nav_group,
sort_order = EXCLUDED.sort_order,
description = EXCLUDED.description;
INSERT INTO app_permission(permission_key, label, category, description)
VALUES
('finance.tax_codes', 'MwSt.-Sätze verwalten', 'Finanzen', 'Kostenarten und MwSt.-Sätze für Finanz-Rechnungspositionen pflegen')
ON CONFLICT (permission_key) DO UPDATE
SET label = EXCLUDED.label,
category = EXCLUDED.category,
description = EXCLUDED.description;
INSERT INTO app_role_page(role_profile_id, page_key, can_view)
SELECT rp.id, 'finance-tax-codes', TRUE
FROM app_role_profile rp
WHERE rp.code IN ('admin','chair')
ON CONFLICT (role_profile_id, page_key) DO UPDATE SET can_view = TRUE;
INSERT INTO app_role_permission(role_profile_id, permission_key, granted)
SELECT rp.id, 'finance.tax_codes', TRUE
FROM app_role_profile rp
WHERE rp.code IN ('admin','chair')
ON CONFLICT (role_profile_id, permission_key) DO UPDATE SET granted = TRUE;
ALTER TABLE finance_tax_code ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance_expense_line ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS finance_tax_code_select ON finance_tax_code;
DROP POLICY IF EXISTS finance_tax_code_modify ON finance_tax_code;
CREATE POLICY finance_tax_code_select ON finance_tax_code FOR SELECT USING (
app_has_permission('finance.read') OR app_has_permission('finance.tax_codes')
);
CREATE POLICY finance_tax_code_modify ON finance_tax_code FOR ALL USING (
app_has_permission('finance.tax_codes') OR app_has_permission('finance.budget')
) WITH CHECK (app_has_permission('finance.tax_codes') OR app_has_permission('finance.budget'));
DROP POLICY IF EXISTS finance_expense_line_select ON finance_expense_line;
DROP POLICY IF EXISTS finance_expense_line_modify ON finance_expense_line;
CREATE POLICY finance_expense_line_select ON finance_expense_line FOR SELECT USING (
app_has_permission('finance.read')
);
CREATE POLICY finance_expense_line_modify ON finance_expense_line FOR ALL USING (
app_has_permission('finance.write')
) WITH CHECK (app_has_permission('finance.write'));
GRANT SELECT, INSERT, UPDATE, DELETE ON finance_tax_code, finance_expense_line TO br_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO br_app;