Phase 9: Kassenverwaltung
Kassenbuch, Mitgliedsbeitraege, Rechnungen an Dritte, Kassenpruefung und die Auswertungen fuer die Jahreshauptversammlung. Die drei Festlegungen aus Phase-9.md sitzen in der Datenbank, nicht im Router: eine festgeschriebene Buchung ist unveraenderbar, ein geprueftes Jahr nimmt kein Datum mehr an, und gerechnet wird mit Prisma.Decimal. Korrigiert wird ueber eine Gegenbuchung mit Verweis - beide Buchungen bleiben stehen, damit im Kassenbuch zu sehen ist, dass etwas berichtigt wurde. Neun Modelle, drei Trigger, zwoelf CHECKs, sechs Router, fuenf Ansichten. Dazu der Bankimport fuer CAMT.053 und MT940 ohne Fremdbibliothek, der Kategorievorschlag aus dem eigenen Buchungsbestand und die Bruecke von Phase 8: aus einem gegengezeichneten Beleg wird eine Buchung, das Foto haengt danach an ihr. Nebenbei behoben: die Rechnungsnummer war ein Wettlauf, und eine Organisation liess sich nicht mehr loeschen, sobald sie eine festgeschriebene Buchung hatte - der Trigger kannte den Aufbewahrungsschalter nicht. 562 Unit-Tests und 239 E2E-Tests gruen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d70acad4cd
commit
4165334c5a
+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