feat(founding-wizard): Gründungs-Wizard für 2-Mann GmbH + 14 Notar-Templates
[migration-approved]
Templates (Migrations 123-136):
- 123 GO-GF (Geschäftsordnung Geschäftsführung)
- 124 SHA (Shareholders' Agreement, 56 Platzhalter)
- 125 Satzung (Articles of Association mit UG-Variante)
- 126 GF-Dienstvertrag (Trennungsprinzip Organ/Anstellung)
- 127 Arbeitsvertrag (AGG-neutral, NachwG, eAU)
- 128 Gesellschafterliste (§ 40 GmbHG)
- 129 GF-Bestellungsbeschluss (mit § 6 Abs. 2 Versicherung)
- 130 HRB-Anmeldung (§§ 7, 8, 39 GmbHG, § 12 HGB)
- 131 IP-Assignment Agreement (Gründer→GmbH)
- 132 Term Sheet (Pre-Seed/Seed VC-Standard)
- 133 Wandeldarlehensvertrag (Convertible Loan)
- 134 Beteiligungsvertrag (Subscription Agreement)
- 135 ESOP/VSOP-Plan (3 Varianten)
- 136 Cap Table
Kategorisierung (Migrations 137-138):
- ALTER TABLE compliance_legal_templates ADD lifecycle_stage TEXT[],
functional_category TEXT (mit CHECK Constraints + GIN-Index)
- Backfill aller 105 Templates: lifecycle_stage (pre_founding|founding|
startup|kmu|konzern) + functional_category (founding_legal|employment|
investor_funding|...)
Backend Founding-Wizard Service:
- template_renderer.py: Handlebars-light ({{VAR}}, {{#IF FLAG}}...{{/IF}})
- wizard_to_context.py: Mapping Wizard-State → SCREAMING_SNAKE_CASE Vars
- markdown_to_docx.py: Markdown → DOCX via python-docx
- founding_wizard_routes.py: POST /v1/founding-wizard/generate
→ liefert base64-DOCX-Files für ausgewählte Templates
Frontend Founding-Wizard (/sdk/founding-wizard):
- 8-Step Wizard (Basics, Gesellschafter, GF, Kapital, Notar, SHA, GF-Verträge, Generate)
- useFoundingWizardForm Hook mit localStorage-Persistenz
- TypeScript Code-Registry (template-categories.ts) als Backup zur DB
- Word-Download via data:URLs (base64)
Tests:
- 20 Unit-Tests grün (Renderer, Context-Mapping, DOCX-Conversion)
- Playwright E2E-Test mit 2-Mann GmbH (Benjamin + Sharang) Test-Daten
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
"""Founding-Wizard Service: rendert Templates + generiert DOCX-Files."""
|
||||
|
||||
from .markdown_to_docx import markdown_to_docx_bytes
|
||||
from .template_renderer import find_undefined_placeholders, render_template
|
||||
from .wizard_to_context import base_context
|
||||
|
||||
__all__ = [
|
||||
"base_context",
|
||||
"find_undefined_placeholders",
|
||||
"markdown_to_docx_bytes",
|
||||
"render_template",
|
||||
]
|
||||
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Konvertiert gerendertes Markdown in eine .docx-Datei mittels python-docx.
|
||||
|
||||
Unterstuetzte Markdown-Elemente:
|
||||
- # / ## / ### / #### / ##### Headings
|
||||
- **bold** und _italic_ inline
|
||||
- Tabellen (Pipe-Syntax)
|
||||
- Listen mit - oder * oder Ziffer.)
|
||||
- Horizontale Linien ---
|
||||
- Code-Inline `code`
|
||||
|
||||
Bewusst minimal — fuer rechtliche Dokumente brauchen wir keine Bilder/Embeds.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from docx import Document
|
||||
from docx.shared import Pt, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
|
||||
HEADING_RE = re.compile(r"^(#{1,5})\s+(.+)$")
|
||||
HR_RE = re.compile(r"^[-_*]{3,}\s*$")
|
||||
LIST_BULLET_RE = re.compile(r"^(\s*)([-*+])\s+(.+)$")
|
||||
LIST_NUMBER_RE = re.compile(r"^(\s*)(\d+)[\.\)]\s+(.+)$")
|
||||
TABLE_ROW_RE = re.compile(r"^\|(.+)\|\s*$")
|
||||
TABLE_SEP_RE = re.compile(r"^\|[\s\-:|]+\|\s*$")
|
||||
|
||||
INLINE_BOLD = re.compile(r"\*\*([^*]+)\*\*")
|
||||
INLINE_ITALIC = re.compile(r"(?<!\*)\*(?!\*)([^*]+)\*(?!\*)|_([^_]+)_")
|
||||
INLINE_CODE = re.compile(r"`([^`]+)`")
|
||||
|
||||
|
||||
def _add_runs(paragraph, text: str) -> None:
|
||||
"""Parse inline-Formatierung und fuege Runs hinzu."""
|
||||
pos = 0
|
||||
tokens: list[tuple[str, str]] = []
|
||||
while pos < len(text):
|
||||
m_bold = INLINE_BOLD.search(text, pos)
|
||||
m_code = INLINE_CODE.search(text, pos)
|
||||
m_italic = INLINE_ITALIC.search(text, pos)
|
||||
|
||||
candidates = [m for m in (m_bold, m_code, m_italic) if m]
|
||||
if not candidates:
|
||||
tokens.append(("plain", text[pos:]))
|
||||
break
|
||||
first = min(candidates, key=lambda m: m.start())
|
||||
if first.start() > pos:
|
||||
tokens.append(("plain", text[pos:first.start()]))
|
||||
if first is m_bold:
|
||||
tokens.append(("bold", first.group(1)))
|
||||
elif first is m_code:
|
||||
tokens.append(("code", first.group(1)))
|
||||
else:
|
||||
content = m_italic.group(1) or m_italic.group(2)
|
||||
tokens.append(("italic", content))
|
||||
pos = first.end()
|
||||
|
||||
for kind, content in tokens:
|
||||
run = paragraph.add_run(content)
|
||||
if kind == "bold":
|
||||
run.bold = True
|
||||
elif kind == "italic":
|
||||
run.italic = True
|
||||
elif kind == "code":
|
||||
run.font.name = "Courier New"
|
||||
run.font.size = Pt(10)
|
||||
|
||||
|
||||
def _parse_table(lines: list[str], start: int) -> tuple[list[list[str]], int]:
|
||||
"""Parst Markdown-Tabelle. Returns (rows, next_line_index)."""
|
||||
rows: list[list[str]] = []
|
||||
i = start
|
||||
while i < len(lines):
|
||||
line = lines[i].rstrip()
|
||||
if not TABLE_ROW_RE.match(line) and not TABLE_SEP_RE.match(line):
|
||||
break
|
||||
if TABLE_SEP_RE.match(line):
|
||||
i += 1
|
||||
continue
|
||||
cells = [c.strip() for c in line.strip("|").split("|")]
|
||||
rows.append(cells)
|
||||
i += 1
|
||||
return rows, i
|
||||
|
||||
|
||||
def _add_table(doc: Document, rows: list[list[str]]) -> None:
|
||||
if not rows:
|
||||
return
|
||||
ncols = max(len(r) for r in rows)
|
||||
table = doc.add_table(rows=len(rows), cols=ncols)
|
||||
table.style = "Light Grid"
|
||||
for r_idx, row in enumerate(rows):
|
||||
for c_idx, cell_text in enumerate(row):
|
||||
if c_idx < ncols:
|
||||
cell = table.rows[r_idx].cells[c_idx]
|
||||
cell.text = ""
|
||||
p = cell.paragraphs[0]
|
||||
_add_runs(p, cell_text)
|
||||
if r_idx == 0:
|
||||
for run in p.runs:
|
||||
run.bold = True
|
||||
|
||||
|
||||
def markdown_to_docx_bytes(markdown_text: str, title: Optional[str] = None) -> bytes:
|
||||
"""Konvertiert Markdown nach DOCX und returns die Bytes."""
|
||||
doc = Document()
|
||||
|
||||
# Basis-Style
|
||||
style = doc.styles["Normal"]
|
||||
style.font.name = "Calibri"
|
||||
style.font.size = Pt(11)
|
||||
|
||||
if title:
|
||||
h = doc.add_heading(title, level=0)
|
||||
h.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
|
||||
lines = markdown_text.split("\n")
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i].rstrip()
|
||||
|
||||
if not line.strip():
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Heading
|
||||
h_match = HEADING_RE.match(line)
|
||||
if h_match:
|
||||
level = len(h_match.group(1))
|
||||
text = h_match.group(2)
|
||||
heading = doc.add_heading(level=min(level, 4))
|
||||
_add_runs(heading, text)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Horizontal Rule
|
||||
if HR_RE.match(line):
|
||||
doc.add_paragraph("─" * 60)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Tabelle
|
||||
if TABLE_ROW_RE.match(line):
|
||||
rows, i = _parse_table(lines, i)
|
||||
_add_table(doc, rows)
|
||||
doc.add_paragraph()
|
||||
continue
|
||||
|
||||
# List Bullet
|
||||
b_match = LIST_BULLET_RE.match(line)
|
||||
if b_match:
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
_add_runs(p, b_match.group(3))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# List Number
|
||||
n_match = LIST_NUMBER_RE.match(line)
|
||||
if n_match:
|
||||
p = doc.add_paragraph(style="List Number")
|
||||
_add_runs(p, n_match.group(3))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Sonst: normaler Paragraph
|
||||
p = doc.add_paragraph()
|
||||
_add_runs(p, line)
|
||||
i += 1
|
||||
|
||||
buf = io.BytesIO()
|
||||
doc.save(buf)
|
||||
return buf.getvalue()
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Handlebars-light Template-Renderer fuer die compliance_legal_templates.
|
||||
|
||||
Unterstuetzte Syntax:
|
||||
- {{VARIABLE_NAME}} - einfache String-Substitution
|
||||
- {{#IF FLAG}}...{{/IF}} - bedingter Block (truthy)
|
||||
- {{#IF NOT FLAG}}...{{/IF}} - negierter bedingter Block
|
||||
|
||||
Bewusst minimal gehalten — keine Loops oder Verschachtelung tiefer Logik.
|
||||
Komplexere Sachen werden im Context vorberechnet.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
# Pattern fuer {{#IF FLAG}}...{{/IF}} und {{#IF NOT FLAG}}...{{/IF}}
|
||||
# Greedy / non-overlapping. Inhalt darf alles enthalten ausser einem geschlossenen {{/IF}}.
|
||||
IF_BLOCK = re.compile(
|
||||
r"\{\{#IF\s+(NOT\s+)?([A-Z_][A-Z0-9_]*)\}\}(.*?)\{\{/IF\}\}",
|
||||
re.DOTALL,
|
||||
)
|
||||
VAR_PATTERN = re.compile(r"\{\{\s*([A-Z_][A-Z0-9_]*)\s*\}\}")
|
||||
|
||||
|
||||
def _is_truthy(val: Any) -> bool:
|
||||
"""Pythonische Truthiness, mit Special-Case: leeres dict/list/str = False."""
|
||||
if val is None:
|
||||
return False
|
||||
if isinstance(val, bool):
|
||||
return val
|
||||
if isinstance(val, (int, float)):
|
||||
return val != 0
|
||||
if isinstance(val, str):
|
||||
return val.strip() != "" and val.lower() not in ("false", "0", "no", "nein")
|
||||
if isinstance(val, (list, dict, tuple, set)):
|
||||
return len(val) > 0
|
||||
return True
|
||||
|
||||
|
||||
def render_template(template: str, context: dict[str, Any]) -> str:
|
||||
"""Rendert ein Template mit dem gegebenen Kontext.
|
||||
|
||||
Algorithmus:
|
||||
1. IF-Bloecke iterativ aufloesen (max 10 Durchlaeufe, damit Nesting funktioniert)
|
||||
2. Variablen substituieren
|
||||
|
||||
Args:
|
||||
template: Markdown-Template mit {{VAR}} und {{#IF FLAG}}...{{/IF}}
|
||||
context: dict mit Variablen — Keys SCREAMING_SNAKE_CASE
|
||||
|
||||
Returns:
|
||||
Gerendetes Markdown
|
||||
"""
|
||||
output = template
|
||||
|
||||
for _ in range(10): # max 10 Levels Nesting
|
||||
def replace_if(match: re.Match[str]) -> str:
|
||||
negated = bool(match.group(1))
|
||||
flag_name = match.group(2)
|
||||
content = match.group(3)
|
||||
flag_val = context.get(flag_name)
|
||||
condition = _is_truthy(flag_val)
|
||||
if negated:
|
||||
condition = not condition
|
||||
return content if condition else ""
|
||||
|
||||
new_output = IF_BLOCK.sub(replace_if, output)
|
||||
if new_output == output:
|
||||
break
|
||||
output = new_output
|
||||
|
||||
def replace_var(match: re.Match[str]) -> str:
|
||||
name = match.group(1)
|
||||
val = context.get(name)
|
||||
if val is None:
|
||||
# Leere Platzhalter sichtbar machen fuer Debugging
|
||||
return f"[{name} fehlt]"
|
||||
if isinstance(val, bool):
|
||||
return "ja" if val else "nein"
|
||||
return str(val)
|
||||
|
||||
output = VAR_PATTERN.sub(replace_var, output)
|
||||
return output
|
||||
|
||||
|
||||
def find_undefined_placeholders(template: str, context: dict[str, Any]) -> list[str]:
|
||||
"""Listet alle Variablen-Platzhalter ohne Wert im Context."""
|
||||
placeholders: set[str] = set()
|
||||
for match in VAR_PATTERN.finditer(template):
|
||||
placeholders.add(match.group(1))
|
||||
for match in IF_BLOCK.finditer(template):
|
||||
placeholders.add(match.group(2))
|
||||
return sorted([p for p in placeholders if p not in context])
|
||||
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
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 _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": "[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": False,
|
||||
"HRB_NUMBER": "[wird vergeben]",
|
||||
"IS_UG": basics.get("legal_form") == "UG",
|
||||
}
|
||||
return ctx
|
||||
Reference in New Issue
Block a user