[split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook) and split all 44 files exceeding 500 LOC into domain-focused modules: - consent-service (Go): models, handlers, services, database splits - backend-core (Python): security_api, rbac_api, pdf_service, auth splits - admin-core (TypeScript): 5 page.tsx + sidebar extractions - pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits - voice-service (Python): enhanced_task_orchestrator split Result: 0 violations, 36 exempted (pipeline, tests, pure-data files). Go build verified clean. No behavior changes — pure structural splits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
160
pitch-deck/lib/finanzplan/engine-betrieb.ts
Normal file
160
pitch-deck/lib/finanzplan/engine-betrieb.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Betriebliche Aufwendungen — formula-based rows + category sums
|
||||
*
|
||||
* Computes formula-driven operating expenses (Fortbildung, Reisekosten, etc.),
|
||||
* Gewerbesteuer, category sums, and Gesamtkosten.
|
||||
*/
|
||||
|
||||
import { Pool } from 'pg'
|
||||
import {
|
||||
MonthlyValues, MONTHS, FOUNDING_MONTH,
|
||||
emptyMonthly,
|
||||
FPBetrieblicheAufwendungen,
|
||||
} from './types'
|
||||
import { sumRows } from './engine-sheets'
|
||||
|
||||
export interface BetriebContext {
|
||||
totalBrutto: MonthlyValues
|
||||
totalPersonal: MonthlyValues
|
||||
totalAfa: MonthlyValues
|
||||
totalRevenue: MonthlyValues
|
||||
totalMaterial: MonthlyValues
|
||||
headcount: MonthlyValues
|
||||
totalBestandskunden: MonthlyValues
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute all formula-based betriebliche aufwendungen rows and sums.
|
||||
* Writes computed values back to DB.
|
||||
*/
|
||||
export async function computeBetrieblicheAufwendungen(
|
||||
pool: Pool,
|
||||
betrieb: FPBetrieblicheAufwendungen[],
|
||||
ctx: BetriebContext,
|
||||
): Promise<{ totalSonstige: MonthlyValues; totalGesamt: MonthlyValues }> {
|
||||
const NUM_FOUNDERS = 2
|
||||
const hcWithoutFounders = emptyMonthly()
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
hcWithoutFounders[`m${m}`] = Math.max(0, ctx.headcount[`m${m}`] - NUM_FOUNDERS)
|
||||
}
|
||||
|
||||
// Formula-based rows: derive from headcount (excl. founders) or customers
|
||||
const formulaRows: { label: string; perUnit: number; source: MonthlyValues }[] = [
|
||||
{ label: 'Fort-/Weiterbildungskosten (F)', perUnit: 300, source: hcWithoutFounders },
|
||||
{ label: 'Reisekosten (F)', perUnit: 75, source: ctx.headcount },
|
||||
{ label: 'Bewirtungskosten (F)', perUnit: 50, source: ctx.totalBestandskunden },
|
||||
{ label: 'Internet/Mobilfunk (F)', perUnit: 50, source: ctx.headcount },
|
||||
]
|
||||
|
||||
for (const fr of formulaRows) {
|
||||
const row = betrieb.find(r => r.row_label === fr.label)
|
||||
if (row) {
|
||||
const computed = emptyMonthly()
|
||||
for (let m = FOUNDING_MONTH; m <= MONTHS; m++) {
|
||||
computed[`m${m}`] = Math.round((fr.source[`m${m}`] || 0) * fr.perUnit)
|
||||
}
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), row.id])
|
||||
row.values = computed
|
||||
}
|
||||
}
|
||||
|
||||
// Berufsgenossenschaft (VBG IT/Büro): ~0.5% of total brutto payroll
|
||||
const bgRow = betrieb.find(r => r.row_label.includes('Berufsgenossenschaft'))
|
||||
if (bgRow) {
|
||||
const computed = emptyMonthly()
|
||||
for (let m = FOUNDING_MONTH; m <= MONTHS; m++) {
|
||||
computed[`m${m}`] = Math.round((ctx.totalBrutto[`m${m}`] || 0) * 0.005)
|
||||
}
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), bgRow.id])
|
||||
bgRow.values = computed
|
||||
}
|
||||
|
||||
// Allgemeine Marketingkosten: 8% of revenue (2026-2028), 10% from 2029
|
||||
const marketingRow = betrieb.find(r => r.row_label.includes('Allgemeine Marketingkosten'))
|
||||
if (marketingRow) {
|
||||
const computed = emptyMonthly()
|
||||
for (let m = FOUNDING_MONTH; m <= MONTHS; m++) {
|
||||
const rate = m <= 36 ? 0.08 : 0.10 // m36 = Dec 2028
|
||||
computed[`m${m}`] = Math.round((ctx.totalRevenue[`m${m}`] || 0) * rate)
|
||||
}
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), marketingRow.id])
|
||||
marketingRow.values = computed
|
||||
}
|
||||
|
||||
// Update Personalkosten row
|
||||
const persBetrieb = betrieb.find(r => r.row_label === 'Personalkosten')
|
||||
if (persBetrieb) {
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(ctx.totalPersonal), persBetrieb.id])
|
||||
persBetrieb.values = ctx.totalPersonal
|
||||
}
|
||||
// Update Abschreibungen row
|
||||
const abrBetrieb = betrieb.find(r => r.row_label === 'Abschreibungen')
|
||||
if (abrBetrieb) {
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(ctx.totalAfa), abrBetrieb.id])
|
||||
abrBetrieb.values = ctx.totalAfa
|
||||
}
|
||||
|
||||
// Gewerbesteuer (F): 12.25% of monthly profit (only when positive)
|
||||
const gewStRow = betrieb.find(r => r.row_label.includes('Gewerbesteuer'))
|
||||
if (gewStRow) {
|
||||
const nonTaxOpex = betrieb.filter(r =>
|
||||
r.category !== 'steuern' && r.category !== 'personal' && r.category !== 'abschreibungen' &&
|
||||
!r.is_sum_row && !r.row_label.includes('Summe') && !r.row_label.includes('SUMME')
|
||||
)
|
||||
const computed = emptyMonthly()
|
||||
for (let m = FOUNDING_MONTH; m <= MONTHS; m++) {
|
||||
const rev = ctx.totalRevenue[`m${m}`] || 0
|
||||
const mat = ctx.totalMaterial[`m${m}`] || 0
|
||||
const pers = ctx.totalPersonal[`m${m}`] || 0
|
||||
const afa = ctx.totalAfa[`m${m}`] || 0
|
||||
let opex = 0
|
||||
for (const r of nonTaxOpex) { opex += r.values[`m${m}`] || 0 }
|
||||
const profit = rev - mat - pers - afa - opex
|
||||
computed[`m${m}`] = profit > 0 ? Math.round(profit * 0.1225) : 0
|
||||
}
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), gewStRow.id])
|
||||
gewStRow.values = computed
|
||||
}
|
||||
|
||||
// Compute category sums
|
||||
const categories = ['steuern', 'versicherungen', 'besondere', 'marketing', 'sonstige']
|
||||
for (const cat of categories) {
|
||||
const sumRow = betrieb.find(r => r.category === cat && r.is_sum_row)
|
||||
const detailRows = betrieb.filter(r => r.category === cat && !r.is_sum_row)
|
||||
if (sumRow && detailRows.length > 0) {
|
||||
const s = sumRows(detailRows)
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(s), sumRow.id])
|
||||
sumRow.values = s
|
||||
}
|
||||
}
|
||||
|
||||
// Summe sonstige (ohne Personal, Abschreibungen)
|
||||
const sonstSumme = betrieb.find(r => r.row_label.includes('Summe sonstige'))
|
||||
if (sonstSumme) {
|
||||
const nonPersonNonAbr = betrieb.filter(r =>
|
||||
r.row_label !== 'Personalkosten' && r.row_label !== 'Abschreibungen' &&
|
||||
!r.row_label.includes('Summe sonstige') && !r.row_label.includes('Gesamtkosten') &&
|
||||
!r.is_sum_row && r.category !== 'personal' && r.category !== 'abschreibungen'
|
||||
)
|
||||
const s = sumRows(nonPersonNonAbr)
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(s), sonstSumme.id])
|
||||
sonstSumme.values = s
|
||||
}
|
||||
|
||||
// Gesamtkosten
|
||||
const gesamtBetrieb = betrieb.find(r => r.row_label.includes('Gesamtkosten') || r.row_label.includes('SUMME Betriebliche'))
|
||||
const totalSonstige = sonstSumme?.values || emptyMonthly()
|
||||
if (gesamtBetrieb) {
|
||||
const g = emptyMonthly()
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
g[`m${m}`] = (ctx.totalPersonal[`m${m}`] || 0) + (ctx.totalAfa[`m${m}`] || 0) + (totalSonstige[`m${m}`] || 0)
|
||||
}
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(g), gesamtBetrieb.id])
|
||||
gesamtBetrieb.values = g
|
||||
}
|
||||
|
||||
return {
|
||||
totalSonstige,
|
||||
totalGesamt: gesamtBetrieb?.values || emptyMonthly(),
|
||||
}
|
||||
}
|
||||
166
pitch-deck/lib/finanzplan/engine-guv.ts
Normal file
166
pitch-deck/lib/finanzplan/engine-guv.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* GuV (Gewinn- und Verlustrechnung) — annual P&L computation
|
||||
*
|
||||
* Computes annual sums, EBIT, taxes (Gewerbesteuer, Körperschaftsteuer)
|
||||
* with Verlustvortrag, and writes tax amounts back to Liquidität.
|
||||
*/
|
||||
|
||||
import { Pool } from 'pg'
|
||||
import {
|
||||
MonthlyValues, AnnualValues, MONTHS,
|
||||
emptyMonthly, annualSums,
|
||||
FPLiquiditaet,
|
||||
} from './types'
|
||||
|
||||
export interface GuvContext {
|
||||
totalRevenue: MonthlyValues
|
||||
totalMaterial: MonthlyValues
|
||||
totalBrutto: MonthlyValues
|
||||
totalSozial: MonthlyValues
|
||||
totalPersonal: MonthlyValues
|
||||
totalAfa: MonthlyValues
|
||||
totalSonstige: MonthlyValues
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute GuV annual values, taxes, and write tax values to Liquidität.
|
||||
* Returns EBIT annual values.
|
||||
*/
|
||||
export async function computeGuV(
|
||||
pool: Pool,
|
||||
scenarioId: string,
|
||||
liquid: FPLiquiditaet[],
|
||||
ctx: GuvContext,
|
||||
): Promise<AnnualValues[]> {
|
||||
const findLiq = (label: string) => liquid.find(r => r.row_label === label)
|
||||
|
||||
const umsatzAnnual = annualSums(ctx.totalRevenue)
|
||||
const materialAnnual = annualSums(ctx.totalMaterial)
|
||||
const personalBruttoAnnual = annualSums(ctx.totalBrutto)
|
||||
const personalSozialAnnual = annualSums(ctx.totalSozial)
|
||||
const personalAnnual = annualSums(ctx.totalPersonal)
|
||||
const afaAnnual = annualSums(ctx.totalAfa)
|
||||
const sonstigeAnnual = annualSums(ctx.totalSonstige)
|
||||
|
||||
// Rohergebnis = Gesamtleistung - Materialaufwand
|
||||
const rohergebnis: AnnualValues = {}
|
||||
for (let y = 2026; y <= 2030; y++) {
|
||||
const k = `y${y}`
|
||||
rohergebnis[k] = Math.round((umsatzAnnual[k] || 0) - (materialAnnual[k] || 0))
|
||||
}
|
||||
|
||||
const guvUpdates: { label: string; values: AnnualValues }[] = [
|
||||
{ label: 'Umsatzerlöse', values: umsatzAnnual },
|
||||
{ label: 'Gesamtleistung', values: umsatzAnnual },
|
||||
{ label: 'Summe Materialaufwand', values: materialAnnual },
|
||||
{ label: 'Rohergebnis', values: rohergebnis },
|
||||
{ label: 'Löhne und Gehälter', values: personalBruttoAnnual },
|
||||
{ label: 'Soziale Abgaben', values: personalSozialAnnual },
|
||||
{ label: 'Summe Personalaufwand', values: personalAnnual },
|
||||
{ label: 'Abschreibungen', values: afaAnnual },
|
||||
{ label: 'Sonst. betriebl. Aufwendungen', values: sonstigeAnnual },
|
||||
]
|
||||
|
||||
for (const { label, values } of guvUpdates) {
|
||||
await pool.query(
|
||||
'UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3',
|
||||
[JSON.stringify(values), scenarioId, label]
|
||||
)
|
||||
}
|
||||
|
||||
// EBIT (Betriebsergebnis)
|
||||
const ebit: AnnualValues = {}
|
||||
for (let y = 2026; y <= 2030; y++) {
|
||||
const k = `y${y}`
|
||||
ebit[k] = Math.round((umsatzAnnual[k] || 0) - (materialAnnual[k] || 0) - (personalAnnual[k] || 0) - (afaAnnual[k] || 0) - (sonstigeAnnual[k] || 0))
|
||||
}
|
||||
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ebit), scenarioId, 'EBIT'])
|
||||
|
||||
// Tax computation with Verlustvortrag
|
||||
const { gewerbesteuer, koerperschaftsteuer, steuernGesamt, ergebnisNachSteuern } = computeTaxes(ebit)
|
||||
|
||||
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(gewerbesteuer), scenarioId, 'Gewerbesteuer'])
|
||||
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(koerperschaftsteuer), scenarioId, 'Körperschaftssteuer'])
|
||||
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(steuernGesamt), scenarioId, 'Steuern gesamt'])
|
||||
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ergebnisNachSteuern), scenarioId, 'Ergebnis nach Steuern'])
|
||||
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ergebnisNachSteuern), scenarioId, 'Jahresüberschuss'])
|
||||
|
||||
// Write taxes to Liquidität (monthly = 1/12 of annual amount)
|
||||
await writeTaxToLiquiditaet(pool, findLiq('Gewerbesteuer'), gewerbesteuer)
|
||||
await writeTaxToLiquiditaet(pool, findLiq('Körperschaftsteuer'), koerperschaftsteuer)
|
||||
|
||||
return [ebit]
|
||||
}
|
||||
|
||||
// --- Tax helpers ---
|
||||
|
||||
// Stockach 78333: Hebesatz 350%
|
||||
// Gewerbesteuer = 3,5% × 3,5 = 12,25%
|
||||
// Körperschaftsteuer = 15% + 5,5% Soli = 15,825%
|
||||
const GEWERBESTEUER_RATE = 0.035 * 3.5 // 12,25%
|
||||
const KOERPERSCHAFTSTEUER_RATE = 0.15 * 1.055 // 15,825% (inkl. Soli)
|
||||
|
||||
function computeTaxes(ebit: AnnualValues) {
|
||||
const gewerbesteuer: AnnualValues = {}
|
||||
const koerperschaftsteuer: AnnualValues = {}
|
||||
const steuernGesamt: AnnualValues = {}
|
||||
const ergebnisNachSteuern: AnnualValues = {}
|
||||
let verlustvortrag = 0
|
||||
|
||||
for (let y = 2026; y <= 2030; y++) {
|
||||
const k = `y${y}`
|
||||
const gewinn = ebit[k] || 0
|
||||
|
||||
if (gewinn <= 0) {
|
||||
verlustvortrag += Math.abs(gewinn)
|
||||
gewerbesteuer[k] = 0
|
||||
koerperschaftsteuer[k] = 0
|
||||
steuernGesamt[k] = 0
|
||||
ergebnisNachSteuern[k] = Math.round(gewinn)
|
||||
} else {
|
||||
// Bis 1 Mio EUR: 100% verrechenbar
|
||||
// Über 1 Mio EUR: nur 60% verrechenbar (Mindestbesteuerung)
|
||||
let verrechenbar = 0
|
||||
if (verlustvortrag > 0) {
|
||||
if (gewinn <= 1000000) {
|
||||
verrechenbar = Math.min(verlustvortrag, gewinn)
|
||||
} else {
|
||||
verrechenbar = Math.min(verlustvortrag, 1000000 + (gewinn - 1000000) * 0.6)
|
||||
}
|
||||
verlustvortrag -= verrechenbar
|
||||
}
|
||||
|
||||
const zuVersteuern = Math.max(0, gewinn - verrechenbar)
|
||||
const gst = Math.round(zuVersteuern * GEWERBESTEUER_RATE)
|
||||
const kst = Math.round(zuVersteuern * KOERPERSCHAFTSTEUER_RATE)
|
||||
|
||||
gewerbesteuer[k] = gst
|
||||
koerperschaftsteuer[k] = kst
|
||||
steuernGesamt[k] = gst + kst
|
||||
ergebnisNachSteuern[k] = Math.round(gewinn - gst - kst)
|
||||
}
|
||||
}
|
||||
|
||||
return { gewerbesteuer, koerperschaftsteuer, steuernGesamt, ergebnisNachSteuern }
|
||||
}
|
||||
|
||||
async function writeTaxToLiquiditaet(
|
||||
pool: Pool,
|
||||
liqRow: FPLiquiditaet | undefined,
|
||||
annualTax: AnnualValues,
|
||||
): Promise<void> {
|
||||
if (!liqRow) return
|
||||
const v = emptyMonthly()
|
||||
for (let y = 2026; y <= 2030; y++) {
|
||||
const jahresBetrag = annualTax[`y${y}`] || 0
|
||||
if (jahresBetrag > 0) {
|
||||
const monatlich = Math.round(jahresBetrag / 12)
|
||||
const startM = (y - 2026) * 12 + 1
|
||||
for (let m = startM; m <= startM + 11 && m <= MONTHS; m++) {
|
||||
v[`m${m}`] = monatlich
|
||||
}
|
||||
}
|
||||
}
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(v), liqRow.id])
|
||||
liqRow.values = v
|
||||
}
|
||||
154
pitch-deck/lib/finanzplan/engine-liquiditaet.ts
Normal file
154
pitch-deck/lib/finanzplan/engine-liquiditaet.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Liquiditaet — rolling cash balance computation
|
||||
*
|
||||
* Computes operative Einzahlungen/Auszahlungen sums,
|
||||
* Überschuss vor Investitionen/Entnahmen, and rolling Kontostand.
|
||||
*/
|
||||
|
||||
import { Pool } from 'pg'
|
||||
import {
|
||||
MonthlyValues, MONTHS,
|
||||
emptyMonthly,
|
||||
FPLiquiditaet,
|
||||
} from './types'
|
||||
|
||||
export interface LiquiditaetContext {
|
||||
totalRevenue: MonthlyValues
|
||||
totalMaterial: MonthlyValues
|
||||
totalPersonal: MonthlyValues
|
||||
totalSonstige: MonthlyValues
|
||||
totalInvest: MonthlyValues
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute all liquidity rows and rolling balance.
|
||||
* Writes computed values back to DB.
|
||||
*/
|
||||
export async function computeLiquiditaet(
|
||||
pool: Pool,
|
||||
liquid: FPLiquiditaet[],
|
||||
ctx: LiquiditaetContext,
|
||||
): Promise<{ endstand: MonthlyValues }> {
|
||||
const findLiq = (label: string) => liquid.find(r => r.row_label === label)
|
||||
|
||||
// Computed rows — link to computed totals
|
||||
const liqUmsatz = findLiq('Umsatzerlöse')
|
||||
if (liqUmsatz) {
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ctx.totalRevenue), liqUmsatz.id])
|
||||
liqUmsatz.values = ctx.totalRevenue
|
||||
}
|
||||
const liqMaterial = findLiq('Materialaufwand')
|
||||
if (liqMaterial) {
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ctx.totalMaterial), liqMaterial.id])
|
||||
liqMaterial.values = ctx.totalMaterial
|
||||
}
|
||||
const liqPersonal = findLiq('Personalkosten')
|
||||
if (liqPersonal) {
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ctx.totalPersonal), liqPersonal.id])
|
||||
liqPersonal.values = ctx.totalPersonal
|
||||
}
|
||||
const liqSonstige = findLiq('Sonstige Kosten')
|
||||
if (liqSonstige) {
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ctx.totalSonstige), liqSonstige.id])
|
||||
liqSonstige.values = ctx.totalSonstige
|
||||
}
|
||||
const liqInvest = findLiq('Investitionen')
|
||||
if (liqInvest) {
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(ctx.totalInvest), liqInvest.id])
|
||||
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')
|
||||
|
||||
// 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')
|
||||
)
|
||||
|
||||
// Summe EINZAHLUNGEN = nur operativ
|
||||
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)
|
||||
}
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), sumEin.id])
|
||||
sumEin.values = s
|
||||
}
|
||||
|
||||
// Summe AUSZAHLUNGEN = nur operativ
|
||||
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)
|
||||
}
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), sumAus.id])
|
||||
sumAus.values = s
|
||||
}
|
||||
|
||||
// OPERATIVER ÜBERSCHUSS VOR INVESTITIONEN
|
||||
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))
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), uebVorInv.id])
|
||||
uebVorInv.values = s
|
||||
}
|
||||
|
||||
// ÜBERSCHUSS VOR ENTNAHMEN = Operativer Überschuss - 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))
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), uebVorEnt.id])
|
||||
uebVorEnt.values = s
|
||||
}
|
||||
|
||||
// ÜBERSCHUSS = Überschuss vor Entnahmen - Entnahmen
|
||||
const entnahmen = findLiq('Kapitalentnahmen/Ausschüttungen')
|
||||
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))
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), ueberschuss.id])
|
||||
ueberschuss.values = s
|
||||
}
|
||||
|
||||
// Rolling Kontostand: Vormonat + Operativer Überschuss + Finanzierung
|
||||
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))
|
||||
}
|
||||
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() }
|
||||
}
|
||||
106
pitch-deck/lib/finanzplan/engine-sheets.ts
Normal file
106
pitch-deck/lib/finanzplan/engine-sheets.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Sheet Calculators — pure computation functions (no DB dependency)
|
||||
*
|
||||
* Used by the main engine to compute Personalkosten, Investitionen,
|
||||
* and aggregate monthly values.
|
||||
*/
|
||||
|
||||
import {
|
||||
MonthlyValues, MONTHS, FOUNDING_MONTH,
|
||||
emptyMonthly, dateToMonth, monthToDate,
|
||||
FPPersonalkosten, FPInvestitionen,
|
||||
} from './types'
|
||||
|
||||
export function computePersonalkosten(positions: FPPersonalkosten[]): FPPersonalkosten[] {
|
||||
return positions.map(p => {
|
||||
const brutto = emptyMonthly()
|
||||
const sozial = emptyMonthly()
|
||||
const total = emptyMonthly()
|
||||
|
||||
if (!p.start_date || !p.brutto_monthly) return { ...p, values_brutto: brutto, values_sozial: sozial, values_total: total }
|
||||
|
||||
const startDate = new Date(p.start_date)
|
||||
const startM = dateToMonth(startDate.getFullYear(), startDate.getMonth() + 1)
|
||||
const endM = p.end_date
|
||||
? dateToMonth(new Date(p.end_date).getFullYear(), new Date(p.end_date).getMonth() + 1)
|
||||
: MONTHS
|
||||
|
||||
for (let m = Math.max(1, startM); m <= Math.min(MONTHS, endM); m++) {
|
||||
const { year } = monthToDate(m)
|
||||
const yearsFromStart = year - startDate.getFullYear()
|
||||
const raise = Math.pow(1 + (p.annual_raise_pct || 0) / 100, yearsFromStart)
|
||||
const monthlyBrutto = Math.round(p.brutto_monthly * raise)
|
||||
|
||||
brutto[`m${m}`] = monthlyBrutto
|
||||
sozial[`m${m}`] = Math.round(monthlyBrutto * (p.ag_sozial_pct || 20.425) / 100)
|
||||
total[`m${m}`] = brutto[`m${m}`] + sozial[`m${m}`]
|
||||
}
|
||||
|
||||
return { ...p, values_brutto: brutto, values_sozial: sozial, values_total: total }
|
||||
})
|
||||
}
|
||||
|
||||
export function computeInvestitionen(items: FPInvestitionen[]): FPInvestitionen[] {
|
||||
return items.map(item => {
|
||||
const invest = emptyMonthly()
|
||||
const afa = emptyMonthly()
|
||||
|
||||
if (!item.purchase_date || !item.purchase_amount) return { ...item, values_invest: invest, values_afa: afa }
|
||||
|
||||
const d = new Date(item.purchase_date)
|
||||
const purchaseM = dateToMonth(d.getFullYear(), d.getMonth() + 1)
|
||||
|
||||
if (purchaseM >= 1 && purchaseM <= MONTHS) {
|
||||
invest[`m${purchaseM}`] = item.purchase_amount
|
||||
}
|
||||
|
||||
// AfA (linear depreciation)
|
||||
if (item.afa_years && item.afa_years > 0) {
|
||||
const afaMonths = item.afa_years * 12
|
||||
const monthlyAfa = Math.round(item.purchase_amount / afaMonths)
|
||||
for (let m = purchaseM; m < purchaseM + afaMonths && m <= MONTHS; m++) {
|
||||
if (m >= 1) afa[`m${m}`] = monthlyAfa
|
||||
}
|
||||
} else {
|
||||
// GWG: full depreciation in purchase month
|
||||
if (purchaseM >= 1 && purchaseM <= MONTHS) {
|
||||
afa[`m${purchaseM}`] = item.purchase_amount
|
||||
}
|
||||
}
|
||||
|
||||
return { ...item, values_invest: invest, values_afa: afa }
|
||||
})
|
||||
}
|
||||
|
||||
export function sumRows(rows: { values: MonthlyValues }[]): MonthlyValues {
|
||||
const result = emptyMonthly()
|
||||
for (const row of rows) {
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
result[`m${m}`] += row.values[`m${m}`] || 0
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function sumField(rows: { [key: string]: MonthlyValues }[], field: string): MonthlyValues {
|
||||
const result = emptyMonthly()
|
||||
for (const row of rows) {
|
||||
const v = row[field] as MonthlyValues
|
||||
if (!v) continue
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
result[`m${m}`] += v[`m${m}`] || 0
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute headcount per month from personal positions.
|
||||
*/
|
||||
export function computeHeadcount(personal: FPPersonalkosten[]): MonthlyValues {
|
||||
const headcount = emptyMonthly()
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
headcount[`m${m}`] = personal.filter(p => (p.values_total[`m${m}`] || 0) > 0).length
|
||||
}
|
||||
return headcount
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Finanzplan Compute Engine
|
||||
* Finanzplan Compute Engine — Orchestrator
|
||||
*
|
||||
* Dependency order:
|
||||
* Personalkosten (independent inputs)
|
||||
@@ -9,113 +9,43 @@
|
||||
* Sonst. betr. Erträge (independent)
|
||||
* Liquidität (aggregates all above)
|
||||
* GuV (annual summary)
|
||||
*
|
||||
* Split into modules:
|
||||
* engine-sheets.ts — pure calculators (no DB)
|
||||
* engine-betrieb.ts — betriebliche aufwendungen
|
||||
* engine-liquiditaet.ts — liquidity / cash flow
|
||||
* engine-guv.ts — GuV / P&L + taxes
|
||||
*/
|
||||
|
||||
import { Pool } from 'pg'
|
||||
import {
|
||||
MonthlyValues, AnnualValues, MONTHS, FOUNDING_MONTH,
|
||||
emptyMonthly, sumMonthly, annualSums, dateToMonth, monthToDate,
|
||||
MonthlyValues, MONTHS, FOUNDING_MONTH,
|
||||
emptyMonthly,
|
||||
FPPersonalkosten, FPInvestitionen, FPBetrieblicheAufwendungen,
|
||||
FPLiquiditaet, FPComputeResult
|
||||
FPLiquiditaet, FPComputeResult,
|
||||
} from './types'
|
||||
import {
|
||||
computePersonalkosten, computeInvestitionen,
|
||||
sumField, computeHeadcount,
|
||||
} from './engine-sheets'
|
||||
import { computeBetrieblicheAufwendungen } from './engine-betrieb'
|
||||
import { computeLiquiditaet } from './engine-liquiditaet'
|
||||
import { computeGuV } from './engine-guv'
|
||||
|
||||
// --- Sheet Calculators ---
|
||||
// Re-export sheet calculators for direct consumers
|
||||
export { computePersonalkosten, computeInvestitionen } from './engine-sheets'
|
||||
|
||||
export function computePersonalkosten(positions: FPPersonalkosten[]): FPPersonalkosten[] {
|
||||
return positions.map(p => {
|
||||
const brutto = emptyMonthly()
|
||||
const sozial = emptyMonthly()
|
||||
const total = emptyMonthly()
|
||||
|
||||
if (!p.start_date || !p.brutto_monthly) return { ...p, values_brutto: brutto, values_sozial: sozial, values_total: total }
|
||||
|
||||
const startDate = new Date(p.start_date)
|
||||
const startM = dateToMonth(startDate.getFullYear(), startDate.getMonth() + 1)
|
||||
const endM = p.end_date
|
||||
? dateToMonth(new Date(p.end_date).getFullYear(), new Date(p.end_date).getMonth() + 1)
|
||||
: MONTHS
|
||||
|
||||
for (let m = Math.max(1, startM); m <= Math.min(MONTHS, endM); m++) {
|
||||
const { year } = monthToDate(m)
|
||||
const yearsFromStart = year - startDate.getFullYear()
|
||||
const raise = Math.pow(1 + (p.annual_raise_pct || 0) / 100, yearsFromStart)
|
||||
const monthlyBrutto = Math.round(p.brutto_monthly * raise)
|
||||
|
||||
brutto[`m${m}`] = monthlyBrutto
|
||||
sozial[`m${m}`] = Math.round(monthlyBrutto * (p.ag_sozial_pct || 20.425) / 100)
|
||||
total[`m${m}`] = brutto[`m${m}`] + sozial[`m${m}`]
|
||||
}
|
||||
|
||||
return { ...p, values_brutto: brutto, values_sozial: sozial, values_total: total }
|
||||
})
|
||||
}
|
||||
|
||||
export function computeInvestitionen(items: FPInvestitionen[]): FPInvestitionen[] {
|
||||
return items.map(item => {
|
||||
const invest = emptyMonthly()
|
||||
const afa = emptyMonthly()
|
||||
|
||||
if (!item.purchase_date || !item.purchase_amount) return { ...item, values_invest: invest, values_afa: afa }
|
||||
|
||||
const d = new Date(item.purchase_date)
|
||||
const purchaseM = dateToMonth(d.getFullYear(), d.getMonth() + 1)
|
||||
|
||||
if (purchaseM >= 1 && purchaseM <= MONTHS) {
|
||||
invest[`m${purchaseM}`] = item.purchase_amount
|
||||
}
|
||||
|
||||
// AfA (linear depreciation)
|
||||
if (item.afa_years && item.afa_years > 0) {
|
||||
const afaMonths = item.afa_years * 12
|
||||
const monthlyAfa = Math.round(item.purchase_amount / afaMonths)
|
||||
for (let m = purchaseM; m < purchaseM + afaMonths && m <= MONTHS; m++) {
|
||||
if (m >= 1) afa[`m${m}`] = monthlyAfa
|
||||
}
|
||||
} else {
|
||||
// GWG: full depreciation in purchase month
|
||||
if (purchaseM >= 1 && purchaseM <= MONTHS) {
|
||||
afa[`m${purchaseM}`] = item.purchase_amount
|
||||
}
|
||||
}
|
||||
|
||||
return { ...item, values_invest: invest, values_afa: afa }
|
||||
})
|
||||
}
|
||||
|
||||
function sumRows(rows: { values: MonthlyValues }[]): MonthlyValues {
|
||||
const result = emptyMonthly()
|
||||
for (const row of rows) {
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
result[`m${m}`] += row.values[`m${m}`] || 0
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function sumField(rows: { [key: string]: MonthlyValues }[], field: string): MonthlyValues {
|
||||
const result = emptyMonthly()
|
||||
for (const row of rows) {
|
||||
const v = row[field] as MonthlyValues
|
||||
if (!v) continue
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
result[`m${m}`] += v[`m${m}`] || 0
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
// Import types used inline
|
||||
type FPUmsatzerloese = import('./types').FPUmsatzerloese
|
||||
type FPMaterialaufwand = import('./types').FPMaterialaufwand
|
||||
|
||||
// --- Main Engine ---
|
||||
|
||||
export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise<FPComputeResult> {
|
||||
// 1. Load all editable data from DB
|
||||
const [
|
||||
personalRows,
|
||||
investRows,
|
||||
betriebRows,
|
||||
liquidRows,
|
||||
kundenSummary,
|
||||
umsatzRows,
|
||||
materialRows,
|
||||
personalRows, investRows, betriebRows, liquidRows,
|
||||
kundenSummary, umsatzRows, materialRows,
|
||||
] = await Promise.all([
|
||||
pool.query('SELECT * FROM fp_personalkosten WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]),
|
||||
pool.query('SELECT * FROM fp_investitionen WHERE scenario_id = $1 ORDER BY sort_order', [scenarioId]),
|
||||
@@ -131,12 +61,8 @@ export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise
|
||||
const totalBrutto = sumField(personal as any, 'values_brutto')
|
||||
const totalSozial = sumField(personal as any, 'values_sozial')
|
||||
const totalPersonal = sumField(personal as any, 'values_total')
|
||||
const headcount = emptyMonthly()
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
headcount[`m${m}`] = personal.filter(p => (p.values_total[`m${m}`] || 0) > 0).length
|
||||
}
|
||||
const headcount = computeHeadcount(personal)
|
||||
|
||||
// Write computed values back to DB
|
||||
for (const p of personal) {
|
||||
await pool.query(
|
||||
'UPDATE fp_personalkosten SET values_brutto = $1, values_sozial = $2, values_total = $3 WHERE id = $4',
|
||||
@@ -156,16 +82,84 @@ export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Umsatzerlöse (quantity × price)
|
||||
const prices = (umsatzRows.rows as FPUmsatzerloese[]).filter(r => r.section === 'price')
|
||||
const quantities = (umsatzRows.rows as FPUmsatzerloese[]).filter(r => r.section === 'quantity')
|
||||
const revenueRows = (umsatzRows.rows as FPUmsatzerloese[]).filter(r => r.section === 'revenue')
|
||||
// 4. Umsatzerlöse (quantity × price) + Materialaufwand
|
||||
const { totalRevenue, totalMaterial } = await computeRevenueAndMaterial(
|
||||
pool, umsatzRows.rows as FPUmsatzerloese[], materialRows.rows as FPMaterialaufwand[]
|
||||
)
|
||||
|
||||
// 5. Bestandskunden (for formula-based costs)
|
||||
const kundenRows = await pool.query(
|
||||
"SELECT segment_name, row_label, values FROM fp_kunden WHERE scenario_id = $1 AND row_label LIKE 'Bestandskunden%' ORDER BY sort_order",
|
||||
[scenarioId]
|
||||
)
|
||||
const totalBestandskunden = emptyMonthly()
|
||||
for (const row of kundenRows.rows) {
|
||||
const rl = (row as { row_label?: string }).row_label || ''
|
||||
if (rl.includes('Bestandskunden') && !rl.includes('gesamt')) {
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
totalBestandskunden[`m${m}`] += row.values?.[`m${m}`] || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cloud-Hosting in Materialaufwand
|
||||
const matRows = materialRows.rows as FPMaterialaufwand[]
|
||||
const cloudRow = matRows.find(r => r.row_label.includes('Cloud-Hosting'))
|
||||
if (cloudRow) {
|
||||
const computed = emptyMonthly()
|
||||
for (let m = FOUNDING_MONTH; m <= MONTHS; m++) {
|
||||
const kunden = totalBestandskunden[`m${m}`] || 0
|
||||
const extraKunden = Math.max(0, kunden - 10)
|
||||
computed[`m${m}`] = Math.round(extraKunden * 100 + 1500)
|
||||
}
|
||||
await pool.query('UPDATE fp_materialaufwand SET values = $1 WHERE id = $2', [JSON.stringify(computed), cloudRow.id])
|
||||
cloudRow.values = computed
|
||||
}
|
||||
|
||||
// 6. Betriebliche Aufwendungen
|
||||
const betrieb = betriebRows.rows as FPBetrieblicheAufwendungen[]
|
||||
const { totalSonstige, totalGesamt } = await computeBetrieblicheAufwendungen(pool, betrieb, {
|
||||
totalBrutto, totalPersonal, totalAfa, totalRevenue, totalMaterial,
|
||||
headcount, totalBestandskunden,
|
||||
})
|
||||
|
||||
// 7. Liquidität
|
||||
const liquid = liquidRows.rows as FPLiquiditaet[]
|
||||
const { endstand } = await computeLiquiditaet(pool, liquid, {
|
||||
totalRevenue, totalMaterial, totalPersonal, totalSonstige, totalInvest,
|
||||
})
|
||||
|
||||
// 8. GuV
|
||||
const guv = await computeGuV(pool, scenarioId, liquid, {
|
||||
totalRevenue, totalMaterial, totalBrutto, totalSozial,
|
||||
totalPersonal, totalAfa, totalSonstige,
|
||||
})
|
||||
|
||||
return {
|
||||
personalkosten: { total_brutto: totalBrutto, total_sozial: totalSozial, total: totalPersonal, positions: personal, headcount },
|
||||
investitionen: { total_invest: totalInvest, total_afa: totalAfa, items: invest },
|
||||
umsatzerloese: { total: totalRevenue },
|
||||
materialaufwand: { total: totalMaterial },
|
||||
betriebliche: { total_sonstige: totalSonstige, total_gesamt: totalGesamt },
|
||||
liquiditaet: { rows: liquid, endstand },
|
||||
guv,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Revenue & Material helpers (kept here to avoid circular deps) ---
|
||||
|
||||
async function computeRevenueAndMaterial(
|
||||
pool: Pool,
|
||||
umsatzAllRows: FPUmsatzerloese[],
|
||||
materialAllRows: FPMaterialaufwand[],
|
||||
): Promise<{ totalRevenue: MonthlyValues; totalMaterial: MonthlyValues }> {
|
||||
const prices = umsatzAllRows.filter(r => r.section === 'price')
|
||||
const quantities = umsatzAllRows.filter(r => r.section === 'quantity')
|
||||
const revenueRows = umsatzAllRows.filter(r => r.section === 'revenue')
|
||||
const totalRevenue = emptyMonthly()
|
||||
|
||||
// Revenue = quantity × price for each module (if qty+price exist)
|
||||
// Match by tier name extracted from parentheses, or exact label match
|
||||
const extractTier = (label: string) => { const m = label.match(/\(([^)]+)\)/); return m ? m[1] : label }
|
||||
// Revenue rows WITHOUT matching qty/price are kept as-is (e.g. Beratung & Service)
|
||||
|
||||
for (const rev of revenueRows) {
|
||||
if (rev.row_label === 'GESAMTUMSATZ') continue
|
||||
const tier = extractTier(rev.row_label)
|
||||
@@ -178,20 +172,18 @@ export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise
|
||||
}
|
||||
await pool.query('UPDATE fp_umsatzerloese SET values = $1 WHERE id = $2', [JSON.stringify(rev.values), rev.id])
|
||||
}
|
||||
// Add ALL revenue rows to total (computed or manual)
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
totalRevenue[`m${m}`] += rev.values[`m${m}`] || 0
|
||||
}
|
||||
}
|
||||
// Update GESAMTUMSATZ
|
||||
const gesamtUmsatz = revenueRows.find(r => r.row_label === 'GESAMTUMSATZ')
|
||||
if (gesamtUmsatz) {
|
||||
await pool.query('UPDATE fp_umsatzerloese SET values = $1 WHERE id = $2', [JSON.stringify(totalRevenue), gesamtUmsatz.id])
|
||||
}
|
||||
|
||||
// 5. Materialaufwand (quantity × unit_cost) — simplified
|
||||
const matCosts = (materialRows.rows as FPMaterialaufwand[]).filter(r => r.section === 'cost')
|
||||
const matUnitCosts = (materialRows.rows as FPMaterialaufwand[]).filter(r => r.section === 'unit_cost')
|
||||
// Materialaufwand
|
||||
const matCosts = materialAllRows.filter(r => r.section === 'cost')
|
||||
const matUnitCosts = materialAllRows.filter(r => r.section === 'unit_cost')
|
||||
const totalMaterial = emptyMonthly()
|
||||
|
||||
for (const cost of matCosts) {
|
||||
@@ -205,7 +197,6 @@ export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise
|
||||
}
|
||||
await pool.query('UPDATE fp_materialaufwand SET values = $1 WHERE id = $2', [JSON.stringify(cost.values), cost.id])
|
||||
}
|
||||
// Add ALL cost rows to total (computed or manual)
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
totalMaterial[`m${m}`] += cost.values[`m${m}`] || 0
|
||||
}
|
||||
@@ -215,450 +206,5 @@ export async function computeFinanzplan(pool: Pool, scenarioId: string): Promise
|
||||
await pool.query('UPDATE fp_materialaufwand SET values = $1 WHERE id = $2', [JSON.stringify(totalMaterial), matSumme.id])
|
||||
}
|
||||
|
||||
// 5b. Headcount without founders (for formula-based costs)
|
||||
const NUM_FOUNDERS = 2
|
||||
const hcWithoutFounders = emptyMonthly()
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
hcWithoutFounders[`m${m}`] = Math.max(0, headcount[`m${m}`] - NUM_FOUNDERS)
|
||||
}
|
||||
|
||||
// 5c. Total Bestandskunden (for Bewirtungskosten — uses totalBestandskunden from Serverkosten above)
|
||||
// Also load enterprise customers separately for legacy compatibility
|
||||
const kundenRows = await pool.query(
|
||||
"SELECT segment_name, row_label, values FROM fp_kunden WHERE scenario_id = $1 AND row_label LIKE 'Bestandskunden%' ORDER BY sort_order",
|
||||
[scenarioId]
|
||||
)
|
||||
const enterpriseKunden = emptyMonthly()
|
||||
for (const row of kundenRows.rows) {
|
||||
if (row.segment_name?.toLowerCase().includes('enterprise')) {
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
enterpriseKunden[`m${m}`] = row.values?.[`m${m}`] || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Betriebliche Aufwendungen — compute formula-based rows + sum rows
|
||||
const betrieb = betriebRows.rows as FPBetrieblicheAufwendungen[]
|
||||
|
||||
// Pre-compute total Bestandskunden (needed for Bewirtungskosten + Serverkosten)
|
||||
const totalBestandskunden = emptyMonthly()
|
||||
for (const row of kundenRows.rows) {
|
||||
const rl = (row as { row_label?: string }).row_label || ''
|
||||
if (rl.includes('Bestandskunden') && !rl.includes('gesamt')) {
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
totalBestandskunden[`m${m}`] += row.values?.[`m${m}`] || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Formula-based rows: derive from headcount (excl. founders) or customers
|
||||
const formulaRows: { label: string; perUnit: number; source: MonthlyValues }[] = [
|
||||
{ label: 'Fort-/Weiterbildungskosten (F)', perUnit: 300, source: hcWithoutFounders },
|
||||
// KFZ costs are manual (from Jan 2028), not formula-based
|
||||
{ label: 'Reisekosten (F)', perUnit: 75, source: headcount },
|
||||
{ label: 'Bewirtungskosten (F)', perUnit: 50, source: totalBestandskunden },
|
||||
{ label: 'Internet/Mobilfunk (F)', perUnit: 50, source: headcount },
|
||||
]
|
||||
|
||||
for (const fr of formulaRows) {
|
||||
const row = betrieb.find(r => r.row_label === fr.label)
|
||||
if (row) {
|
||||
const computed = emptyMonthly()
|
||||
for (let m = FOUNDING_MONTH; m <= MONTHS; m++) {
|
||||
computed[`m${m}`] = Math.round((fr.source[`m${m}`] || 0) * fr.perUnit)
|
||||
}
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), row.id])
|
||||
row.values = computed
|
||||
}
|
||||
}
|
||||
|
||||
// Berufsgenossenschaft (VBG IT/Büro): ~0.5% of total brutto payroll
|
||||
const bgRow = betrieb.find(r => r.row_label.includes('Berufsgenossenschaft'))
|
||||
if (bgRow) {
|
||||
const computed = emptyMonthly()
|
||||
for (let m = FOUNDING_MONTH; m <= MONTHS; m++) {
|
||||
computed[`m${m}`] = Math.round((totalBrutto[`m${m}`] || 0) * 0.005)
|
||||
}
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), bgRow.id])
|
||||
bgRow.values = computed
|
||||
}
|
||||
|
||||
// Allgemeine Marketingkosten: 8% of revenue (2026-2028), 10% from 2029
|
||||
const marketingRow = betrieb.find(r => r.row_label.includes('Allgemeine Marketingkosten'))
|
||||
if (marketingRow) {
|
||||
const computed = emptyMonthly()
|
||||
for (let m = FOUNDING_MONTH; m <= MONTHS; m++) {
|
||||
const rate = m <= 36 ? 0.08 : 0.10 // m36 = Dec 2028
|
||||
computed[`m${m}`] = Math.round((totalRevenue[`m${m}`] || 0) * rate)
|
||||
}
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), marketingRow.id])
|
||||
marketingRow.values = computed
|
||||
}
|
||||
|
||||
// Serverkosten now in Materialaufwand — compute Cloud-Hosting formula there
|
||||
const matRows = materialRows.rows as FPMaterialaufwand[]
|
||||
const cloudRow = matRows.find(r => r.row_label.includes('Cloud-Hosting'))
|
||||
if (cloudRow) {
|
||||
const computed = emptyMonthly()
|
||||
for (let m = FOUNDING_MONTH; m <= MONTHS; m++) {
|
||||
const kunden = totalBestandskunden[`m${m}`] || 0
|
||||
const extraKunden = Math.max(0, kunden - 10) // first 10 included in base
|
||||
computed[`m${m}`] = Math.round(extraKunden * 100 + 1500)
|
||||
}
|
||||
await pool.query('UPDATE fp_materialaufwand SET values = $1 WHERE id = $2', [JSON.stringify(computed), cloudRow.id])
|
||||
cloudRow.values = computed
|
||||
}
|
||||
|
||||
// Update Personalkosten row
|
||||
const persBetrieb = betrieb.find(r => r.row_label === 'Personalkosten')
|
||||
if (persBetrieb) {
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(totalPersonal), persBetrieb.id])
|
||||
persBetrieb.values = totalPersonal
|
||||
}
|
||||
// Update Abschreibungen row
|
||||
const abrBetrieb = betrieb.find(r => r.row_label === 'Abschreibungen')
|
||||
if (abrBetrieb) {
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(totalAfa), abrBetrieb.id])
|
||||
abrBetrieb.values = totalAfa
|
||||
}
|
||||
|
||||
// Gewerbesteuer (F): 12.25% of monthly profit (only when positive)
|
||||
// Monthly profit = Revenue - Material - Personnel - AfA - other opex (excl. taxes)
|
||||
const gewStRow = betrieb.find(r => r.row_label.includes('Gewerbesteuer'))
|
||||
if (gewStRow) {
|
||||
const nonTaxOpex = betrieb.filter(r =>
|
||||
r.category !== 'steuern' && r.category !== 'personal' && r.category !== 'abschreibungen' &&
|
||||
!r.is_sum_row && !r.row_label.includes('Summe') && !r.row_label.includes('SUMME')
|
||||
)
|
||||
const computed = emptyMonthly()
|
||||
for (let m = FOUNDING_MONTH; m <= MONTHS; m++) {
|
||||
const rev = totalRevenue[`m${m}`] || 0
|
||||
const mat = totalMaterial[`m${m}`] || 0
|
||||
const pers = totalPersonal[`m${m}`] || 0
|
||||
const afa = totalAfa[`m${m}`] || 0
|
||||
let opex = 0
|
||||
for (const r of nonTaxOpex) { opex += r.values[`m${m}`] || 0 }
|
||||
const profit = rev - mat - pers - afa - opex
|
||||
computed[`m${m}`] = profit > 0 ? Math.round(profit * 0.1225) : 0
|
||||
}
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(computed), gewStRow.id])
|
||||
gewStRow.values = computed
|
||||
}
|
||||
|
||||
// Compute category sums
|
||||
const categories = ['steuern', 'versicherungen', 'besondere', 'marketing', 'sonstige']
|
||||
for (const cat of categories) {
|
||||
const sumRow = betrieb.find(r => r.category === cat && r.is_sum_row)
|
||||
const detailRows = betrieb.filter(r => r.category === cat && !r.is_sum_row)
|
||||
if (sumRow && detailRows.length > 0) {
|
||||
const s = sumRows(detailRows)
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(s), sumRow.id])
|
||||
sumRow.values = s
|
||||
}
|
||||
}
|
||||
|
||||
// Summe sonstige (ohne Personal, Abschreibungen)
|
||||
const sonstSumme = betrieb.find(r => r.row_label.includes('Summe sonstige'))
|
||||
if (sonstSumme) {
|
||||
const nonPersonNonAbr = betrieb.filter(r =>
|
||||
r.row_label !== 'Personalkosten' && r.row_label !== 'Abschreibungen' &&
|
||||
!r.row_label.includes('Summe sonstige') && !r.row_label.includes('Gesamtkosten') &&
|
||||
!r.is_sum_row && r.category !== 'personal' && r.category !== 'abschreibungen'
|
||||
)
|
||||
const s = sumRows(nonPersonNonAbr)
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(s), sonstSumme.id])
|
||||
sonstSumme.values = s
|
||||
}
|
||||
|
||||
// Gesamtkosten
|
||||
const gesamtBetrieb = betrieb.find(r => r.row_label.includes('Gesamtkosten') || r.row_label.includes('SUMME Betriebliche'))
|
||||
const totalSonstige = sonstSumme?.values || emptyMonthly()
|
||||
if (gesamtBetrieb) {
|
||||
const g = emptyMonthly()
|
||||
for (let m = 1; m <= MONTHS; m++) {
|
||||
g[`m${m}`] = (totalPersonal[`m${m}`] || 0) + (totalAfa[`m${m}`] || 0) + (totalSonstige[`m${m}`] || 0)
|
||||
}
|
||||
await pool.query('UPDATE fp_betriebliche_aufwendungen SET values = $1 WHERE id = $2', [JSON.stringify(g), gesamtBetrieb.id])
|
||||
gesamtBetrieb.values = g
|
||||
}
|
||||
|
||||
// 7. Liquidität
|
||||
const liquid = liquidRows.rows as FPLiquiditaet[]
|
||||
const findLiq = (label: string) => liquid.find(r => r.row_label === label)
|
||||
|
||||
// Computed rows
|
||||
const liqUmsatz = findLiq('Umsatzerlöse')
|
||||
if (liqUmsatz) {
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalRevenue), liqUmsatz.id])
|
||||
liqUmsatz.values = totalRevenue
|
||||
}
|
||||
const liqMaterial = findLiq('Materialaufwand')
|
||||
if (liqMaterial) {
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalMaterial), liqMaterial.id])
|
||||
liqMaterial.values = totalMaterial
|
||||
}
|
||||
const liqPersonal = findLiq('Personalkosten')
|
||||
if (liqPersonal) {
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalPersonal), liqPersonal.id])
|
||||
liqPersonal.values = totalPersonal
|
||||
}
|
||||
const liqSonstige = findLiq('Sonstige Kosten')
|
||||
if (liqSonstige) {
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalSonstige), liqSonstige.id])
|
||||
liqSonstige.values = totalSonstige
|
||||
}
|
||||
const liqInvest = findLiq('Investitionen')
|
||||
if (liqInvest) {
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(totalInvest), liqInvest.id])
|
||||
liqInvest.values = totalInvest
|
||||
}
|
||||
|
||||
// Compute sums and rolling balance
|
||||
// WICHTIG: Überschuss = nur operativer Cashflow (ohne Kapitaleinzahlungen)
|
||||
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')
|
||||
|
||||
// Dynamically categorize rows by row_type instead of hardcoded labels
|
||||
// Operative Einzahlungen (OHNE Eigenkapital, Fremdkapital, Stammkapital, Wandeldarlehen)
|
||||
const einzahlungenOperativ = ['Umsatzerlöse', 'Sonst. betriebl. Erträge', 'Anzahlungen']
|
||||
// Finanzierung: match any row with these keywords (handles renamed labels)
|
||||
const finanzierungRows = liquid.filter(r =>
|
||||
r.row_type === 'einzahlung' &&
|
||||
!einzahlungenOperativ.includes(r.row_label) &&
|
||||
!r.row_label.includes('Summe')
|
||||
)
|
||||
// Operative Auszahlungen
|
||||
const auszahlungenOperativ = ['Materialaufwand', 'Personalkosten', 'Sonstige Kosten', 'Umsatzsteuer', 'Gewerbesteuer', 'Körperschaftsteuer']
|
||||
// Finanz-Auszahlungen: any auszahlung not in operativ list
|
||||
const finanzAuszahlungRows = liquid.filter(r =>
|
||||
r.row_type === 'auszahlung' &&
|
||||
!auszahlungenOperativ.includes(r.row_label) &&
|
||||
!r.row_label.includes('Summe')
|
||||
)
|
||||
|
||||
// Summe EINZAHLUNGEN = nur operativ (für die Zeile "Summe Einzahlungen")
|
||||
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)
|
||||
}
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), sumEin.id])
|
||||
sumEin.values = s
|
||||
}
|
||||
|
||||
// Summe AUSZAHLUNGEN = nur operativ
|
||||
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)
|
||||
}
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), sumAus.id])
|
||||
sumAus.values = s
|
||||
}
|
||||
|
||||
// OPERATIVER ÜBERSCHUSS VOR INVESTITIONEN = operative Einzahlungen - operative Auszahlungen
|
||||
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))
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), uebVorInv.id])
|
||||
uebVorInv.values = s
|
||||
}
|
||||
|
||||
// ÜBERSCHUSS VOR ENTNAHMEN = Operativer Überschuss - 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))
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), uebVorEnt.id])
|
||||
uebVorEnt.values = s
|
||||
}
|
||||
|
||||
// ÜBERSCHUSS = Überschuss vor Entnahmen - Entnahmen (immer noch rein operativ)
|
||||
const entnahmen = findLiq('Kapitalentnahmen/Ausschüttungen')
|
||||
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))
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(s), ueberschuss.id])
|
||||
ueberschuss.values = s
|
||||
}
|
||||
|
||||
// Rolling Kontostand: Vormonat + Operativer Überschuss + Finanzierung
|
||||
// Finanzierung = Eigenkapital + Fremdkapital - Kreditrückzahlungen
|
||||
if (kontostand && liquiditaet && ueberschuss) {
|
||||
// Berechne monatliche Finanzierungs-Cashflows
|
||||
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}`])
|
||||
// LIQUIDITÄT = Kontostand + Operativer Überschuss + Finanzierung
|
||||
lq[`m${m}`] = Math.round(ks[`m${m}`] + (ueberschuss.values[`m${m}`] || 0) + (finCF[`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
|
||||
}
|
||||
|
||||
// 8. GuV — compute annual values
|
||||
const guv: AnnualValues[] = []
|
||||
const umsatzAnnual = annualSums(totalRevenue)
|
||||
const materialAnnual = annualSums(totalMaterial)
|
||||
const personalBruttoAnnual = annualSums(totalBrutto)
|
||||
const personalSozialAnnual = annualSums(totalSozial)
|
||||
const personalAnnual = annualSums(totalPersonal)
|
||||
const afaAnnual = annualSums(totalAfa)
|
||||
const sonstigeAnnual = annualSums(totalSonstige)
|
||||
|
||||
// Write GuV rows
|
||||
// Rohergebnis = Gesamtleistung - Materialaufwand
|
||||
const rohergebnis: AnnualValues = {}
|
||||
for (let y = 2026; y <= 2030; y++) {
|
||||
const k = `y${y}`
|
||||
rohergebnis[k] = Math.round((umsatzAnnual[k] || 0) - (materialAnnual[k] || 0))
|
||||
}
|
||||
|
||||
const guvUpdates: { label: string; values: AnnualValues }[] = [
|
||||
{ label: 'Umsatzerlöse', values: umsatzAnnual },
|
||||
{ label: 'Gesamtleistung', values: umsatzAnnual },
|
||||
{ label: 'Summe Materialaufwand', values: materialAnnual },
|
||||
{ label: 'Rohergebnis', values: rohergebnis },
|
||||
{ label: 'Löhne und Gehälter', values: personalBruttoAnnual },
|
||||
{ label: 'Soziale Abgaben', values: personalSozialAnnual },
|
||||
{ label: 'Summe Personalaufwand', values: personalAnnual },
|
||||
{ label: 'Abschreibungen', values: afaAnnual },
|
||||
{ label: 'Sonst. betriebl. Aufwendungen', values: sonstigeAnnual },
|
||||
]
|
||||
|
||||
for (const { label, values } of guvUpdates) {
|
||||
await pool.query(
|
||||
'UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3',
|
||||
[JSON.stringify(values), scenarioId, label]
|
||||
)
|
||||
}
|
||||
|
||||
// EBIT (Betriebsergebnis)
|
||||
const ebit: AnnualValues = {}
|
||||
for (let y = 2026; y <= 2030; y++) {
|
||||
const k = `y${y}`
|
||||
ebit[k] = Math.round((umsatzAnnual[k] || 0) - (materialAnnual[k] || 0) - (personalAnnual[k] || 0) - (afaAnnual[k] || 0) - (sonstigeAnnual[k] || 0))
|
||||
}
|
||||
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ebit), scenarioId, 'EBIT'])
|
||||
|
||||
// Steuerberechnung (nur auf Gewinne, mit Verlustvortrag)
|
||||
// Stockach 78333: Hebesatz 350%
|
||||
// Gewerbesteuer = 3,5% × 3,5 = 12,25%
|
||||
// Körperschaftsteuer = 15% + 5,5% Soli = 15,825%
|
||||
// Gesamt: ~28,075%
|
||||
const GEWERBESTEUER_RATE = 0.035 * 3.5 // 12,25%
|
||||
const KOERPERSCHAFTSTEUER_RATE = 0.15 * 1.055 // 15,825% (inkl. Soli)
|
||||
|
||||
const gewerbesteuer: AnnualValues = {}
|
||||
const koerperschaftsteuer: AnnualValues = {}
|
||||
const steuernGesamt: AnnualValues = {}
|
||||
const ergebnisNachSteuern: AnnualValues = {}
|
||||
let verlustvortrag = 0 // kumulierter Verlustvortrag
|
||||
|
||||
for (let y = 2026; y <= 2030; y++) {
|
||||
const k = `y${y}`
|
||||
const gewinn = ebit[k] || 0
|
||||
|
||||
if (gewinn <= 0) {
|
||||
// Verlust: keine Steuern, Verlustvortrag aufbauen
|
||||
verlustvortrag += Math.abs(gewinn)
|
||||
gewerbesteuer[k] = 0
|
||||
koerperschaftsteuer[k] = 0
|
||||
steuernGesamt[k] = 0
|
||||
ergebnisNachSteuern[k] = Math.round(gewinn)
|
||||
} else {
|
||||
// Gewinn: Verlustvortrag verrechnen
|
||||
// Bis 1 Mio EUR: 100% verrechenbar
|
||||
// Über 1 Mio EUR: nur 60% verrechenbar (Mindestbesteuerung)
|
||||
let verrechenbar = 0
|
||||
if (verlustvortrag > 0) {
|
||||
if (gewinn <= 1000000) {
|
||||
verrechenbar = Math.min(verlustvortrag, gewinn)
|
||||
} else {
|
||||
verrechenbar = Math.min(verlustvortrag, 1000000 + (gewinn - 1000000) * 0.6)
|
||||
}
|
||||
verlustvortrag -= verrechenbar
|
||||
}
|
||||
|
||||
const zuVersteuern = Math.max(0, gewinn - verrechenbar)
|
||||
const gst = Math.round(zuVersteuern * GEWERBESTEUER_RATE)
|
||||
const kst = Math.round(zuVersteuern * KOERPERSCHAFTSTEUER_RATE)
|
||||
|
||||
gewerbesteuer[k] = gst
|
||||
koerperschaftsteuer[k] = kst
|
||||
steuernGesamt[k] = gst + kst
|
||||
ergebnisNachSteuern[k] = Math.round(gewinn - gst - kst)
|
||||
}
|
||||
}
|
||||
|
||||
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(gewerbesteuer), scenarioId, 'Gewerbesteuer'])
|
||||
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(koerperschaftsteuer), scenarioId, 'Körperschaftssteuer'])
|
||||
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(steuernGesamt), scenarioId, 'Steuern gesamt'])
|
||||
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ergebnisNachSteuern), scenarioId, 'Ergebnis nach Steuern'])
|
||||
await pool.query('UPDATE fp_guv SET values = $1 WHERE scenario_id = $2 AND row_label = $3', [JSON.stringify(ergebnisNachSteuern), scenarioId, 'Jahresüberschuss'])
|
||||
|
||||
// Steuern auch in Liquidität eintragen (monatlich = 1/12 des Jahresbetrags)
|
||||
const liqGewSt = findLiq('Gewerbesteuer')
|
||||
const liqKSt = findLiq('Körperschaftsteuer')
|
||||
if (liqGewSt) {
|
||||
const v = emptyMonthly()
|
||||
for (let y = 2026; y <= 2030; y++) {
|
||||
const jahresBetrag = gewerbesteuer[`y${y}`] || 0
|
||||
if (jahresBetrag > 0) {
|
||||
const monatlich = Math.round(jahresBetrag / 12)
|
||||
const startM = (y - 2026) * 12 + 1
|
||||
for (let m = startM; m <= startM + 11 && m <= MONTHS; m++) {
|
||||
v[`m${m}`] = monatlich
|
||||
}
|
||||
}
|
||||
}
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(v), liqGewSt.id])
|
||||
liqGewSt.values = v
|
||||
}
|
||||
if (liqKSt) {
|
||||
const v = emptyMonthly()
|
||||
for (let y = 2026; y <= 2030; y++) {
|
||||
const jahresBetrag = koerperschaftsteuer[`y${y}`] || 0
|
||||
if (jahresBetrag > 0) {
|
||||
const monatlich = Math.round(jahresBetrag / 12)
|
||||
const startM = (y - 2026) * 12 + 1
|
||||
for (let m = startM; m <= startM + 11 && m <= MONTHS; m++) {
|
||||
v[`m${m}`] = monatlich
|
||||
}
|
||||
}
|
||||
}
|
||||
await pool.query('UPDATE fp_liquiditaet SET values = $1 WHERE id = $2', [JSON.stringify(v), liqKSt.id])
|
||||
liqKSt.values = v
|
||||
}
|
||||
|
||||
return {
|
||||
personalkosten: { total_brutto: totalBrutto, total_sozial: totalSozial, total: totalPersonal, positions: personal, headcount },
|
||||
investitionen: { total_invest: totalInvest, total_afa: totalAfa, items: invest },
|
||||
umsatzerloese: { total: totalRevenue },
|
||||
materialaufwand: { total: totalMaterial },
|
||||
betriebliche: { total_sonstige: totalSonstige, total_gesamt: gesamtBetrieb?.values || emptyMonthly() },
|
||||
liquiditaet: { rows: liquid, endstand: liquiditaet?.values || emptyMonthly() },
|
||||
guv: [ebit],
|
||||
}
|
||||
return { totalRevenue, totalMaterial }
|
||||
}
|
||||
|
||||
// Import to fix type errors
|
||||
type FPUmsatzerloese = import('./types').FPUmsatzerloese
|
||||
type FPMaterialaufwand = import('./types').FPMaterialaufwand
|
||||
|
||||
Reference in New Issue
Block a user