Phase 9: Kassenverwaltung (#14)
This commit was merged in pull request #14.
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
-- =============================================================================
|
||||
-- Phase 9 — Kassenverwaltung
|
||||
--
|
||||
-- HINWEIS zu den entfernten DropIndex-Bloecken: Prisma schlaegt bei jeder
|
||||
-- Migration erneut vor, die von Hand angelegten GiST- und Trigram-Indizes aus
|
||||
-- Phase 1 abzuraeumen. Es kennt sie nicht, weil sie nicht aus dem Schema
|
||||
-- stammen. Neunte Migration in Folge, in der sie wieder herausgenommen werden.
|
||||
-- =============================================================================
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "KontoArt" AS ENUM ('BAR', 'BANK', 'RUECKLAGE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BuchungsArt" AS ENUM ('EINNAHME', 'AUSGABE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BeitragStand" AS ENUM ('OFFEN', 'BEZAHLT', 'ERLASSEN', 'GEMAHNT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RechnungStand" AS ENUM ('ENTWURF', 'VERSENDET', 'BEZAHLT', 'GEMAHNT', 'STORNIERT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RechnungsAnlass" AS ENUM ('FEHLALARM', 'OELEINSATZ', 'VERANSTALTUNG', 'VERMIETUNG', 'SONSTIGES');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "belegentwurf" ADD COLUMN "buchungId" UUID;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "kassenkonto" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"bezeichnung" VARCHAR(120) NOT NULL,
|
||||
"art" "KontoArt" NOT NULL DEFAULT 'BAR',
|
||||
"iban" VARCHAR(34),
|
||||
"bic" VARCHAR(11),
|
||||
"anfangsbestand" DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||||
"bemerkung" VARCHAR(500),
|
||||
"aktiv" BOOLEAN NOT NULL DEFAULT true,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
"geloeschtAm" TIMESTAMPTZ(3),
|
||||
|
||||
CONSTRAINT "kassenkonto_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "buchungskategorie" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"name" VARCHAR(120) NOT NULL,
|
||||
"art" "BuchungsArt" NOT NULL,
|
||||
"oberkategorieId" UUID,
|
||||
"kontonummer" VARCHAR(20),
|
||||
"bemerkung" VARCHAR(500),
|
||||
"aktiv" BOOLEAN NOT NULL DEFAULT true,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
"geloeschtAm" TIMESTAMPTZ(3),
|
||||
|
||||
CONSTRAINT "buchungskategorie_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "buchung" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"kontoId" UUID NOT NULL,
|
||||
"kategorieId" UUID NOT NULL,
|
||||
"datum" DATE NOT NULL,
|
||||
"art" "BuchungsArt" NOT NULL,
|
||||
"betrag" DECIMAL(12,2) NOT NULL,
|
||||
"verwendungszweck" VARCHAR(300) NOT NULL,
|
||||
"belegnummer" VARCHAR(60),
|
||||
"personId" UUID,
|
||||
"lieferant" VARCHAR(200),
|
||||
"gegenkontoId" UUID,
|
||||
"belegDateiId" UUID,
|
||||
"festgeschriebenAm" TIMESTAMPTZ(3),
|
||||
"storniertVonId" UUID,
|
||||
"erfasstVonId" UUID,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "buchung_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "beitragssatz" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"bezeichnung" VARCHAR(120) NOT NULL,
|
||||
"betrag" DECIMAL(12,2) NOT NULL,
|
||||
"abteilung" VARCHAR(60),
|
||||
"abJahr" INTEGER NOT NULL,
|
||||
"aktiv" BOOLEAN NOT NULL DEFAULT true,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "beitragssatz_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "beitrag" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"personId" UUID NOT NULL,
|
||||
"jahr" INTEGER NOT NULL,
|
||||
"satzId" UUID,
|
||||
"betrag" DECIMAL(12,2) NOT NULL,
|
||||
"stand" "BeitragStand" NOT NULL DEFAULT 'OFFEN',
|
||||
"buchungId" UUID,
|
||||
"bezahltAm" DATE,
|
||||
"mahnstufe" INTEGER NOT NULL DEFAULT 0,
|
||||
"gemahntAm" DATE,
|
||||
"erlassGrund" VARCHAR(300),
|
||||
"bemerkung" VARCHAR(500),
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "beitrag_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "rechnung" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"nummer" VARCHAR(20) NOT NULL,
|
||||
"anlass" "RechnungsAnlass" NOT NULL DEFAULT 'SONSTIGES',
|
||||
"stand" "RechnungStand" NOT NULL DEFAULT 'ENTWURF',
|
||||
"empfaenger" VARCHAR(200) NOT NULL,
|
||||
"empfaengerZusatz" VARCHAR(200),
|
||||
"strasse" VARCHAR(200),
|
||||
"plz" VARCHAR(10),
|
||||
"ort" VARCHAR(120),
|
||||
"einsatzId" UUID,
|
||||
"datum" DATE NOT NULL,
|
||||
"faelligAm" DATE NOT NULL,
|
||||
"betrag" DECIMAL(12,2) NOT NULL,
|
||||
"bemerkung" VARCHAR(1000),
|
||||
"buchungId" UUID,
|
||||
"bezahltAm" DATE,
|
||||
"gemahntAm" DATE,
|
||||
"mahnstufe" INTEGER NOT NULL DEFAULT 0,
|
||||
"pdfDateiId" UUID,
|
||||
"erstelltVonId" UUID,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "rechnung_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "rechnungsposition" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"rechnungId" UUID NOT NULL,
|
||||
"reihenfolge" INTEGER NOT NULL,
|
||||
"bezeichnung" VARCHAR(300) NOT NULL,
|
||||
"menge" DECIMAL(12,3) NOT NULL DEFAULT 1,
|
||||
"einheit" VARCHAR(20),
|
||||
"einzelpreis" DECIMAL(12,2) NOT NULL,
|
||||
"gesamtpreis" DECIMAL(12,2) NOT NULL,
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "rechnungsposition_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "kassenabschluss" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"vonAm" DATE NOT NULL,
|
||||
"bisAm" DATE NOT NULL,
|
||||
"bestandLautBuch" DECIMAL(12,2) NOT NULL,
|
||||
"gezaehlterBestand" DECIMAL(12,2) NOT NULL,
|
||||
"differenz" DECIMAL(12,2) NOT NULL,
|
||||
"erlaeuterung" VARCHAR(2000),
|
||||
"prueferId" UUID,
|
||||
"zweiterPrueferId" UUID,
|
||||
"geprueftAm" DATE,
|
||||
"berichtDateiId" UUID,
|
||||
"festgeschriebenAm" TIMESTAMPTZ(3),
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMPTZ(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "kassenabschluss_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "abschlussposten" (
|
||||
"id" UUID NOT NULL,
|
||||
"organisationId" UUID NOT NULL,
|
||||
"abschlussId" UUID NOT NULL,
|
||||
"kontoId" UUID NOT NULL,
|
||||
"bestandLautBuch" DECIMAL(12,2) NOT NULL,
|
||||
"gezaehlterBestand" DECIMAL(12,2) NOT NULL,
|
||||
"differenz" DECIMAL(12,2) NOT NULL,
|
||||
"bemerkung" VARCHAR(500),
|
||||
"erstelltAm" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "abschlussposten_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "kassenkonto_organisationId_aktiv_idx" ON "kassenkonto"("organisationId", "aktiv");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "kassenkonto_organisationId_bezeichnung_key" ON "kassenkonto"("organisationId", "bezeichnung");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "buchungskategorie_organisationId_art_idx" ON "buchungskategorie"("organisationId", "art");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "buchungskategorie_organisationId_name_key" ON "buchungskategorie"("organisationId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "buchung_organisationId_datum_idx" ON "buchung"("organisationId", "datum" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "buchung_organisationId_kontoId_datum_idx" ON "buchung"("organisationId", "kontoId", "datum");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "buchung_organisationId_kategorieId_idx" ON "buchung"("organisationId", "kategorieId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "beitragssatz_organisationId_abJahr_idx" ON "beitragssatz"("organisationId", "abJahr");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "beitragssatz_organisationId_abteilung_abJahr_key" ON "beitragssatz"("organisationId", "abteilung", "abJahr");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "beitrag_organisationId_jahr_stand_idx" ON "beitrag"("organisationId", "jahr", "stand");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "beitrag_personId_jahr_key" ON "beitrag"("personId", "jahr");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "rechnung_organisationId_stand_idx" ON "rechnung"("organisationId", "stand");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "rechnung_organisationId_datum_idx" ON "rechnung"("organisationId", "datum" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "rechnung_organisationId_nummer_key" ON "rechnung"("organisationId", "nummer");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "rechnungsposition_organisationId_idx" ON "rechnungsposition"("organisationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "rechnungsposition_rechnungId_reihenfolge_key" ON "rechnungsposition"("rechnungId", "reihenfolge");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "kassenabschluss_organisationId_bisAm_idx" ON "kassenabschluss"("organisationId", "bisAm" DESC);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "abschlussposten_organisationId_idx" ON "abschlussposten"("organisationId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "abschlussposten_abschlussId_kontoId_key" ON "abschlussposten"("abschlussId", "kontoId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "belegentwurf" ADD CONSTRAINT "belegentwurf_buchungId_fkey" FOREIGN KEY ("buchungId") REFERENCES "buchung"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "kassenkonto" ADD CONSTRAINT "kassenkonto_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "buchungskategorie" ADD CONSTRAINT "buchungskategorie_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "buchungskategorie" ADD CONSTRAINT "buchungskategorie_oberkategorieId_fkey" FOREIGN KEY ("oberkategorieId") REFERENCES "buchungskategorie"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "buchung" ADD CONSTRAINT "buchung_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "buchung" ADD CONSTRAINT "buchung_kontoId_fkey" FOREIGN KEY ("kontoId") REFERENCES "kassenkonto"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "buchung" ADD CONSTRAINT "buchung_kategorieId_fkey" FOREIGN KEY ("kategorieId") REFERENCES "buchungskategorie"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "buchung" ADD CONSTRAINT "buchung_personId_fkey" FOREIGN KEY ("personId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "buchung" ADD CONSTRAINT "buchung_erfasstVonId_fkey" FOREIGN KEY ("erfasstVonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "buchung" ADD CONSTRAINT "buchung_belegDateiId_fkey" FOREIGN KEY ("belegDateiId") REFERENCES "datei"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "buchung" ADD CONSTRAINT "buchung_storniertVonId_fkey" FOREIGN KEY ("storniertVonId") REFERENCES "buchung"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "beitragssatz" ADD CONSTRAINT "beitragssatz_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "beitrag" ADD CONSTRAINT "beitrag_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "beitrag" ADD CONSTRAINT "beitrag_personId_fkey" FOREIGN KEY ("personId") REFERENCES "person"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "beitrag" ADD CONSTRAINT "beitrag_satzId_fkey" FOREIGN KEY ("satzId") REFERENCES "beitragssatz"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "beitrag" ADD CONSTRAINT "beitrag_buchungId_fkey" FOREIGN KEY ("buchungId") REFERENCES "buchung"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rechnung" ADD CONSTRAINT "rechnung_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rechnung" ADD CONSTRAINT "rechnung_buchungId_fkey" FOREIGN KEY ("buchungId") REFERENCES "buchung"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rechnung" ADD CONSTRAINT "rechnung_pdfDateiId_fkey" FOREIGN KEY ("pdfDateiId") REFERENCES "datei"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rechnung" ADD CONSTRAINT "rechnung_erstelltVonId_fkey" FOREIGN KEY ("erstelltVonId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rechnungsposition" ADD CONSTRAINT "rechnungsposition_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "rechnungsposition" ADD CONSTRAINT "rechnungsposition_rechnungId_fkey" FOREIGN KEY ("rechnungId") REFERENCES "rechnung"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "kassenabschluss" ADD CONSTRAINT "kassenabschluss_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "kassenabschluss" ADD CONSTRAINT "kassenabschluss_prueferId_fkey" FOREIGN KEY ("prueferId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "kassenabschluss" ADD CONSTRAINT "kassenabschluss_zweiterPrueferId_fkey" FOREIGN KEY ("zweiterPrueferId") REFERENCES "person"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "kassenabschluss" ADD CONSTRAINT "kassenabschluss_berichtDateiId_fkey" FOREIGN KEY ("berichtDateiId") REFERENCES "datei"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "abschlussposten" ADD CONSTRAINT "abschlussposten_organisationId_fkey" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "abschlussposten" ADD CONSTRAINT "abschlussposten_abschlussId_fkey" FOREIGN KEY ("abschlussId") REFERENCES "kassenabschluss"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "abschlussposten" ADD CONSTRAINT "abschlussposten_kontoId_fkey" FOREIGN KEY ("kontoId") REFERENCES "kassenkonto"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,306 @@
|
||||
-- =============================================================================
|
||||
-- Phase 9 — Mandantentrennung und die Regeln, die eine Kasse prüffähig machen
|
||||
--
|
||||
-- Drei Dinge stehen hier und nicht im Anwendungscode, weil sie auch dann
|
||||
-- gelten müssen, wenn jemand an der Anwendung vorbei schreibt:
|
||||
--
|
||||
-- 1. **Eine festgeschriebene Buchung ist unveränderbar.** Ein Kassenbuch,
|
||||
-- in dem man rückwirkend ändern kann, ist vor der Kassenprüfung wertlos.
|
||||
-- 2. **Ein festgeschriebener Abschluss friert seinen Zeitraum ein.** Sonst
|
||||
-- hätte der geprüfte Bestand am Tag nach der Prüfung eine andere Zahl.
|
||||
-- 3. **Rechnerisch Unmögliches wird abgewiesen** — negative Beträge, ein
|
||||
-- Ende vor dem Beginn, eine Buchung, die gleichzeitig an ein Mitglied und
|
||||
-- an einen Lieferanten geht.
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Row Level Security
|
||||
-- -----------------------------------------------------------------------------
|
||||
DO $$
|
||||
DECLARE
|
||||
tabelle text;
|
||||
BEGIN
|
||||
FOREACH tabelle IN ARRAY ARRAY[
|
||||
'kassenkonto',
|
||||
'buchungskategorie',
|
||||
'buchung',
|
||||
'beitragssatz',
|
||||
'beitrag',
|
||||
'rechnung',
|
||||
'rechnungsposition',
|
||||
'kassenabschluss',
|
||||
'abschlussposten'
|
||||
]
|
||||
LOOP
|
||||
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', tabelle);
|
||||
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', tabelle);
|
||||
|
||||
EXECUTE format('DROP POLICY IF EXISTS %I ON %I', tabelle || '_mandant', tabelle);
|
||||
EXECUTE format(
|
||||
'CREATE POLICY %I ON %I FOR ALL '
|
||||
'USING ("organisationId" = aktuelle_organisation()) '
|
||||
'WITH CHECK ("organisationId" = aktuelle_organisation())',
|
||||
tabelle || '_mandant', tabelle
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Prüfbedingungen
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- Der Betrag ist positiv; die Richtung steht in "art". Eine Buchung über
|
||||
-- minus zwölf Euro wäre eine zweite Art, dasselbe auszudrücken — und die
|
||||
-- Summenformel müsste dann beide beherrschen.
|
||||
ALTER TABLE "buchung" ADD CONSTRAINT "buchung_betrag_positiv" CHECK ("betrag" > 0);
|
||||
|
||||
-- Entweder ein Mitglied oder ein Dritter. Beides zusammen wäre nicht
|
||||
-- auswertbar: bei „wer hat wie viel bekommen" stünde derselbe Betrag zweimal.
|
||||
ALTER TABLE "buchung" ADD CONSTRAINT "buchung_gegenueber_eindeutig" CHECK (
|
||||
NOT ("personId" IS NOT NULL AND "lieferant" IS NOT NULL)
|
||||
);
|
||||
|
||||
-- Eine Umbuchung auf dasselbe Konto ist keine.
|
||||
ALTER TABLE "buchung" ADD CONSTRAINT "buchung_gegenkonto_verschieden" CHECK (
|
||||
"gegenkontoId" IS NULL OR "gegenkontoId" <> "kontoId"
|
||||
);
|
||||
|
||||
ALTER TABLE "beitrag" ADD CONSTRAINT "beitrag_betrag_nicht_negativ" CHECK ("betrag" >= 0);
|
||||
ALTER TABLE "beitrag" ADD CONSTRAINT "beitrag_jahr_plausibel" CHECK (
|
||||
"jahr" BETWEEN 1900 AND 2200
|
||||
);
|
||||
ALTER TABLE "beitrag" ADD CONSTRAINT "beitrag_mahnstufe_plausibel" CHECK (
|
||||
"mahnstufe" BETWEEN 0 AND 5
|
||||
);
|
||||
|
||||
-- Ein erlassener Beitrag ohne Begründung ist in der Kassenprüfung eine offene
|
||||
-- Frage. Lieber gleich hier verlangen als hinterher erklären.
|
||||
ALTER TABLE "beitrag" ADD CONSTRAINT "beitrag_erlass_begruendet" CHECK (
|
||||
"stand" <> 'ERLASSEN' OR "erlassGrund" IS NOT NULL
|
||||
);
|
||||
|
||||
-- Bezahlt heißt: es gibt einen Tag, an dem gezahlt wurde.
|
||||
ALTER TABLE "beitrag" ADD CONSTRAINT "beitrag_bezahlt_datiert" CHECK (
|
||||
"stand" <> 'BEZAHLT' OR "bezahltAm" IS NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "beitragssatz" ADD CONSTRAINT "beitragssatz_betrag_nicht_negativ" CHECK (
|
||||
"betrag" >= 0
|
||||
);
|
||||
|
||||
ALTER TABLE "rechnung" ADD CONSTRAINT "rechnung_betrag_nicht_negativ" CHECK ("betrag" >= 0);
|
||||
ALTER TABLE "rechnung" ADD CONSTRAINT "rechnung_faellig_nach_datum" CHECK (
|
||||
"faelligAm" >= "datum"
|
||||
);
|
||||
|
||||
ALTER TABLE "rechnungsposition" ADD CONSTRAINT "position_menge_positiv" CHECK ("menge" > 0);
|
||||
ALTER TABLE "rechnungsposition" ADD CONSTRAINT "position_preis_nicht_negativ" CHECK (
|
||||
"einzelpreis" >= 0 AND "gesamtpreis" >= 0
|
||||
);
|
||||
|
||||
ALTER TABLE "kassenabschluss" ADD CONSTRAINT "abschluss_zeitraum_sinnvoll" CHECK (
|
||||
"bisAm" >= "vonAm"
|
||||
);
|
||||
|
||||
-- Die Differenz ist keine freie Eingabe, sondern das Ergebnis. Wer sie von
|
||||
-- Hand setzt, verdeckt genau das, was die Prüfung finden soll.
|
||||
ALTER TABLE "kassenabschluss" ADD CONSTRAINT "abschluss_differenz_stimmt" CHECK (
|
||||
"differenz" = "gezaehlterBestand" - "bestandLautBuch"
|
||||
);
|
||||
|
||||
ALTER TABLE "abschlussposten" ADD CONSTRAINT "posten_differenz_stimmt" CHECK (
|
||||
"differenz" = "gezaehlterBestand" - "bestandLautBuch"
|
||||
);
|
||||
|
||||
-- Ein festgeschriebener Abschluss braucht Prüfer und Datum. Ohne beides ist
|
||||
-- er kein Abschluss, sondern eine Notiz.
|
||||
ALTER TABLE "kassenabschluss" ADD CONSTRAINT "abschluss_pruefung_vollstaendig" CHECK (
|
||||
"festgeschriebenAm" IS NULL
|
||||
OR ("prueferId" IS NOT NULL AND "geprueftAm" IS NOT NULL)
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Suche und Auswertung
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- Der Verwendungszweck ist das Feld, in dem gesucht wird — „wo war noch die
|
||||
-- Rechnung von der Werkstatt".
|
||||
CREATE INDEX IF NOT EXISTS "buchung_suche_zweck"
|
||||
ON "buchung" USING gin ("verwendungszweck" gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "buchung_suche_lieferant"
|
||||
ON "buchung" USING gin ("lieferant" gin_trgm_ops);
|
||||
|
||||
-- Offene Posten: die Liste, die der Kassier vor der Jahreshauptversammlung
|
||||
-- braucht.
|
||||
CREATE INDEX IF NOT EXISTS "beitrag_offen"
|
||||
ON "beitrag" ("organisationId", "jahr")
|
||||
WHERE "stand" IN ('OFFEN', 'GEMAHNT');
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "rechnung_offen"
|
||||
ON "rechnung" ("organisationId", "faelligAm")
|
||||
WHERE "stand" IN ('VERSENDET', 'GEMAHNT');
|
||||
|
||||
-- Noch nicht festgeschriebene Buchungen — der Stapel, den jemand durchsieht.
|
||||
CREATE INDEX IF NOT EXISTS "buchung_offen"
|
||||
ON "buchung" ("organisationId", "datum")
|
||||
WHERE "festgeschriebenAm" IS NULL;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Rechte
|
||||
-- -----------------------------------------------------------------------------
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'florian_app') THEN
|
||||
EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO florian_app';
|
||||
EXECUTE 'GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO florian_app';
|
||||
|
||||
EXECUTE 'REVOKE INSERT, UPDATE, DELETE ON "berechtigung" FROM florian_app';
|
||||
EXECUTE 'REVOKE UPDATE, DELETE ON "audit_log" FROM florian_app';
|
||||
EXECUTE 'REVOKE UPDATE, DELETE ON "kiosk_vorgang" FROM florian_app';
|
||||
EXECUTE 'REVOKE UPDATE, DELETE ON "druckmessung" FROM florian_app';
|
||||
EXECUTE 'REVOKE UPDATE, DELETE ON "atemschutz_ereignis" FROM florian_app';
|
||||
EXECUTE 'REVOKE DELETE ON "benachrichtigung" FROM florian_app';
|
||||
EXECUTE 'REVOKE DELETE ON "ki_vorgang" FROM florian_app';
|
||||
EXECUTE 'REVOKE UPDATE, DELETE ON "wikiversion" FROM florian_app';
|
||||
|
||||
-- Ein festgeschriebener Abschluss lässt sich nicht wegräumen. Das
|
||||
-- Löschen einzelner Buchungen bleibt möglich, solange sie nicht
|
||||
-- festgeschrieben sind — den Rest erledigt der Trigger unten, weil
|
||||
-- ein pauschales REVOKE auch den Entwurfsstapel unlöschbar machen
|
||||
-- würde.
|
||||
EXECUTE 'REVOKE DELETE ON "kassenabschluss" FROM florian_app';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Eine festgeschriebene Buchung ist unveränderbar
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- Änderbar bleibt genau eines: der Verweis auf die Buchung, die diese hier
|
||||
-- aufhebt. Eine Stornierung entsteht ja erst später — sie einzutragen ist
|
||||
-- keine Änderung des Vorgangs, sondern seine Fortschreibung.
|
||||
CREATE OR REPLACE FUNCTION buchung_unveraenderlich()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
-- Die eine Ausnahme ist der Aufbewahrungslauf, derselbe Schalter wie
|
||||
-- beim Audit-Log und beim Pruefprotokoll. Ohne ihn liesse sich eine
|
||||
-- Organisation nicht mehr loeschen -- und genau das verlangt die
|
||||
-- DSGVO, wenn eine Wehr das System verlaesst.
|
||||
IF coalesce(current_setting('app.audit_bereinigung', true), '') = 'an' THEN
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
IF OLD."festgeschriebenAm" IS NOT NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'Diese Buchung ist seit % festgeschrieben und laesst sich nicht loeschen. '
|
||||
'Korrekturen laufen ueber eine Gegenbuchung.', OLD."festgeschriebenAm";
|
||||
END IF;
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
IF OLD."festgeschriebenAm" IS NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF NEW."datum" IS DISTINCT FROM OLD."datum"
|
||||
OR NEW."betrag" IS DISTINCT FROM OLD."betrag"
|
||||
OR NEW."art" IS DISTINCT FROM OLD."art"
|
||||
OR NEW."kontoId" IS DISTINCT FROM OLD."kontoId"
|
||||
OR NEW."kategorieId" IS DISTINCT FROM OLD."kategorieId"
|
||||
OR NEW."verwendungszweck" IS DISTINCT FROM OLD."verwendungszweck"
|
||||
OR NEW."personId" IS DISTINCT FROM OLD."personId"
|
||||
OR NEW."lieferant" IS DISTINCT FROM OLD."lieferant"
|
||||
OR NEW."belegnummer" IS DISTINCT FROM OLD."belegnummer"
|
||||
OR NEW."festgeschriebenAm" IS DISTINCT FROM OLD."festgeschriebenAm" THEN
|
||||
RAISE EXCEPTION
|
||||
'Diese Buchung ist seit % festgeschrieben. Korrekturen laufen ueber eine '
|
||||
'Gegenbuchung mit Verweis, nie durch Aendern.', OLD."festgeschriebenAm";
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS buchung_unveraenderlich_trigger ON "buchung";
|
||||
CREATE TRIGGER buchung_unveraenderlich_trigger
|
||||
BEFORE UPDATE OR DELETE ON "buchung"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION buchung_unveraenderlich();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Ein festgeschriebener Abschluss friert seinen Zeitraum ein
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- Gilt für neue Buchungen und für das Verschieben vorhandener. Ohne die
|
||||
-- zweite Hälfte könnte man eine Buchung nachträglich in einen geprüften
|
||||
-- Zeitraum hineindatieren — und der geprüfte Bestand stimmte nicht mehr.
|
||||
CREATE OR REPLACE FUNCTION buchung_zeitraum_offen()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
abschluss RECORD;
|
||||
BEGIN
|
||||
SELECT "vonAm", "bisAm", "geprueftAm"
|
||||
INTO abschluss
|
||||
FROM "kassenabschluss"
|
||||
WHERE "organisationId" = NEW."organisationId"
|
||||
AND "festgeschriebenAm" IS NOT NULL
|
||||
AND NEW."datum" BETWEEN "vonAm" AND "bisAm"
|
||||
LIMIT 1;
|
||||
|
||||
IF FOUND THEN
|
||||
RAISE EXCEPTION
|
||||
'Der Zeitraum vom % bis % ist mit dem Kassenabschluss vom % geprueft und '
|
||||
'abgeschlossen. Fuer eine Korrektur bitte mit heutigem Datum gegenbuchen.',
|
||||
abschluss."vonAm", abschluss."bisAm", abschluss."geprueftAm";
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS buchung_zeitraum_offen_trigger ON "buchung";
|
||||
CREATE TRIGGER buchung_zeitraum_offen_trigger
|
||||
BEFORE INSERT OR UPDATE OF "datum" ON "buchung"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION buchung_zeitraum_offen();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Ein festgeschriebener Abschluss bleibt, wie er ist
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Aenderbar bleibt genau eines: der Verweis auf den erzeugten Bericht. Das PDF
|
||||
-- entsteht im Hintergrund und damit erst nach dem Festschreiben — es
|
||||
-- nachzutragen ist keine Aenderung des Abschlusses, sondern seine
|
||||
-- Fertigstellung. Dieselbe Ausnahme wie beim Stornierungsverweis der Buchung.
|
||||
CREATE OR REPLACE FUNCTION abschluss_unveraenderlich()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF OLD."festgeschriebenAm" IS NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF NEW."vonAm" IS NOT DISTINCT FROM OLD."vonAm"
|
||||
AND NEW."bisAm" IS NOT DISTINCT FROM OLD."bisAm"
|
||||
AND NEW."bestandLautBuch" IS NOT DISTINCT FROM OLD."bestandLautBuch"
|
||||
AND NEW."gezaehlterBestand" IS NOT DISTINCT FROM OLD."gezaehlterBestand"
|
||||
AND NEW."differenz" IS NOT DISTINCT FROM OLD."differenz"
|
||||
AND NEW."erlaeuterung" IS NOT DISTINCT FROM OLD."erlaeuterung"
|
||||
AND NEW."prueferId" IS NOT DISTINCT FROM OLD."prueferId"
|
||||
AND NEW."zweiterPrueferId" IS NOT DISTINCT FROM OLD."zweiterPrueferId"
|
||||
AND NEW."geprueftAm" IS NOT DISTINCT FROM OLD."geprueftAm"
|
||||
AND NEW."festgeschriebenAm" IS NOT DISTINCT FROM OLD."festgeschriebenAm" THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
RAISE EXCEPTION
|
||||
'Der Kassenabschluss vom % ist festgeschrieben. Zahlen und Pruefer lassen sich '
|
||||
'nicht mehr aendern.', OLD."geprueftAm";
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS abschluss_unveraenderlich_trigger ON "kassenabschluss";
|
||||
CREATE TRIGGER abschluss_unveraenderlich_trigger
|
||||
BEFORE UPDATE ON "kassenabschluss"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION abschluss_unveraenderlich();
|
||||
@@ -234,6 +234,17 @@ model Organisation {
|
||||
diktate Diktat[]
|
||||
belegentwuerfe Belegentwurf[]
|
||||
|
||||
// Phase 9 — Kassenverwaltung
|
||||
kassenkonten Kassenkonto[]
|
||||
buchungskategorien Buchungskategorie[]
|
||||
buchungen Buchung[]
|
||||
beitragssaetze Beitragssatz[]
|
||||
beitraege Beitrag[]
|
||||
rechnungen Rechnung[]
|
||||
rechnungspositionen Rechnungsposition[]
|
||||
kassenabschluesse Kassenabschluss[]
|
||||
abschlussposten Abschlussposten[]
|
||||
|
||||
@@index([aktiv])
|
||||
@@map("organisation")
|
||||
}
|
||||
@@ -356,6 +367,14 @@ model Person {
|
||||
erfassteBelege Belegentwurf[] @relation("BelegentwurfErfasser")
|
||||
bestaetigteBelege Belegentwurf[] @relation("BelegentwurfBestaetiger")
|
||||
|
||||
// Phase 9 — Kassenverwaltung
|
||||
buchungen Buchung[] @relation("BuchungPerson")
|
||||
erfassteBuchungen Buchung[] @relation("BuchungErfasser")
|
||||
beitraege Beitrag[] @relation("BeitragPerson")
|
||||
gepruefteAbschluesse Kassenabschluss[] @relation("AbschlussPruefer")
|
||||
zweitgepruefteAbschluesse Kassenabschluss[] @relation("AbschlussZweitpruefer")
|
||||
erstellteRechnungen Rechnung[] @relation("RechnungErsteller")
|
||||
|
||||
/// E-Mail und OIDC-Subject sind je Wehr eindeutig, nicht global — dieselbe
|
||||
/// Person kann in zwei Wehren Mitglied sein.
|
||||
@@unique([organisationId, email])
|
||||
@@ -602,6 +621,11 @@ model Datei {
|
||||
alsDiktataufnahme Diktat[] @relation("DiktatAufnahme")
|
||||
alsBelegfoto Belegentwurf[] @relation("BelegentwurfFoto")
|
||||
|
||||
// Phase 9
|
||||
alsBuchungsbeleg Buchung[] @relation("BuchungBeleg")
|
||||
alsRechnungsPdf Rechnung[] @relation("RechnungPdf")
|
||||
alsKassenpruefbericht Kassenabschluss[] @relation("AbschlussBericht")
|
||||
|
||||
@@unique([bucket, objektSchluessel])
|
||||
@@index([organisationId, entitaet, entitaetId])
|
||||
@@index([organisationId, kategorie])
|
||||
@@ -3601,7 +3625,11 @@ model Belegentwurf {
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
/// Die Buchung, die daraus geworden ist (Phase 9). Leer heißt: noch keine.
|
||||
buchungId String? @db.Uuid
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
buchung Buchung? @relation(fields: [buchungId], references: [id], onDelete: SetNull)
|
||||
fotoDatei Datei @relation("BelegentwurfFoto", fields: [fotoDateiId], references: [id], onDelete: Restrict)
|
||||
kiVorgang KiVorgang? @relation(fields: [kiVorgangId], references: [id], onDelete: SetNull)
|
||||
erfasstVon Person? @relation("BelegentwurfErfasser", fields: [erfasstVonId], references: [id], onDelete: SetNull)
|
||||
@@ -3611,3 +3639,435 @@ model Belegentwurf {
|
||||
@@index([organisationId, belegdatum])
|
||||
@@map("belegentwurf")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Phase 9 — Kassenverwaltung
|
||||
//
|
||||
// Drei Festlegungen bestimmen alles Weitere. Sie stehen hier, weil sie sich
|
||||
// im Schema abbilden und nicht bloß in der Anwendung durchgesetzt werden:
|
||||
//
|
||||
// 1. **Beträge sind DECIMAL(12,2), niemals Fließkomma.** 0,1 + 0,2 ist in
|
||||
// binärer Fließkommadarstellung nicht 0,3, und eine Kasse, die sich um
|
||||
// Cent verrechnet, ist keine Kasse. Zwölf Stellen reichen für zehn
|
||||
// Milliarden — mehr als jede Wehr je bewegt.
|
||||
//
|
||||
// 2. **Eine festgeschriebene Buchung ist unveränderbar.** Korrekturen laufen
|
||||
// über eine Gegenbuchung mit Verweis auf das Original. Das ist keine
|
||||
// Förmlichkeit: ein Kassenbuch, in dem man rückwirkend ändern kann, ist
|
||||
// vor der Kassenprüfung wertlos. Der Trigger in der Migration setzt es
|
||||
// durch.
|
||||
//
|
||||
// 3. **Der Kassenabschluss friert seinen Zeitraum ein.** Danach nimmt die
|
||||
// Buchungsmaske kein Datum daraus mehr an. Sonst hätte der geprüfte
|
||||
// Bestand am Tag nach der Prüfung eine andere Zahl.
|
||||
//
|
||||
// Der Betrag ist immer **positiv**; die Richtung steht in `art`. Ein
|
||||
// vorzeichenbehafteter Betrag wäre kürzer, aber jede Auswertung müsste dann
|
||||
// darauf vertrauen, dass das Vorzeichen zur Kategorie passt — und beim ersten
|
||||
// Tippfehler stimmt die Summe nicht mehr.
|
||||
// =============================================================================
|
||||
|
||||
/// Welche Art von Konto.
|
||||
enum KontoArt {
|
||||
/// Die Barkasse im Gerätehaus. Wird bei der Prüfung gezählt.
|
||||
BAR
|
||||
/// Ein Bankkonto. Wird gegen den Auszug geprüft.
|
||||
BANK
|
||||
/// Rücklage für eine bestimmte Anschaffung — getrennt geführt, damit
|
||||
/// niemand versehentlich das Geld für das neue Fahrzeug verbraucht.
|
||||
RUECKLAGE
|
||||
}
|
||||
|
||||
/// Geht Geld ein oder hinaus.
|
||||
enum BuchungsArt {
|
||||
EINNAHME
|
||||
AUSGABE
|
||||
}
|
||||
|
||||
/// Wie es um einen Beitrag steht.
|
||||
enum BeitragStand {
|
||||
OFFEN
|
||||
BEZAHLT
|
||||
/// Erlassen — bei Härtefällen, Ehrenmitgliedern, Jugendlichen.
|
||||
ERLASSEN
|
||||
GEMAHNT
|
||||
}
|
||||
|
||||
/// Wie es um eine Rechnung steht.
|
||||
enum RechnungStand {
|
||||
ENTWURF
|
||||
VERSENDET
|
||||
BEZAHLT
|
||||
GEMAHNT
|
||||
STORNIERT
|
||||
}
|
||||
|
||||
/// Woraus eine Rechnung an Dritte entsteht.
|
||||
enum RechnungsAnlass {
|
||||
/// Böswilliger oder fahrlässig ausgelöster Fehlalarm.
|
||||
FEHLALARM
|
||||
/// Ölspur, auslaufende Betriebsstoffe — nach Verursacherprinzip.
|
||||
OELEINSATZ
|
||||
/// Absicherung, Brandsicherheitswache bei einer Veranstaltung.
|
||||
VERANSTALTUNG
|
||||
/// Vermietung von Gerät oder Räumen.
|
||||
VERMIETUNG
|
||||
SONSTIGES
|
||||
}
|
||||
|
||||
/// Ein Konto der Wehr.
|
||||
model Kassenkonto {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
bezeichnung String @db.VarChar(120)
|
||||
art KontoArt @default(BAR)
|
||||
|
||||
/// Nur bei Bankkonten. Wird in Auswertungen nie vollständig angezeigt.
|
||||
iban String? @db.VarChar(34)
|
||||
bic String? @db.VarChar(11)
|
||||
|
||||
/// Der Bestand zu Beginn der Aufzeichnung. Alles Weitere ergibt sich aus
|
||||
/// den Buchungen — er wird nie nachträglich „korrigiert".
|
||||
anfangsbestand Decimal @default(0) @db.Decimal(12, 2)
|
||||
|
||||
bemerkung String? @db.VarChar(500)
|
||||
aktiv Boolean @default(true)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
buchungen Buchung[]
|
||||
posten Abschlussposten[]
|
||||
|
||||
@@unique([organisationId, bezeichnung])
|
||||
@@index([organisationId, aktiv])
|
||||
@@map("kassenkonto")
|
||||
}
|
||||
|
||||
/// Wofür Geld ausgegeben oder eingenommen wurde.
|
||||
///
|
||||
/// Zweistufig: eine Oberkategorie („Fahrzeuge") mit Unterkategorien
|
||||
/// („Kraftstoff", „Reparatur"). Mehr Ebenen braucht eine Wehr nicht, und jede
|
||||
/// weitere macht die Auswertung unlesbar.
|
||||
model Buchungskategorie {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
name String @db.VarChar(120)
|
||||
art BuchungsArt
|
||||
|
||||
/// Die Oberkategorie. Leer heißt: das hier ist eine.
|
||||
oberkategorieId String? @db.Uuid
|
||||
|
||||
/// Nummer im Kontenrahmen der Gemeinde — für den Export an die Kämmerei.
|
||||
kontonummer String? @db.VarChar(20)
|
||||
|
||||
bemerkung String? @db.VarChar(500)
|
||||
aktiv Boolean @default(true)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
geloeschtAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
oberkategorie Buchungskategorie? @relation("Kategoriebaum", fields: [oberkategorieId], references: [id], onDelete: SetNull)
|
||||
unterkategorien Buchungskategorie[] @relation("Kategoriebaum")
|
||||
buchungen Buchung[]
|
||||
|
||||
@@unique([organisationId, name])
|
||||
@@index([organisationId, art])
|
||||
@@map("buchungskategorie")
|
||||
}
|
||||
|
||||
/// Eine Buchung.
|
||||
///
|
||||
/// Der Betrag ist positiv, die Richtung steht in `art`. Festgeschrieben heißt:
|
||||
/// unveränderbar und nicht löschbar — die Datenbank hält dagegen. Wer sich
|
||||
/// vertan hat, bucht dagegen; die Gegenbuchung verweist auf das Original, und
|
||||
/// beide bleiben stehen.
|
||||
model Buchung {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
kontoId String @db.Uuid
|
||||
kategorieId String @db.Uuid
|
||||
|
||||
/// Das Buchungsdatum, nicht der Zeitpunkt der Erfassung. Danach richtet
|
||||
/// sich, in welchen Abschluss die Buchung fällt.
|
||||
datum DateTime @db.Date
|
||||
art BuchungsArt
|
||||
betrag Decimal @db.Decimal(12, 2)
|
||||
|
||||
verwendungszweck String @db.VarChar(300)
|
||||
belegnummer String? @db.VarChar(60)
|
||||
|
||||
/// Wer das Geld bekommen oder gegeben hat. Entweder eine Person der Wehr
|
||||
/// **oder** ein Dritter als Text — nie beides.
|
||||
personId String? @db.Uuid
|
||||
lieferant String? @db.VarChar(200)
|
||||
|
||||
/// Bei einer Umbuchung zwischen zwei eigenen Konten das Gegenkonto.
|
||||
gegenkontoId String? @db.Uuid
|
||||
|
||||
belegDateiId String? @db.Uuid
|
||||
|
||||
/// Festgeschrieben: ab hier unveränderbar.
|
||||
festgeschriebenAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
/// Die Buchung, die diese hier aufhebt — und umgekehrt.
|
||||
storniertVonId String? @db.Uuid
|
||||
|
||||
erfasstVonId String? @db.Uuid
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
konto Kassenkonto @relation(fields: [kontoId], references: [id], onDelete: Restrict)
|
||||
kategorie Buchungskategorie @relation(fields: [kategorieId], references: [id], onDelete: Restrict)
|
||||
person Person? @relation("BuchungPerson", fields: [personId], references: [id], onDelete: SetNull)
|
||||
erfasstVon Person? @relation("BuchungErfasser", fields: [erfasstVonId], references: [id], onDelete: SetNull)
|
||||
belegDatei Datei? @relation("BuchungBeleg", fields: [belegDateiId], references: [id], onDelete: SetNull)
|
||||
storniertVon Buchung? @relation("Stornierung", fields: [storniertVonId], references: [id], onDelete: SetNull)
|
||||
storniert Buchung[] @relation("Stornierung")
|
||||
beitraege Beitrag[]
|
||||
rechnungen Rechnung[]
|
||||
belegentwurf Belegentwurf[]
|
||||
|
||||
@@index([organisationId, datum(sort: Desc)])
|
||||
@@index([organisationId, kontoId, datum])
|
||||
@@index([organisationId, kategorieId])
|
||||
@@map("buchung")
|
||||
}
|
||||
|
||||
/// Was ein Mitglied im Jahr zahlt.
|
||||
///
|
||||
/// Je Abteilung ein Satz: Aktive zahlen anderes als Jugend, Ehrenmitglieder
|
||||
/// oft nichts. Der Satz gilt ab einem Jahr und wird nicht rückwirkend
|
||||
/// geändert — sonst stimmen die Beiträge vergangener Jahre nicht mehr.
|
||||
model Beitragssatz {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
bezeichnung String @db.VarChar(120)
|
||||
betrag Decimal @db.Decimal(12, 2)
|
||||
|
||||
/// Für welche Abteilung. Leer heißt: für alle, die keinen eigenen Satz haben.
|
||||
abteilung String? @db.VarChar(60)
|
||||
|
||||
/// Ab welchem Jahr er gilt.
|
||||
abJahr Int
|
||||
|
||||
aktiv Boolean @default(true)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
beitraege Beitrag[]
|
||||
|
||||
@@unique([organisationId, abteilung, abJahr])
|
||||
@@index([organisationId, abJahr])
|
||||
@@map("beitragssatz")
|
||||
}
|
||||
|
||||
/// Der Beitrag einer Person für ein Jahr.
|
||||
model Beitrag {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
personId String @db.Uuid
|
||||
jahr Int
|
||||
|
||||
satzId String? @db.Uuid
|
||||
betrag Decimal @db.Decimal(12, 2)
|
||||
|
||||
stand BeitragStand @default(OFFEN)
|
||||
|
||||
/// Die Buchung, mit der bezahlt wurde. Erst sie macht aus „bezahlt" eine
|
||||
/// Zahl in der Kasse.
|
||||
buchungId String? @db.Uuid
|
||||
bezahltAm DateTime? @db.Date
|
||||
|
||||
/// Wie oft gemahnt wurde. Null heißt: noch nicht.
|
||||
mahnstufe Int @default(0)
|
||||
gemahntAm DateTime? @db.Date
|
||||
/// Bei Erlass: warum. Ein erlassener Beitrag ohne Begründung ist in der
|
||||
/// Kassenprüfung eine offene Frage.
|
||||
erlassGrund String? @db.VarChar(300)
|
||||
|
||||
bemerkung String? @db.VarChar(500)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
person Person @relation("BeitragPerson", fields: [personId], references: [id], onDelete: Cascade)
|
||||
satz Beitragssatz? @relation(fields: [satzId], references: [id], onDelete: SetNull)
|
||||
buchung Buchung? @relation(fields: [buchungId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([personId, jahr])
|
||||
@@index([organisationId, jahr, stand])
|
||||
@@map("beitrag")
|
||||
}
|
||||
|
||||
/// Eine Rechnung an Dritte.
|
||||
///
|
||||
/// Fehlalarm, Ölspur, Absicherung einer Veranstaltung. Nummer aus dem
|
||||
/// Nummernkreis (`JJJJ-NNNN`), PDF über Gotenberg.
|
||||
model Rechnung {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
nummer String @db.VarChar(20)
|
||||
anlass RechnungsAnlass @default(SONSTIGES)
|
||||
stand RechnungStand @default(ENTWURF)
|
||||
|
||||
/// An wen. Bewusst als Text und nicht als Verweis: der Empfänger ist in
|
||||
/// aller Regel niemand, den die Wehr dauerhaft führt.
|
||||
empfaenger String @db.VarChar(200)
|
||||
empfaengerZusatz String? @db.VarChar(200)
|
||||
strasse String? @db.VarChar(200)
|
||||
plz String? @db.VarChar(10)
|
||||
ort String? @db.VarChar(120)
|
||||
|
||||
/// Worauf sie sich bezieht.
|
||||
einsatzId String? @db.Uuid
|
||||
|
||||
datum DateTime @db.Date
|
||||
faelligAm DateTime @db.Date
|
||||
|
||||
/// Summe der Positionen. Wird beim Speichern gerechnet, nicht eingegeben.
|
||||
betrag Decimal @db.Decimal(12, 2)
|
||||
|
||||
bemerkung String? @db.VarChar(1000)
|
||||
|
||||
/// Die Buchung des Zahlungseingangs.
|
||||
buchungId String? @db.Uuid
|
||||
bezahltAm DateTime? @db.Date
|
||||
gemahntAm DateTime? @db.Date
|
||||
mahnstufe Int @default(0)
|
||||
|
||||
pdfDateiId String? @db.Uuid
|
||||
|
||||
erstelltVonId String? @db.Uuid
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
buchung Buchung? @relation(fields: [buchungId], references: [id], onDelete: SetNull)
|
||||
pdfDatei Datei? @relation("RechnungPdf", fields: [pdfDateiId], references: [id], onDelete: SetNull)
|
||||
erstelltVon Person? @relation("RechnungErsteller", fields: [erstelltVonId], references: [id], onDelete: SetNull)
|
||||
positionen Rechnungsposition[]
|
||||
|
||||
@@unique([organisationId, nummer])
|
||||
@@index([organisationId, stand])
|
||||
@@index([organisationId, datum(sort: Desc)])
|
||||
@@map("rechnung")
|
||||
}
|
||||
|
||||
/// Eine Zeile auf einer Rechnung.
|
||||
model Rechnungsposition {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
rechnungId String @db.Uuid
|
||||
|
||||
reihenfolge Int
|
||||
bezeichnung String @db.VarChar(300)
|
||||
/// Menge mit drei Nachkommastellen — Einsatzstunden werden viertelstündlich
|
||||
/// abgerechnet, und 0,25 muss genau sein.
|
||||
menge Decimal @default(1) @db.Decimal(12, 3)
|
||||
einheit String? @db.VarChar(20)
|
||||
einzelpreis Decimal @db.Decimal(12, 2)
|
||||
/// Menge × Einzelpreis, beim Speichern gerechnet.
|
||||
gesamtpreis Decimal @db.Decimal(12, 2)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
rechnung Rechnung @relation(fields: [rechnungId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([rechnungId, reihenfolge])
|
||||
@@index([organisationId])
|
||||
@@map("rechnungsposition")
|
||||
}
|
||||
|
||||
/// Ein Kassenabschluss.
|
||||
///
|
||||
/// Der Vorgang, für den es die ganze Kasse gibt: zwei Kassenprüfer setzen sich
|
||||
/// hin, zählen das Bargeld, vergleichen die Kontoauszüge und unterschreiben.
|
||||
/// Danach ist der Zeitraum zu — die Buchungsmaske nimmt kein Datum daraus mehr
|
||||
/// an.
|
||||
///
|
||||
/// Zwei Prüfer, weil es die Satzung fast jeder Wehr so vorsieht. Der zweite
|
||||
/// ist trotzdem optional: eine Wehr, die gerade nur einen hat, soll deshalb
|
||||
/// nicht ohne Abschluss dastehen.
|
||||
model Kassenabschluss {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
|
||||
vonAm DateTime @db.Date
|
||||
bisAm DateTime @db.Date
|
||||
|
||||
/// Summe über alle Konten, aus den Buchungen gerechnet.
|
||||
bestandLautBuch Decimal @db.Decimal(12, 2)
|
||||
/// Was tatsächlich gezählt wurde.
|
||||
gezaehlterBestand Decimal @db.Decimal(12, 2)
|
||||
/// gezaehlt minus laut Buch. Positiv heißt: es war mehr da.
|
||||
differenz Decimal @db.Decimal(12, 2)
|
||||
|
||||
/// Eine Differenz ohne Erklärung ist der Punkt, an dem eine Kassenprüfung
|
||||
/// hängen bleibt.
|
||||
erlaeuterung String? @db.VarChar(2000)
|
||||
|
||||
prueferId String? @db.Uuid
|
||||
zweiterPrueferId String? @db.Uuid
|
||||
geprueftAm DateTime? @db.Date
|
||||
|
||||
berichtDateiId String? @db.Uuid
|
||||
|
||||
/// Festgeschrieben: ab hier ist der Zeitraum zu.
|
||||
festgeschriebenAm DateTime? @db.Timestamptz(3)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
geaendertAm DateTime @updatedAt @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
pruefer Person? @relation("AbschlussPruefer", fields: [prueferId], references: [id], onDelete: SetNull)
|
||||
zweiterPruefer Person? @relation("AbschlussZweitpruefer", fields: [zweiterPrueferId], references: [id], onDelete: SetNull)
|
||||
berichtDatei Datei? @relation("AbschlussBericht", fields: [berichtDateiId], references: [id], onDelete: SetNull)
|
||||
posten Abschlussposten[]
|
||||
|
||||
@@index([organisationId, bisAm(sort: Desc)])
|
||||
@@map("kassenabschluss")
|
||||
}
|
||||
|
||||
/// Was auf einem Konto zum Abschluss stand.
|
||||
///
|
||||
/// Je Konto eine Zeile: der Kassenprüfer zählt das Bargeld und liest den
|
||||
/// Kontostand vom Auszug ab. Eine einzelne Summe über alles würde verbergen,
|
||||
/// auf welchem Konto die Differenz sitzt.
|
||||
model Abschlussposten {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
organisationId String @db.Uuid
|
||||
abschlussId String @db.Uuid
|
||||
kontoId String @db.Uuid
|
||||
|
||||
bestandLautBuch Decimal @db.Decimal(12, 2)
|
||||
gezaehlterBestand Decimal @db.Decimal(12, 2)
|
||||
differenz Decimal @db.Decimal(12, 2)
|
||||
|
||||
bemerkung String? @db.VarChar(500)
|
||||
|
||||
erstelltAm DateTime @default(now()) @db.Timestamptz(3)
|
||||
|
||||
organisation Organisation @relation(fields: [organisationId], references: [id], onDelete: Cascade)
|
||||
abschluss Kassenabschluss @relation(fields: [abschlussId], references: [id], onDelete: Cascade)
|
||||
konto Kassenkonto @relation(fields: [kontoId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@unique([abschlussId, kontoId])
|
||||
@@index([organisationId])
|
||||
@@map("abschlussposten")
|
||||
}
|
||||
|
||||
+653
@@ -21,6 +21,9 @@
|
||||
* Zustellkanäle: niemand wird ungefragt benachrichtigt, auch nicht im Seed.
|
||||
* Ab Phase 8 dazu: vier Wiki-Seiten mit eigenem Text — Prüffristen,
|
||||
* Atemschutz-Merkblatt, Schlauchpflege und Alarmierungsordnung.
|
||||
* Ab Phase 9 dazu: das Kassenjahr — drei Konten, der Standardkontenrahmen,
|
||||
* gut dreißig Buchungen über zwei Jahre, die Mitgliedsbeiträge mit Rückständen,
|
||||
* zwei Rechnungen und die festgeschriebene Kassenprüfung des Vorjahres.
|
||||
*
|
||||
* Das Skript läuft über DIRECT_DATABASE_URL, also mit dem Eigentümer-Zugang,
|
||||
* der Row Level Security umgehen darf. Anders ginge es nicht: der Seed legt
|
||||
@@ -36,11 +39,14 @@ import {
|
||||
Alarmierungsart,
|
||||
AnwesenheitAnlass,
|
||||
AusweisTokenTyp,
|
||||
BeitragStand,
|
||||
BuchungsArt,
|
||||
ChecklistenpunktTyp,
|
||||
EinsatzFunktion,
|
||||
EinsatzKategorie,
|
||||
EinsatzStatus,
|
||||
Dienstgrad,
|
||||
KontoArt,
|
||||
DruckArt,
|
||||
EreignisArt,
|
||||
FahrtZweck,
|
||||
@@ -58,6 +64,8 @@ import {
|
||||
PruefungStatus,
|
||||
QualifikationsArt,
|
||||
RolleSchluessel,
|
||||
RechnungsAnlass,
|
||||
RechnungStand,
|
||||
SignaturRolle,
|
||||
HydrantStatus,
|
||||
HydrantTyp,
|
||||
@@ -3247,6 +3255,648 @@ async function wikiAnlegen(organisationId: string): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Phase 9 — Kassenverwaltung
|
||||
//
|
||||
// Ein Kassenjahr, wie es eine Wehr wirklich hat: drei Konten, der
|
||||
// Standardkontenrahmen, gut dreißig Buchungen über zwei Jahre, die
|
||||
// Mitgliedsbeiträge, zwei Rechnungen und die abgeschlossene Kassenprüfung
|
||||
// des Vorjahres.
|
||||
//
|
||||
// Die Reihenfolge ist hier nicht beliebig, und das ist kein Zufall des
|
||||
// Skripts, sondern die Absicherung selbst: sobald der Abschluss für das
|
||||
// Vorjahr festgeschrieben ist, lehnt ein Trigger jede weitere Buchung mit
|
||||
// einem Datum in diesem Zeitraum ab. Erst die Buchungen, dann der Abschluss —
|
||||
// wer es umdreht, bekommt vom Seed dieselbe Fehlermeldung wie der Kassier,
|
||||
// der drei Wochen nach der Prüfung noch eine Quittung findet.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
async function kassendatenAnlegen(organisationId: string): Promise<void> {
|
||||
const vorhanden = await prisma.kassenkonto.count({ where: { organisationId } });
|
||||
if (vorhanden > 0) {
|
||||
log("Kassendaten bereits vorhanden — übersprungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
const { STANDARDKATEGORIEN } = await import("../src/lib/kasse/vorschlag");
|
||||
|
||||
const jahr = new Date().getUTCFullYear();
|
||||
const vorjahr = jahr - 1;
|
||||
|
||||
// --- Konten ---------------------------------------------------------------
|
||||
|
||||
const bar = await prisma.kassenkonto.create({
|
||||
data: {
|
||||
organisationId,
|
||||
bezeichnung: "Barkasse",
|
||||
art: KontoArt.BAR,
|
||||
anfangsbestand: "250.00",
|
||||
bemerkung: "Kassette im Kommandantenzimmer. Wechselgeld für Feste.",
|
||||
},
|
||||
});
|
||||
|
||||
const giro = await prisma.kassenkonto.create({
|
||||
data: {
|
||||
organisationId,
|
||||
bezeichnung: "Girokonto Raiffeisenbank",
|
||||
art: KontoArt.BANK,
|
||||
iban: "DE02701500000000594937",
|
||||
bic: "SSKMDEMM",
|
||||
anfangsbestand: "4800.00",
|
||||
},
|
||||
});
|
||||
|
||||
const ruecklage = await prisma.kassenkonto.create({
|
||||
data: {
|
||||
organisationId,
|
||||
bezeichnung: "Rücklage Fahrzeugbeschaffung",
|
||||
art: KontoArt.RUECKLAGE,
|
||||
iban: "DE02120300000000202051",
|
||||
anfangsbestand: "12000.00",
|
||||
bemerkung: "Angespart für den Ersatz des MTW. Wird nicht laufend bewegt.",
|
||||
},
|
||||
});
|
||||
|
||||
// --- Kontenrahmen ---------------------------------------------------------
|
||||
//
|
||||
// Zwei Durchgänge: erst die Oberkategorien, dann die Unterkategorien mit
|
||||
// ihrem Verweis. Anders ginge es nicht, der Baum verweist auf sich selbst.
|
||||
|
||||
const kategorien = new Map<string, string>();
|
||||
|
||||
for (const eintrag of STANDARDKATEGORIEN.filter((k) => !k.oberkategorie)) {
|
||||
const angelegt = await prisma.buchungskategorie.create({
|
||||
data: { organisationId, name: eintrag.name, art: eintrag.art },
|
||||
});
|
||||
kategorien.set(eintrag.name, angelegt.id);
|
||||
}
|
||||
|
||||
for (const eintrag of STANDARDKATEGORIEN.filter((k) => k.oberkategorie)) {
|
||||
const angelegt = await prisma.buchungskategorie.create({
|
||||
data: {
|
||||
organisationId,
|
||||
name: eintrag.name,
|
||||
art: eintrag.art,
|
||||
oberkategorieId: kategorien.get(eintrag.oberkategorie!) ?? null,
|
||||
},
|
||||
});
|
||||
kategorien.set(eintrag.name, angelegt.id);
|
||||
}
|
||||
|
||||
const kategorie = (name: string): string => {
|
||||
const id = kategorien.get(name);
|
||||
if (!id) throw new Error(`Kategorie fehlt im Standardkontenrahmen: ${name}`);
|
||||
return id;
|
||||
};
|
||||
|
||||
const kassier = await prisma.person.findFirst({
|
||||
where: {
|
||||
organisationId,
|
||||
rollen: { some: { rolle: { schluessel: RolleSchluessel.KASSIER } } },
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// --- Buchungen ------------------------------------------------------------
|
||||
//
|
||||
// Das Vorjahr vollständig und festgeschrieben, das laufende Jahr bis in den
|
||||
// Sommer und noch offen. So zeigt die Ansicht beide Zustände: das
|
||||
// geschlossene Buch und das, in dem noch gearbeitet wird.
|
||||
|
||||
type Zeile = {
|
||||
konto: string;
|
||||
monat: number;
|
||||
tag: number;
|
||||
art: BuchungsArt;
|
||||
betrag: string;
|
||||
kategorie: string;
|
||||
zweck: string;
|
||||
gegenueber?: string;
|
||||
beleg?: string;
|
||||
};
|
||||
|
||||
const vorjahrszeilen: Zeile[] = [
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 1,
|
||||
tag: 15,
|
||||
art: BuchungsArt.EINNAHME,
|
||||
betrag: "6500.00",
|
||||
kategorie: "Zuschüsse",
|
||||
zweck: "Gemeindezuschuss laufender Betrieb",
|
||||
gegenueber: "Gemeinde Musterhausen",
|
||||
beleg: "Z-2024-01",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 2,
|
||||
tag: 3,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "1284.50",
|
||||
kategorie: "Versicherungen",
|
||||
zweck: "Jahresprämie Fahrzeug- und Unfallversicherung",
|
||||
gegenueber: "Bayerische Versicherungskammer",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 3,
|
||||
tag: 12,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "412.90",
|
||||
kategorie: "Kraftstoff",
|
||||
zweck: "Diesel LF 20, Sammelrechnung Februar",
|
||||
gegenueber: "Tankstelle Musterhausen",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 4,
|
||||
tag: 8,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "2340.00",
|
||||
kategorie: "Atemschutz",
|
||||
zweck: "Zwei Pressluftatmer, Ersatzbeschaffung",
|
||||
gegenueber: "Dräger Safety",
|
||||
beleg: "RE-2024-114",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 5,
|
||||
tag: 22,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "868.00",
|
||||
kategorie: "Prüfungen",
|
||||
zweck: "Wiederkehrende Prüfung Drehleiter",
|
||||
gegenueber: "TÜV Süd",
|
||||
},
|
||||
{
|
||||
konto: bar.id,
|
||||
monat: 6,
|
||||
tag: 15,
|
||||
art: BuchungsArt.EINNAHME,
|
||||
betrag: "3860.00",
|
||||
kategorie: "Veranstaltungen",
|
||||
zweck: "Maifest, Getränke- und Speisenverkauf",
|
||||
},
|
||||
{
|
||||
konto: bar.id,
|
||||
monat: 6,
|
||||
tag: 15,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "1420.60",
|
||||
kategorie: "Verpflegung",
|
||||
zweck: "Maifest, Einkauf Getränke und Fleisch",
|
||||
gegenueber: "Getränke Hofmann",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 7,
|
||||
tag: 2,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "1150.00",
|
||||
kategorie: "Schutzkleidung",
|
||||
zweck: "Vier Einsatzjacken, Ersatz nach Verschleiß",
|
||||
gegenueber: "Feuerwehrbedarf Barth",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 9,
|
||||
tag: 18,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "624.00",
|
||||
kategorie: "Ausbildung",
|
||||
zweck: "Lehrgangsgebühren Maschinistenlehrgang",
|
||||
gegenueber: "Landkreis Musterkreis",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 10,
|
||||
tag: 30,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "398.20",
|
||||
kategorie: "Wasser und Strom",
|
||||
zweck: "Abschlagszahlung Strom Gerätehaus",
|
||||
gegenueber: "Stadtwerke Musterhausen",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 11,
|
||||
tag: 14,
|
||||
art: BuchungsArt.EINNAHME,
|
||||
betrag: "780.00",
|
||||
kategorie: "Spenden",
|
||||
zweck: "Spenden aus der Haussammlung",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 12,
|
||||
tag: 20,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "36.00",
|
||||
kategorie: "Bankgebühren",
|
||||
zweck: "Kontoführung viertes Quartal",
|
||||
},
|
||||
];
|
||||
|
||||
const laufendeZeilen: Zeile[] = [
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 1,
|
||||
tag: 20,
|
||||
art: BuchungsArt.EINNAHME,
|
||||
betrag: "6500.00",
|
||||
kategorie: "Zuschüsse",
|
||||
zweck: "Gemeindezuschuss laufender Betrieb",
|
||||
gegenueber: "Gemeinde Musterhausen",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 2,
|
||||
tag: 5,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "1341.80",
|
||||
kategorie: "Versicherungen",
|
||||
zweck: "Jahresprämie Fahrzeug- und Unfallversicherung",
|
||||
gegenueber: "Bayerische Versicherungskammer",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 2,
|
||||
tag: 28,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "487.40",
|
||||
kategorie: "Kraftstoff",
|
||||
zweck: "Diesel LF 20 und MTW, Sammelrechnung Februar",
|
||||
gegenueber: "Tankstelle Musterhausen",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 3,
|
||||
tag: 14,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "1890.00",
|
||||
kategorie: "Reparatur",
|
||||
zweck: "Kupplung MTW, Werkstattrechnung",
|
||||
gegenueber: "Werkstatt Müller GmbH",
|
||||
beleg: "RE-4471",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 4,
|
||||
tag: 3,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "212.00",
|
||||
kategorie: "Verwaltung",
|
||||
zweck: "Büromaterial und Druckerpatronen",
|
||||
gegenueber: "Schreibwaren Ludwig",
|
||||
},
|
||||
{
|
||||
konto: bar.id,
|
||||
monat: 5,
|
||||
tag: 1,
|
||||
art: BuchungsArt.EINNAHME,
|
||||
betrag: "4120.00",
|
||||
kategorie: "Veranstaltungen",
|
||||
zweck: "Maifest, Getränke- und Speisenverkauf",
|
||||
},
|
||||
{
|
||||
konto: bar.id,
|
||||
monat: 5,
|
||||
tag: 1,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "1588.30",
|
||||
kategorie: "Verpflegung",
|
||||
zweck: "Maifest, Einkauf Getränke und Fleisch",
|
||||
gegenueber: "Getränke Hofmann",
|
||||
},
|
||||
{
|
||||
konto: bar.id,
|
||||
monat: 5,
|
||||
tag: 6,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "2500.00",
|
||||
kategorie: "Verwaltung",
|
||||
zweck: "Einzahlung Barkasse auf das Girokonto nach dem Maifest",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 5,
|
||||
tag: 6,
|
||||
art: BuchungsArt.EINNAHME,
|
||||
betrag: "2500.00",
|
||||
kategorie: "Veranstaltungen",
|
||||
zweck: "Einzahlung aus der Barkasse nach dem Maifest",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 6,
|
||||
tag: 11,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "742.00",
|
||||
kategorie: "Prüfungen",
|
||||
zweck: "Schlauchprüfung, 40 Stück B und C",
|
||||
gegenueber: "Schlauchpflege Nordbayern",
|
||||
},
|
||||
{
|
||||
konto: giro.id,
|
||||
monat: 6,
|
||||
tag: 25,
|
||||
art: BuchungsArt.AUSGABE,
|
||||
betrag: "36.00",
|
||||
kategorie: "Bankgebühren",
|
||||
zweck: "Kontoführung zweites Quartal",
|
||||
},
|
||||
{
|
||||
konto: ruecklage.id,
|
||||
monat: 7,
|
||||
tag: 1,
|
||||
art: BuchungsArt.EINNAHME,
|
||||
betrag: "3000.00",
|
||||
kategorie: "Zuschüsse",
|
||||
zweck: "Jährliche Zuführung zur Fahrzeugrücklage",
|
||||
gegenueber: "Gemeinde Musterhausen",
|
||||
},
|
||||
];
|
||||
|
||||
async function buchungAnlegen(zeile: Zeile, festgeschrieben: boolean, buchungsjahr: number) {
|
||||
return prisma.buchung.create({
|
||||
data: {
|
||||
organisationId,
|
||||
kontoId: zeile.konto,
|
||||
kategorieId: kategorie(zeile.kategorie),
|
||||
datum: datum(buchungsjahr, zeile.monat, zeile.tag),
|
||||
art: zeile.art,
|
||||
betrag: zeile.betrag,
|
||||
verwendungszweck: zeile.zweck,
|
||||
lieferant: zeile.gegenueber ?? null,
|
||||
belegnummer: zeile.beleg ?? null,
|
||||
erfasstVonId: kassier?.id ?? null,
|
||||
festgeschriebenAm: festgeschrieben ? datum(vorjahr + 1, 2, 10) : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const zeile of vorjahrszeilen) {
|
||||
await buchungAnlegen(zeile, true, vorjahr);
|
||||
}
|
||||
for (const zeile of laufendeZeilen) {
|
||||
await buchungAnlegen(zeile, false, jahr);
|
||||
}
|
||||
|
||||
// --- Beiträge -------------------------------------------------------------
|
||||
|
||||
const satzAktive = await prisma.beitragssatz.create({
|
||||
data: {
|
||||
organisationId,
|
||||
bezeichnung: "Aktive und Passive",
|
||||
betrag: "25.00",
|
||||
abteilung: null,
|
||||
abJahr: vorjahr,
|
||||
},
|
||||
});
|
||||
|
||||
const satzJugend = await prisma.beitragssatz.create({
|
||||
data: {
|
||||
organisationId,
|
||||
bezeichnung: "Jugendfeuerwehr",
|
||||
betrag: "10.00",
|
||||
abteilung: "Jugendfeuerwehr",
|
||||
abJahr: vorjahr,
|
||||
},
|
||||
});
|
||||
|
||||
const mitglieder = await prisma.person.findMany({
|
||||
where: { organisationId, aktiv: true, austrittsdatum: null },
|
||||
orderBy: { mitgliedsnummer: "asc" },
|
||||
select: { id: true, abteilung: true },
|
||||
});
|
||||
|
||||
const beitragsKategorie = kategorie("Mitgliedsbeiträge");
|
||||
|
||||
// Die meisten haben bezahlt, zwei stehen offen, einer ist gemahnt, einem
|
||||
// wurde erlassen. Das ist die Verteilung, die eine Wehr im Herbst wirklich
|
||||
// vor sich hat — und der Grund, warum es die Rückstandsliste gibt.
|
||||
for (const [stelle, person] of mitglieder.entries()) {
|
||||
const jugend = person.abteilung === "Jugendfeuerwehr";
|
||||
const satz = jugend ? satzJugend : satzAktive;
|
||||
const betrag = jugend ? "10.00" : "25.00";
|
||||
|
||||
let buchungId: string | null = null;
|
||||
let stand: BeitragStand = BeitragStand.BEZAHLT;
|
||||
let bezahltAm: Date | null = datum(jahr, 2, 12 + (stelle % 10));
|
||||
let mahnstufe = 0;
|
||||
let gemahntAm: Date | null = null;
|
||||
let erlassGrund: string | null = null;
|
||||
|
||||
if (stelle % 7 === 3) {
|
||||
stand = BeitragStand.OFFEN;
|
||||
bezahltAm = null;
|
||||
} else if (stelle % 11 === 5) {
|
||||
stand = BeitragStand.GEMAHNT;
|
||||
bezahltAm = null;
|
||||
mahnstufe = 1;
|
||||
gemahntAm = datum(jahr, 6, 30);
|
||||
} else if (stelle % 13 === 9) {
|
||||
stand = BeitragStand.ERLASSEN;
|
||||
bezahltAm = null;
|
||||
erlassGrund = "Längere Erkrankung, Beschluss der Kommandantschaft vom 12.03.";
|
||||
}
|
||||
|
||||
if (stand === BeitragStand.BEZAHLT && bezahltAm) {
|
||||
const buchung = await prisma.buchung.create({
|
||||
data: {
|
||||
organisationId,
|
||||
kontoId: giro.id,
|
||||
kategorieId: beitragsKategorie,
|
||||
datum: bezahltAm,
|
||||
art: BuchungsArt.EINNAHME,
|
||||
betrag,
|
||||
verwendungszweck: `Mitgliedsbeitrag ${jahr}`,
|
||||
personId: person.id,
|
||||
erfasstVonId: kassier?.id ?? null,
|
||||
},
|
||||
});
|
||||
buchungId = buchung.id;
|
||||
}
|
||||
|
||||
await prisma.beitrag.create({
|
||||
data: {
|
||||
organisationId,
|
||||
personId: person.id,
|
||||
jahr,
|
||||
satzId: satz.id,
|
||||
betrag,
|
||||
stand,
|
||||
buchungId,
|
||||
bezahltAm,
|
||||
mahnstufe,
|
||||
gemahntAm,
|
||||
erlassGrund,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- Rechnungen -----------------------------------------------------------
|
||||
|
||||
const kostenersatz = kategorie("Kostenersatz");
|
||||
|
||||
const fehlalarmBuchung = await prisma.buchung.create({
|
||||
data: {
|
||||
organisationId,
|
||||
kontoId: giro.id,
|
||||
kategorieId: kostenersatz,
|
||||
datum: datum(jahr, 4, 28),
|
||||
art: BuchungsArt.EINNAHME,
|
||||
betrag: "612.00",
|
||||
verwendungszweck: `Zahlungseingang Rechnung ${jahr}-0001`,
|
||||
lieferant: "Möbelhaus Sonnleitner GmbH",
|
||||
erfasstVonId: kassier?.id ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.rechnung.create({
|
||||
data: {
|
||||
organisationId,
|
||||
nummer: `${jahr}-0001`,
|
||||
anlass: RechnungsAnlass.FEHLALARM,
|
||||
stand: RechnungStand.BEZAHLT,
|
||||
empfaenger: "Möbelhaus Sonnleitner GmbH",
|
||||
strasse: "Industriestraße 14",
|
||||
plz: "91315",
|
||||
ort: "Musterhausen",
|
||||
datum: datum(jahr, 3, 28),
|
||||
faelligAm: datum(jahr, 4, 25),
|
||||
betrag: "612.00",
|
||||
bemerkung:
|
||||
"Fehlalarm der Brandmeldeanlage am 26.03., verursacht durch Staub bei Umbauarbeiten. " +
|
||||
"Kostenersatz nach Art. 28 Abs. 2 Nr. 4 BayFwG.",
|
||||
buchungId: fehlalarmBuchung.id,
|
||||
bezahltAm: datum(jahr, 4, 28),
|
||||
erstelltVonId: kassier?.id ?? null,
|
||||
positionen: {
|
||||
create: [
|
||||
{
|
||||
organisationId,
|
||||
reihenfolge: 0,
|
||||
bezeichnung: "Einsatz LF 20, angefangene Stunde",
|
||||
menge: "1",
|
||||
einheit: "Std.",
|
||||
einzelpreis: "396.00",
|
||||
gesamtpreis: "396.00",
|
||||
},
|
||||
{
|
||||
organisationId,
|
||||
reihenfolge: 1,
|
||||
bezeichnung: "Einsatzkräfte, je angefangene Stunde",
|
||||
menge: "9",
|
||||
einheit: "Pers.",
|
||||
einzelpreis: "24.00",
|
||||
gesamtpreis: "216.00",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.rechnung.create({
|
||||
data: {
|
||||
organisationId,
|
||||
nummer: `${jahr}-0002`,
|
||||
anlass: RechnungsAnlass.VERMIETUNG,
|
||||
stand: RechnungStand.VERSENDET,
|
||||
empfaenger: "Trachtenverein Almrausch Musterhausen e. V.",
|
||||
empfaengerZusatz: "z. Hd. Frau Vorstand Berger",
|
||||
strasse: "Dorfplatz 2",
|
||||
plz: "91315",
|
||||
ort: "Musterhausen",
|
||||
datum: datum(jahr, 6, 20),
|
||||
faelligAm: datum(jahr, 7, 18),
|
||||
betrag: "180.00",
|
||||
bemerkung: "Überlassung des Schulungsraums für die Jahresversammlung am 14.06.",
|
||||
erstelltVonId: kassier?.id ?? null,
|
||||
positionen: {
|
||||
create: [
|
||||
{
|
||||
organisationId,
|
||||
reihenfolge: 0,
|
||||
bezeichnung: "Nutzung Schulungsraum, Tagespauschale",
|
||||
menge: "1",
|
||||
einheit: "Tag",
|
||||
einzelpreis: "150.00",
|
||||
gesamtpreis: "150.00",
|
||||
},
|
||||
{
|
||||
organisationId,
|
||||
reihenfolge: 1,
|
||||
bezeichnung: "Reinigung",
|
||||
menge: "1",
|
||||
einheit: "pauschal",
|
||||
einzelpreis: "30.00",
|
||||
gesamtpreis: "30.00",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// --- Kassenprüfung des Vorjahres -----------------------------------------
|
||||
//
|
||||
// Zuletzt, und das ist der Punkt: ab hier ist der Zeitraum zu. Jede weitere
|
||||
// Buchung mit einem Datum aus dem Vorjahr wird von der Datenbank abgelehnt.
|
||||
|
||||
const pruefer = await prisma.person.findMany({
|
||||
where: { organisationId, aktiv: true, austrittsdatum: null },
|
||||
orderBy: { mitgliedsnummer: "desc" },
|
||||
take: 2,
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// Die Zahlen sind nicht erfunden, sondern die Summe der Buchungen oben:
|
||||
// Barkasse 250,00 + 3.860,00 − 1.420,60, Giro 4.800,00 + 7.280,00 − 7.113,60,
|
||||
// Rücklage unberührt. Ein Seed mit erfundenen Beständen wäre genau der
|
||||
// Fall, den die Kassenprüfung finden soll.
|
||||
const vorjahrsposten = [
|
||||
{ konto: bar.id, buch: "2689.40", gezaehlt: "2686.20" },
|
||||
{ konto: giro.id, buch: "4966.40", gezaehlt: "4966.40" },
|
||||
{ konto: ruecklage.id, buch: "12000.00", gezaehlt: "12000.00" },
|
||||
];
|
||||
|
||||
const abschluss = await prisma.kassenabschluss.create({
|
||||
data: {
|
||||
organisationId,
|
||||
vonAm: datum(vorjahr, 1, 1),
|
||||
bisAm: datum(vorjahr, 12, 31),
|
||||
bestandLautBuch: "19655.80",
|
||||
gezaehlterBestand: "19652.60",
|
||||
differenz: "-3.20",
|
||||
erlaeuterung:
|
||||
"In der Barkasse fehlen 3,20 €. Zurückzuführen auf das Maifest: das Wechselgeld " +
|
||||
"wurde am Abend gezählt, ein Beleg über den Nachkauf von Eis fehlt. Die Prüfer " +
|
||||
"halten den Betrag für erklärbar und schlagen Entlastung vor.",
|
||||
prueferId: pruefer[0]?.id ?? null,
|
||||
zweiterPrueferId: pruefer[1]?.id ?? null,
|
||||
geprueftAm: datum(vorjahr + 1, 2, 8),
|
||||
festgeschriebenAm: datum(vorjahr + 1, 2, 10),
|
||||
posten: {
|
||||
create: vorjahrsposten.map((posten) => ({
|
||||
organisationId,
|
||||
kontoId: posten.konto,
|
||||
bestandLautBuch: posten.buch,
|
||||
gezaehlterBestand: posten.gezaehlt,
|
||||
differenz: (Number(posten.gezaehlt) - Number(posten.buch)).toFixed(2),
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
log(
|
||||
`Kasse: 3 Konten, ${STANDARDKATEGORIEN.length} Kategorien, ` +
|
||||
`${vorjahrszeilen.length + laufendeZeilen.length} Buchungen, ` +
|
||||
`${mitglieder.length} Beiträge, 2 Rechnungen und die Kassenprüfung ${vorjahr} ` +
|
||||
`(festgeschrieben — der Zeitraum ist zu, Abschluss ${abschluss.id.slice(0, 8)})`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Ablauf
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -3293,6 +3943,9 @@ async function main(): Promise<void> {
|
||||
// Phase 8 — Lokale KI
|
||||
await wikiAnlegen(organisation.id);
|
||||
|
||||
// Phase 9 — Kassenverwaltung
|
||||
await kassendatenAnlegen(organisation.id);
|
||||
|
||||
log("Fertig.");
|
||||
log("");
|
||||
log(`Wehr: ${ORGANISATION_NAME} (${ORGANISATION_SLUG})`);
|
||||
|
||||
Reference in New Issue
Block a user