feat(buchhaltung): budget types, erstattungen, recurring tab move, overview dividers, order completion guard
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user