mirror of
https://github.com/Klotzkette/claude-fuer-deutsches-recht
synced 2026-06-09 10:03:19 +00:00
Make Liquiditätsplanung plugin standalone
Make Liquiditätsplanung plugin standalone - Add the Excel generator directly to the standalone liquiditaetsplanung plugin. - Bump the plugin manifest to 0.2.1. - Extend structural validation to require the standalone generator. Validated with plugin structure checks, Claude plugin validation, standalone XLSX generation, and local ZIP inspection.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "liquiditaetsplanung",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "Eigenständiges Power-Plugin Liquiditätsvorschau nach deutschem Recht. Wochenaktuelle 3-Wochen-Vorschau (§ 17 InsO), rollierende 13/26/52-Wochen-Planung und gerichtsfeste Liquiditätsbilanz nach BGH-Schema (BGHZ 217, 129 Passiva II; BGHZ 163, 134 10-%-Schwelle/3-Wochen-Horizont; BGH 28.06.2022 II ZR 112/21; BGH 28.04.2022 IX ZR 48/21; BGH 23.01.2025 IX ZR 229/22; BGH 11.03.2025 II ZR 139/23). Erzeugt zwingend eine Excel-Tabelle nach hinterlegter Vorlage und auf Wunsch ein interaktives HTML-Padlet oder Markdown-Artefakt. Banking optional (manuell, Datei-Import CAMT.053/MT940/CSV/DATEV-OPOS, Connector). Memo nur auf Anfrage. Voll autark; ergänzt sich mit den Plugins insolvenzrecht und steuerberater-werkzeuge, hängt aber nicht davon ab.",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"author": {
|
||||
|
||||
+684
@@ -0,0 +1,684 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
build_liquiditaetsplan.py
|
||||
=========================
|
||||
|
||||
Erstellt eine rollierende Liquiditätsvorschau (13 / 26 / 52 Wochen) im
|
||||
Layout der Klotzkette-Vorlage `vorlage-liquiditaetsplan.xlsx`:
|
||||
|
||||
Bestand Start Planung
|
||||
Kassenbestand
|
||||
Kontostand
|
||||
Liquidität Wochenanfang (hellblau, fett)
|
||||
Einnahmen (gelb, fett)
|
||||
Umsatzerlöse
|
||||
erhaltene Anzahlungen
|
||||
sonstige Einnahmen
|
||||
Summe Einnahmen
|
||||
Ausgaben (rot, fett)
|
||||
Löhne und Gehälter einschl. LSt
|
||||
Sozialversicherung
|
||||
Waren/ Material
|
||||
Miete
|
||||
Heizung/Strom/Wasser
|
||||
Verwaltung
|
||||
Werbung/Marketing
|
||||
Fahrzeug-/Leasingkosten
|
||||
Versicherungen
|
||||
Beratungskosten
|
||||
sonstige Ausgaben
|
||||
Investitionen
|
||||
Betriebliche Steuern
|
||||
sonstige Ausgaben (II)
|
||||
Darlehenstilgung
|
||||
Zinsen
|
||||
Privatentnahmen
|
||||
Summe Ausgaben
|
||||
Cash Flow Woche
|
||||
Liquidität Woche Ende (hellblau, fett)
|
||||
|
||||
Erweitert um:
|
||||
fällige Verbindlichkeiten Folgewoche
|
||||
3-Wochen-Lücke (kumuliert)
|
||||
Lücken-Quote (%)
|
||||
Ampel § 17 InsO (bedingte Formatierung GRÜN/GELB/ROT)
|
||||
|
||||
Sheets: 13-Wochen, 26-Wochen, 52-Wochen, Fortfuehrungsprognose, Annahmen.
|
||||
|
||||
Eingabe: YAML- oder JSON-Datei. Beispiel-YAML siehe Beispielakte.
|
||||
|
||||
Verwendung:
|
||||
python build_liquiditaetsplan.py --eingabe mandant.yaml \\
|
||||
--ausgabe liquiditaetsplan.xlsx
|
||||
|
||||
Quellen:
|
||||
- BGH, Urt. v. 24.05.2005 – IX ZR 123/04, BGHZ 163, 134 Rn. 12 ff. (§ 17 InsO)
|
||||
- BGH, Urt. v. 19.11.2019 – II ZR 233/18, NJW 2020, 1809 Rn. 17 ff.
|
||||
- IDW S 6 (Sanierungskonzepte), IDW S 11 (Insolvenzeröffnungsgründe)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import openpyxl
|
||||
from openpyxl.formatting.rule import CellIsRule
|
||||
from openpyxl.styles import Alignment, Font, PatternFill, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
try:
|
||||
import yaml # type: ignore
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Eingabe
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
def load_input(path: Path) -> dict[str, Any]:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if path.suffix.lower() in {".yaml", ".yml"}:
|
||||
if yaml is None:
|
||||
return load_simple_yaml(text, path)
|
||||
return yaml.safe_load(text) or {}
|
||||
if path.suffix.lower() == ".json":
|
||||
return json.loads(text)
|
||||
raise SystemExit(f"Unbekanntes Eingabeformat: {path.suffix}")
|
||||
|
||||
|
||||
def load_simple_yaml(text: str, path: Path) -> dict[str, Any]:
|
||||
"""Kleiner YAML-Fallback für die mitgelieferten Akten, falls PyYAML fehlt."""
|
||||
root: dict[Any, Any] = {}
|
||||
stack: list[tuple[int, dict[Any, Any] | list[Any]]] = [(-1, root)]
|
||||
lines: list[tuple[int, int, str]] = []
|
||||
|
||||
for lineno, raw_line in enumerate(text.splitlines(), start=1):
|
||||
if "\t" in raw_line[: len(raw_line) - len(raw_line.lstrip())]:
|
||||
raise SystemExit(f"{path}:{lineno}: Tabs in Einrückungen werden nicht unterstützt.")
|
||||
|
||||
line = strip_yaml_comment(raw_line).rstrip()
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
indent = len(line) - len(line.lstrip(" "))
|
||||
content = line[indent:]
|
||||
lines.append((lineno, indent, content))
|
||||
|
||||
for index, (lineno, indent, content) in enumerate(lines):
|
||||
while indent <= stack[-1][0]:
|
||||
stack.pop()
|
||||
parent = stack[-1][1]
|
||||
|
||||
if content.startswith("- "):
|
||||
if not isinstance(parent, list):
|
||||
raise SystemExit(f"{path}:{lineno}: YAML-Liste ohne Listenkontext.")
|
||||
|
||||
item_text = content[2:].strip()
|
||||
split_at = find_yaml_key_separator(item_text)
|
||||
if item_text and split_at is not None:
|
||||
item: dict[Any, Any] = {}
|
||||
parent.append(item)
|
||||
stack.append((indent, item))
|
||||
|
||||
key_text = item_text[:split_at].strip()
|
||||
value_text = item_text[split_at + 1:].strip()
|
||||
if not key_text:
|
||||
raise SystemExit(f"{path}:{lineno}: Leerer YAML-Schlüssel.")
|
||||
key = parse_yaml_scalar(key_text)
|
||||
if value_text == "":
|
||||
child: dict[Any, Any] | list[Any]
|
||||
child = [] if next_yaml_child_is_list(lines, index, indent) else {}
|
||||
item[key] = child
|
||||
stack.append((next_yaml_child_stack_indent(lines, index, indent), child))
|
||||
else:
|
||||
item[key] = parse_yaml_scalar(value_text)
|
||||
elif item_text:
|
||||
parent.append(parse_yaml_scalar(item_text))
|
||||
else:
|
||||
item = {}
|
||||
parent.append(item)
|
||||
stack.append((next_yaml_child_stack_indent(lines, index, indent), item))
|
||||
continue
|
||||
|
||||
if not isinstance(parent, dict):
|
||||
raise SystemExit(f"{path}:{lineno}: YAML-Schlüssel ohne Mapping-Kontext.")
|
||||
|
||||
split_at = find_yaml_key_separator(content)
|
||||
if split_at is None:
|
||||
raise SystemExit(f"{path}:{lineno}: YAML-Zeile nicht verstanden: {content!r}")
|
||||
|
||||
key_text = content[:split_at].strip()
|
||||
value_text = content[split_at + 1:].strip()
|
||||
if not key_text:
|
||||
raise SystemExit(f"{path}:{lineno}: Leerer YAML-Schlüssel.")
|
||||
|
||||
key = parse_yaml_scalar(key_text)
|
||||
|
||||
if value_text == "":
|
||||
child: dict[Any, Any] | list[Any]
|
||||
child = [] if next_yaml_child_is_list(lines, index, indent) else {}
|
||||
parent[key] = child
|
||||
stack.append((next_yaml_child_stack_indent(lines, index, indent), child))
|
||||
else:
|
||||
parent[key] = parse_yaml_scalar(value_text)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
def next_yaml_child_is_list(lines: list[tuple[int, int, str]], index: int, indent: int) -> bool:
|
||||
for _, next_indent, content in lines[index + 1:]:
|
||||
if next_indent <= indent:
|
||||
return False
|
||||
if next_indent > indent:
|
||||
return content.startswith("- ")
|
||||
return False
|
||||
|
||||
|
||||
def next_yaml_child_stack_indent(lines: list[tuple[int, int, str]], index: int, indent: int) -> int:
|
||||
for _, next_indent, _ in lines[index + 1:]:
|
||||
if next_indent > indent:
|
||||
return next_indent - 1
|
||||
return indent
|
||||
|
||||
|
||||
def strip_yaml_comment(line: str) -> str:
|
||||
quote: str | None = None
|
||||
escaped = False
|
||||
for i, ch in enumerate(line):
|
||||
if quote == '"':
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif ch == "\\":
|
||||
escaped = True
|
||||
elif ch == quote:
|
||||
quote = None
|
||||
elif quote == "'":
|
||||
if ch == quote:
|
||||
quote = None
|
||||
else:
|
||||
if ch in {"'", '"'}:
|
||||
quote = ch
|
||||
elif ch == "#":
|
||||
return line[:i]
|
||||
return line
|
||||
|
||||
|
||||
def find_yaml_key_separator(content: str) -> int | None:
|
||||
quote: str | None = None
|
||||
escaped = False
|
||||
for i, ch in enumerate(content):
|
||||
if quote == '"':
|
||||
if escaped:
|
||||
escaped = False
|
||||
elif ch == "\\":
|
||||
escaped = True
|
||||
elif ch == quote:
|
||||
quote = None
|
||||
elif quote == "'":
|
||||
if ch == quote:
|
||||
quote = None
|
||||
else:
|
||||
if ch in {"'", '"'}:
|
||||
quote = ch
|
||||
elif ch == ":":
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
def parse_yaml_scalar(value: str) -> Any:
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
return json.loads(value)
|
||||
if value.startswith("'") and value.endswith("'"):
|
||||
return value[1:-1].replace("''", "'")
|
||||
|
||||
lower = value.lower()
|
||||
if lower in {"null", "~"}:
|
||||
return None
|
||||
if lower == "true":
|
||||
return True
|
||||
if lower == "false":
|
||||
return False
|
||||
|
||||
normalized = value.replace("_", "")
|
||||
try:
|
||||
return int(normalized)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return float(normalized)
|
||||
except ValueError:
|
||||
return value
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Styles (1:1 nach Vorlage)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
FONT_DEFAULT = Font(name="Calibri", size=10)
|
||||
FONT_BOLD = Font(name="Calibri", size=10, bold=True)
|
||||
|
||||
FILL_HEADER_GRAY = PatternFill("solid", fgColor="D8D8D8")
|
||||
FILL_LIQUI_BLUE = PatternFill("solid", fgColor="00CCFF")
|
||||
FILL_EIN_YELLOW = PatternFill("solid", fgColor="FFFF00")
|
||||
FILL_AUS_RED = PatternFill("solid", fgColor="FF0000")
|
||||
FILL_SUM_LIGHT = PatternFill("solid", fgColor="F2F2F2")
|
||||
|
||||
NUM_FMT = '#,##0_);[Red](#,##0)'
|
||||
NUM_FMT_PCT = '0.0%'
|
||||
|
||||
THIN = Side(border_style="thin", color="BFBFBF")
|
||||
BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Wochenraster
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
def monday_of(d: dt.date) -> dt.date:
|
||||
return d - dt.timedelta(days=d.weekday())
|
||||
|
||||
|
||||
def week_buckets(start: dt.date, n_weeks: int) -> list[tuple[int, dt.date, dt.date]]:
|
||||
mon = monday_of(start)
|
||||
weeks = []
|
||||
for i in range(n_weeks):
|
||||
a = mon + dt.timedelta(days=7 * i)
|
||||
b = a + dt.timedelta(days=6)
|
||||
_, iso_week, _ = a.isocalendar()
|
||||
weeks.append((iso_week, a, b))
|
||||
return weeks
|
||||
|
||||
|
||||
# Vorlagen-Layout: Spalte A = Label, Spalte B = "Bestand Start"-Spalte (KW vor Start),
|
||||
# Spalten C ff. = Planungs-KW.
|
||||
LABEL_COL = 1 # A
|
||||
START_COL = 2 # B = Bestand Start (KW vor Stichtag)
|
||||
PLAN_COL_START = 3 # C ff. = KW 1 ff.
|
||||
|
||||
# Zeilen exakt wie Vorlage:
|
||||
R_TITLE = 1
|
||||
R_STAND = 2
|
||||
R_BESTAND_HEADER = 6
|
||||
R_KASSE = 7
|
||||
R_KONTO = 8
|
||||
R_LIQUI_START = 9
|
||||
R_EIN_HEADER = 11
|
||||
R_EIN_UMSATZ = 12
|
||||
R_EIN_ANZAHLUNG = 13
|
||||
R_EIN_SONST = 14
|
||||
R_EIN_SUMME = 15
|
||||
R_AUS_HEADER = 17
|
||||
R_AUS_LOHN = 18
|
||||
R_AUS_SV = 19
|
||||
R_AUS_WAREN = 20
|
||||
R_AUS_MIETE = 21
|
||||
R_AUS_ENERGIE = 22
|
||||
R_AUS_VERWALTUNG = 23
|
||||
R_AUS_WERBUNG = 24
|
||||
R_AUS_LEASING = 25
|
||||
R_AUS_VERSICH = 26
|
||||
R_AUS_BERATUNG = 27
|
||||
R_AUS_SONST = 28
|
||||
R_AUS_INVEST = 29
|
||||
R_AUS_STEUERN = 30
|
||||
R_AUS_SONST2 = 31
|
||||
R_AUS_TILGUNG = 32
|
||||
R_AUS_ZINSEN = 33
|
||||
R_AUS_ENTNAHME = 34
|
||||
R_AUS_SUMME = 35
|
||||
R_CASHFLOW = 37
|
||||
R_LIQUI_END = 39
|
||||
# Klotzkette-Ergänzung: § 17 InsO-Block
|
||||
R_FAELLIG = 41
|
||||
R_LUECKE_3W = 42
|
||||
R_QUOTE = 43
|
||||
R_AMPEL = 44
|
||||
|
||||
# Mapping: Bucket-Key -> Zeile
|
||||
EIN_KEYS = {
|
||||
"umsatz": R_EIN_UMSATZ,
|
||||
"anzahlungen": R_EIN_ANZAHLUNG,
|
||||
"sonstige": R_EIN_SONST,
|
||||
}
|
||||
AUS_KEYS = {
|
||||
"lohn": R_AUS_LOHN,
|
||||
"sv": R_AUS_SV,
|
||||
"waren": R_AUS_WAREN,
|
||||
"miete": R_AUS_MIETE,
|
||||
"energie": R_AUS_ENERGIE,
|
||||
"verwaltung": R_AUS_VERWALTUNG,
|
||||
"werbung": R_AUS_WERBUNG,
|
||||
"leasing": R_AUS_LEASING,
|
||||
"versicherung": R_AUS_VERSICH,
|
||||
"beratung": R_AUS_BERATUNG,
|
||||
"sonstige": R_AUS_SONST,
|
||||
"investitionen": R_AUS_INVEST,
|
||||
"steuern": R_AUS_STEUERN,
|
||||
"sonstige2": R_AUS_SONST2,
|
||||
"tilgung": R_AUS_TILGUNG,
|
||||
"zinsen": R_AUS_ZINSEN,
|
||||
"entnahmen": R_AUS_ENTNAHME,
|
||||
}
|
||||
|
||||
|
||||
def build_skeleton(ws, daten: dict, n_weeks: int):
|
||||
"""Statische Zellen und Beschriftungen anlegen."""
|
||||
ws.sheet_view.showGridLines = False
|
||||
ws.column_dimensions["A"].width = 28
|
||||
ws.column_dimensions[get_column_letter(START_COL)].width = 12
|
||||
for i in range(n_weeks):
|
||||
ws.column_dimensions[get_column_letter(PLAN_COL_START + i)].width = 13
|
||||
|
||||
# Titel
|
||||
c = ws.cell(row=R_TITLE, column=1, value="Liquiditätsplan")
|
||||
c.font = FONT_BOLD
|
||||
ws.cell(row=R_STAND, column=1,
|
||||
value=f"Stand: {daten.get('stichtag', '')} – {daten.get('mandant', {}).get('name', '')}").font = FONT_DEFAULT
|
||||
|
||||
# Header Zeile 6 – Bestand Start
|
||||
h = ws.cell(row=R_BESTAND_HEADER, column=LABEL_COL, value="Bestand Start Planung")
|
||||
h.font = FONT_BOLD
|
||||
h.fill = FILL_HEADER_GRAY
|
||||
h.number_format = NUM_FMT
|
||||
|
||||
# KW-Header
|
||||
_, iso_week, _ = monday_of(dt.date.fromisoformat(daten["stichtag"])).isocalendar()
|
||||
prev_kw = iso_week - 1 if iso_week > 1 else 52
|
||||
kw_cell = ws.cell(row=R_BESTAND_HEADER, column=START_COL, value=f"KW {prev_kw}")
|
||||
kw_cell.font = FONT_BOLD
|
||||
kw_cell.alignment = Alignment(horizontal="center")
|
||||
|
||||
stichtag = dt.date.fromisoformat(daten["stichtag"])
|
||||
for i, (kw, a, b) in enumerate(week_buckets(stichtag, n_weeks)):
|
||||
c = ws.cell(row=R_BESTAND_HEADER, column=PLAN_COL_START + i, value=f"KW {kw}")
|
||||
c.font = FONT_BOLD
|
||||
c.alignment = Alignment(horizontal="center")
|
||||
|
||||
# Zeilen-Labels
|
||||
def put_label(row, text, bold=False, fill=None, numfmt=None):
|
||||
cell = ws.cell(row=row, column=LABEL_COL, value=text)
|
||||
if bold:
|
||||
cell.font = FONT_BOLD
|
||||
else:
|
||||
cell.font = FONT_DEFAULT
|
||||
if fill is not None:
|
||||
cell.fill = fill
|
||||
if numfmt:
|
||||
cell.number_format = numfmt
|
||||
|
||||
put_label(R_KASSE, "Kassenbestand")
|
||||
put_label(R_KONTO, "Kontostand")
|
||||
put_label(R_LIQUI_START, "Liquidität Wochenanfang", bold=True, fill=FILL_LIQUI_BLUE, numfmt=NUM_FMT)
|
||||
|
||||
put_label(R_EIN_HEADER, "Einnahmen", bold=True, fill=FILL_EIN_YELLOW, numfmt=NUM_FMT)
|
||||
put_label(R_EIN_UMSATZ, "Umsatzerlöse")
|
||||
put_label(R_EIN_ANZAHLUNG, "erhaltene Anzahlungen")
|
||||
put_label(R_EIN_SONST, "sonstige Einnahmen")
|
||||
put_label(R_EIN_SUMME, "Summe Einnahmen", bold=True, numfmt=NUM_FMT)
|
||||
|
||||
put_label(R_AUS_HEADER, " Ausgaben", bold=True, fill=FILL_AUS_RED, numfmt=NUM_FMT)
|
||||
put_label(R_AUS_LOHN, "Löhne und Gehälter einschl. LSt")
|
||||
put_label(R_AUS_SV, "Sozialversicherung")
|
||||
put_label(R_AUS_WAREN, "Waren/ Material")
|
||||
put_label(R_AUS_MIETE, "Miete")
|
||||
put_label(R_AUS_ENERGIE, "Heizung/Strom/Wasser")
|
||||
put_label(R_AUS_VERWALTUNG, "Verwaltung")
|
||||
put_label(R_AUS_WERBUNG, "Werbung/Marketing")
|
||||
put_label(R_AUS_LEASING, "Fahrzeug-/Leasingkosten")
|
||||
put_label(R_AUS_VERSICH, "Versicherungen")
|
||||
put_label(R_AUS_BERATUNG, "Beratungskosten")
|
||||
put_label(R_AUS_SONST, "sonstige Ausgaben")
|
||||
put_label(R_AUS_INVEST, "Investitionen")
|
||||
put_label(R_AUS_STEUERN, "Betriebliche Steuern")
|
||||
put_label(R_AUS_SONST2, "sonstige Ausgaben")
|
||||
put_label(R_AUS_TILGUNG, "Darlehenstilgung")
|
||||
put_label(R_AUS_ZINSEN, "Zinsen")
|
||||
put_label(R_AUS_ENTNAHME, "Privatentnahmen")
|
||||
put_label(R_AUS_SUMME, "Summe Ausgaben", bold=True, numfmt=NUM_FMT)
|
||||
|
||||
put_label(R_CASHFLOW, "Cash Flow Woche", bold=True, numfmt=NUM_FMT)
|
||||
put_label(R_LIQUI_END, "Liquidität Woche Ende", bold=True, fill=FILL_LIQUI_BLUE, numfmt=NUM_FMT)
|
||||
|
||||
# Ampel-Block (Klotzkette-Erweiterung)
|
||||
put_label(R_FAELLIG, "fällige Verb. Folgewoche", bold=True, numfmt=NUM_FMT)
|
||||
put_label(R_LUECKE_3W, "3-Wochen-Lücke (kumuliert)", bold=True, numfmt=NUM_FMT)
|
||||
put_label(R_QUOTE, "Lücken-Quote (%)", bold=True, numfmt=NUM_FMT_PCT)
|
||||
put_label(R_AMPEL, "Ampel § 17 InsO", bold=True)
|
||||
|
||||
|
||||
def fill_sheet(ws, daten: dict, n_weeks: int):
|
||||
"""Werte und Formeln in die Spalten B (Start) und C..(C+n) eintragen."""
|
||||
# Bestand Start (Spalte B = KW vor Stichtag)
|
||||
kasse_start = float(daten.get("kassenbestand_start", 0))
|
||||
konto_start = float(daten.get("kontostand_start", 0))
|
||||
ws.cell(row=R_KASSE, column=START_COL, value=kasse_start).number_format = NUM_FMT
|
||||
ws.cell(row=R_KONTO, column=START_COL, value=konto_start).number_format = NUM_FMT
|
||||
ws.cell(row=R_LIQUI_START, column=START_COL,
|
||||
value=f"={get_column_letter(START_COL)}{R_KASSE}+{get_column_letter(START_COL)}{R_KONTO}"
|
||||
).number_format = NUM_FMT
|
||||
ws.cell(row=R_LIQUI_START, column=START_COL).font = FONT_BOLD
|
||||
ws.cell(row=R_LIQUI_START, column=START_COL).fill = FILL_LIQUI_BLUE
|
||||
|
||||
plan = daten.get("plan", {}) or {}
|
||||
|
||||
for i in range(n_weeks):
|
||||
col = PLAN_COL_START + i
|
||||
L = get_column_letter(col)
|
||||
L_prev = get_column_letter(col - 1)
|
||||
kw_data = plan.get(i, {}) if isinstance(plan, dict) else {}
|
||||
if not kw_data and isinstance(plan, list) and i < len(plan):
|
||||
kw_data = plan[i]
|
||||
|
||||
# Liquidität Wochenanfang = Liq. Woche Ende der Vorwoche
|
||||
# (für Spalte C = i=0 → B (Bestand Start), sonst Vorspalte R_LIQUI_END)
|
||||
if i == 0:
|
||||
ws.cell(row=R_LIQUI_START, column=col,
|
||||
value=f"={get_column_letter(START_COL)}{R_LIQUI_START}")
|
||||
else:
|
||||
ws.cell(row=R_LIQUI_START, column=col,
|
||||
value=f"={L_prev}{R_LIQUI_END}")
|
||||
ws.cell(row=R_LIQUI_START, column=col).font = FONT_BOLD
|
||||
ws.cell(row=R_LIQUI_START, column=col).fill = FILL_LIQUI_BLUE
|
||||
ws.cell(row=R_LIQUI_START, column=col).number_format = NUM_FMT
|
||||
|
||||
# Einnahmen
|
||||
ein = kw_data.get("einnahmen", {}) if isinstance(kw_data, dict) else {}
|
||||
for key, row in EIN_KEYS.items():
|
||||
val = float(ein.get(key, 0))
|
||||
c = ws.cell(row=row, column=col, value=val)
|
||||
c.number_format = NUM_FMT
|
||||
# Summe Einnahmen
|
||||
c = ws.cell(row=R_EIN_SUMME, column=col,
|
||||
value=f"=SUM({L}{R_EIN_UMSATZ}:{L}{R_EIN_SONST})")
|
||||
c.font = FONT_BOLD
|
||||
c.number_format = NUM_FMT
|
||||
|
||||
# Ausgaben
|
||||
aus = kw_data.get("ausgaben", {}) if isinstance(kw_data, dict) else {}
|
||||
for key, row in AUS_KEYS.items():
|
||||
val = float(aus.get(key, 0))
|
||||
c = ws.cell(row=row, column=col, value=val)
|
||||
c.number_format = NUM_FMT
|
||||
# Summe Ausgaben
|
||||
c = ws.cell(row=R_AUS_SUMME, column=col,
|
||||
value=f"=SUM({L}{R_AUS_LOHN}:{L}{R_AUS_ENTNAHME})")
|
||||
c.font = FONT_BOLD
|
||||
c.number_format = NUM_FMT
|
||||
|
||||
# Cash Flow Woche
|
||||
c = ws.cell(row=R_CASHFLOW, column=col,
|
||||
value=f"={L}{R_EIN_SUMME}-{L}{R_AUS_SUMME}")
|
||||
c.font = FONT_BOLD
|
||||
c.number_format = NUM_FMT
|
||||
|
||||
# Liquidität Woche Ende
|
||||
c = ws.cell(row=R_LIQUI_END, column=col,
|
||||
value=f"={L}{R_LIQUI_START}+{L}{R_CASHFLOW}")
|
||||
c.font = FONT_BOLD
|
||||
c.fill = FILL_LIQUI_BLUE
|
||||
c.number_format = NUM_FMT
|
||||
|
||||
# § 17 InsO-Block: fällige Verbindlichkeiten Folgewoche
|
||||
faellig = float(kw_data.get("faellig_folgewoche", 0))
|
||||
c = ws.cell(row=R_FAELLIG, column=col, value=faellig)
|
||||
c.number_format = NUM_FMT
|
||||
|
||||
# 3-Wochen-Lücke und Quote/Ampel – muss in zweitem Durchgang, da Bezug auf Nachbarspalten
|
||||
for i in range(n_weeks):
|
||||
col = PLAN_COL_START + i
|
||||
L = get_column_letter(col)
|
||||
# Lücke = MAX(0, Summe fälliger Verb. t..t+2 - (Liquidität Woche Ende_t + Summe Einnahmen_t+1 + Summe Einnahmen_t+2))
|
||||
if i + 2 < n_weeks:
|
||||
L1 = get_column_letter(col + 1)
|
||||
L2 = get_column_letter(col + 2)
|
||||
faellig_sum = f"{L}{R_FAELLIG}+{L1}{R_FAELLIG}+{L2}{R_FAELLIG}"
|
||||
mittel = f"{L}{R_LIQUI_END}+{L1}{R_EIN_SUMME}+{L2}{R_EIN_SUMME}"
|
||||
ws.cell(row=R_LUECKE_3W, column=col,
|
||||
value=f"=MAX(0,({faellig_sum})-({mittel}))").number_format = NUM_FMT
|
||||
else:
|
||||
ws.cell(row=R_LUECKE_3W, column=col,
|
||||
value=f"=MAX(0,{L}{R_FAELLIG}-{L}{R_LIQUI_END})").number_format = NUM_FMT
|
||||
# Quote
|
||||
ws.cell(row=R_QUOTE, column=col,
|
||||
value=f"=IFERROR({L}{R_LUECKE_3W}/{L}{R_FAELLIG},0)"
|
||||
).number_format = NUM_FMT_PCT
|
||||
# Ampel
|
||||
c = ws.cell(row=R_AMPEL, column=col,
|
||||
value=(f'=IF({L}{R_QUOTE}>=0.1,"ROT",'
|
||||
f'IF({L}{R_QUOTE}>0,"GELB","GRÜN"))'))
|
||||
c.font = FONT_BOLD
|
||||
c.alignment = Alignment(horizontal="center")
|
||||
|
||||
# bedingte Formatierung Ampel
|
||||
first_col = get_column_letter(PLAN_COL_START)
|
||||
last_col = get_column_letter(PLAN_COL_START + n_weeks - 1)
|
||||
rng = f"{first_col}{R_AMPEL}:{last_col}{R_AMPEL}"
|
||||
ws.conditional_formatting.add(
|
||||
rng, CellIsRule(operator="equal", formula=['"ROT"'],
|
||||
fill=PatternFill("solid", fgColor="F4B7BA"),
|
||||
font=Font(bold=True, color="9C0006")))
|
||||
ws.conditional_formatting.add(
|
||||
rng, CellIsRule(operator="equal", formula=['"GELB"'],
|
||||
fill=PatternFill("solid", fgColor="FFE9A8"),
|
||||
font=Font(bold=True, color="8F6A00")))
|
||||
ws.conditional_formatting.add(
|
||||
rng, CellIsRule(operator="equal", formula=['"GRÜN"'],
|
||||
fill=PatternFill("solid", fgColor="C9E5C1"),
|
||||
font=Font(bold=True, color="2C6E1F")))
|
||||
|
||||
ws.freeze_panes = ws.cell(row=R_BESTAND_HEADER + 1, column=PLAN_COL_START)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Fortführungsprognose und Annahmen
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
def build_prognose_sheet(wb, daten):
|
||||
ws = wb.create_sheet("Fortfuehrungsprognose")
|
||||
ws.sheet_view.showGridLines = False
|
||||
ws.column_dimensions["A"].width = 32
|
||||
ws.column_dimensions["B"].width = 60
|
||||
ws.column_dimensions["C"].width = 22
|
||||
|
||||
t = ws.cell(row=1, column=1, value="Fortführungsprognose nach IDW S 6 / IDW S 11")
|
||||
t.font = Font(name="Calibri", size=14, bold=True)
|
||||
|
||||
for i, h in enumerate(["IDW-S-6-Element", "Befund / Annahme", "Status"]):
|
||||
c = ws.cell(row=3, column=1 + i, value=h)
|
||||
c.font = FONT_BOLD
|
||||
c.fill = FILL_HEADER_GRAY
|
||||
c.alignment = Alignment(horizontal="center")
|
||||
|
||||
elemente = [
|
||||
("1. Auftrag/Auftragsdurchführung", daten.get("prognose", {}).get("auftrag", "")),
|
||||
("2. Basisinformationen", daten.get("prognose", {}).get("basis", "")),
|
||||
("3. Krisenstadium und Ursachen", daten.get("prognose", {}).get("krise", "")),
|
||||
("4. Leitbild des sanierten Unternehmens", daten.get("prognose", {}).get("leitbild", "")),
|
||||
("5. Maßnahmen zur Krisenbewältigung", daten.get("prognose", {}).get("massnahmen", "")),
|
||||
("6. Integrierte Planung 24+ Monate", daten.get("prognose", {}).get("planung", "")),
|
||||
("7. Aussage Sanierungsfähigkeit", daten.get("prognose", {}).get("sanierung", "")),
|
||||
("8. Fortbestehensprognose § 19 InsO", daten.get("prognose", {}).get("fortbestehen", "")),
|
||||
]
|
||||
for i, (label, befund) in enumerate(elemente):
|
||||
r = 4 + i
|
||||
ws.cell(row=r, column=1, value=label).font = FONT_BOLD
|
||||
ws.cell(row=r, column=2, value=befund).alignment = Alignment(wrap_text=True, vertical="top")
|
||||
ws.cell(row=r, column=3, value="").alignment = Alignment(horizontal="center")
|
||||
ws.row_dimensions[r].height = 60
|
||||
for c in range(1, 4):
|
||||
ws.cell(row=r, column=c).border = BORDER
|
||||
|
||||
r = 4 + len(elemente) + 2
|
||||
ws.cell(row=r, column=1, value="Quellen:").font = FONT_BOLD
|
||||
quellen = [
|
||||
"BGH, Urt. v. 24.05.2005 – IX ZR 123/04, BGHZ 163, 134 Rn. 12 ff. (§ 17 InsO).",
|
||||
"BGH, Urt. v. 12.10.2006 – IX ZR 228/03, NJW 2007, 78 Rn. 16 ff. (Indizienkatalog).",
|
||||
"BGH, Urt. v. 19.11.2019 – II ZR 233/18, NJW 2020, 1809 Rn. 17 ff. (Fortbestehensprognose).",
|
||||
"BGH, Urt. v. 09.10.2012 – II ZR 298/11, BGHZ 195, 42 Rn. 12 ff. (insolvenzrechtl. Überschuldung).",
|
||||
"IDW S 6 (Sanierungskonzepte), IDW S 11 (Insolvenzeröffnungsgründe).",
|
||||
"K. Schmidt/Herchen, K. Schmidt, InsO, 20. Aufl. 2023, §§ 17, 19 InsO.",
|
||||
"Uhlenbruck/Mock, InsO, 16. Aufl. 2024, §§ 17, 19 InsO.",
|
||||
]
|
||||
for i, q in enumerate(quellen):
|
||||
ws.cell(row=r + 1 + i, column=1, value="• " + q)
|
||||
|
||||
|
||||
def build_annahmen_sheet(wb, daten):
|
||||
ws = wb.create_sheet("Annahmen")
|
||||
ws.sheet_view.showGridLines = False
|
||||
ws.column_dimensions["A"].width = 36
|
||||
ws.column_dimensions["B"].width = 60
|
||||
|
||||
ws.cell(row=1, column=1, value="Annahmen und Inputs").font = Font(name="Calibri", size=14, bold=True)
|
||||
rows = [
|
||||
("Mandant", daten.get("mandant", {}).get("name", "")),
|
||||
("Rechtsform", daten.get("mandant", {}).get("rechtsform", "")),
|
||||
("Branche", daten.get("mandant", {}).get("branche", "")),
|
||||
("Mitarbeiter", daten.get("mandant", {}).get("mitarbeiter", "")),
|
||||
("Stichtag", daten.get("stichtag", "")),
|
||||
("Kassenbestand Start", daten.get("kassenbestand_start", 0)),
|
||||
("Kontostand Start", daten.get("kontostand_start", 0)),
|
||||
("Kreditlinie gesamt", daten.get("kreditlinie_gesamt", 0)),
|
||||
("Kreditlinie genutzt", daten.get("kreditlinie_genutzt", 0)),
|
||||
("Szenario", daten.get("szenario", "Base")),
|
||||
]
|
||||
for i, (k, v) in enumerate(rows):
|
||||
r = 3 + i
|
||||
ws.cell(row=r, column=1, value=k).font = FONT_BOLD
|
||||
ws.cell(row=r, column=2, value=v)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Main
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--eingabe", "-i", required=True, type=Path)
|
||||
parser.add_argument("--ausgabe", "-o", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
daten = load_input(args.eingabe)
|
||||
|
||||
wb = openpyxl.Workbook()
|
||||
wb.remove(wb.active)
|
||||
|
||||
for name, n in [("13-Wochen", 13), ("26-Wochen", 26), ("52-Wochen", 52)]:
|
||||
ws = wb.create_sheet(name)
|
||||
build_skeleton(ws, daten, n)
|
||||
fill_sheet(ws, daten, n)
|
||||
|
||||
build_prognose_sheet(wb, daten)
|
||||
build_annahmen_sheet(wb, daten)
|
||||
|
||||
wb.save(args.ausgabe)
|
||||
print(f"OK: geschrieben nach {args.ausgabe}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -119,6 +119,8 @@ function checkMarketplace() {
|
||||
for (const asset of ['assets/excel/Liquiditaetsplan-Wochenbasis.xlsx', 'assets/padlet/liquiditaets-padlet.html', 'assets/markdown/liquiditaets-artefakt-vorlage.md']) {
|
||||
if (!exists(path.join(pluginRoot, asset))) errors.push(`${plugin.name}: missing standalone asset ${asset}`);
|
||||
}
|
||||
const generator = 'skills/liquiditaetsvorschau-3-6-12-monate/werkzeuge/build_liquiditaetsplan.py';
|
||||
if (!exists(path.join(pluginRoot, generator))) errors.push(`${plugin.name}: missing standalone generator ${generator}`);
|
||||
for (const pdf of ['BGH_II_ZR_88-16_vom_2017-12-19.pdf', 'BGH_II_ZR_112-21_vom_2022-06-28.pdf', 'BGH_IX_ZR_48-21_vom_2022-04-28.pdf', 'BGH_IX_ZR_229-22_vom_2025-01-23.pdf', 'BGH_II_ZR_139-23_vom_2025-03-11.pdf']) {
|
||||
if (!exists(path.join(pluginRoot, 'references', 'rechtsprechung', pdf))) errors.push(`${plugin.name}: missing BGH PDF references/rechtsprechung/${pdf}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user