feat: Finanzplan Phase 1-4 — DB + Engine + API + Spreadsheet-UI
Phase 1: DB-Schema (12 fp_* Tabellen) + Excel-Import (332 Zeilen importiert) Phase 2: Compute Engine (Personal, Invest, Umsatz, Material, Betrieblich, Liquiditaet, GuV) Phase 3: API (/api/finanzplan/ — GET sheets, PUT cells, POST compute) Phase 4: Spreadsheet-UI (FinanzplanSlide als Annex mit Tab-Leiste, editierbarem Grid, Jahres-Navigation) Zusaetzlich: - Gruendungsdatum verschoben: Feb→Aug 2026 (DB + Personalkosten) - Neue Preisstaffel: Startup/<10 MA ab 3.600 EUR/Jahr (14-Tage-Test, Kreditkarte) - Competition-Slide: Pricing-Tiers aktualisiert Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
246
pitch-deck/scripts/001_finanzplan_tables.sql
Normal file
246
pitch-deck/scripts/001_finanzplan_tables.sql
Normal file
@@ -0,0 +1,246 @@
|
||||
-- ============================================================================
|
||||
-- BreakPilot ComplAI — Finanzplan Database Schema
|
||||
-- Mirrors Excel: "Breakpilot ComplAI Finanzplan.xlsm" (10 Reiter)
|
||||
-- Monthly granularity: Jan 2026 – Dec 2030 (60 months)
|
||||
-- Values stored as JSONB: {"m1": ..., "m2": ..., "m60": ...}
|
||||
-- ============================================================================
|
||||
|
||||
-- Scenarios (extends existing pitch_fm_scenarios)
|
||||
CREATE TABLE IF NOT EXISTS fp_scenarios (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL DEFAULT 'Base Case',
|
||||
description TEXT,
|
||||
is_default BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Insert default scenario
|
||||
INSERT INTO fp_scenarios (name, description, is_default)
|
||||
VALUES ('Base Case', 'Basisdaten aus Excel-Import', true)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- KUNDEN (6 Segmente × ~20 Zeilen = ~120 Datenzeilen)
|
||||
-- Each segment has: base customer count (editable) + module percentages
|
||||
-- Formulas: module_count = ROUNDDOWN(base_count * percentage)
|
||||
-- ============================================================================
|
||||
CREATE TABLE fp_kunden (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE,
|
||||
segment_name TEXT NOT NULL, -- 'Care (Privat)', 'Horse (Händler)', etc.
|
||||
segment_index INT NOT NULL, -- 1-6
|
||||
row_label TEXT NOT NULL, -- 'Modul 1', 'Modul 2', ...
|
||||
row_index INT NOT NULL, -- position within segment
|
||||
percentage NUMERIC(5,3), -- multiplier (e.g. 0.9, 0.25)
|
||||
formula_type TEXT, -- 'literal', 'roundup_pct', 'rounddown_pct', 'cumulative', null
|
||||
is_editable BOOLEAN DEFAULT false, -- true for base inputs (Modul 1 per segment)
|
||||
values JSONB NOT NULL DEFAULT '{}', -- {m1: 0, m2: 0, ... m60: 0}
|
||||
excel_row INT, -- original Excel row number
|
||||
sort_order INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Summary rows (aggregated across all 6 segments)
|
||||
CREATE TABLE fp_kunden_summary (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE,
|
||||
row_label TEXT NOT NULL, -- 'Modul 1', 'Modul 2', ...
|
||||
row_index INT NOT NULL,
|
||||
values JSONB NOT NULL DEFAULT '{}', -- computed: sum of 6 segments
|
||||
excel_row INT,
|
||||
sort_order INT NOT NULL
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- UMSATZERLOESE (Revenue = Quantity × Price)
|
||||
-- Section 1 (rows 3-23): Computed revenue per module
|
||||
-- Section 2 (rows 27-46): Quantity (from Kunden)
|
||||
-- Section 3 (rows 49-73): Prices (editable VK excl. MwSt.)
|
||||
-- ============================================================================
|
||||
CREATE TABLE fp_umsatzerloese (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE,
|
||||
section TEXT NOT NULL, -- 'revenue', 'quantity', 'price'
|
||||
row_label TEXT NOT NULL, -- 'Modul 1', 'Modul 2', ...
|
||||
row_index INT NOT NULL,
|
||||
is_editable BOOLEAN DEFAULT false, -- only prices are editable
|
||||
values JSONB NOT NULL DEFAULT '{}',
|
||||
excel_row INT,
|
||||
sort_order INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- MATERIALAUFWAND (sehr einfach: nur Mac Mini + Mac Studio)
|
||||
-- Cost = Quantity × Unit Cost (EK)
|
||||
-- Mac Mini EK: 3.200 EUR, VK: 4.800 EUR (50% Aufschlag)
|
||||
-- Mac Studio EK: 13.000 EUR, VK: 19.500 EUR (50% Aufschlag)
|
||||
-- ============================================================================
|
||||
CREATE TABLE fp_materialaufwand (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE,
|
||||
section TEXT NOT NULL, -- 'cost', 'quantity', 'unit_cost'
|
||||
row_label TEXT NOT NULL,
|
||||
row_index INT NOT NULL,
|
||||
is_editable BOOLEAN DEFAULT false, -- only unit costs are editable
|
||||
values JSONB NOT NULL DEFAULT '{}',
|
||||
excel_row INT,
|
||||
sort_order INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- PERSONALKOSTEN (20 Positionen)
|
||||
-- Structured: Name, Position, Start, End, Brutto, Raise%
|
||||
-- Computed: monthly salary × AG-Sozialversicherung
|
||||
-- ============================================================================
|
||||
CREATE TABLE fp_personalkosten (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE,
|
||||
person_name TEXT NOT NULL,
|
||||
person_nr TEXT, -- '001', '002', ...
|
||||
position TEXT, -- 'GF', 'Vertrieb', 'Entwicklung', ...
|
||||
start_date DATE,
|
||||
end_date DATE, -- null = permanent
|
||||
brutto_monthly NUMERIC(10,2), -- Bruttogehalt/Monat
|
||||
annual_raise_pct NUMERIC(5,2) DEFAULT 3.0,
|
||||
ag_sozial_pct NUMERIC(5,2) DEFAULT 20.425, -- AG-Anteil Sozialversicherung
|
||||
is_editable BOOLEAN DEFAULT true,
|
||||
-- Computed monthly values
|
||||
values_brutto JSONB NOT NULL DEFAULT '{}', -- monthly brutto
|
||||
values_sozial JSONB NOT NULL DEFAULT '{}', -- monthly AG-Sozial
|
||||
values_total JSONB NOT NULL DEFAULT '{}', -- brutto + sozial
|
||||
excel_row INT,
|
||||
sort_order INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- BETRIEBLICHE AUFWENDUNGEN (~40 Kostenposten)
|
||||
-- Most are fixed monthly values (editable)
|
||||
-- Some are computed (Summen, Abschreibungen)
|
||||
-- ============================================================================
|
||||
CREATE TABLE fp_betriebliche_aufwendungen (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE,
|
||||
category TEXT NOT NULL, -- 'raumkosten', 'versicherungen', 'marketing', etc.
|
||||
row_label TEXT NOT NULL,
|
||||
row_index INT NOT NULL,
|
||||
is_editable BOOLEAN DEFAULT true,
|
||||
is_sum_row BOOLEAN DEFAULT false, -- true for category subtotals
|
||||
formula_desc TEXT, -- e.g. 'SUM(rows 9-18)', '=Personalkosten!Y4'
|
||||
values JSONB NOT NULL DEFAULT '{}',
|
||||
excel_row INT,
|
||||
sort_order INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- INVESTITIONEN (Anlagegüter mit AfA)
|
||||
-- Each item: name, amount, purchase date, useful life, depreciation
|
||||
-- ============================================================================
|
||||
CREATE TABLE fp_investitionen (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE,
|
||||
item_name TEXT NOT NULL,
|
||||
category TEXT, -- 'gwg', 'ausstattung', etc.
|
||||
purchase_amount NUMERIC(12,2) NOT NULL,
|
||||
purchase_date DATE,
|
||||
afa_years INT, -- useful life in years
|
||||
afa_end_date DATE, -- computed end date
|
||||
is_editable BOOLEAN DEFAULT true,
|
||||
-- Computed: monthly investment amount (in purchase month) and depreciation
|
||||
values_invest JSONB NOT NULL DEFAULT '{}', -- investment amount per month
|
||||
values_afa JSONB NOT NULL DEFAULT '{}', -- monthly depreciation
|
||||
excel_row INT,
|
||||
sort_order INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- SONST. BETRIEBLICHE ERTRAEGE (6 Kategorien × 3 Zeilen)
|
||||
-- ============================================================================
|
||||
CREATE TABLE fp_sonst_ertraege (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE,
|
||||
category TEXT NOT NULL, -- '1-Interne Kostenstellen', '2-Entwicklung National', etc.
|
||||
row_label TEXT,
|
||||
row_index INT NOT NULL,
|
||||
is_editable BOOLEAN DEFAULT true,
|
||||
is_sum_row BOOLEAN DEFAULT false,
|
||||
values JSONB NOT NULL DEFAULT '{}',
|
||||
excel_row INT,
|
||||
sort_order INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- LIQUIDITAET (computed from all above)
|
||||
-- ============================================================================
|
||||
CREATE TABLE fp_liquiditaet (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE,
|
||||
row_label TEXT NOT NULL,
|
||||
row_type TEXT NOT NULL, -- 'einzahlung', 'auszahlung', 'ueberschuss', 'kontostand'
|
||||
is_editable BOOLEAN DEFAULT false, -- only Eigenkapital, Fremdkapital, Entnahmen editable
|
||||
formula_desc TEXT,
|
||||
values JSONB NOT NULL DEFAULT '{}',
|
||||
excel_row INT,
|
||||
sort_order INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- GUV JAHRESABSCHLUSS (annual summary, 5 years)
|
||||
-- ============================================================================
|
||||
CREATE TABLE fp_guv (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE,
|
||||
row_label TEXT NOT NULL,
|
||||
row_index INT NOT NULL,
|
||||
is_sum_row BOOLEAN DEFAULT false,
|
||||
formula_desc TEXT,
|
||||
values JSONB NOT NULL DEFAULT '{}', -- {y2026: ..., y2027: ..., y2030: ...}
|
||||
excel_row INT,
|
||||
sort_order INT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- CELL OVERRIDES (for scenario-specific manual edits)
|
||||
-- ============================================================================
|
||||
CREATE TABLE fp_cell_overrides (
|
||||
id SERIAL PRIMARY KEY,
|
||||
scenario_id UUID REFERENCES fp_scenarios(id) ON DELETE CASCADE,
|
||||
sheet_name TEXT NOT NULL, -- 'kunden', 'personalkosten', etc.
|
||||
row_id INT NOT NULL, -- references the id in the sheet table
|
||||
month_key TEXT NOT NULL, -- 'm1', 'm2', ... 'm60' or 'y2026', etc.
|
||||
override_value NUMERIC,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(scenario_id, sheet_name, row_id, month_key)
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- INDEXES
|
||||
-- ============================================================================
|
||||
CREATE INDEX idx_fp_kunden_scenario ON fp_kunden(scenario_id);
|
||||
CREATE INDEX idx_fp_kunden_summary_scenario ON fp_kunden_summary(scenario_id);
|
||||
CREATE INDEX idx_fp_umsatz_scenario ON fp_umsatzerloese(scenario_id);
|
||||
CREATE INDEX idx_fp_material_scenario ON fp_materialaufwand(scenario_id);
|
||||
CREATE INDEX idx_fp_personal_scenario ON fp_personalkosten(scenario_id);
|
||||
CREATE INDEX idx_fp_betrieb_scenario ON fp_betriebliche_aufwendungen(scenario_id);
|
||||
CREATE INDEX idx_fp_invest_scenario ON fp_investitionen(scenario_id);
|
||||
CREATE INDEX idx_fp_sonst_scenario ON fp_sonst_ertraege(scenario_id);
|
||||
CREATE INDEX idx_fp_liquid_scenario ON fp_liquiditaet(scenario_id);
|
||||
CREATE INDEX idx_fp_guv_scenario ON fp_guv(scenario_id);
|
||||
CREATE INDEX idx_fp_overrides_lookup ON fp_cell_overrides(scenario_id, sheet_name, row_id);
|
||||
583
pitch-deck/scripts/import-finanzplan.py
Normal file
583
pitch-deck/scripts/import-finanzplan.py
Normal file
@@ -0,0 +1,583 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Import BreakPilot Finanzplan Excel into PostgreSQL fp_* tables.
|
||||
Usage: python3 scripts/import-finanzplan.py <path-to-xlsm>
|
||||
|
||||
Requires: pip3 install openpyxl psycopg2-binary
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
from datetime import date, datetime
|
||||
import openpyxl
|
||||
import psycopg2
|
||||
from psycopg2.extras import Json
|
||||
|
||||
# --- Config ---
|
||||
DB_URL = os.environ.get('DATABASE_URL', 'postgresql://breakpilot:breakpilot@localhost:5432/breakpilot_db')
|
||||
MONTHS = 60 # Jan 2026 – Dec 2030
|
||||
# Excel columns: D=4 (month 1) through BQ=69 (month 60, approx)
|
||||
# Actually: D=m1(Jan2026), E=m2(Feb2026), ..., O=m12(Dec2026),
|
||||
# Q=m13(Jan2027), ..., AB=m24(Dec2027), etc.
|
||||
# Year-column (C, P, AC, AP, BC) = annual sums, skip those
|
||||
# Monthly columns per year: D-O (12), Q-AB (12), AD-AO (12), AQ-BB (12), BD-BO (12)
|
||||
|
||||
# Map: month_index (1-60) -> Excel column index (1-based)
|
||||
def build_month_columns():
|
||||
"""Build mapping from month 1-60 to Excel column index."""
|
||||
cols = []
|
||||
# Year 1 (2026): cols D(4) - O(15) = 12 months
|
||||
for c in range(4, 16):
|
||||
cols.append(c)
|
||||
# Year 2 (2027): cols Q(17) - AB(28) = 12 months
|
||||
for c in range(17, 29):
|
||||
cols.append(c)
|
||||
# Year 3 (2028): cols AD(30) - AO(41) = 12 months
|
||||
for c in range(30, 42):
|
||||
cols.append(c)
|
||||
# Year 4 (2029): cols AQ(43) - BB(54) = 12 months
|
||||
for c in range(43, 55):
|
||||
cols.append(c)
|
||||
# Year 5 (2030): cols BD(56) - BO(67) = 12 months
|
||||
for c in range(56, 68):
|
||||
cols.append(c)
|
||||
return cols
|
||||
|
||||
MONTH_COLS = build_month_columns()
|
||||
|
||||
def read_monthly_values(ws, row, data_only=True):
|
||||
"""Read 60 monthly values from an Excel row."""
|
||||
values = {}
|
||||
for m_idx, col in enumerate(MONTH_COLS):
|
||||
v = ws.cell(row, col).value
|
||||
if v is not None and v != '' and not isinstance(v, str):
|
||||
try:
|
||||
values[f'm{m_idx+1}'] = round(float(v), 2)
|
||||
except (ValueError, TypeError):
|
||||
values[f'm{m_idx+1}'] = 0
|
||||
else:
|
||||
values[f'm{m_idx+1}'] = 0
|
||||
return values
|
||||
|
||||
def safe_str(v):
|
||||
if v is None:
|
||||
return ''
|
||||
return str(v).strip()
|
||||
|
||||
def safe_float(v, default=0):
|
||||
if v is None:
|
||||
return default
|
||||
try:
|
||||
return float(v)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
def safe_date(v):
|
||||
if v is None:
|
||||
return None
|
||||
if isinstance(v, datetime):
|
||||
return v.date()
|
||||
if isinstance(v, date):
|
||||
return v
|
||||
return None
|
||||
|
||||
|
||||
def import_personalkosten(cur, ws, scenario_id):
|
||||
"""Import Personalkosten sheet (rows 10-29 = 20 positions)."""
|
||||
print(" Importing Personalkosten...")
|
||||
count = 0
|
||||
for i in range(20):
|
||||
row = 10 + i
|
||||
name = safe_str(ws.cell(row, 1).value)
|
||||
nr = safe_str(ws.cell(row, 2).value)
|
||||
position = safe_str(ws.cell(row, 3).value)
|
||||
start = safe_date(ws.cell(row, 4).value)
|
||||
end = safe_date(ws.cell(row, 5).value)
|
||||
brutto = safe_float(ws.cell(row, 7).value)
|
||||
raise_pct = safe_float(ws.cell(row, 8).value, 3.0)
|
||||
|
||||
if not name and not nr:
|
||||
continue
|
||||
|
||||
# Read computed monthly totals from the "Personalaufwand" section
|
||||
# The actual monthly values are in a different row range (rows 32-51 for brutto, 56-75 for sozial)
|
||||
# But we store the inputs and let the engine compute
|
||||
values_total = {} # will be computed by engine
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO fp_personalkosten
|
||||
(scenario_id, person_name, person_nr, position, start_date, end_date,
|
||||
brutto_monthly, annual_raise_pct, is_editable,
|
||||
values_brutto, values_sozial, values_total, excel_row, sort_order)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, true, %s, %s, %s, %s, %s)
|
||||
""", (scenario_id, name, nr, position, start, end,
|
||||
brutto, raise_pct, Json({}), Json({}), Json({}), row, i + 1))
|
||||
count += 1
|
||||
print(f" -> {count} Positionen importiert")
|
||||
|
||||
|
||||
def import_betriebliche_aufwendungen(cur, ws, scenario_id):
|
||||
"""Import Betriebliche Aufwendungen (rows 3-47)."""
|
||||
print(" Importing Betriebliche Aufwendungen...")
|
||||
|
||||
# Define the structure based on Excel analysis
|
||||
rows_config = [
|
||||
(3, 'personal', 'Personalkosten', False, True, '=Personalkosten total'),
|
||||
(4, 'raumkosten', 'Raumkosten', True, False, None),
|
||||
(5, 'steuern', 'Betriebliche Steuern', False, True, '=SUM(6,7)'),
|
||||
(6, 'steuern', 'Gewerbesteuer', True, False, None),
|
||||
(7, 'steuern', 'KFZ-Steuern', True, False, None),
|
||||
(8, 'versicherungen', 'Versich./Beitraege', False, True, '=SUM(9:18)'),
|
||||
(9, 'versicherungen', 'IHK', True, False, None),
|
||||
(10, 'versicherungen', 'Rundfunkbeitrag', True, False, None),
|
||||
(11, 'versicherungen', 'Berufsgenossenschaft', True, False, None),
|
||||
(12, 'versicherungen', 'Bundesanzeiger/Transparenzregister', True, False, None),
|
||||
(13, 'versicherungen', 'D&O-Versicherung', True, False, None),
|
||||
(14, 'versicherungen', 'E&O-Versicherung', True, False, None),
|
||||
(15, 'versicherungen', 'Produkthaftpflicht', True, False, None),
|
||||
(16, 'versicherungen', 'Cyber-Versicherung', True, False, None),
|
||||
(17, 'versicherungen', 'Rechtsschutzversicherung', True, False, None),
|
||||
(18, 'versicherungen', 'KFZ-Versicherung', True, False, None),
|
||||
(19, 'besondere', 'Besondere Kosten', False, True, '=SUM(20:22)'),
|
||||
(20, 'besondere', 'Schutzrechte/Lizenzkosten', True, False, None),
|
||||
(21, 'besondere', 'Marketing Videos', True, False, None),
|
||||
(22, 'besondere', 'Fort-/Weiterbildungskosten', True, False, None),
|
||||
(23, 'fahrzeug', 'Fahrzeugkosten', True, False, None),
|
||||
(24, 'marketing', 'Werbe-/Reisekosten', False, True, '=SUM(25:30)'),
|
||||
(25, 'marketing', 'Reisekosten', True, False, None),
|
||||
(26, 'marketing', 'Teilnahme an Messen', True, False, None),
|
||||
(27, 'marketing', 'Allgemeine Marketingkosten', True, False, None),
|
||||
(28, 'marketing', 'Marketing-Agentur', True, False, None),
|
||||
(29, 'marketing', 'Editorial Content', True, False, None),
|
||||
(30, 'marketing', 'Bewirtungskosten', True, False, None),
|
||||
(31, 'warenabgabe', 'Kosten Warenabgabe', True, False, None),
|
||||
(32, 'abschreibungen', 'Abschreibungen', False, False, '=Investitionen AfA'),
|
||||
(33, 'reparatur', 'Reparatur/Instandh.', True, False, None),
|
||||
(34, 'sonstige', 'Sonstige Kosten', False, True, '=SUM(35:45)'),
|
||||
(35, 'sonstige', 'Telefon', True, False, None),
|
||||
(36, 'sonstige', 'Bankgebuehren', True, False, None),
|
||||
(37, 'sonstige', 'Buchfuehrung', True, False, None),
|
||||
(38, 'sonstige', 'Jahresabschluss', True, False, None),
|
||||
(39, 'sonstige', 'Rechts-/Beratungskosten', True, False, None),
|
||||
(40, 'sonstige', 'Werkzeuge/Kleingeraete', True, False, None),
|
||||
(41, 'sonstige', 'Serverkosten (Cloud)', True, False, None),
|
||||
(42, 'sonstige', 'Verbrauchsmaterialien', True, False, None),
|
||||
(43, 'sonstige', 'Mietkosten Software', True, False, None),
|
||||
(44, 'sonstige', 'Nebenkosten Geldverkehr', True, False, None),
|
||||
(46, 'summe', 'Summe sonstige (ohne Pers., Abschr.)', False, True, '=computed'),
|
||||
(47, 'summe', 'Gesamtkosten (Klasse 6)', False, True, '=computed'),
|
||||
]
|
||||
|
||||
count = 0
|
||||
for idx, (row, cat, label, editable, is_sum, formula) in enumerate(rows_config):
|
||||
values = read_monthly_values(ws, row)
|
||||
cur.execute("""
|
||||
INSERT INTO fp_betriebliche_aufwendungen
|
||||
(scenario_id, category, row_label, row_index, is_editable, is_sum_row,
|
||||
formula_desc, values, excel_row, sort_order)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (scenario_id, cat, label, row, editable, is_sum,
|
||||
formula, Json(values), row, idx + 1))
|
||||
count += 1
|
||||
print(f" -> {count} Kostenposten importiert")
|
||||
|
||||
|
||||
def import_investitionen(cur, ws, scenario_id):
|
||||
"""Import Investitionen (rows 6-42)."""
|
||||
print(" Importing Investitionen...")
|
||||
count = 0
|
||||
for i in range(37):
|
||||
row = 6 + i
|
||||
name = safe_str(ws.cell(row, 1).value)
|
||||
amount = safe_float(ws.cell(row, 2).value)
|
||||
purchase = safe_date(ws.cell(row, 3).value)
|
||||
afa_years = safe_float(ws.cell(row, 4).value)
|
||||
afa_end = safe_date(ws.cell(row, 5).value)
|
||||
|
||||
if not name and amount == 0:
|
||||
continue
|
||||
|
||||
# Read monthly investment values (col G onwards in Investitionen sheet)
|
||||
# This sheet has different column mapping — dates as headers
|
||||
# For simplicity, store purchase amount and let engine compute AfA
|
||||
cur.execute("""
|
||||
INSERT INTO fp_investitionen
|
||||
(scenario_id, item_name, category, purchase_amount, purchase_date,
|
||||
afa_years, afa_end_date, is_editable,
|
||||
values_invest, values_afa, excel_row, sort_order)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, true, %s, %s, %s, %s)
|
||||
""", (scenario_id, name, 'ausstattung' if 'Ausstattung' in name else 'gwg',
|
||||
amount, purchase, int(afa_years) if afa_years else None, afa_end,
|
||||
Json({}), Json({}), row, count + 1))
|
||||
count += 1
|
||||
print(f" -> {count} Investitionsgueter importiert")
|
||||
|
||||
|
||||
def import_sonst_ertraege(cur, ws, scenario_id):
|
||||
"""Import Sonst. betr. Ertraege (6 categories)."""
|
||||
print(" Importing Sonst. betr. Ertraege...")
|
||||
categories = [
|
||||
(3, 8, 'Interne Kostenstellen'),
|
||||
(9, 12, 'Entwicklung National'),
|
||||
(13, 16, 'Entwicklung International'),
|
||||
(17, 20, 'Beratungsdienstleistung'),
|
||||
(21, 24, 'Zuwendungen'),
|
||||
(25, 28, 'TBD'),
|
||||
]
|
||||
count = 0
|
||||
for sum_row, end_row, cat_name in categories:
|
||||
# Sum row
|
||||
values = read_monthly_values(ws, sum_row)
|
||||
cur.execute("""
|
||||
INSERT INTO fp_sonst_ertraege
|
||||
(scenario_id, category, row_label, row_index, is_editable, is_sum_row,
|
||||
values, excel_row, sort_order)
|
||||
VALUES (%s, %s, %s, %s, false, true, %s, %s, %s)
|
||||
""", (scenario_id, cat_name, cat_name, sum_row, Json(values), sum_row, count + 1))
|
||||
count += 1
|
||||
# Detail rows
|
||||
for r in range(sum_row + 1, end_row + 1):
|
||||
values = read_monthly_values(ws, r)
|
||||
label = safe_str(ws.cell(r, 1).value) or f'Position {r - sum_row}'
|
||||
cur.execute("""
|
||||
INSERT INTO fp_sonst_ertraege
|
||||
(scenario_id, category, row_label, row_index, is_editable, is_sum_row,
|
||||
values, excel_row, sort_order)
|
||||
VALUES (%s, %s, %s, %s, true, false, %s, %s, %s)
|
||||
""", (scenario_id, cat_name, label, r, Json(values), r, count + 1))
|
||||
count += 1
|
||||
# Total row (29)
|
||||
values = read_monthly_values(ws, 29)
|
||||
cur.execute("""
|
||||
INSERT INTO fp_sonst_ertraege
|
||||
(scenario_id, category, row_label, row_index, is_editable, is_sum_row,
|
||||
values, excel_row, sort_order)
|
||||
VALUES (%s, %s, %s, %s, false, true, %s, %s, %s)
|
||||
""", (scenario_id, 'GESAMT', 'GESAMTUMSATZ', 29, Json(values), 29, count + 1))
|
||||
print(f" -> {count + 1} Ertragsposten importiert")
|
||||
|
||||
|
||||
def import_liquiditaet(cur, ws, scenario_id):
|
||||
"""Import Liquiditaet (rows 4-27)."""
|
||||
print(" Importing Liquiditaet...")
|
||||
rows_config = [
|
||||
(4, 'Umsatzerloese', 'einzahlung', False, '=Umsatzerloese!GESAMT'),
|
||||
(5, 'Sonst. betriebl. Ertraege', 'einzahlung', False, '=Sonst.Ertraege!GESAMT'),
|
||||
(6, 'Anzahlungen', 'einzahlung', True, None),
|
||||
(7, 'Neuer Eigenkapitalzugang', 'einzahlung', True, None),
|
||||
(8, 'Erhaltenes Fremdkapital', 'einzahlung', True, None),
|
||||
(9, 'Summe EINZAHLUNGEN', 'einzahlung', False, '=SUM(4:8)'),
|
||||
(12, 'Materialaufwand', 'auszahlung', False, '=Materialaufwand!SUMME'),
|
||||
(13, 'Personalkosten', 'auszahlung', False, '=Personalkosten!Total'),
|
||||
(14, 'Sonstige Kosten', 'auszahlung', False, '=Betriebliche!Summe_sonstige'),
|
||||
(15, 'Kreditrueckzahlungen', 'auszahlung', True, None),
|
||||
(16, 'Umsatzsteuer', 'auszahlung', True, None),
|
||||
(17, 'Gewerbesteuer', 'auszahlung', True, None),
|
||||
(18, 'Koerperschaftsteuer', 'auszahlung', True, None),
|
||||
(19, 'Summe AUSZAHLUNGEN', 'auszahlung', False, '=SUM(12:18)'),
|
||||
(21, 'UEBERSCHUSS VOR INVESTITIONEN', 'ueberschuss', False, '=Einzahlungen-Auszahlungen'),
|
||||
(22, 'Investitionen', 'ueberschuss', False, '=Investitionen!Gesamt'),
|
||||
(23, 'UEBERSCHUSS VOR ENTNAHMEN', 'ueberschuss', False, '=21-22'),
|
||||
(24, 'Kapitalentnahmen/Ausschuettungen', 'ueberschuss', True, None),
|
||||
(25, 'UEBERSCHUSS', 'ueberschuss', False, '=23-24'),
|
||||
(26, 'Kontostand zu Beginn des Monats', 'kontostand', False, '=prev_month_liquiditaet'),
|
||||
(27, 'LIQUIDITAET', 'kontostand', False, '=26+25'),
|
||||
]
|
||||
count = 0
|
||||
for row, label, row_type, editable, formula in rows_config:
|
||||
values = read_monthly_values(ws, row)
|
||||
cur.execute("""
|
||||
INSERT INTO fp_liquiditaet
|
||||
(scenario_id, row_label, row_type, is_editable, formula_desc,
|
||||
values, excel_row, sort_order)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (scenario_id, label, row_type, editable, formula,
|
||||
Json(values), row, count + 1))
|
||||
count += 1
|
||||
print(f" -> {count} Liquiditaetszeilen importiert")
|
||||
|
||||
|
||||
def import_kunden(cur, ws, scenario_id):
|
||||
"""Import Kunden (6 segments, rows 26-167)."""
|
||||
print(" Importing Kunden...")
|
||||
|
||||
# Summary rows (4-23) = aggregation across segments
|
||||
for i in range(20):
|
||||
row = 4 + i
|
||||
label = safe_str(ws.cell(row, 1).value)
|
||||
if not label:
|
||||
label = f'Produkt {i + 1}'
|
||||
values = read_monthly_values(ws, row)
|
||||
cur.execute("""
|
||||
INSERT INTO fp_kunden_summary
|
||||
(scenario_id, row_label, row_index, values, excel_row, sort_order)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""", (scenario_id, label, row, Json(values), row, i + 1))
|
||||
|
||||
# 6 segments, each starts 24 rows apart: 26, 50, 74, 98, 122, 146
|
||||
segment_starts = [
|
||||
(26, 'Care (Privat)'),
|
||||
(50, 'Horse (Haendler)'),
|
||||
(74, 'Segment 3'),
|
||||
(98, 'Segment 4'),
|
||||
(122, 'Segment 5'),
|
||||
(146, 'Segment 6'),
|
||||
]
|
||||
|
||||
# Read segment names from Excel
|
||||
for idx, (start_row, default_name) in enumerate(segment_starts):
|
||||
seg_name = safe_str(ws.cell(start_row, 1).value) or default_name
|
||||
|
||||
# Each segment: row+2 = header, row+2..row+21 = module rows
|
||||
module_start = start_row + 2 # row 28, 52, 76, 100, 124, 148
|
||||
count = 0
|
||||
for m in range(20):
|
||||
row = module_start + m
|
||||
label = safe_str(ws.cell(row, 1).value)
|
||||
if not label:
|
||||
continue
|
||||
pct = safe_float(ws.cell(row, 2).value)
|
||||
pct_label = safe_str(ws.cell(row, 2).value)
|
||||
values = read_monthly_values(ws, row)
|
||||
|
||||
# First module per segment (m=0) is the base editable input
|
||||
is_base = (m == 0)
|
||||
formula_type = None
|
||||
if pct_label == 'gerechnet':
|
||||
formula_type = 'cumulative'
|
||||
elif pct > 0 and not is_base:
|
||||
formula_type = 'rounddown_pct'
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO fp_kunden
|
||||
(scenario_id, segment_name, segment_index, row_label, row_index,
|
||||
percentage, formula_type, is_editable, values, excel_row, sort_order)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (scenario_id, seg_name, idx + 1, label, row,
|
||||
pct if pct else None, formula_type, is_base,
|
||||
Json(values), row, count + 1))
|
||||
count += 1
|
||||
print(f" -> Kunden importiert (6 Segmente)")
|
||||
|
||||
|
||||
def import_umsatzerloese(cur, ws, scenario_id):
|
||||
"""Import Umsatzerloese (revenue, quantity, prices)."""
|
||||
print(" Importing Umsatzerloese...")
|
||||
count = 0
|
||||
|
||||
# Revenue rows (3-23): computed = quantity * price
|
||||
for i in range(21):
|
||||
row = 3 + i
|
||||
label = safe_str(ws.cell(row, 1).value) or f'Produkt {i+1}'
|
||||
if not label.strip():
|
||||
continue
|
||||
values = read_monthly_values(ws, row)
|
||||
cur.execute("""
|
||||
INSERT INTO fp_umsatzerloese
|
||||
(scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order)
|
||||
VALUES (%s, 'revenue', %s, %s, false, %s, %s, %s)
|
||||
""", (scenario_id, label, row, Json(values), row, count + 1))
|
||||
count += 1
|
||||
|
||||
# Total row (24)
|
||||
values = read_monthly_values(ws, 24)
|
||||
cur.execute("""
|
||||
INSERT INTO fp_umsatzerloese
|
||||
(scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order)
|
||||
VALUES (%s, 'revenue', 'GESAMTUMSATZ', 24, false, %s, 24, %s)
|
||||
""", (scenario_id, Json(values), count + 1))
|
||||
count += 1
|
||||
|
||||
# Quantity rows (27-46): from Kunden
|
||||
for i in range(20):
|
||||
row = 27 + i
|
||||
label = safe_str(ws.cell(row, 1).value) or f'Produkt {i+1}'
|
||||
if not label.strip():
|
||||
continue
|
||||
values = read_monthly_values(ws, row)
|
||||
cur.execute("""
|
||||
INSERT INTO fp_umsatzerloese
|
||||
(scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order)
|
||||
VALUES (%s, 'quantity', %s, %s, false, %s, %s, %s)
|
||||
""", (scenario_id, label, row, Json(values), row, count + 1))
|
||||
count += 1
|
||||
|
||||
# Price rows (49-73): editable VK prices
|
||||
for i in range(25):
|
||||
row = 49 + i
|
||||
label = safe_str(ws.cell(row, 1).value) or f'Produkt {i+1}'
|
||||
if not label.strip():
|
||||
continue
|
||||
values = read_monthly_values(ws, row)
|
||||
cur.execute("""
|
||||
INSERT INTO fp_umsatzerloese
|
||||
(scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order)
|
||||
VALUES (%s, 'price', %s, %s, true, %s, %s, %s)
|
||||
""", (scenario_id, label, row, Json(values), row, count + 1))
|
||||
count += 1
|
||||
print(f" -> {count} Umsatzzeilen importiert")
|
||||
|
||||
|
||||
def import_materialaufwand(cur, ws, scenario_id):
|
||||
"""Import Materialaufwand (simplified: only Mac Mini + Mac Studio)."""
|
||||
print(" Importing Materialaufwand...")
|
||||
count = 0
|
||||
|
||||
# Cost rows (3-23): computed = quantity * unit_cost
|
||||
for i in range(21):
|
||||
row = 3 + i
|
||||
label = safe_str(ws.cell(row, 1).value)
|
||||
if not label:
|
||||
continue
|
||||
values = read_monthly_values(ws, row)
|
||||
cur.execute("""
|
||||
INSERT INTO fp_materialaufwand
|
||||
(scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order)
|
||||
VALUES (%s, 'cost', %s, %s, false, %s, %s, %s)
|
||||
""", (scenario_id, label, row, Json(values), row, count + 1))
|
||||
count += 1
|
||||
|
||||
# Total (24)
|
||||
values = read_monthly_values(ws, 24)
|
||||
cur.execute("""
|
||||
INSERT INTO fp_materialaufwand
|
||||
(scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order)
|
||||
VALUES (%s, 'cost', 'SUMME', 24, false, %s, 24, %s)
|
||||
""", (scenario_id, Json(values), count + 1))
|
||||
count += 1
|
||||
|
||||
# Unit cost rows (51-73): editable EK prices
|
||||
for i in range(23):
|
||||
row = 51 + i
|
||||
label = safe_str(ws.cell(row, 1).value)
|
||||
if not label:
|
||||
continue
|
||||
values = read_monthly_values(ws, row)
|
||||
cur.execute("""
|
||||
INSERT INTO fp_materialaufwand
|
||||
(scenario_id, section, row_label, row_index, is_editable, values, excel_row, sort_order)
|
||||
VALUES (%s, 'unit_cost', %s, %s, true, %s, %s, %s)
|
||||
""", (scenario_id, label, row, Json(values), row, count + 1))
|
||||
count += 1
|
||||
print(f" -> {count} Materialzeilen importiert (Mac Mini 3.200 / Mac Studio 13.000 EK)")
|
||||
|
||||
|
||||
def import_guv(cur, ws, scenario_id):
|
||||
"""Import GuV Jahresabschluss (annual summary)."""
|
||||
print(" Importing GuV Jahresabschluss...")
|
||||
# Annual columns: B=2026, C=2027, D=2028, E=2029, F=2030
|
||||
year_cols = {2: 'y2026', 3: 'y2027', 4: 'y2028', 5: 'y2029', 6: 'y2030'}
|
||||
|
||||
rows_config = [
|
||||
(3, 'Umsatzerloese', False, '=Umsatzerloese!Jahressumme'),
|
||||
(4, 'Bestandsveraenderungen', True, None),
|
||||
(5, 'Gesamtleistung', False, '=SUM(3:4)'),
|
||||
(9, 'Sonst. betriebl. Ertraege', False, '=Sonst.Ertraege!Jahressumme'),
|
||||
(10, 'Summe sonst. Ertraege', False, '=SUM(8:9)'),
|
||||
(13, 'Materialaufwand Waren', False, '=Materialaufwand!Jahressumme'),
|
||||
(14, 'Materialaufwand Leistungen', False, '=Materialaufwand!bezogene_Leistungen'),
|
||||
(15, 'Summe Materialaufwand', False, '=13+14'),
|
||||
(17, 'Rohergebnis', False, '=5+10-15'),
|
||||
(20, 'Loehne und Gehaelter', False, '=Personalkosten!Brutto'),
|
||||
(21, 'Soziale Abgaben', False, '=Personalkosten!Sozial'),
|
||||
(22, 'Summe Personalaufwand', False, '=20+21'),
|
||||
(25, 'Abschreibungen', False, '=Investitionen!AfA'),
|
||||
(27, 'Sonst. betriebl. Aufwendungen', False, '=Betriebliche!Summe_sonstige'),
|
||||
(29, 'EBIT', False, '=5+10-15-22-25-27'),
|
||||
(31, 'Zinsertraege', True, None),
|
||||
(33, 'Zinsaufwendungen', True, None),
|
||||
(35, 'Steuern gesamt', False, '=36+37'),
|
||||
(36, 'Koerperschaftssteuer', False, '=computed'),
|
||||
(37, 'Gewerbesteuer', False, '=computed'),
|
||||
(39, 'Ergebnis nach Steuern', False, '=29+31-33-35'),
|
||||
(41, 'Sonstige Steuern', True, None),
|
||||
(43, 'Jahresueberschuss', False, '=39-41'),
|
||||
]
|
||||
|
||||
count = 0
|
||||
for row, label, is_edit, formula in rows_config:
|
||||
values = {}
|
||||
for col, key in year_cols.items():
|
||||
v = ws.cell(row, col).value
|
||||
values[key] = round(float(v), 2) if v and not isinstance(v, str) else 0
|
||||
cur.execute("""
|
||||
INSERT INTO fp_guv
|
||||
(scenario_id, row_label, row_index, is_sum_row, formula_desc,
|
||||
values, excel_row, sort_order)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (scenario_id, label, row, formula is not None and not is_edit,
|
||||
formula, Json(values), row, count + 1))
|
||||
count += 1
|
||||
print(f" -> {count} GuV-Zeilen importiert")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 import-finanzplan.py <path-to-xlsm>")
|
||||
sys.exit(1)
|
||||
|
||||
xlsx_path = sys.argv[1]
|
||||
print(f"Opening: {xlsx_path}")
|
||||
wb = openpyxl.load_workbook(xlsx_path, data_only=True)
|
||||
|
||||
print(f"Connecting to: {DB_URL.split('@')[1] if '@' in DB_URL else DB_URL}")
|
||||
conn = psycopg2.connect(DB_URL)
|
||||
cur = conn.cursor()
|
||||
|
||||
# Schema already applied separately — skip if tables exist
|
||||
cur.execute("SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = 'fp_scenarios')")
|
||||
if not cur.fetchone()[0]:
|
||||
schema_path = os.path.join(os.path.dirname(__file__), '001_finanzplan_tables.sql')
|
||||
print(f"Applying schema: {schema_path}")
|
||||
with open(schema_path) as f:
|
||||
cur.execute(f.read())
|
||||
conn.commit()
|
||||
else:
|
||||
print("Schema already exists, skipping.")
|
||||
|
||||
# Get or create default scenario
|
||||
cur.execute("SELECT id FROM fp_scenarios WHERE is_default = true LIMIT 1")
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
scenario_id = row[0]
|
||||
# Clear existing data for re-import
|
||||
for table in ['fp_kunden', 'fp_kunden_summary', 'fp_umsatzerloese', 'fp_materialaufwand',
|
||||
'fp_personalkosten', 'fp_betriebliche_aufwendungen', 'fp_investitionen',
|
||||
'fp_sonst_ertraege', 'fp_liquiditaet', 'fp_guv']:
|
||||
cur.execute(f"DELETE FROM {table} WHERE scenario_id = %s", (scenario_id,))
|
||||
else:
|
||||
cur.execute("INSERT INTO fp_scenarios (name, is_default) VALUES ('Base Case', true) RETURNING id")
|
||||
scenario_id = cur.fetchone()[0]
|
||||
|
||||
print(f"Scenario ID: {scenario_id}")
|
||||
print(f"\nImporting sheets...")
|
||||
|
||||
# Import each sheet
|
||||
import_kunden(cur, wb['Kunden'], scenario_id)
|
||||
import_umsatzerloese(cur, wb['Umsatzerlöse'], scenario_id)
|
||||
import_materialaufwand(cur, wb['Materialaufwand'], scenario_id)
|
||||
import_personalkosten(cur, wb['Personalkosten'], scenario_id)
|
||||
import_betriebliche_aufwendungen(cur, wb['Betriebliche Aufwendungen'], scenario_id)
|
||||
import_investitionen(cur, wb['Investitionen'], scenario_id)
|
||||
import_sonst_ertraege(cur, wb['Sonst. betr. Erträge'], scenario_id)
|
||||
import_liquiditaet(cur, wb['Liquidität'], scenario_id)
|
||||
import_guv(cur, wb['GuV Jahresabschluss'], scenario_id)
|
||||
|
||||
conn.commit()
|
||||
print(f"\nImport abgeschlossen!")
|
||||
|
||||
# Summary
|
||||
for table in ['fp_kunden', 'fp_kunden_summary', 'fp_umsatzerloese', 'fp_materialaufwand',
|
||||
'fp_personalkosten', 'fp_betriebliche_aufwendungen', 'fp_investitionen',
|
||||
'fp_sonst_ertraege', 'fp_liquiditaet', 'fp_guv']:
|
||||
cur.execute(f"SELECT COUNT(*) FROM {table} WHERE scenario_id = %s", (scenario_id,))
|
||||
count = cur.fetchone()[0]
|
||||
print(f" {table}: {count} Zeilen")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user