feat(buchhaltung): add transfers, bank statements, Haushaltsplan, and PDF export

This commit is contained in:
Matthias Hochmeister
2026-03-30 17:05:18 +02:00
parent 2eb59e9ff1
commit 5acfd7cc4f
14 changed files with 1911 additions and 10 deletions

View File

@@ -8,6 +8,7 @@ import fs from 'fs';
import notificationService from './notification.service';
import { permissionService } from './permission.service';
import ausruestungsanfrageService from './ausruestungsanfrage.service';
import buchhaltungService from './buchhaltung.service';
// ---------------------------------------------------------------------------
// Catalog (shared ausruestung_artikel via ausruestungsanfrageService)
@@ -381,6 +382,17 @@ async function updateOrderStatus(id: number, status: string, userId: string, for
}
}
// Block transition to abgeschlossen if positions have missing prices
if (status === 'abgeschlossen') {
const posCheck = await pool.query(
`SELECT COUNT(*) FROM bestellpositionen WHERE bestellung_id = $1 AND (einzelpreis IS NULL OR einzelpreis = 0)`,
[id]
);
if (parseInt(posCheck.rows[0].count) > 0) {
throw new Error('Alle Positionen müssen einen Einzelpreis haben, bevor die Bestellung abgeschlossen werden kann.');
}
}
const updates: string[] = ['status = $1', 'aktualisiert_am = NOW()'];
const params: unknown[] = [status];
let paramIndex = 2;
@@ -452,6 +464,13 @@ async function updateOrderStatus(id: number, status: string, userId: string, for
});
}
// Create pending buchhaltung transaction when order is completed
if (status === 'abgeschlossen') {
buchhaltungService.createPendingFromBestellung(id, userId).catch(err =>
logger.error('createPendingFromBestellung failed (non-fatal)', { err, orderId: id })
);
}
return result.rows[0];
} catch (error) {
logger.error('BestellungService.updateOrderStatus failed', { error, id });

View File

@@ -354,6 +354,98 @@ async function deactivateBankkonto(id: number) {
}
}
async function getBankkontoStatement(
bankkontoId: number,
filters?: { von?: string; bis?: string }
) {
try {
// 1. Get the bank account
const kontoResult = await pool.query(
`SELECT * FROM buchhaltung_bankkonten WHERE id = $1`,
[bankkontoId]
);
if (!kontoResult.rows[0]) return null;
// 2. Fetch all transactions involving this bank account (both directions for transfers)
const conditions: string[] = [
`(t.bankkonto_id = $1 OR t.transfer_ziel_bankkonto_id = $1)`
];
const values: unknown[] = [bankkontoId];
let idx = 2;
if (filters?.von) { conditions.push(`t.datum >= $${idx++}`); values.push(filters.von); }
if (filters?.bis) { conditions.push(`t.datum <= $${idx++}`); values.push(filters.bis); }
// Only include booked/approved transactions for the statement
conditions.push(`t.status IN ('gebucht', 'freigegeben')`);
const where = `WHERE ${conditions.join(' AND ')}`;
const txResult = await pool.query(
`SELECT t.*,
k.bezeichnung as konto_bezeichnung,
k.kontonummer as konto_kontonummer,
bk.bezeichnung as bankkonto_bezeichnung,
bk2.bezeichnung AS transfer_ziel_bezeichnung
FROM buchhaltung_transaktionen t
LEFT JOIN buchhaltung_konten k ON t.konto_id = k.id
LEFT JOIN buchhaltung_bankkonten bk ON t.bankkonto_id = bk.id
LEFT JOIN buchhaltung_bankkonten bk2 ON t.transfer_ziel_bankkonto_id = bk2.id
${where}
ORDER BY t.datum ASC, t.id ASC`,
values
);
// 3. Compute running balance
let laufenderSaldo = 0;
let gesamteinnahmen = 0;
let gesamtausgaben = 0;
const rows = txResult.rows.map(row => {
const betrag = parseFloat(row.betrag);
let aenderung = 0;
if (row.typ === 'transfer') {
if (row.bankkonto_id === bankkontoId) {
// Money leaving this account
aenderung = -betrag;
gesamtausgaben += betrag;
} else {
// Money arriving at this account (transfer_ziel_bankkonto_id matches)
aenderung = betrag;
gesamteinnahmen += betrag;
}
} else if (row.typ === 'einnahme') {
aenderung = betrag;
gesamteinnahmen += betrag;
} else if (row.typ === 'ausgabe') {
aenderung = -betrag;
gesamtausgaben += betrag;
}
laufenderSaldo += aenderung;
return {
...row,
aenderung,
laufender_saldo: Math.round(laufenderSaldo * 100) / 100,
};
});
return {
konto: kontoResult.rows[0],
rows,
summary: {
gesamteinnahmen: Math.round(gesamteinnahmen * 100) / 100,
gesamtausgaben: Math.round(gesamtausgaben * 100) / 100,
saldo: Math.round(laufenderSaldo * 100) / 100,
},
};
} catch (error) {
logger.error('BuchhaltungService.getBankkontoStatement failed', { error, bankkontoId });
throw new Error('Kontoauszug konnte nicht geladen werden');
}
}
// ---------------------------------------------------------------------------
// Konten (Budget Accounts)
// ---------------------------------------------------------------------------
@@ -445,10 +537,12 @@ async function getKontoDetail(kontoId: number) {
`SELECT t.*,
k.bezeichnung as konto_bezeichnung,
k.kontonummer as konto_kontonummer,
bk.bezeichnung as bankkonto_bezeichnung
bk.bezeichnung as bankkonto_bezeichnung,
bk2.bezeichnung AS transfer_ziel_bezeichnung
FROM buchhaltung_transaktionen t
LEFT JOIN buchhaltung_konten k ON t.konto_id = k.id
LEFT JOIN buchhaltung_bankkonten bk ON t.bankkonto_id = bk.id
LEFT JOIN buchhaltung_bankkonten bk2 ON t.transfer_ziel_bankkonto_id = bk2.id
WHERE t.konto_id = $1
ORDER BY t.datum DESC, t.id DESC`,
[kontoId]
@@ -702,10 +796,12 @@ async function listTransaktionen(filters: {
`SELECT t.*,
k.bezeichnung as konto_bezeichnung,
k.kontonummer as konto_kontonummer,
bk.bezeichnung as bankkonto_bezeichnung
bk.bezeichnung as bankkonto_bezeichnung,
bk2.bezeichnung AS transfer_ziel_bezeichnung
FROM buchhaltung_transaktionen t
LEFT JOIN buchhaltung_konten k ON t.konto_id = k.id
LEFT JOIN buchhaltung_bankkonten bk ON t.bankkonto_id = bk.id
LEFT JOIN buchhaltung_bankkonten bk2 ON t.transfer_ziel_bankkonto_id = bk2.id
${where}
ORDER BY t.datum DESC, t.id DESC`,
values
@@ -723,10 +819,12 @@ async function getTransaktionById(id: number) {
`SELECT t.*,
k.bezeichnung as konto_bezeichnung,
k.kontonummer as konto_kontonummer,
bk.bezeichnung as bankkonto_bezeichnung
bk.bezeichnung as bankkonto_bezeichnung,
bk2.bezeichnung AS transfer_ziel_bezeichnung
FROM buchhaltung_transaktionen t
LEFT JOIN buchhaltung_konten k ON t.konto_id = k.id
LEFT JOIN buchhaltung_bankkonten bk ON t.bankkonto_id = bk.id
LEFT JOIN buchhaltung_bankkonten bk2 ON t.transfer_ziel_bankkonto_id = bk2.id
WHERE t.id = $1`,
[id]
);
@@ -909,6 +1007,59 @@ async function deleteTransaktion(id: number) {
}
}
// ---------------------------------------------------------------------------
// Transfers (Umbuchungen)
// ---------------------------------------------------------------------------
async function createTransfer(
data: {
haushaltsjahr_id: number;
bankkonto_id: number;
transfer_ziel_bankkonto_id: number;
betrag: number;
datum: string;
beschreibung?: string;
beleg_nr?: string;
},
userId: string
) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Debit row (money leaves source account)
const debitResult = await client.query(
`INSERT INTO buchhaltung_transaktionen
(haushaltsjahr_id, bankkonto_id, transfer_ziel_bankkonto_id, typ, betrag, datum, beschreibung, beleg_nr, erstellt_von, status)
VALUES ($1, $2, $3, 'transfer', $4, $5, $6, $7, $8, 'gebucht')
RETURNING *`,
[data.haushaltsjahr_id, data.bankkonto_id, data.transfer_ziel_bankkonto_id, data.betrag, data.datum, data.beschreibung || null, data.beleg_nr || null, userId]
);
// Credit row (money arrives at target account)
const creditResult = await client.query(
`INSERT INTO buchhaltung_transaktionen
(haushaltsjahr_id, bankkonto_id, transfer_ziel_bankkonto_id, typ, betrag, datum, beschreibung, beleg_nr, erstellt_von, status)
VALUES ($1, $2, $3, 'transfer', $4, $5, $6, $7, $8, 'gebucht')
RETURNING *`,
[data.haushaltsjahr_id, data.transfer_ziel_bankkonto_id, data.bankkonto_id, data.betrag, data.datum, data.beschreibung || null, data.beleg_nr || null, userId]
);
await client.query('COMMIT');
await logAudit(debitResult.rows[0].id, 'transfer_erstellt', { betrag: data.betrag, von: data.bankkonto_id, nach: data.transfer_ziel_bankkonto_id }, userId);
await logAudit(creditResult.rows[0].id, 'transfer_erstellt', { betrag: data.betrag, von: data.bankkonto_id, nach: data.transfer_ziel_bankkonto_id }, userId);
return { debit: debitResult.rows[0], credit: creditResult.rows[0] };
} catch (error) {
await client.query('ROLLBACK');
logger.error('BuchhaltungService.createTransfer failed', { error });
throw new Error('Umbuchung konnte nicht erstellt werden');
} finally {
client.release();
}
}
// ---------------------------------------------------------------------------
// Belege (Receipts)
// ---------------------------------------------------------------------------
@@ -1435,6 +1586,331 @@ async function getErstattungLinks(transaktionId: number): Promise<{
}
}
// ---------------------------------------------------------------------------
// Bestellung → Buchhaltung Integration
// ---------------------------------------------------------------------------
async function createPendingFromBestellung(bestellungId: number, userId: string) {
try {
// 1. Fetch the bestellung with total
const bestResult = await pool.query(
`SELECT b.*, l.name AS lieferant_name, COALESCE(SUM(bp.menge * bp.einzelpreis), 0) AS gesamtbetrag
FROM bestellungen b
LEFT JOIN lieferanten l ON l.id = b.lieferant_id
LEFT JOIN bestellpositionen bp ON bp.bestellung_id = b.id
WHERE b.id = $1
GROUP BY b.id, l.name`,
[bestellungId]
);
if (!bestResult.rows[0]) {
logger.warn('createPendingFromBestellung: Bestellung not found', { bestellungId });
return;
}
const bestellung = bestResult.rows[0];
const gesamtbetrag = parseFloat(bestellung.gesamtbetrag);
if (gesamtbetrag <= 0) {
logger.info('createPendingFromBestellung: Gesamtbetrag is 0, skipping', { bestellungId });
return;
}
// 2. Find active Haushaltsjahr
const hjResult = await pool.query(
`SELECT id FROM buchhaltung_haushaltsjahre WHERE abgeschlossen = FALSE ORDER BY jahr DESC LIMIT 1`
);
if (!hjResult.rows[0]) {
logger.info('createPendingFromBestellung: No active Haushaltsjahr found, skipping', { bestellungId });
return;
}
const haushaltsjahrId = hjResult.rows[0].id;
// 3. Find default bank account
const bkResult = await pool.query(
`SELECT id FROM buchhaltung_bankkonten WHERE aktiv = TRUE ORDER BY ist_standard DESC LIMIT 1`
);
const bankkontoId = bkResult.rows[0]?.id || null;
// 4. Insert pending transaction
const label = bestellung.laufende_nummer ? `Bestellung #${bestellung.laufende_nummer}` : `Bestellung #${bestellung.id}`;
await pool.query(
`INSERT INTO buchhaltung_transaktionen
(haushaltsjahr_id, bankkonto_id, typ, betrag, datum, beschreibung, empfaenger_auftraggeber, bestellung_id, erstellt_von, status)
VALUES ($1, $2, 'ausgabe', $3, NOW(), $4, $5, $6, $7, 'entwurf')`,
[haushaltsjahrId, bankkontoId, gesamtbetrag, label, bestellung.lieferant_name || null, bestellungId, userId]
);
logger.info('createPendingFromBestellung: Created pending transaction', { bestellungId, gesamtbetrag });
} catch (error) {
logger.error('createPendingFromBestellung failed (non-fatal)', { error, bestellungId });
// Non-fatal: don't throw
}
}
// ---------------------------------------------------------------------------
// Planung (Budget Plans)
// ---------------------------------------------------------------------------
async function listPlanungen() {
try {
const result = await pool.query(
`SELECT p.*,
hj.bezeichnung AS haushaltsjahr_bezeichnung,
hj.jahr AS haushaltsjahr_jahr,
COALESCE(pos.positionen_count, 0) AS positionen_count,
COALESCE(pos.total_gwg, 0) AS total_gwg,
COALESCE(pos.total_anlagen, 0) AS total_anlagen,
COALESCE(pos.total_instandhaltung, 0) AS total_instandhaltung
FROM buchhaltung_planung p
LEFT JOIN buchhaltung_haushaltsjahre hj ON hj.id = p.haushaltsjahr_id
LEFT JOIN LATERAL (
SELECT COUNT(*) AS positionen_count,
SUM(budget_gwg) AS total_gwg,
SUM(budget_anlagen) AS total_anlagen,
SUM(budget_instandhaltung) AS total_instandhaltung
FROM buchhaltung_planpositionen
WHERE planung_id = p.id
) pos ON true
ORDER BY p.erstellt_am DESC`
);
return result.rows;
} catch (error) {
logger.error('BuchhaltungService.listPlanungen failed', { error });
throw new Error('Planungen konnten nicht geladen werden');
}
}
async function getPlanungById(id: number) {
try {
const planungResult = await pool.query(
`SELECT p.*,
hj.bezeichnung AS haushaltsjahr_bezeichnung,
hj.jahr AS haushaltsjahr_jahr
FROM buchhaltung_planung p
LEFT JOIN buchhaltung_haushaltsjahre hj ON hj.id = p.haushaltsjahr_id
WHERE p.id = $1`,
[id]
);
if (!planungResult.rows[0]) return null;
const positionenResult = await pool.query(
`SELECT pp.*,
k.bezeichnung AS konto_bezeichnung,
k.kontonummer AS konto_kontonummer
FROM buchhaltung_planpositionen pp
LEFT JOIN buchhaltung_konten k ON pp.konto_id = k.id
WHERE pp.planung_id = $1
ORDER BY pp.sort_order, pp.id`,
[id]
);
return {
...planungResult.rows[0],
positionen: positionenResult.rows,
};
} catch (error) {
logger.error('BuchhaltungService.getPlanungById failed', { error, id });
throw new Error('Planung konnte nicht geladen werden');
}
}
async function createPlanung(data: { haushaltsjahr_id: number; bezeichnung: string }, userId: string) {
try {
const result = await pool.query(
`INSERT INTO buchhaltung_planung (haushaltsjahr_id, bezeichnung, erstellt_von)
VALUES ($1, $2, $3)
RETURNING *`,
[data.haushaltsjahr_id, data.bezeichnung, userId]
);
return result.rows[0];
} catch (error) {
logger.error('BuchhaltungService.createPlanung failed', { error });
throw new Error('Planung konnte nicht erstellt werden');
}
}
async function updatePlanung(id: number, data: { bezeichnung?: string; status?: string }) {
try {
const fields: string[] = [];
const values: unknown[] = [];
let idx = 1;
if (data.bezeichnung !== undefined) { fields.push(`bezeichnung = $${idx++}`); values.push(data.bezeichnung); }
if (data.status !== undefined) { fields.push(`status = $${idx++}`); values.push(data.status); }
if (fields.length === 0) throw new Error('Keine Felder zum Aktualisieren');
values.push(id);
const result = await pool.query(
`UPDATE buchhaltung_planung SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`,
values
);
return result.rows[0] || null;
} catch (error) {
logger.error('BuchhaltungService.updatePlanung failed', { error, id });
throw new Error('Planung konnte nicht aktualisiert werden');
}
}
async function deletePlanung(id: number) {
try {
await pool.query(`DELETE FROM buchhaltung_planung WHERE id = $1`, [id]);
} catch (error) {
logger.error('BuchhaltungService.deletePlanung failed', { error, id });
throw new Error('Planung konnte nicht gelöscht werden');
}
}
async function createPlanposition(planungId: number, data: { konto_id?: number | null; bezeichnung: string; budget_gwg?: number; budget_anlagen?: number; budget_instandhaltung?: number; notizen?: string; sort_order?: number }) {
try {
const result = await pool.query(
`INSERT INTO buchhaltung_planpositionen (planung_id, konto_id, bezeichnung, budget_gwg, budget_anlagen, budget_instandhaltung, notizen, sort_order)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *`,
[planungId, data.konto_id || null, data.bezeichnung, data.budget_gwg || 0, data.budget_anlagen || 0, data.budget_instandhaltung || 0, data.notizen || null, data.sort_order || 0]
);
return result.rows[0];
} catch (error) {
logger.error('BuchhaltungService.createPlanposition failed', { error, planungId });
throw new Error('Planposition konnte nicht erstellt werden');
}
}
async function updatePlanposition(id: number, data: { konto_id?: number | null; bezeichnung?: string; budget_gwg?: number; budget_anlagen?: number; budget_instandhaltung?: number; notizen?: string; sort_order?: number }) {
try {
const fields: string[] = [];
const values: unknown[] = [];
let idx = 1;
if (data.konto_id !== undefined) { fields.push(`konto_id = $${idx++}`); values.push(data.konto_id); }
if (data.bezeichnung !== undefined) { fields.push(`bezeichnung = $${idx++}`); values.push(data.bezeichnung); }
if (data.budget_gwg !== undefined) { fields.push(`budget_gwg = $${idx++}`); values.push(data.budget_gwg); }
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 (data.sort_order !== undefined) { fields.push(`sort_order = $${idx++}`); values.push(data.sort_order); }
if (fields.length === 0) throw new Error('Keine Felder zum Aktualisieren');
values.push(id);
const result = await pool.query(
`UPDATE buchhaltung_planpositionen SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`,
values
);
return result.rows[0] || null;
} catch (error) {
logger.error('BuchhaltungService.updatePlanposition failed', { error, id });
throw new Error('Planposition konnte nicht aktualisiert werden');
}
}
async function deletePlanposition(id: number) {
try {
await pool.query(`DELETE FROM buchhaltung_planpositionen WHERE id = $1`, [id]);
} catch (error) {
logger.error('BuchhaltungService.deletePlanposition failed', { error, id });
throw new Error('Planposition konnte nicht gelöscht werden');
}
}
async function getPlanpositionenByPlanung(planungId: number) {
try {
const result = await pool.query(
`SELECT pp.*,
k.bezeichnung AS konto_bezeichnung,
k.kontonummer AS konto_kontonummer
FROM buchhaltung_planpositionen pp
LEFT JOIN buchhaltung_konten k ON pp.konto_id = k.id
WHERE pp.planung_id = $1
ORDER BY pp.sort_order, pp.id`,
[planungId]
);
return result.rows;
} catch (error) {
logger.error('BuchhaltungService.getPlanpositionenByPlanung failed', { error, planungId });
throw new Error('Planpositionen konnten nicht geladen werden');
}
}
async function createHaushaltsjahrFromPlan(planungId: number, userId: string) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const planResult = await client.query(
`SELECT p.*, hj.jahr, hj.beginn, hj.ende
FROM buchhaltung_planung p
LEFT JOIN buchhaltung_haushaltsjahre hj ON hj.id = p.haushaltsjahr_id
WHERE p.id = $1`,
[planungId]
);
if (!planResult.rows[0]) throw new Error('Planung nicht gefunden');
const plan = planResult.rows[0];
const nextYear = plan.jahr + 1;
// Check if Haushaltsjahr for next year already exists
const existingResult = await client.query(
`SELECT id FROM buchhaltung_haushaltsjahre WHERE jahr = $1`,
[nextYear]
);
if (existingResult.rows.length > 0) {
throw Object.assign(new Error(`Haushaltsjahr ${nextYear} existiert bereits`), { statusCode: 409 });
}
// Create new Haushaltsjahr
const hjResult = await client.query(
`INSERT INTO buchhaltung_haushaltsjahre (jahr, bezeichnung, beginn, ende, erstellt_von)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`,
[nextYear, `Haushaltsjahr ${nextYear}`, `${nextYear}-01-01`, `${nextYear}-12-31`, userId]
);
const newHj = hjResult.rows[0];
// Get all positions from the plan
const posResult = await client.query(
`SELECT pp.*, k.kontonummer, k.bezeichnung AS konto_bezeichnung, k.konto_typ_id, k.parent_id, k.kategorie_id, k.budget_typ
FROM buchhaltung_planpositionen pp
LEFT JOIN buchhaltung_konten k ON pp.konto_id = k.id
WHERE pp.planung_id = $1
ORDER BY pp.sort_order, pp.id`,
[planungId]
);
// Create konten in the new Haushaltsjahr from plan positions
const kontoIdMap = new Map<number, number>(); // old konto_id -> new konto_id
for (const pos of posResult.rows) {
if (!pos.konto_id) continue;
// Map parent_id to new parent if it was already created
const newParentId = pos.parent_id ? (kontoIdMap.get(pos.parent_id) || null) : null;
const kontoResult = await client.query(
`INSERT INTO buchhaltung_konten (haushaltsjahr_id, konto_typ_id, kontonummer, bezeichnung, parent_id, budget_gwg, budget_anlagen, budget_instandhaltung, erstellt_von, kategorie_id, budget_typ)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (haushaltsjahr_id, kontonummer) DO UPDATE SET
budget_gwg = EXCLUDED.budget_gwg,
budget_anlagen = EXCLUDED.budget_anlagen,
budget_instandhaltung = EXCLUDED.budget_instandhaltung
RETURNING id`,
[newHj.id, pos.konto_typ_id, pos.kontonummer, pos.konto_bezeichnung || pos.bezeichnung, newParentId, pos.budget_gwg, pos.budget_anlagen, pos.budget_instandhaltung, userId, pos.kategorie_id, pos.budget_typ || 'detailliert']
);
kontoIdMap.set(pos.konto_id, kontoResult.rows[0].id);
}
// Mark plan as abgeschlossen
await client.query(
`UPDATE buchhaltung_planung SET status = 'abgeschlossen' WHERE id = $1`,
[planungId]
);
await client.query('COMMIT');
await logAudit(null, 'haushaltsjahr_aus_plan_erstellt', { planung_id: planungId, haushaltsjahr_id: newHj.id, jahr: nextYear }, userId);
return { haushaltsjahr: newHj };
} catch (error: any) {
await client.query('ROLLBACK');
logger.error('BuchhaltungService.createHaushaltsjahrFromPlan failed', { error, planungId });
if (error.statusCode) throw error;
throw new Error('Haushaltsjahr konnte nicht aus Planung erstellt werden');
} finally {
client.release();
}
}
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
@@ -1459,6 +1935,7 @@ const buchhaltungService = {
createBankkonto,
updateBankkonto,
deactivateBankkonto,
getBankkontoStatement,
getAllKonten,
getKontoById,
getKontenTree,
@@ -1475,6 +1952,7 @@ const buchhaltungService = {
bookTransaktion,
stornoTransaktion,
deleteTransaktion,
createTransfer,
getBelegeByTransaktion,
uploadBeleg,
deleteBeleg,
@@ -1495,6 +1973,17 @@ const buchhaltungService = {
exportTransaktionenCsv,
createErstattung,
getErstattungLinks,
createPendingFromBestellung,
listPlanungen,
getPlanungById,
createPlanung,
updatePlanung,
deletePlanung,
createPlanposition,
updatePlanposition,
deletePlanposition,
getPlanpositionenByPlanung,
createHaushaltsjahrFromPlan,
};
export default buchhaltungService;