Merge remote-tracking branch 'gitea/main'
Some checks failed
Build pitch-deck / build-push-deploy (push) Failing after 1m13s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 49s
CI / test-python-voice (push) Successful in 38s
CI / test-bqas (push) Successful in 31s

# Conflicts:
#	pitch-deck/components/slides/MilestonesSlide.tsx
#	pitch-deck/lib/finanzplan/engine.ts
This commit is contained in:
Benjamin Admin
2026-04-27 13:14:54 +02:00
21 changed files with 624 additions and 354 deletions

View File

@@ -1,8 +1,8 @@
/**
* Liquiditaet — rolling cash balance computation
*
* Computes operative Einzahlungen/Auszahlungen sums,
* Überschuss vor Investitionen/Entnahmen, and rolling Kontostand.
* Computes Einzahlungen/Auszahlungen sums (dynamic row_type-based),
* Ueberschuss vor Investitionen/Entnahmen, and rolling Kontostand/Liquiditaet.
*/
import { Pool } from 'pg'
@@ -58,52 +58,57 @@ export async function computeLiquiditaet(
liqInvest.values = ctx.totalInvest
}
// Compute sums and rolling balance
const sumEin = findLiq('Summe EINZAHLUNGEN')
const sumAus = findLiq('Summe AUSZAHLUNGEN')
const uebVorInv = findLiq('ÜBERSCHUSS VOR INVESTITIONEN')
const uebVorEnt = findLiq('ÜBERSCHUSS VOR ENTNAHMEN')
const ueberschuss = findLiq('ÜBERSCHUSS')
const kontostand = findLiq('Kontostand zu Beginn des Monats')
const liquiditaet = findLiq('LIQUIDITÄT')
// Compute sums and rolling balance — dynamic row_type-based (handles any label conventions)
await computeRollingBalance(pool, liquid)
// Dynamically categorize rows by row_type
const einzahlungenOperativ = ['Umsatzerlöse', 'Sonst. betriebl. Erträge', 'Anzahlungen']
const finanzierungRows = liquid.filter(r =>
r.row_type === 'einzahlung' &&
!einzahlungenOperativ.includes(r.row_label) &&
!r.row_label.includes('Summe')
)
const auszahlungenOperativ = ['Materialaufwand', 'Personalkosten', 'Sonstige Kosten', 'Umsatzsteuer', 'Gewerbesteuer', 'Körperschaftsteuer']
const finanzAuszahlungRows = liquid.filter(r =>
r.row_type === 'auszahlung' &&
!auszahlungenOperativ.includes(r.row_label) &&
!r.row_label.includes('Summe')
)
return { endstand: liquid.find(r => r.row_type === 'kontostand' && r.row_label.includes('LIQUIDIT'))?.values || emptyMonthly() }
}
// Summe EINZAHLUNGEN = nur operativ
/**
* Recompute Summe AUSZAHLUNGEN -> UEBERSCHUSS chain -> rolling balance.
* Called both in initial pass and after tax values are written from GuV.
*/
export async function computeRollingBalance(
pool: Pool,
liquid: FPLiquiditaet[],
): Promise<void> {
const findLiqMatch = (options: string[]) => liquid.find(r => options.includes(r.row_label))
const sumEin = findLiqMatch(['Summe ERTRÄGE', 'Summe EINZAHLUNGEN'])
const sumAus = findLiqMatch(['Summe AUSZAHLUNGEN'])
const uebVorInv = findLiqMatch(['ÜBERSCHUSS VOR INVESTITIONEN', 'UEBERSCHUSS VOR INVESTITIONEN'])
const uebVorEnt = findLiqMatch(['ÜBERSCHUSS VOR ENTNAHMEN', 'UEBERSCHUSS VOR ENTNAHMEN'])
const ueberschuss = findLiqMatch(['ÜBERSCHUSS', 'UEBERSCHUSS'])
const liqInvest = liquid.find(r => r.row_label === 'Investitionen')
// Kontostand: label varies per scenario (with/without parentheses)
const kontostand = liquid.find(r => r.row_type === 'kontostand' && !r.row_label.includes('LIQUIDIT'))
const liquiditaet = liquid.find(r => r.row_type === 'kontostand' && r.row_label.includes('LIQUIDIT'))
// Summe ERTRAEGE = ALL einzahlungen (dynamic — works regardless of how many rows exist)
if (sumEin) {
const s = emptyMonthly()
for (const label of einzahlungenOperativ) {
const row = findLiq(label)
if (row) for (let m = 1; m <= MONTHS; m++) s[`m${m}`] += Math.round(row.values[`m${m}`] || 0)
for (const row of liquid) {
if (row.row_type === 'einzahlung' && row.id !== sumEin.id) {
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] += Math.round(row.values[`m${m}`] || 0)
}
}
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), sumEin.id])
sumEin.values = s
}
// Summe AUSZAHLUNGEN = nur operativ
// Summe AUSZAHLUNGEN = ALL auszahlungen (dynamic)
if (sumAus) {
const s = emptyMonthly()
for (const label of auszahlungenOperativ) {
const row = findLiq(label)
if (row) for (let m = 1; m <= MONTHS; m++) s[`m${m}`] += Math.round(row.values[`m${m}`] || 0)
for (const row of liquid) {
if (row.row_type === 'auszahlung' && row.id !== sumAus.id) {
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] += Math.round(row.values[`m${m}`] || 0)
}
}
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), sumAus.id])
sumAus.values = s
}
// OPERATIVER ÜBERSCHUSS VOR INVESTITIONEN
// UEBERSCHUSS VOR INVESTITIONEN = Summe ERTRAEGE - Summe AUSZAHLUNGEN (total cashflow)
if (uebVorInv && sumEin && sumAus) {
const s = emptyMonthly()
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((sumEin.values[`m${m}`] || 0) - (sumAus.values[`m${m}`] || 0))
@@ -111,7 +116,7 @@ export async function computeLiquiditaet(
uebVorInv.values = s
}
// ÜBERSCHUSS VOR ENTNAHMEN = Operativer Überschuss - Investitionen
// UEBERSCHUSS VOR ENTNAHMEN = UEBERSCHUSS VOR INVESTITIONEN - Investitionen
if (uebVorEnt && uebVorInv && liqInvest) {
const s = emptyMonthly()
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((uebVorInv.values[`m${m}`] || 0) - (liqInvest.values[`m${m}`] || 0))
@@ -119,8 +124,8 @@ export async function computeLiquiditaet(
uebVorEnt.values = s
}
// ÜBERSCHUSS = Überschuss vor Entnahmen - Entnahmen
const entnahmen = findLiq('Kapitalentnahmen/Ausschüttungen')
// UEBERSCHUSS = UEBERSCHUSS VOR ENTNAHMEN - Kapitalentnahmen
const entnahmen = findLiqMatch(['Kapitalentnahmen/Ausschüttungen', 'Kapitalentnahmen/Ausschuettungen'])
if (ueberschuss && uebVorEnt && entnahmen) {
const s = emptyMonthly()
for (let m = 1; m <= MONTHS; m++) s[`m${m}`] = Math.round((uebVorEnt.values[`m${m}`] || 0) - (entnahmen.values[`m${m}`] || 0))
@@ -128,27 +133,18 @@ export async function computeLiquiditaet(
ueberschuss.values = s
}
// Rolling Kontostand: Vormonat + Operativer Überschuss + Finanzierung
// Rolling balance: LIQUIDITAET[m] = LIQUIDITAET[m-1] + UEBERSCHUSS[m]
// UEBERSCHUSS now includes ALL cash flows (operative + financing + repayments)
if (kontostand && liquiditaet && ueberschuss) {
const finCF = emptyMonthly()
for (const row of finanzierungRows) {
for (let m = 1; m <= MONTHS; m++) finCF[`m${m}`] += Math.round(row.values[`m${m}`] || 0)
}
for (const row of finanzAuszahlungRows) {
for (let m = 1; m <= MONTHS; m++) finCF[`m${m}`] -= Math.round(row.values[`m${m}`] || 0)
}
const ks = emptyMonthly()
const lq = emptyMonthly()
for (let m = 1; m <= MONTHS; m++) {
ks[`m${m}`] = m === 1 ? 0 : Math.round(lq[`m${m - 1}`])
lq[`m${m}`] = Math.round(ks[`m${m}`] + (ueberschuss.values[`m${m}`] || 0) + (finCF[`m${m}`] || 0))
lq[`m${m}`] = Math.round(ks[`m${m}`] + (ueberschuss.values[`m${m}`] || 0))
}
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ks), kontostand.id])
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(lq), liquiditaet.id])
kontostand.values = ks
liquiditaet.values = lq
}
return { endstand: liquiditaet?.values || emptyMonthly() }
}