feat(buchhaltung): budget types, erstattungen, recurring tab move, overview dividers, order completion guard

This commit is contained in:
Matthias Hochmeister
2026-03-30 14:07:04 +02:00
parent 13aa4be599
commit b21abce9e3
10 changed files with 615 additions and 140 deletions

View File

@@ -471,18 +471,28 @@ async function validateSubPotBudget(
}
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 },
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; kategorie_id?: number | null; budget_typ?: string; budget_gesamt?: number },
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);
}
// Child konten inherit parent's budget_typ
let budgetTyp = data.budget_typ || 'detailliert';
if (data.parent_id) {
const parentRow = await pool.query(`SELECT budget_typ FROM buchhaltung_konten WHERE id = $1`, [data.parent_id]);
if (parentRow.rows[0]) {
budgetTyp = parentRow.rows[0].budget_typ;
}
}
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)
`INSERT INTO buchhaltung_konten (haushaltsjahr_id, konto_typ_id, kontonummer, bezeichnung, parent_id, budget_gwg, budget_anlagen, budget_instandhaltung, notizen, erstellt_von, kategorie_id, budget_typ, budget_gesamt)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
RETURNING *`,
[data.haushaltsjahr_id, data.konto_typ_id || null, data.kontonummer, data.bezeichnung, data.parent_id || null, data.budget_gwg || 0, data.budget_anlagen || 0, data.budget_instandhaltung || 0, data.notizen || null, userId]
[data.haushaltsjahr_id, data.konto_typ_id || null, data.kontonummer, data.bezeichnung, data.parent_id || null, data.budget_gwg || 0, data.budget_anlagen || 0, data.budget_instandhaltung || 0, data.notizen || null, userId, data.kategorie_id ?? null, budgetTyp, data.budget_gesamt || 0]
);
return result.rows[0];
} catch (error: any) {
@@ -497,7 +507,7 @@ async function createKonto(
async function updateKonto(
id: number,
data: { konto_typ_id?: number; kontonummer?: string; bezeichnung?: string; parent_id?: number | null; budget_gwg?: number; budget_anlagen?: number; budget_instandhaltung?: number; notizen?: string }
data: { konto_typ_id?: number; kontonummer?: string; bezeichnung?: string; parent_id?: number | null; budget_gwg?: number; budget_anlagen?: number; budget_instandhaltung?: number; notizen?: string; kategorie_id?: number | null; budget_typ?: string; budget_gesamt?: number }
) {
try {
// Budget validation for sub-pots
@@ -527,7 +537,27 @@ async function updateKonto(
if (data.budget_anlagen !== undefined) { fields.push(`budget_anlagen = $${idx++}`); values.push(data.budget_anlagen); }
if (data.budget_instandhaltung !== undefined) { fields.push(`budget_instandhaltung = $${idx++}`); values.push(data.budget_instandhaltung); }
if (data.notizen !== undefined) { fields.push(`notizen = $${idx++}`); values.push(data.notizen || null); }
if ('kategorie_id' in data) { fields.push(`kategorie_id = $${idx++}`); values.push(data.kategorie_id ?? null); }
if (data.budget_typ !== undefined) { fields.push(`budget_typ = $${idx++}`); values.push(data.budget_typ); }
if (data.budget_gesamt !== undefined) { fields.push(`budget_gesamt = $${idx++}`); values.push(data.budget_gesamt); }
if (fields.length === 0) throw new Error('Keine Felder zum Aktualisieren');
// Child konten must inherit parent's budget_typ
if (data.budget_typ !== undefined) {
const currentRow = await pool.query(`SELECT parent_id FROM buchhaltung_konten WHERE id = $1`, [id]);
const parentId = data.parent_id !== undefined ? data.parent_id : currentRow.rows[0]?.parent_id;
if (parentId) {
const parentRow = await pool.query(`SELECT budget_typ FROM buchhaltung_konten WHERE id = $1`, [parentId]);
if (parentRow.rows[0]) {
// Override budget_typ with parent's value
const btIdx = fields.findIndex(f => f.startsWith('budget_typ'));
if (btIdx !== -1) {
values[btIdx] = parentRow.rows[0].budget_typ;
}
}
}
}
values.push(id);
const result = await pool.query(
`UPDATE buchhaltung_konten SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`,
@@ -1231,6 +1261,94 @@ async function exportTransaktionenCsv(haushaltsjahrId: number): Promise<string>
}
}
// ---------------------------------------------------------------------------
// Erstattungen (Reimbursements)
// ---------------------------------------------------------------------------
interface Transaktion {
id: number;
[key: string]: unknown;
}
async function createErstattung(data: {
konto_id: number;
bankkonto_id: number;
betrag: number;
datum: string;
beschreibung?: string;
empfaenger_auftraggeber?: string;
verwendungszweck?: string;
ausgabe_ids: number[];
erstellt_von?: number;
}): Promise<Transaktion> {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Look up haushaltsjahr_id from the konto
const kontoResult = await client.query(
`SELECT haushaltsjahr_id FROM buchhaltung_konten WHERE id = $1`,
[data.konto_id]
);
const haushaltsjahrId = kontoResult.rows[0]?.haushaltsjahr_id;
const txResult = await client.query(
`INSERT INTO buchhaltung_transaktionen (typ, konto_id, bankkonto_id, betrag, datum, beschreibung, empfaenger_auftraggeber, verwendungszweck, erstellt_von, haushaltsjahr_id, status)
VALUES ('einnahme', $1, $2, $3, $4, $5, $6, $7, $8, $9, 'entwurf')
RETURNING *`,
[data.konto_id, data.bankkonto_id, data.betrag, data.datum, data.beschreibung || null, data.empfaenger_auftraggeber || null, data.verwendungszweck || null, data.erstellt_von || null, haushaltsjahrId]
);
const tx = txResult.rows[0];
for (const ausgabeId of data.ausgabe_ids) {
await client.query(
`INSERT INTO buchhaltung_erstattung_zuordnungen (erstattung_transaktion_id, ausgabe_transaktion_id) VALUES ($1, $2)`,
[tx.id, ausgabeId]
);
}
await client.query('COMMIT');
return tx;
} catch (error) {
await client.query('ROLLBACK');
logger.error('BuchhaltungService.createErstattung failed', { error });
throw new Error('Erstattung konnte nicht erstellt werden');
} finally {
client.release();
}
}
async function getErstattungLinks(transaktionId: number): Promise<{
erstattung?: any;
ausgaben?: any[];
}> {
try {
// If this transaction is an Ausgabe, find its reimbursement
const erstattungResult = await pool.query(
`SELECT t.* FROM buchhaltung_transaktionen t
JOIN buchhaltung_erstattung_zuordnungen ez ON t.id = ez.erstattung_transaktion_id
WHERE ez.ausgabe_transaktion_id = $1`,
[transaktionId]
);
// If this transaction is an Erstattung, find its linked Ausgaben
const ausgabenResult = await pool.query(
`SELECT t.* FROM buchhaltung_transaktionen t
JOIN buchhaltung_erstattung_zuordnungen ez ON t.id = ez.ausgabe_transaktion_id
WHERE ez.erstattung_transaktion_id = $1`,
[transaktionId]
);
return {
erstattung: erstattungResult.rows[0] || undefined,
ausgaben: ausgabenResult.rows,
};
} catch (error) {
logger.error('BuchhaltungService.getErstattungLinks failed', { error, transaktionId });
throw new Error('Erstattungsverknüpfungen konnten nicht geladen werden');
}
}
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
@@ -1286,6 +1404,8 @@ const buchhaltungService = {
updateWiederkehrend,
deleteWiederkehrend,
exportTransaktionenCsv,
createErstattung,
getErstattungLinks,
};
export default buchhaltungService;