|
|
|
|
@@ -6,6 +6,68 @@ import pool from '../config/database';
|
|
|
|
|
import logger from '../utils/logger';
|
|
|
|
|
import fs from 'fs';
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Kategorien (Categories)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
async function getKategorien(haushaltsjahr_id: number) {
|
|
|
|
|
try {
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
`SELECT * FROM buchhaltung_kategorien WHERE haushaltsjahr_id = $1 ORDER BY sortierung, bezeichnung`,
|
|
|
|
|
[haushaltsjahr_id]
|
|
|
|
|
);
|
|
|
|
|
return result.rows;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('BuchhaltungService.getKategorien failed', { error });
|
|
|
|
|
throw new Error('Kategorien konnten nicht geladen werden');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createKategorie(data: { haushaltsjahr_id: number; bezeichnung: string; sortierung?: number }) {
|
|
|
|
|
try {
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
`INSERT INTO buchhaltung_kategorien (haushaltsjahr_id, bezeichnung, sortierung)
|
|
|
|
|
VALUES ($1, $2, $3)
|
|
|
|
|
RETURNING *`,
|
|
|
|
|
[data.haushaltsjahr_id, data.bezeichnung, data.sortierung ?? 0]
|
|
|
|
|
);
|
|
|
|
|
return result.rows[0];
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('BuchhaltungService.createKategorie failed', { error });
|
|
|
|
|
throw new Error('Kategorie konnte nicht erstellt werden');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function updateKategorie(id: number, data: { bezeichnung?: string; sortierung?: number }) {
|
|
|
|
|
try {
|
|
|
|
|
const fields: string[] = [];
|
|
|
|
|
const values: unknown[] = [];
|
|
|
|
|
let idx = 1;
|
|
|
|
|
if (data.bezeichnung !== undefined) { fields.push(`bezeichnung = $${idx++}`); values.push(data.bezeichnung); }
|
|
|
|
|
if (data.sortierung !== undefined) { fields.push(`sortierung = $${idx++}`); values.push(data.sortierung); }
|
|
|
|
|
if (fields.length === 0) throw new Error('Keine Felder zum Aktualisieren');
|
|
|
|
|
values.push(id);
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
`UPDATE buchhaltung_kategorien SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`,
|
|
|
|
|
values
|
|
|
|
|
);
|
|
|
|
|
return result.rows[0] || null;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('BuchhaltungService.updateKategorie failed', { error, id });
|
|
|
|
|
throw new Error('Kategorie konnte nicht aktualisiert werden');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteKategorie(id: number) {
|
|
|
|
|
try {
|
|
|
|
|
await pool.query(`UPDATE buchhaltung_konten SET kategorie_id = NULL WHERE kategorie_id = $1`, [id]);
|
|
|
|
|
await pool.query(`DELETE FROM buchhaltung_kategorien WHERE id = $1`, [id]);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('BuchhaltungService.deleteKategorie failed', { error, id });
|
|
|
|
|
throw new Error('Kategorie konnte nicht gelöscht werden');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Haushaltsjahre (Fiscal Years)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
@@ -285,6 +347,7 @@ async function getKontenTree(haushaltsjahrId: number) {
|
|
|
|
|
`SELECT k.*,
|
|
|
|
|
kt.bezeichnung as konto_typ_bezeichnung, kt.art as konto_typ_art,
|
|
|
|
|
pk.bezeichnung AS parent_bezeichnung,
|
|
|
|
|
k.kategorie_id, kat.bezeichnung AS kategorie_bezeichnung,
|
|
|
|
|
COALESCE(SUM(CASE WHEN t.typ='ausgabe' AND t.ausgaben_typ='gwg' AND t.status='gebucht' THEN t.betrag ELSE 0 END), 0) AS spent_gwg,
|
|
|
|
|
COALESCE(SUM(CASE WHEN t.typ='ausgabe' AND t.ausgaben_typ='anlagen' AND t.status='gebucht' THEN t.betrag ELSE 0 END), 0) AS spent_anlagen,
|
|
|
|
|
COALESCE(SUM(CASE WHEN t.typ='ausgabe' AND t.ausgaben_typ='instandhaltung' AND t.status='gebucht' THEN t.betrag ELSE 0 END), 0) AS spent_instandhaltung,
|
|
|
|
|
@@ -292,9 +355,10 @@ async function getKontenTree(haushaltsjahrId: number) {
|
|
|
|
|
FROM buchhaltung_konten k
|
|
|
|
|
LEFT JOIN buchhaltung_konto_typen kt ON k.konto_typ_id = kt.id
|
|
|
|
|
LEFT JOIN buchhaltung_konten pk ON pk.id = k.parent_id
|
|
|
|
|
LEFT JOIN buchhaltung_kategorien kat ON kat.id = k.kategorie_id
|
|
|
|
|
LEFT JOIN buchhaltung_transaktionen t ON t.konto_id = k.id AND t.haushaltsjahr_id = $1
|
|
|
|
|
WHERE k.haushaltsjahr_id = $1 AND k.aktiv = TRUE
|
|
|
|
|
GROUP BY k.id, kt.bezeichnung, kt.art, pk.bezeichnung
|
|
|
|
|
GROUP BY k.id, kt.bezeichnung, kt.art, pk.bezeichnung, kat.bezeichnung
|
|
|
|
|
ORDER BY k.kontonummer`,
|
|
|
|
|
[haushaltsjahrId]
|
|
|
|
|
);
|
|
|
|
|
@@ -363,11 +427,57 @@ async function getPendingCount(haushaltsjahrId?: number): Promise<number> {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function validateSubPotBudget(
|
|
|
|
|
parentId: number,
|
|
|
|
|
budgetGwg: number,
|
|
|
|
|
budgetAnlagen: number,
|
|
|
|
|
budgetInstandhaltung: number,
|
|
|
|
|
excludeKontoId?: number
|
|
|
|
|
) {
|
|
|
|
|
const parentResult = await pool.query(
|
|
|
|
|
`SELECT budget_gwg, budget_anlagen, budget_instandhaltung FROM buchhaltung_konten WHERE id = $1`,
|
|
|
|
|
[parentId]
|
|
|
|
|
);
|
|
|
|
|
if (!parentResult.rows[0]) return; // parent not found, skip validation
|
|
|
|
|
|
|
|
|
|
const parent = parentResult.rows[0];
|
|
|
|
|
const excludeClause = excludeKontoId ? ` AND id != $2` : '';
|
|
|
|
|
const siblingParams: unknown[] = [parentId];
|
|
|
|
|
if (excludeKontoId) siblingParams.push(excludeKontoId);
|
|
|
|
|
|
|
|
|
|
const siblingResult = await pool.query(
|
|
|
|
|
`SELECT COALESCE(SUM(budget_gwg), 0) AS sum_gwg,
|
|
|
|
|
COALESCE(SUM(budget_anlagen), 0) AS sum_anlagen,
|
|
|
|
|
COALESCE(SUM(budget_instandhaltung), 0) AS sum_instandhaltung
|
|
|
|
|
FROM buchhaltung_konten WHERE parent_id = $1${excludeClause}`,
|
|
|
|
|
siblingParams
|
|
|
|
|
);
|
|
|
|
|
const sibling = siblingResult.rows[0];
|
|
|
|
|
|
|
|
|
|
const checks: { label: string; sum: number; parentBudget: number }[] = [
|
|
|
|
|
{ label: 'GWG', sum: parseFloat(sibling.sum_gwg) + budgetGwg, parentBudget: parseFloat(parent.budget_gwg) },
|
|
|
|
|
{ label: 'Anlagen', sum: parseFloat(sibling.sum_anlagen) + budgetAnlagen, parentBudget: parseFloat(parent.budget_anlagen) },
|
|
|
|
|
{ label: 'Instandhaltung', sum: parseFloat(sibling.sum_instandhaltung) + budgetInstandhaltung, parentBudget: parseFloat(parent.budget_instandhaltung) },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const c of checks) {
|
|
|
|
|
if (c.parentBudget > 0 && c.sum > c.parentBudget) {
|
|
|
|
|
throw Object.assign(
|
|
|
|
|
new Error(`Budget ${c.label}: Summe der Untertöpfe (${c.sum.toFixed(2)} €) übersteigt das übergeordnete Budget (${c.parentBudget.toFixed(2)} €)`),
|
|
|
|
|
{ statusCode: 400 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createKonto(
|
|
|
|
|
data: { haushaltsjahr_id: number; konto_typ_id?: number; kontonummer: string; bezeichnung: string; parent_id?: number | null; budget_gwg?: number; budget_anlagen?: number; budget_instandhaltung?: number; notizen?: string },
|
|
|
|
|
userId: string
|
|
|
|
|
) {
|
|
|
|
|
try {
|
|
|
|
|
if (data.parent_id && (data.budget_gwg || data.budget_anlagen || data.budget_instandhaltung)) {
|
|
|
|
|
await validateSubPotBudget(data.parent_id, data.budget_gwg || 0, data.budget_anlagen || 0, data.budget_instandhaltung || 0);
|
|
|
|
|
}
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
`INSERT INTO buchhaltung_konten (haushaltsjahr_id, konto_typ_id, kontonummer, bezeichnung, parent_id, budget_gwg, budget_anlagen, budget_instandhaltung, notizen, erstellt_von)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
|
|
|
@@ -377,6 +487,7 @@ async function createKonto(
|
|
|
|
|
return result.rows[0];
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error('BuchhaltungService.createKonto failed', { error });
|
|
|
|
|
if (error.statusCode) throw error;
|
|
|
|
|
if (error.code === '23505' && error.constraint === 'buchhaltung_konten_haushaltsjahr_id_kontonummer_key') {
|
|
|
|
|
throw Object.assign(new Error(`Kontonummer "${data.kontonummer}" existiert bereits in diesem Haushaltsjahr`), { statusCode: 409 });
|
|
|
|
|
}
|
|
|
|
|
@@ -389,6 +500,22 @@ async function updateKonto(
|
|
|
|
|
data: { konto_typ_id?: number; kontonummer?: string; bezeichnung?: string; parent_id?: number | null; budget_gwg?: number; budget_anlagen?: number; budget_instandhaltung?: number; notizen?: string }
|
|
|
|
|
) {
|
|
|
|
|
try {
|
|
|
|
|
// Budget validation for sub-pots
|
|
|
|
|
const hasBudgetChange = data.budget_gwg !== undefined || data.budget_anlagen !== undefined || data.budget_instandhaltung !== undefined;
|
|
|
|
|
const hasParentChange = data.parent_id !== undefined;
|
|
|
|
|
if (hasBudgetChange || hasParentChange) {
|
|
|
|
|
const current = await pool.query(`SELECT parent_id, budget_gwg, budget_anlagen, budget_instandhaltung FROM buchhaltung_konten WHERE id = $1`, [id]);
|
|
|
|
|
if (current.rows[0]) {
|
|
|
|
|
const parentId = data.parent_id !== undefined ? data.parent_id : current.rows[0].parent_id;
|
|
|
|
|
if (parentId) {
|
|
|
|
|
const bGwg = data.budget_gwg !== undefined ? data.budget_gwg : parseFloat(current.rows[0].budget_gwg);
|
|
|
|
|
const bAnl = data.budget_anlagen !== undefined ? data.budget_anlagen : parseFloat(current.rows[0].budget_anlagen);
|
|
|
|
|
const bIns = data.budget_instandhaltung !== undefined ? data.budget_instandhaltung : parseFloat(current.rows[0].budget_instandhaltung);
|
|
|
|
|
await validateSubPotBudget(parentId, bGwg, bAnl, bIns, id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fields: string[] = [];
|
|
|
|
|
const values: unknown[] = [];
|
|
|
|
|
let idx = 1;
|
|
|
|
|
@@ -407,8 +534,9 @@ async function updateKonto(
|
|
|
|
|
values
|
|
|
|
|
);
|
|
|
|
|
return result.rows[0] || null;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error('BuchhaltungService.updateKonto failed', { error, id });
|
|
|
|
|
if (error.statusCode) throw error;
|
|
|
|
|
throw new Error('Konto konnte nicht aktualisiert werden');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -953,14 +1081,16 @@ async function createWiederkehrend(
|
|
|
|
|
intervall: string;
|
|
|
|
|
naechste_ausfuehrung: string;
|
|
|
|
|
aktiv?: boolean;
|
|
|
|
|
ausfuehrungstag?: string;
|
|
|
|
|
ausfuehrungs_monat?: number;
|
|
|
|
|
},
|
|
|
|
|
userId: string
|
|
|
|
|
) {
|
|
|
|
|
try {
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
`INSERT INTO buchhaltung_wiederkehrend
|
|
|
|
|
(bezeichnung, konto_id, bankkonto_id, typ, betrag, beschreibung, empfaenger_auftraggeber, intervall, naechste_ausfuehrung, aktiv, erstellt_von)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
|
|
|
(bezeichnung, konto_id, bankkonto_id, typ, betrag, beschreibung, empfaenger_auftraggeber, intervall, naechste_ausfuehrung, aktiv, erstellt_von, ausfuehrungstag, ausfuehrungs_monat)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
|
|
|
RETURNING *`,
|
|
|
|
|
[
|
|
|
|
|
data.bezeichnung,
|
|
|
|
|
@@ -974,6 +1104,8 @@ async function createWiederkehrend(
|
|
|
|
|
data.naechste_ausfuehrung,
|
|
|
|
|
data.aktiv !== false,
|
|
|
|
|
userId,
|
|
|
|
|
data.ausfuehrungstag || 'erster',
|
|
|
|
|
data.ausfuehrungs_monat ?? null,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
return result.rows[0];
|
|
|
|
|
@@ -996,6 +1128,8 @@ async function updateWiederkehrend(
|
|
|
|
|
intervall?: string;
|
|
|
|
|
naechste_ausfuehrung?: string;
|
|
|
|
|
aktiv?: boolean;
|
|
|
|
|
ausfuehrungstag?: string;
|
|
|
|
|
ausfuehrungs_monat?: number | null;
|
|
|
|
|
}
|
|
|
|
|
) {
|
|
|
|
|
try {
|
|
|
|
|
@@ -1012,6 +1146,8 @@ async function updateWiederkehrend(
|
|
|
|
|
if (data.intervall !== undefined) { fields.push(`intervall = $${idx++}`); values.push(data.intervall); }
|
|
|
|
|
if (data.naechste_ausfuehrung !== undefined) { fields.push(`naechste_ausfuehrung = $${idx++}`); values.push(data.naechste_ausfuehrung); }
|
|
|
|
|
if (data.aktiv !== undefined) { fields.push(`aktiv = $${idx++}`); values.push(data.aktiv); }
|
|
|
|
|
if (data.ausfuehrungstag !== undefined) { fields.push(`ausfuehrungstag = $${idx++}`); values.push(data.ausfuehrungstag); }
|
|
|
|
|
if (data.ausfuehrungs_monat !== undefined) { fields.push(`ausfuehrungs_monat = $${idx++}`); values.push(data.ausfuehrungs_monat); }
|
|
|
|
|
if (fields.length === 0) throw new Error('Keine Felder zum Aktualisieren');
|
|
|
|
|
values.push(id);
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
@@ -1100,6 +1236,10 @@ async function exportTransaktionenCsv(haushaltsjahrId: number): Promise<string>
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
const buchhaltungService = {
|
|
|
|
|
getKategorien,
|
|
|
|
|
createKategorie,
|
|
|
|
|
updateKategorie,
|
|
|
|
|
deleteKategorie,
|
|
|
|
|
getAllHaushaltsjahre,
|
|
|
|
|
getHaushaltsjahrById,
|
|
|
|
|
getCurrentHaushaltsjahr,
|
|
|
|
|
|