""" Mapping vom Wizard-State (frontend) auf den Template-Context (Render-Variablen). Frontend liefert ein JSON-Payload mit den Wizard-Schritten. Hier konvertieren wir es in eine flache Dict-Struktur, deren Keys SCREAMING_SNAKE_CASE sind und zu den Platzhaltern in den Templates passen (z.B. {{COMPANY_NAME}}). Pro Dokumenttyp (document_type) wird der jeweils benoetigte Subset gebaut. """ from __future__ import annotations from typing import Any def _gs_table(gesellschafter: list[dict[str, Any]], stammkapital: int) -> str: """Erzeugt eine Markdown-Tabelle der Gesellschafter.""" rows = [] for g in gesellschafter: nb = int(g.get("nennbetrag_eur") or 0) pct = (nb / max(stammkapital, 1)) * 100 if stammkapital else 0 rows.append( f"| {g.get('anteil_nr', '')} | {g.get('name', '')} | " f"{g.get('geburtsdatum') or g.get('adresse', '')} | " f"{g.get('adresse', '')} | {g.get('anteil_nr', '')} | " f"{nb:,} | {pct:.2f}% |".replace(",", ".") ) return "\n".join(rows) def _parties_list(gesellschafter: list[dict[str, Any]]) -> str: """Aufzaehlung der Parteien fuer SHA, IP-Assignment etc.""" lines = [] for idx, g in enumerate(gesellschafter): letter = chr(ord("a") + idx) line = f"{letter}) **{g.get('name', '')}**" if g.get("geburtsdatum"): line += f", geboren am {g['geburtsdatum']}" if g.get("adresse"): line += f", wohnhaft in {g['adresse']}" lines.append(line + ",") return "\n".join(lines) def _parties_list_with_shares(gesellschafter: list[dict[str, Any]]) -> str: """Erzeugt nummerierte Liste der Gesellschafter mit Anteilen fuer § 3 Satzung.""" lines = [] for g in gesellschafter: nr = g.get("anteil_nr", "?") name = g.get("name", "") nb = int(g.get("nennbetrag_eur") or 0) lines.append( f"{nr}. {name} übernimmt den Geschäftsanteil Nr. {nr} mit einem " f"Nennbetrag von {nb:,} Euro.".replace(",", ".") ) return "\n".join(lines) def _gf_liste(gf: list[dict[str, Any]]) -> str: """Liste der Geschaeftsfuehrer fuer Bestellungsbeschluss / HRB-Anmeldung.""" lines = [] for g in gf: line = f"- **{g.get('name', '')}**" if g.get("geburtsdatum"): line += f", geboren am {g['geburtsdatum']}" if g.get("adresse"): line += f", wohnhaft in {g['adresse']}" if g.get("internal_role"): line += f" — {g['internal_role']}" lines.append(line) return "\n".join(lines) def _company_purpose_bullets(bullets: list[str]) -> str: return "\n".join(bullets) if bullets else "a) Allgemeine geschäftliche Tätigkeit" def _roles_description(gesellschafter: list[dict[str, Any]]) -> str: """Generiert Anlage-A Rollenbeschreibung pro Gesellschafter.""" lines = [] for idx, g in enumerate(gesellschafter): name = g.get("name", "") role = g.get("internal_role") or "Gesellschafter" lines.append(f"({idx + 2}) **{role} — {name}**") lines.append(f"Verantwortlich für die operative Leitung im Bereich {role}.\n") return "\n".join(lines) def _einzahlungsaufstellung(gesellschafter: list[dict[str, Any]], quote_pct: int) -> str: rows = [] for g in gesellschafter: nb = int(g.get("nennbetrag_eur") or 0) paid = int(nb * quote_pct / 100) rows.append(f"- {g.get('name', '')}: {paid:,} EUR von {nb:,} EUR ({quote_pct}%)".replace(",", ".")) return "\n".join(rows) def base_context(state: dict[str, Any]) -> dict[str, Any]: """Gemeinsamer Context fuer alle Dokumente.""" basics = state.get("basics", {}) capital = state.get("capital", {}) notar = state.get("notar", {}) gesellschafter = state.get("gesellschafter", []) gf_list = [g for g in gesellschafter if g.get("is_geschaeftsfuehrer")] sha = state.get("sha", {}) stammkapital = int(capital.get("stammkapital_eur") or 25000) num_gf = len(gf_list) num_gs = len(gesellschafter) has_academic = any(g.get("has_academic_background") for g in gesellschafter) ctx: dict[str, Any] = { # Company "COMPANY_NAME": basics.get("company_name", ""), "COMPANY_LEGAL_FORM": basics.get("legal_form", "GmbH"), "COMPANY_SEAT": basics.get("company_seat", ""), "COMPANY_ADDRESS": basics.get("company_address", ""), "COMPANY_PURPOSE_DESCRIPTION": basics.get("company_purpose_description", ""), "COMPANY_PURPOSE_BULLETS": _company_purpose_bullets(basics.get("company_purpose_bullets", [])), "COMPANY_PURPOSE_SHORT": basics.get("industry", "")[:120], "BUSINESS_YEAR": basics.get("business_year", "Kalenderjahr"), "FIRST_YEAR_END": "31. Dezember des Eintragungsjahres", "PUBLICATION_VENUE": "Bundesanzeiger", # Capital "STAMMKAPITAL_EUR": f"{stammkapital:,}".replace(",", "."), "STAMMKAPITAL_HALF_EUR": f"{stammkapital // 2:,}".replace(",", "."), "EINLAGE_METHOD": capital.get("einlage_method", "Geld"), "EINLAGE_QUOTE_INITIAL_PCT": capital.get("einlage_quote_initial_pct", 50), "EINLAGE_QUOTE_REMAINING_PCT": 100 - int(capital.get("einlage_quote_initial_pct") or 50), "EINLAGE_QUOTE_INITIAL_LESS_THAN_100": (capital.get("einlage_quote_initial_pct") or 50) < 100, "EINZAHLUNGSAUFSTELLUNG": _einzahlungsaufstellung(gesellschafter, capital.get("einlage_quote_initial_pct") or 50), "HAS_SACHEINLAGE": capital.get("has_sacheinlage", False), "VERZUGSFRIST_TAGE": 30, "EINZIEHUNG_MEHRHEIT_PCT": 75, "VORKAUFSRECHT_TAGE": 14, "EINBERUFUNGSFRIST_TAGE": 7, "VOTING_UNIT_EUR": "1,00", "ERBFALL_AUFGRIFFSFRIST_MONATE": 6, "ERBFALL_MEHRHEIT_PCT": 75, "AUFLOESUNG_MEHRHEIT_PCT": 75, "GRUENDUNGSKOSTEN_MAX_EUR": f"{int(stammkapital / 10):,}".replace(",", "."), # Gesellschafter "PARTIES_LIST": _parties_list(gesellschafter), "PARTIES_LIST_WITH_SHARES": _parties_list_with_shares(gesellschafter), "GESELLSCHAFTER_TABELLE": _gs_table(gesellschafter, stammkapital), "GESCHAEFTSFUEHRER_LISTE": _gf_liste(gf_list), "GESELLSCHAFTER_LISTE": _gf_liste(gesellschafter), # GF "NUM_GF": num_gf, "NUM_GF_TEXT": {1: "einen", 2: "zwei", 3: "drei", 4: "vier", 5: "fünf"}.get(num_gf, str(num_gf)), "IS_SINGLE_GF": num_gf == 1, "IS_MULTI_GF": num_gf > 1, "NUM_GF_IS_2": num_gf == 2, "NUM_GF_GT_2": num_gf > 2, "IS_MULTI_GESELLSCHAFTER": num_gs > 1, "IS_FOUNDER_GROUP": num_gs >= 2, "VERTRETUNGSART": "Gesamtvertretung; bei nur einem Geschäftsführer Einzelvertretung", # Notar "NOTARY_NAME": notar.get("notary_name", ""), "NOTARY_PLACE": notar.get("notary_place", ""), "NOTARY_ADDRESS": notar.get("notary_address", ""), "NOTARY_URNR": notar.get("urnr", "[wird beim Termin vergeben]"), "NOTARIAL_DATE": notar.get("notarial_date", "[Notartermin folgt]"), "NOTARY_BEGLAUBIGUNG_URNR": "[wird beim Termin vergeben]", "NOTARIAL_LOCATION": notar.get("notary_place", ""), "ANMELDUNG_TYP": "Ersteintragung gemäß § 7 GmbHG", "ANMELDUNG_DATE": notar.get("notarial_date", "[Notartermin folgt]"), "REGISTRY_COURT_ADDRESS": "[Adresse des zuständigen Registergerichts]", "COMPANY_REGISTRY_COURT": basics.get("register_court") or "[zuständiges Amtsgericht]", "REGISTER_COURT": basics.get("register_court") or "[zuständiges Amtsgericht]", # Common "DOCUMENT_VERSION": "1.0.0", "EFFECTIVE_DATE": notar.get("notarial_date", "[Datum der Beurkundung]"), "RESOLUTION_DATE": notar.get("notarial_date", "[Datum der Beurkundung]"), "NEXT_REVIEW_DATE": "[+ 12 Monate]", "SIGNATURES_BLOCK": "Unterschriften gemäß notarieller Beurkundung", # SHA Flags "HAS_SHA": sha.get("has_sha", True), "HAS_GO_GF": True, "HAS_ACADEMIC_FOUNDER": has_academic, "HAS_RESEARCH_FOCUS": basics.get("has_research_focus", False), "HAS_BEIRAT": sha.get("has_beirat", False), "HAS_TEXAS_SHOOTOUT": sha.get("has_texas_shootout", False), "HAS_CEO_DESIGNATION": sha.get("has_ceo_designation", False), "CEO_NAME": sha.get("ceo_name", ""), "HAS_HRB": bool(basics.get("hrb_number")), "HRB_NUMBER": basics.get("hrb_number") or "[wird vergeben]", "IS_UG": basics.get("legal_form") == "UG", # GO-GF dynamische §-Numerierung "P_INFO": 5, "P_GESELLSCHAFTER": 4 if num_gf == 1 else 4, "P_AUSSER": 5, "P_ENT": 6, "P_FIN": 7, "P_PERS": 8, "P_IK": 9, "P_NEB": 10, "P_DOC": 10, "P_DOC_NEXT": 2, "P_DOC_NEXT_2": 3, "P_END": 11, "LAST_PARA_4": 4 if num_gf == 2 else 5, # SHA dynamische §-Numerierung (mit/ohne Beirat) "P_NONCOMPETE": 16 if sha.get("has_beirat") else 15, "P_CONFIDENTIAL": 17 if sha.get("has_beirat") else 16, "P_TERM": 18 if sha.get("has_beirat") else 17, "P_FINAL": 19 if sha.get("has_beirat") else 18, "P_IP_PARA_6": 6 if has_academic else 3, "P_IP_PARA_7": 7 if has_academic else 4, "P_IP_PARA_8": 8 if has_academic else 5, "P_DEADLOCK_FINAL": 5, "P_DEADLOCK_LAST": 6, "LAST_ROLE_PARA": len(gesellschafter) + 2, "LAST_ROLE_PARA_PLUS_1": len(gesellschafter) + 3, # Satzung dynamische §-Numerierung "P_EINZIEHUNG": 7, "P_VORKAUF": 8, "P_TAGALONG": 9, "P_DRAGALONG": 10, "P_VERSAMMLUNG": 11, "P_JA": 12, "P_ERGEBNIS": 13, "P_AUFGRIFF": 14, "P_ABTRETUNG": 15, "P_ERBE": 16, "P_AUFL": 17, "P_SCHLUSS": 18, # SHA Eskalation und sonstige Schwellenwerte "ESKALATION_TAGE_INTERN": 5, "ESKALATION_TAGE_GESELLSCHAFTER": 14, "ERHEBLICH_EUR": "10.000", "DEADLOCK_FRIST_TAGE": 30, "MEDIATION_INIT_TAGE": 7, "MEDIATOR_FRIST_TAGE": 5, "MEDIATION_MAX_TAGE": 30, "SHOOTOUT_FRIST_TAGE": 14, "SHOOTOUT_ABWICKLUNG_TAGE": 60, "ESKALATION_TAGE": 30, "JURISDICTION_LOCATION": basics.get("company_seat", "[Sitz]"), "PARA_181_DETAILS": "soweit Geschäftsführer von den Beschränkungen befreit", "ARCHIV_VERANTWORTLICH": "Geschäftsführung", "DOKUMENTATIONS_SYSTEM": "elektronischen Dokumentenmanagement", "ARCHIVIERUNG_JAHRE": 10, "REVIEW_VERANTWORTLICH": "Geschäftsführung", "MEETING_OPERATIVE_FREQ": "wöchentliche", "MEETING_STRATEGIE_FREQ": "monatliche", "SCHWELLE_EINZEL_EUR": "10.000", "SCHWELLE_EINZEL_EUR_PLUS_1": "10.001", "SCHWELLE_GEMEINSAM_EUR": "50.000", "SCHWELLE_GESELLSCHAFTER_EUR": "50.000", "BUDGET_ABWEICHUNG_PCT": 10, "VERTRAG_LAUFZEIT_MONATE": 24, "VERTRAG_WERT_EUR": "50.000", "LIQUIDITAET_MIN_MONATE": 3, "FORECAST_HORIZON_MONTHS": 12, "SCHLUESSELPERSON_GEHALT_EUR": "80.000", "NEBENTAETIGKEIT_MAX_STUNDEN": 8, # SHA-Spezifika "VESTING_START_EVENT": "Eintragung der Gesellschaft im Handelsregister", "VESTING_MONTHS": sha.get("vesting_months", 48), "CLIFF_MONTHS": sha.get("cliff_months", 12), "ACCELERATION_THRESHOLD_PCT": 50, "ACCELERATION_PCT": 100, "BAD_LEAVER_UNVESTED_PCT": 20, "FMV_AGREEMENT_DAYS": 14, "ABFINDUNG_RATEN_MAX": 24, "NON_SOLICIT_MONTHS": 12, "VORKAUFSRECHT_TAGE": 14, "TAG_ALONG_THRESHOLD_PCT": sha.get("tag_along_threshold_pct", 20), "TAG_ALONG_FRIST_TAGE": 14, "DRAG_ALONG_THRESHOLD_PCT": sha.get("drag_along_threshold_pct", 75), "RESERVED_MATTERS_MAJORITY_PCT": sha.get("reserved_matters_majority_pct", 75), "ASSET_THRESHOLD_EUR": "50.000", "ESOP_POOL_PCT": sha.get("esop_pool_pct", 0), "INVESTOR_INFO_THRESHOLD_EUR": "50.000", "ANNUAL_REPORT_MONTHS": 6, "BEIRAT_MAX_MITGLIEDER": 5, "BEIRAT_FREQ": "vierteljährlich", "PASSIVE_INVEST_PCT": 5, "POST_EXIT_GOOD_MONTHS": 12, "POST_EXIT_BAD_MONTHS": 24, "ROLES_DESCRIPTION": _roles_description(gesellschafter), "SIGNATURE_DATE": notar.get("notarial_date", "[Datum]"), # Gesellschafterliste "LIST_DATE": notar.get("notarial_date", "[Datum]"), "LIST_AUTHOR": gf_list[0].get("name", "") if gf_list else "", "LIST_AUTHOR_ROLE": "Geschäftsführer", "LIST_REASON": "Erstaufstellung gemäß § 40 GmbHG", "SIGNATORY_NAME": gf_list[0].get("name", "") if gf_list else "", "SIGNATORY_ROLE": "Geschäftsführer", "SIGNATORY_2_NAME": gf_list[1].get("name", "") if len(gf_list) > 1 else "", "SIGNATORY_2_ROLE": "Geschäftsführer", "MULTI_SIGNATORY": len(gf_list) > 1, # Bestellungsbeschluss "MEETING_LOCATION": notar.get("notary_place", "[Notarsitz]"), "RESOLUTION_FORM": "notariell beurkundet", "ANWESENHEITSQUOTE_PCT": 100, "IS_EINSTIMMIG": True, "BESCHLUSS_MEHRHEIT_PCT": 100, "IS_PRESENCE_MEETING": True, "IS_SINGLE_APPOINTMENT": num_gf == 1, "IS_MULTI_APPOINTMENT": num_gf > 1, "IS_FIRST_APPOINTMENT": True, "IS_PLURAL_GF": num_gf > 1, "GF_NAME": gf_list[0].get("name", "") if gf_list else "", "GF_BIRTHDATE": gf_list[0].get("geburtsdatum", "") if gf_list else "", "GF_BIRTHDATE_PLACE": "[Geburtsort]", "GF_ADDRESS": gf_list[0].get("adresse", "") if gf_list else "", "GF_VERTRETUNG": "einzelvertretungsberechtigt" if num_gf == 1 else "gemeinsam mit einem weiteren Geschäftsführer vertretungsberechtigt", "GF_PARA_181_RELEASE": True, "GF_LISTE_MIT_VERTRETUNGSART": "\n".join( f"- {g.get('name', '')}, geb. {g.get('geburtsdatum', '')}, wohnhaft in {g.get('adresse', '')}, " f"vertretungsberechtigt {'allein' if num_gf == 1 else 'gemeinsam'}; § 181 BGB-Befreiung erteilt" for g in gf_list ), "HAS_RESSORT_ZUWEISUNG": True, "HAS_DIENSTVERTRAG": True, "SIGNATURES_GESELLSCHAFTER": "\n".join( f"___________________________\n{g.get('name', '')}" for g in gesellschafter ), "HAS_VERSICHERUNG_BESTELLT": True, "BELEHRUNG_DURCH": "den beurkundenden Notar", "HAS_DELAYED_START": False, # HRB-Anmeldung "VERTRETUNGSREGELUNG": ( "Die Gesellschaft wird durch einen Geschäftsführer allein vertreten." if num_gf == 1 else "Die Gesellschaft wird durch zwei Geschäftsführer gemeinsam vertreten. " "Bei nur einem bestellten Geschäftsführer Einzelvertretung." ), "GF_SIGNATURES_BEGLAUBIGUNG": "\n".join( f"___________________________\n{g.get('name', '')}, Geschäftsführer" for g in gf_list ), "HAS_EMPFANGSBERECHTIGTER": False, "EMPFANGSBERECHTIGTER_NAME": "", "EMPFANGSBERECHTIGTER_ADDRESS": "", "HAS_GENEHMIGUNG": False, "GENEHMIGUNG_DETAILS": "", "NEXT_DOC_NUMBER": 6, # GF-Dienstvertrag (Defaults für alle GFs, einzelne Felder per Contract überschreiben) "COMPANY_REPRESENTATIVE": "die Gesellschafterversammlung", "APPOINTMENT_DATE": notar.get("notarial_date", "[Datum]"), "GF_INTERNAL_TITLE": gf_list[0].get("internal_role", "Geschäftsführer") if gf_list else "Geschäftsführer", "CONTRACT_START_DATE": notar.get("notarial_date", "[Datum]"), "HAS_PARA_181_RELEASE": True, "PARA_181_RELEASE_DATE": notar.get("notarial_date", "[Datum]"), "HAS_BONUS": False, "HAS_TANTIEME": False, "HAS_COMPANY_CAR": False, "HAS_BAV": False, "HAS_HINTERBLIEBENEN_VERSORGUNG": False, "HAS_KOPPLUNG_BESTELLUNG_VERTRAG": False, "HAS_NONCOMPETE_COMPENSATION": False, "POST_CONTRACT_NONCOMPETE_MONTHS": 12, "GROSS_ANNUAL_SALARY_EUR": "84.000", "COMPANY_CAR_CLASS": "", "BAV_EMPLOYER_PCT": 0, "SV_STATUS": "sozialversicherungsfrei", "VACATION_DAYS": 30, "KRANKHEIT_FORTZAHLUNG_WOCHEN": 6, "AU_BESCHEINIGUNG_TAG": 4, "HINTERBLIEBENEN_VERSORGUNG_MONATE": 6, "DO_INSURANCE_EUR": "5.000.000", "KUENDIGUNGSFRIST_GESELLSCHAFT_MONATE": 6, "KUENDIGUNGSFRIST_GF_MONATE": 3, "ANNEX_LIST": "- Anlage 1: Bonusplan (sofern vereinbart)\n- Anlage 2: D&O-Versicherungspolice", # IP-Assignment "ASSIGNOR_NAME": gf_list[0].get("name", "") if gf_list else "", "ASSIGNOR_BIRTHDATE": gf_list[0].get("geburtsdatum", "") if gf_list else "", "ASSIGNOR_ADDRESS": gf_list[0].get("adresse", "") if gf_list else "", "ASSIGNOR_ROLE": gf_list[0].get("internal_role", "Gründer und Geschäftsführer") if gf_list else "Gründer", "AGREEMENT_DATE": notar.get("notarial_date", "[Datum]"), "HAS_BAR_VERGUETUNG": False, "HAS_SHARES_AS_COMPENSATION": True, "HAS_NO_VERGUETUNG": False, "IP_VERGUETUNG_EUR": 0, "ZAHLUNGSFRIST_TAGE": 30, "GUARANTEE_VERJAEHRUNG_JAHRE": 3, "HAS_ACADEMIC_BACKGROUND": has_academic, "SIGNATURE_LOCATION": basics.get("company_seat", "[Sitz]"), "IP_LIST_DETAILS": "- Software-Architektur und Quellcode (bestehend zum Zeitpunkt der Gründung)\n- Konzepte, Designs, Datenbankstrukturen\n- Marken, Logos, Domainnamen", "IP_EXCEPTIONS_DETAILS": "Keine Ausnahmen bekannt.", } # Ressort-Variablen aus GF-Liste ableiten (1 Ressort pro GF) ressort_defaults = [ ("Operative & Kommerzielle Leitung", "Finanzen, HR, Vertrieb, Business Development, operative Steuerung"), ("Technik & Engineering", "Softwareentwicklung, Architektur, Infrastruktur, Sicherheit, technische Roadmap"), ("Research & Partnerships", "Forschungskooperationen, Drittmittel, wissenschaftliche Methodik"), ] for idx, gf in enumerate(gf_list[:3]): n = idx + 1 default_name, default_aufgaben = ressort_defaults[idx] if idx < 3 else ("Allgemeine Leitung", "Sonstige Aufgaben") ctx[f"RESSORT_{n}_NAME"] = gf.get("internal_role") or default_name ctx[f"RESSORT_{n}_GF"] = gf.get("name", "") ctx[f"RESSORT_{n}_AUFGABEN"] = f"- {default_aufgaben}" ctx["HAS_RESSORT_3"] = len(gf_list) >= 3 return ctx