Files
buchhaltung/Aktuelle planung.txt
T
2026-04-26 07:51:39 +02:00

4065 lines
85 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Die Frage ist, heben wir die den Scanner in die Umgebung oder Mappen wir einen Ordner in das Reposotory. Vielleicht machen wir es auch über die Ordnerstruktur. Manche Dokumente werden an verschiedenen Stellen benötigt, Wohngeld Brauche ich für die Übersicht aber auch für die Nebenkosten und die Steuer.
Um das Elster Formular besser vorzubereiten wäre eine Auswertung des letzten Dokumente hilfreich. Aber damit das vernünftig ausgewertet werden kann, wird OCR nicht reichen, auch hier wird KI benötigt. Mein Vorschlag, wir nutzen auch hier meine ollama Instanz und jagen die Daten mit falschem werten in die Modelle und dann korrigiert in unsere Applikation..was sagst du zu den vorgehen?
profile
kimi-k2.5:cloud
Nachgedacht für 11 Sekunden
Das ist ein exzellenter architektonischer Ansatz! Die Kombination aus ordner-basiertem Ingest, graphenartiger Dokumentenverwendung und Ollama für Kontext-verstärkte Extraktion löst genau die Probleme, die kommerzielle Tools haben (starre Workflows & Cloud-Abhängigkeit).
🏗️ Architektur-Vorschlag: "Smart Document Graph"
1. Scan-Strategie: Ordner-Mapping (Hot Folder)
Nicht den Scanner direkt integrieren, sondern einen Watchdog-Ordner:
yaml
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# docker-compose.yml Erweiterung
services:
scanner-watchdog:
image: python:3.11-slim
volumes:
- ./scan-inbox:/app/inbox:ro # Netzwerk-Scanordner (SMB/NFS)
- ./scan-processing:/app/processing # Temporär
- ./scan-archive:/app/archive # Dauerhaft (Original)
environment:
- OLLAMA_URL=http://ollama:11434
- DOCUMENT_API=http://app:3000/api/documents
command: python watchdog.py
ollama:
image: ollama/ollama:latest
container_name: buchhaltung_ollama
volumes:
- ollama_data:/root/.ollama
- ./ollama-modelfile:/modelfile # Für Custom Modelle
deploy:
resources:
limits:
memory: 8G # Wichtig für LLMs!
networks:
- app-network
Warum Ordner-Mapping?
Der Scanner speichert per SMB/FTP in ./scan-inbox
Mehrere Quellen: Scanner, E-Mail-Attachments (via IMAP), manueller Upload
Idempotenz: Datei-Hash prüfen, Duplikate vermeiden
2. Dokumenten-Modell: "Single Source of Truth" mit Multi-Context
Ein Dokument existiert physikalisch einmal, wird aber logisch mehrfach referenziert:
sql
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-- Dokumente (Single Source)
CREATE TABLE documents (
id UUID PRIMARY KEY,
file_path VARCHAR(500) UNIQUE, -- z.B. "archive/2024/scan_001.pdf"
file_hash VARCHAR(64), -- SHA256 für Duplikat-Erkennung
ocr_raw TEXT, -- Roh-OCR
extracted_json JSONB, -- Ollama-Extraktion
extraction_version INTEGER, -- Für Iterationen
created_at TIMESTAMP
);
-- Dokument-Context (Nutzung an verschiedenen Stellen)
CREATE TABLE document_usages (
id UUID PRIMARY KEY,
document_id UUID REFERENCES documents(id),
context_type VARCHAR(50), -- 'wohngeld', 'euer', 'steuer', 'privat'
context_id UUID, -- Foreign Key zur jeweiligen Tabelle
extracted_subset JSONB, -- Nur relevante Felder für diesen Context
confidence FLOAT,
verified BOOLEAN DEFAULT false,
corrected_data JSONB, -- User-Korrekturen (für Training)
created_at TIMESTAMP
);
-- Beispiel: Eine Wohngeldabrechnung
-- documents: Das gescannte PDF
-- document_usages:
-- 1. context='wohngeld', context_id=[mietverhaeltnis_id]
-- 2. context='euer', context_id=[geschaeftsjahr_id]
-- 3. context='steuer', context_id=[steuerjahr_id]
3. Ollama-Integration: "Falsch-Values" Workflow (Active Learning)
Dein Ansatz ist genial das nennt man "Human-in-the-Loop" mit Few-Shot Learning:
python
Einklappen
Ausführen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# ollama_processor.py
import ollama
import json
from typing import Dict, Optional
class DocumentIntelligence:
def __init__(self):
self.model = "mixtral" # oder "llama3", "mistral", "codellama"
# Für deutsche Finanzdokumente evtl. "dolphin-mixtral"
def extract_with_history(self, ocr_text: str, document_type: str,
previous_corrections: Optional[list] = None) -> Dict:
"""
Nutzt vorherige Korrekturen für bessere Extraktion
"""
# Few-Shot Beispiele aus corrected_data laden
examples = ""
if previous_corrections:
for corr in previous_corrections[-3:]: # Letzte 3 Korrekturen
examples += f"""
Beispiel:
Input: {corr['raw_text']}
Extrahiert: {json.dumps(corr['wrong_extraction'])}
Korrektur: {json.dumps(corr['corrected'])}
"""
prompt = f"""Du bist ein deutscher Steuerberater-Assistent.
Analysiere folgenden OCR-Text eines {document_type}-Dokuments.
{examples}
Extrahiere folgende Felder als JSON:
- betrag_gesamt (Zahl)
- datum (ISO 8601)
- kostenstelle (Kategorie: Immobilie, Hausmeister, Finanzierung, Versicherung)
- steuerlich_absetzbar (boolean)
- mwst_satz (7 oder 19 oder 0)
- zahlungsziel (Tage oder null)
WICHTIG: Wenn du unsicher bist, setze den Wert auf null oder "unbekannt".
OCR-Text:
{ocr_text}
Antwort nur als JSON:"""
response = ollama.generate(model=self.model, prompt=prompt)
try:
return json.loads(response['response'])
except:
return {"error": "parse_failed", "raw": response['response']}
def validate_against_history(self, current_doc: Dict,
historical_docs: list) -> Dict:
"""
Plausibilitätscheck: "Grundsteuer um 20% gestiegen?"
"""
if not historical_docs:
return {"warnings": []}
prompt = f"""Vergleiche dieses Dokument mit historischen Daten:
Aktuell: {json.dumps(current_doc)}
Historie (letzte 3): {json.dumps(historical_docs)}
Gib Warnungen aus als JSON-Array:
- auffaellige_abweichungen (z.B. "Kosten um 50% gestiegen")
- fehlende_felder (was fehlt vs. letztes Jahr)
- empfohlene_kostenstelle (basierend auf Ähnlichkeit)
Antwort als JSON:"""
response = ollama.generate(model=self.model, prompt=prompt)
return json.loads(response['response'])
4. Der Korrektur-Workflow (Feedback-Loop)
typescript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// Frontend-Komponente: "Smart Correction Interface"
interface DocumentCorrectionProps {
documentId: string;
initialExtraction: any; // Ollama's "falsche" Werte
documentType: string;
}
const SmartCorrection: React.FC<DocumentCorrectionProps> = ({
documentId,
initialExtraction
}) => {
const [values, setValues] = useState(initialExtraction);
const [isCorrecting, setIsCorrecting] = useState(false);
const handleCorrection = async (field: string, correctValue: any) => {
const oldValue = values[field];
// Speichere Korrektur für Ollama-Lernen
await fetch('/api/ml/feedback', {
method: 'POST',
body: JSON.stringify({
documentId,
field,
wrongValue: oldValue,
correctValue,
context: values, // Voller Kontext
ocrSnippet: getRelevantOCR(field) // Textausschnitt
})
});
// Aktualisiere UI
setValues({...values, [field]: correctValue});
};
// "Rot markiert" = Confidence < 0.7 oder null
const getFieldColor = (field: string) => {
const val = values[field];
if (val === null || val === "unbekannt") return "border-red-500 bg-red-50";
if (initialExtraction.confidence?.[field] < 0.7) return "border-yellow-400";
return "border-green-500";
};
return (
<div className="grid grid-cols-2 gap-4">
<div className="document-preview">
<img src={`/api/documents/${documentId}/preview`} />
{/* OCR-Highlighting beim Hover über Feld */}
</div>
<div className="extraction-form">
{Object.entries(values).map(([key, val]) => (
<div key={key} className={`field ${getFieldColor(key)}`}>
<label>{key}</label>
<input
value={val || ""}
onChange={(e) => handleCorrection(key, e.target.value)}
className={val === null ? "text-red-600 font-bold" : ""}
/>
{val === null && <span className="error-badge">Fehlt!</span>}
</div>
))}
<button onClick={() => propagateToContexts(documentId, values)}>
Für alle Kontexte übernehmen
</button>
</div>
</div>
);
};
5. Kontext-Sensitive Verwendung
Wenn du das Wohngeld-Dokument in die Steuer übernimmst:
python
Einklappen
Ausführen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def propagate_document_context(document_id: str, source_context: str, target_context: str):
"""
Überträgt Dokumentendaten zwischen Kontexten mit Ollama-Transformation
"""
doc = get_document(document_id)
source_data = doc['usages'][source_context]['extracted_subset']
# Ollama transformiert Wohngeld-Daten in Steuer-relevante Daten
prompt = f"""
Transformiere diese Nebenkostenabrechnungs-Daten in Steuerliche Kostenstellen (EÜR).
Input (Wohngeld):
{json.dumps(source_data)}
Regeln:
- Heizkosten → Heizkosten (Steuerlich absetzbar bei Vermietung)
- Grundsteuer → Grundsteuer
- Versicherungen → Versicherungskosten
- Hausmeister → Instandhaltungskosten (sofern nicht unterhaltend)
Output als JSON mit Feldern: konto_skr03, betrag, absetzbar_ja_nein, anmerkung
"""
transformation = ollama.generate(model='mixtral', prompt=prompt)
# Speichere in neuem Context
create_document_usage(
document_id=document_id,
context_type=target_context,
extracted_subset=json.loads(transformation),
inherited_from=source_context
)
6. Elster-Vorbereitung mit "Memory"
Für die Elster-Auswertung lädst du die letzten Dokumente in den Kontext:
python
Einklappen
Ausführen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def prepare_elster_summary(year: int):
# Hole alle Steuer-relevanten Dokumente des Jahres
docs = get_documents_by_context('steuer', year)
# Gruppiere nach Kategorien
summary_prompt = f"""
Basierend auf folgenden Dokumenten des Jahres {year}, erstelle eine Elster-Vorlage.
Dokumente:
{json.dumps([d['extracted_subset'] for d in docs])}
Erstelle:
1. EÜR-Vorlage (Einnahmen-Überschuss-Rechnung)
2. Anlage AUS (Ausländische Einkünfte) falls relevant
3. Anlage EÜR (Betriebseinnahmen/-ausgaben)
Markiere Unstimmigkeiten und fehlende Belege rot.
"""
return ollama.generate(model='mixtral', prompt=summary_prompt)
📁 Ordner-Struktur Vorschlag
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
./data
├── /scan-inbox # Temporär (Watchdog verarbeitet)
├── /scan-processing # Aktive OCR-Verarbeitung
├── /archive # Original-PDFs (Jahres-Struktur)
│ ├── 2023/
│ ├── 2024/
│ └── ...
├── /extracted # JSON-Exports (Backup)
│ ├── ollama-raw/ # Roh-Extraktionen
│ └── corrected/ # Korrigierte Versionen
└── /training-data # Für Ollama-Fine-Tuning
└── corrections.jsonl # Deine Korrekturen als Training
⚠️ Wichtige Hinweise
Ollama Performance:
Mixtral braucht mindestens 6-8GB RAM
Nutze OLLAMA_KEEP_ALIVE=24h damit das Modell im Speicher bleibt
Für schnelle Tests: llama3 (weniger präzise, aber schneller)
Feedback-Loop persistieren:
json
Einklappen
Kopieren
1
2
// corrections.jsonl Format für späteres Training
{"prompt": "OCR: Heizkstr 1.234,50\nFelder:", "completion": "{\"heizkosten\": 1234.50}"}
Versionierung:
Speichere jeden Extraktions-Versuch (extraction_version)
Ollama lernt daraus, welche Versionen besser waren
Backup-Strategie:
Dokumente sind unveränderlich (immutable)
Nur die extracted_json und document_usages ändern sich
Soll ich die Watchdog-Implementierung (Python-Skript für Ordner-Überwachung) oder das Ollama-Prompt-Engineering für deutsche Finanzdokumente detailliert ausarbeiten?
Dass olama läuft zwar lokal, aber es sind auch online Modelle dahinter. Lass uns daher die Zahlen vorher durch Platzhalter oder falsche Werte ersetzen, genau wie Namen und Anschrift. Die url ist ollama.lan:11434
Durch die Namensauflösung kann diese URL auch in anderen Umgebungen oder beim Verschieben des Container genutzt werden. Als Modell nutze ich aktuell überwiegend kimi2.5
Wenn wir ein lokales Modell benötigen kann favorisiere ich aktuell glm-4.7
profile
kimi-k2.5:cloud
Nachgedacht für 7 Sekunden
Ausgezeichnete Security-Herangehensweise! Ein Privacy Gateway mit Pre/Post-Processing ist genau der richtige Ansatz für sensible Finanzdaten. Hier ist die Architektur für eine anonymisierte KI-Verarbeitung:
🔒 Privacy-First Architektur
Einklappen
Speichern
Kopieren
1
2
3
4
Scan → [Anonymisierungs-Engine] → Ollama (kimi2.5/glm-4.7) → [Re-Identifizierung] → DB
↓ ↓
Mapping-Tabelle Anonymisierte
(Redis/Postgres) Extraktion
1. Anonymisierungs-Engine (Pre-Processing)
Wir ersetzen Entities konsistent (über das gesamte Dokument gleiche Fake-Werte):
python
Einklappen
Ausführen
Speichern
Kopieren
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# privacy_guard.py
mapping[placeholder] = {"type": "name", "original": name, "hash": self._hash(name)}
self._store_mapping(placeholder, name)
# 2. Adressen (Straßen, PLZ, Orte)
address_pattern = r'([A-Z][a-zäöüß]+(?:straße|str\.|weg|platz)\s+\d+\s*,?\s*\d{5}\s+[A-Z][a-zäöüß]+)'
addresses = re.findall(address_pattern, text, re.IGNORECASE)
for i, addr in enumerate(set(addresses)):
placeholder = f"[ADRESSE_{i}]"
text = text.replace(addr, placeholder)
mapping[placeholder] = {"type": "address", "original": addr}
self._store_mapping(placeholder, addr)
# 3. IBANs (DE\d{20})
iban_pattern = r'(DE\d{2}\s?(?:\d{4}\s?){4}\d{2})'
ibans = re.findall(iban_pattern, text)
for i, iban in enumerate(set(ibans)):
fake_iban = f"DE99{str(i).zfill(2)}99999999999999" # Fake aber valide Struktur
text = text.replace(iban, fake_iban)
mapping[fake_iban] = {"type": "iban", "original": iban.replace(" ", "")}
self._store_mapping(fake_iban, iban)
# 4. Zahlen (Beträge) - Konsistente Pseudonymisierung
# Wichtig: Struktur erhalten (Dezimalstellen, Trennzeichen)
amount_pattern = r'(\d{1,3}(?:\.\d{3})*,\d{2})' # deutsches Format
def replace_amount(match):
original = match.group(1)
# Deterministische Pseudonymisierung (gleicher Betrag = gleicher Fake)
fake_amount = self._pseudo_number(original)
mapping[fake_amount] = {"type": "amount", "original": original}
self._store_mapping(fake_amount, original)
return fake_amount
text = re.sub(amount_pattern, replace_amount, text)
# 5. Datum - Verschiebung um fixe Tage (z.B. +100 Tage)
date_pattern = r'(\d{2}\.\d{2}\.\d{4})'
dates = re.findall(date_pattern, text)
for date in set(dates):
fake_date = self._shift_date(date, days=100)
text = text.replace(date, fake_date)
mapping[fake_date] = {"type": "date", "original": date}
self._store_mapping(fake_date, date)
return text, mapping
def _pseudo_number(self, original: str) -> str:
"""Deterministische Fake-Zahl generieren"""
# Hash-basiert, damit gleiche Beträge gleiche Fakes ergeben
h = hashlib.md5(original.encode()).hexdigest()
# Erste 4 Ziffern als Seed
num = int(h[:4], 16)
# Realistischer Betrag zwischen 10,00 und 9999,99
fake = (num % 9990) + 10
return f"{fake},00" # oder mit originaler Dezimalstelle
def _shift_date(self, date_str: str, days: int) -> str:
"""Datum verschieben um X Tage"""
from datetime import datetime, timedelta
d = datetime.strptime(date_str, "%d.%m.%Y")
d += timedelta(days=days)
return d.strftime("%d.%m.%Y")
def _hash(self, value: str) -> str:
return hashlib.sha256(value.encode()).hexdigest()[:8]
def _store_mapping(self, fake: str, original: str):
"""Speichere Mapping für 24h (oder persistenter)"""
self.redis.hset(self.session_id, fake, original)
self.redis.expire(self.session_id, 86400) # 24h TTL
2. Ollama-Client mit Fallback
python
Einklappen
Ausführen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# ollama_client.py
import ollama
import requests
from typing import Optional
class SecureOllamaClient:
def __init__(self, base_url: str = "http://ollama.lan:11434"):
self.base_url = base_url
self.primary_model = "kimi2.5" # Externes Modell
self.fallback_model = "glm-4.7" # Lokales Fallback
def extract_financial_data(self, anonymized_text: str, doc_type: str):
"""
Versucht zuerst kimi2.5, fallback zu glm-4.7 bei Fehler
"""
prompt = self._build_prompt(anonymized_text, doc_type)
# Versuch 1: Online-Modell (kimi2.5)
try:
response = self._generate(
model=self.primary_model,
prompt=prompt,
timeout=30 # Angenommen extern schneller
)
return {
"data": self._parse_json(response),
"model_used": self.primary_model,
"anonymized": True
}
except Exception as e:
print(f"Primary model failed: {e}, trying fallback...")
# Versuch 2: Lokales Modell (glm-4.7)
try:
response = self._generate(
model=self.fallback_model,
prompt=prompt,
timeout=120 # Lokal langsamer
)
return {
"data": self._parse_json(response),
"model_used": self.fallback_model,
"anonymized": True
}
except Exception as e:
raise Exception(f"Both models failed: {e}")
def _build_prompt(self, text: str, doc_type: str) -> str:
return f"""Analysiere dieses anonymisierte deutsche Finanzdokument ({doc_type}).
WICHTIG: Alle Zahlen sind pseudonymisiert (nicht real).
Extrahiere die STRUKTUR und KATEGORIEN, ignoriere die konkreten Werte.
Gesucht:
- betrag_gesamt: [NUMMER_FELD]
- datum: [DATUM_FELD]
- kostenstelle: Eine von [Immobilie, Hausmeister, Finanzierung, Versicherung, Steuer, Sonstiges]
- steuerlich_absetzbar: [ja/nein/unbekannt]
- mwst_satz: [7/19/0/unbekannt]
- zahlungsziel: [Tage oder unbekannt]
Dokument:
{text}
Antwort NUR als JSON ohne Erklärungen."""
def _generate(self, model: str, prompt: str, timeout: int):
return ollama.generate(
model=model,
prompt=prompt,
options={"temperature": 0.1}, # Wenig Kreativität für Extraktion
host=self.base_url
)
def _parse_json(self, response):
import json
try:
return json.loads(response['response'])
except:
# Versuch JSON aus Text zu extrahieren
text = response['response']
start = text.find('{')
end = text.rfind('}') + 1
return json.loads(text[start:end])
3. Re-Identifizierung (Post-Processing)
python
Einklappen
Ausführen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# re_identify.py
import redis
class ReIdentifier:
def __init__(self):
self.redis = redis.Redis(host='redis', port=6379, db=1)
def restore(self, doc_id: str, extracted_data: dict) -> dict:
"""
Ersetzt Platzhalter zurück zu Originalwerten
"""
mapping_key = f"privacy:{doc_id}"
mapping = self.redis.hgetall(mapping_key)
if not mapping:
raise Exception(f"No mapping found for {doc_id}")
# Decode bytes zu str
mapping = {k.decode(): v.decode() for k, v in mapping.items()}
restored = {}
for key, value in extracted_data.items():
if isinstance(value, str):
# Ersetze alle Vorkommen von Platzhaltern
for fake, original in mapping.items():
value = value.replace(fake, original)
restored[key] = value
else:
restored[key] = value
return restored
4. Docker-Compose Integration
yaml
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
version: '3.8'
services:
app:
build: .
environment:
- OLLAMA_URL=http://ollama.lan:11434
- REDIS_URL=redis://redis:6379
- PRIVACY_MODE=strict # strict|relaxed|off
networks:
- frontend
- backend
- ollama-network # Externes Netz für ollama.lan
privacy-processor:
image: python:3.11-slim
volumes:
- ./privacy_guard.py:/app/privacy_guard.py
- ./ollama_client.py:/app/ollama_client.py
environment:
- OLLAMA_HOST=ollama.lan:11434
networks:
- backend
- ollama-network
command: python -u /app/worker.py
redis:
image: redis:7-alpine
networks:
- backend
volumes:
- redis_privacy:/data
# Optional: Lokaler Ollama als Fallback
ollama-local:
image: ollama/ollama:latest
container_name: ollama-local
volumes:
- ollama_data:/root/.ollama
environment:
- OLLAMA_KEEP_ALIVE=24h
networks:
- backend
profiles: ["local-ai"] # Nur bei Bedarf starten
networks:
ollama-network:
external: true # Bereich existierendes Netzwerk wo ollama.lan auflösbar ist
name: ollama-net
frontend:
driver: bridge
backend:
driver: bridge
volumes:
redis_privacy:
ollama_data:
5. Workflow-Integration
python
Einklappen
Ausführen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# document_pipeline.py
class SecureDocumentPipeline:
def __init__(self):
self.privacy = PrivacyGuard()
self.ollama = SecureOllamaClient("http://ollama.lan:11434")
self.restorer = ReIdentifier()
def process(self, file_path: str, doc_id: str) -> dict:
# 1. OCR
raw_text = self.ocr(file_path)
# 2. Anonymisieren
anon_text, mapping = self.privacy.anonymize(raw_text, doc_id)
# 3. KI-Verarbeitung (anonym)
result = self.ollama.extract_financial_data(anon_text, "rechnung")
# 4. Re-Identifizierung
real_data = self.restorer.restore(doc_id, result['data'])
# 5. Speichern mit Verweis auf Mapping (für Audit)
return {
"extracted": real_data,
"model": result['model_used'],
"privacy_hash": self.privacy._hash(raw_text), # Für Nachweis
"anonymized_preview": anon_text[:200] + "..." # Debug
}
🔐 Security-Best Practices
Mapping-Löschung: Nach 24h automatisch löschen (TTL in Redis)
Audit-Log: Speichere nur Hashes der Originaldaten, nie die Mapping-Tabelle langfristig
Zugangskontrolle: privacy: Namespace in Redis separat absichern
Fallback-Logging: Wenn glm-4.7 genutzt wird (lokal), logge dies für Compliance (keine Datenlecks)
⚠️ Hinweis zu kimi2.5
Falls kimi2.5 das Kimi K2.5 von Moonshot AI ist (chinesisches Modell), beachte:
Unterstützt möglicherweise nicht das Ollama-API-Format nativ
Dann benötigst du einen Adapter (OpenAI-kompatibles Format):
python
Einklappen
Ausführen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
def _generate_kimi(self, prompt: str):
# Falls kimi2.5 über OpenAI-API läuft
import openai
client = openai.OpenAI(
base_url="http://ollama.lan:11434/v1", # Ollama kompatibel
api_key="dummy"
)
return client.chat.completions.create(
model="kimi2.5",
messages=[{"role": "user", "content": prompt}]
)
Soll ich den Regex-Classifier für deutsche Finanzdokumente (spezifisch für Wohngeld/Steuer) verfeinern oder die Redis-Mapping-Strategie für langfristige Korrektur-Workflows optimieren?
Ich kann aus Elster glaube ich keine Screenshots erstellen. Aus Sicherheitsgründen ist das gesperrt. Es gibt aber online bestimmt Lösungen für Probleme der Art. Beides klingt total sinnvoll
profile
kimi-k2.5:cloud
Nachgedacht für 50 Sekunden
Du hast recht Elster hat einen Kopierschutz (DRM/Overlay), der Screenshots blockiert. Aber wir können trotzdem an die Daten und das Layout kommen, und ja: Wir sollten beide Ansätze kombinieren (Regex-Classifier + Redis-Mapping). Hier ist die pragmatische Lösung:
📄 Elster-Daten ohne Screenshots
1. Mein ELSTER → PDF-Export (funktioniert immer)
Einloggen → "Meine Steuerberechnung" → "Als PDF speichern" (nicht drucken, speichern!)
Diese PDFs hast du ohne DRM und kannst sie verarbeiten
Oder: "Steuerliche Feststellung" → XML-Export (für Entwickler optimal)
2. BMF Musterformulare (für das UI-Design)
Das Bundesfinanzministerium stellt leere Musterformulare bereit:
https://www.formulare-bfinv.de/
Dort findest du alle Anlagen (EÜR, AUS, EKS) als PDF ohne Schutz
Perfekt als Vorlage für dein Interface
3. DATEV-Export (falls du einen Steuerberater hast)
Falls du schon einen Steuerberater nutzt: DATEV-XML oder CSV-Export der vorherigen Jahre → Import in deine App als "Musterdaten"
🔧 Kombinierte Architektur: "Smart Classifier + Persistent Mapping"
Wir verbinden beide Welten:
python
Einklappen
Ausführen
Speichern
Kopieren
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# combined_processor.py
prompt=prompt,
host="http://ollama.lan:11434",
options={"temperature": 0.2}
)
return json.loads(response['response'])
except:
# Fallback zu glm-4.7
response = ollama.generate(
model="glm-4.7",
prompt=prompt,
host="http://ollama.lan:11434"
)
return json.loads(response['response'])
def _store_for_review(self, doc_id: str, result: Dict, original_text: str):
"""
Redis-Struktur für langfristiges Lernen:
- Hash: Korrektur-Vorschläge
- Stream: Audit-Log
- Sorted Set: Confidence-Score für Priorisierung
"""
pipe = self.redis.pipeline()
# 1. Vorschlag speichern (TTL 30 Tage)
pipe.hset(f"proposal:{doc_id}", mapping={
'json': json.dumps(result),
'timestamp': datetime.now().isoformat(),
'confidence': result.get('confidence', 0.5),
'status': 'pending_review'
})
pipe.expire(f"proposal:{doc_id}", 2592000) # 30 Tage
# 2. In Review-Queue (nach Confidence sortiert)
score = 1.0 - result.get('confidence', 0.5) # Niedrige Confidence = hohe Priorität
pipe.zadd("review_queue", {doc_id: score})
# 3. Muster speichern für ähnliche Dokumente (für zukünftiges Regex-Training)
doc_fingerprint = self._create_fingerprint(original_text)
pipe.set(f"fingerprint:{doc_fingerprint}", doc_id, ex=86400*365) # 1 Jahr
pipe.execute()
def confirm_correction(self, doc_id: str, human_corrections: Dict):
"""
Wird aufgerufen wenn User im UI korrigiert hat
"""
# 1. Originalen Vorschlag laden
original = self.redis.hget(f"proposal:{doc_id}", 'json')
if not original:
return
original_data = json.loads(original)
# 2. Diff berechnen
diff = self._calculate_diff(original_data, human_corrections)
# 3. Als Trainingsbeispiel speichern (für Few-Shot Prompting)
training_example = {
'text_snippet': human_corrections.get('_anonymized_text', ''),
'wrong': original_data,
'correct': human_corrections,
'diff': diff
}
# In Redis Stream (append-only log)
self.redis.xadd("corrections_stream", {
'data': json.dumps(training_example),
'doc_id': doc_id,
'timestamp': datetime.now().isoformat()
})
# 4. Status aktualisieren
self.redis.hset(f"proposal:{doc_id}", 'status', 'confirmed')
self.redis.zrem("review_queue", doc_id)
# 5. Regex-Patterns verbessern (wenn oft gleiche Korrektur)
self._update_patterns_if_trend(diff)
return {'status': 'learned', 'confidence_improvement': diff}
def _create_fingerprint(self, text: str) -> str:
"""Erstellt einen Hash für Dokumenten-Typ-Erkennung"""
# Normalisiere: Lowercase, nur Keywords, sortiert
words = re.findall(r'\b(Abrechnung|Steuer|Rechnung|GmbH|Heizkosten|Miete)\b', text.lower())
fingerprint = ' '.join(sorted(set(words)))
return hashlib.md5(fingerprint.encode()).hexdigest()[:16]
def _update_patterns_if_trend(self, diff: Dict):
"""
Wenn 5x dieselbe Korrektur passiert: Regex-Pattern anpassen
"""
# Implementierung: Zähler in Redis inkrementieren
# Bei Threshold: Neue Regex-Regel generieren oder bestehende Gewichten
pass
🎯 Praktische Umsetzung für Elster-ähnliche Formulare
Da du keine Screenshots machen kannst, nutzen wir Struktur-Templates:
typescript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ElsterFormular.tsx - Basierend auf BMF-Mustern
const ANLAGEN_CONFIG = {
'EÜR': {
sections: ['Betriebseinnahmen', 'Betriebsausgaben', 'Sonstige'],
fields: [
{ id: 'einkuenfte_4_8', label: 'Einkünfte §4 Abs. 8', type: 'currency' },
{ id: 'gesamtaufwand', label: 'Gesamtaufwand', type: 'currency', calculated: true }
]
},
'AUS': {
// Ausländische Einkünfte
}
};
// Die App erstellt das Formular basierend auf dieser Config
// Der User füllt es aus oder lädt PDFs hoch die automatisch gemappt werden
🔄 Workflow-Zusammenfassung
PDF Upload (aus Mein ELSTER Export) → OCR
Regex-Schicht: Extrahiert 80% der Felder (schnell, kostenlos, offline)
KI-Schicht: Nur für unklare Fälle (anonymisiert über ollama.lan:11434)
Redis-Queue: Zeigt dem User rot markierte Felder zur Korrektur
Lernen: Korrigierte Werte werden als Few-Shot Examples für kimi2.5/glm-4.7 gespeichert
Verbesserung: Nach 10 Korrekturen ähnlicher Art → neues Regex-Pattern automatisch vorschlagen
Soll ich den UI-Teil für die Review-Queue (rot markierte Felder mit "Häkchen"-System) oder das Template-System für die Elster-Anlagen (EÜR, AUS etc.) detailliert ausarbeiten?
Mach gleich beides. Das klingt nach einen guten Fortschritt.
profile
kimi-k2.5:cloud
Nachgedacht für 41 Sekunden
Perfekt! Hier ist das komplette Human-in-the-Loop Review-Interface plus das konfigurierbare Steuerformular-System:
1. 🔍 Review-Queue UI (React + TypeScript)
typescript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// types/review.ts
export interface DocumentField {
id: string;
key: string;
value: string | number | null;
originalValue: string | number | null;
confidence: number; // 0.0 - 1.0
source: 'regex' | 'ollama' | 'manual';
status: 'pending' | 'confirmed' | 'corrected' | 'missing';
fieldType: 'currency' | 'date' | 'text' | 'percentage' | 'tax_id';
validation?: {
required: boolean;
pattern?: RegExp;
min?: number;
max?: number;
};
}
export interface ReviewItem {
docId: string;
filename: string;
thumbnailUrl: string;
uploadedAt: string;
totalFields: number;
confirmedFields: number;
priority: number; // Höher = dringender (niedrige Confidence)
fields: DocumentField[];
}
tsx
Einklappen
Speichern
Kopieren
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// components/ReviewQueue.tsx
);
}
return (
<div className={`p-4 rounded-lg border-2 ${confidenceColor} transition-all`}>
<div className="flex justify-between items-start mb-2">
<label className="text-sm font-semibold text-gray-700 flex items-center gap-2">
{field.key}
{field.confidence < 0.5 && <AlertCircle className="w-4 h-4 text-red-500" />}
<span className="text-xs font-normal text-gray-500">(via {field.source})</span>
</label>
<div className="flex gap-1">
<button
onClick={() => onConfirm()}
className="p-1 hover:bg-green-200 rounded text-green-700"
title="Akzeptieren"
>
<Check className="w-5 h-5" />
</button>
<button
onClick={() => setIsEditing(true)}
className="p-1 hover:bg-blue-200 rounded text-blue-700"
title="Korrigieren"
>
<Eye className="w-5 h-5" />
</button>
<button
onClick={onSkip}
className="p-1 hover:bg-red-200 rounded text-red-700"
title="Überspringen"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{isEditing ? (
<div className="flex gap-2 mt-2">
<input
type={field.fieldType === 'currency' ? 'number' : 'text'}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
className="flex-1 border rounded px-3 py-2"
placeholder={field.key}
step={field.fieldType === 'currency' ? '0.01' : undefined}
/>
<button
onClick={() => { onConfirm(editValue); setIsEditing(false); }}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Speichern
</button>
</div>
) : (
<div className="flex justify-between items-center">
<div className={`text-lg ${!field.value ? 'text-red-600 italic' : 'text-gray-900'}`}>
{field.value ? formatValue(field.value, field.fieldType) : 'Fehlende Daten'}
</div>
<div className="text-xs text-gray-500">
Confidence: {(field.confidence * 100).toFixed(0)}%
</div>
</div>
)}
{/* Plausibilitätswarnungen */}
{field.validation && field.value && (
<>
{field.validation.min && Number(field.value) < field.validation.min && (
<div className="mt-2 text-xs text-orange-600">
⚠️ Wert unter Minimum ({field.validation.min})
</div>
)}
{field.validation.pattern && !field.validation.pattern.test(String(field.value)) && (
<div className="mt-2 text-xs text-red-600">
⚠️ Format ungültig
</div>
)}
</>
)}
</div>
);
};
2. 📋 Template-System für Elster-Anlagen
typescript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// config/taxForms.ts
export interface TaxFormField {
id: string;
label: string;
type: 'currency' | 'date' | 'text' | 'calculation' | 'selection';
elsterFieldId?: string; // z.B. "EUE-Einkuenfte-4-8"
section: string;
readonly?: boolean;
calculation?: string; // z.B. "fieldA + fieldB - fieldC"
validation?: {
required?: boolean;
min?: number;
max?: number;
pattern?: string;
};
options?: string[]; // Für selection-type
defaultValue?: any;
helpText?: string;
}
export interface TaxFormSection {
id: string;
title: string;
description?: string;
order: number;
fields: string[]; // Referenz auf Field-IDs
}
export interface TaxFormTemplate {
id: string;
name: string; // z.B. "EÜR 2024"
description: string;
anlageType: 'EUE' | 'AUS' | 'EKS' | 'KAP' | 'custom';
sections: TaxFormSection[];
fields: Record<string, TaxFormField>;
calculations: {
[key: string]: {
dependsOn: string[];
formula: (values: Record<string, number>) => number;
};
};
validationRules?: Array<{
condition: string;
message: string;
severity: 'error' | 'warning';
}>;
}
typescript
Einklappen
Speichern
Kopieren
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// templates/euer2024.ts
'miete': {
id: 'miete',
label: 'Miete und Pacht',
type: 'currency',
section: 'betriebsausgaben',
defaultValue: 0
},
'abschreibungen': {
id: 'abschreibungen',
label: 'Abschreibungen',
type: 'currency',
section: 'betriebsausgaben',
defaultValue: 0,
helpText: 'AfA auf bewegliche Wirtschaftsgüter'
},
'sonstige_ausgaben': {
id: 'sonstige_ausgaben',
label: 'Sonstige Betriebsausgaben',
type: 'currency',
section: 'betriebsausgaben',
defaultValue: 0
},
'gesamt_ausgaben': {
id: 'gesamt_ausgaben',
label: 'Gesamtausgaben',
type: 'calculation',
section: 'betriebsausgaben',
readonly: true,
calculation: 'wareneinsatz + lohnkosten + miete + abschreibungen + sonstige_ausgaben'
},
'reingewinn': {
id: 'reingewinn',
label: 'Reingewinn',
type: 'calculation',
section: 'ergebnis',
readonly: true,
calculation: 'gesamt_einnahmen - gesamt_ausgaben'
},
'verlust': {
id: 'verlust',
label: 'Verlust',
type: 'calculation',
section: 'ergebnis',
readonly: true,
// Nur anzeigen wenn negativ
calculation: 'gesamt_einnahmen - gesamt_ausgaben'
}
},
calculations: {
'gesamt_einnahmen': {
dependsOn: ['einnahmen_4_8', 'einnahmen_13_b', 'sonstige_einnahmen'],
formula: (vals) => (vals.einnahmen_4_8 || 0) + (vals.einnahmen_13_b || 0) + (vals.sonstige_einnahmen || 0)
},
'gesamt_ausgaben': {
dependsOn: ['wareneinsatz', 'lohnkosten', 'miete', 'abschreibungen', 'sonstige_ausgaben'],
formula: (vals) => Object.values(vals).reduce((a, b) => (a || 0) + (b || 0), 0)
},
'reingewinn': {
dependsOn: ['gesamt_einnahmen', 'gesamt_ausgaben'],
formula: (vals) => vals.gesamt_einnahmen - vals.gesamt_ausgaben
}
},
validationRules: [
{
condition: 'reingewinn < -100000',
message: 'Verlust über 100.000 € - bitte prüfen!',
severity: 'warning'
},
{
condition: 'gesamt_einnahmen < 0',
message: 'Einnahmen können nicht negativ sein',
severity: 'error'
},
{
condition: 'einnahmen_4_8 > 1000000',
message: 'Hohe Einnahmen - Splitting-Berechtigung prüfen',
severity: 'warning'
}
]
};
tsx
Einklappen
Speichern
Kopieren
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// components/TaxFormRenderer.tsx
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-center gap-2 mb-2 text-yellow-800 font-semibold">
<AlertTriangle className="w-5 h-5" />
Plausibilitätsprüfung
</div>
<ul className="list-disc list-inside text-sm text-yellow-700">
{validationErrors.map((err, idx) => (
<li key={idx}>{err}</li>
))}
</ul>
</div>
)}
</div>
<div className="flex gap-6">
{/* Navigation Sidebar */}
<div className="w-64 flex-shrink-0">
<div className="bg-white rounded-lg shadow-sm p-4">
{template.sections.map(section => (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
className={`w-full text-left px-4 py-3 rounded-lg mb-1 transition-colors ${
activeSection === section.id
? 'bg-blue-50 text-blue-700 font-semibold'
: 'hover:bg-gray-50 text-gray-700'
}`}
>
<div className="text-sm">{section.title}</div>
{section.description && (
<div className="text-xs text-gray-500 mt-1">{section.description}</div>
)}
</button>
))}
</div>
{/* Zusammenfassung */}
<div className="bg-white rounded-lg shadow-sm p-4 mt-4">
<h3 className="font-semibold text-gray-900 mb-3">Ergebnisvoransicht</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Einnahmen:</span>
<span className="font-mono text-green-700">{formatCurrency(calculated.gesamt_einnahmen || 0)}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Ausgaben:</span>
<span className="font-mono text-red-700">{formatCurrency(calculated.gesamt_ausgaben || 0)}</span>
</div>
<div className="border-t pt-2 flex justify-between font-bold">
<span>Ergebnis:</span>
<span className={`font-mono ${(calculated.reingewinn || 0) >= 0 ? 'text-green-700' : 'text-red-700'}`}>
{formatCurrency(calculated.reingewinn || 0)}
</span>
</div>
</div>
</div>
</div>
{/* Formular-Bereich */}
<div className="flex-1">
{template.sections.map(section => (
section.id === activeSection && (
<div key={section.id} className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-xl font-bold text-gray-900 mb-2">{section.title}</h2>
{section.description && (
<p className="text-gray-600 mb-6">{section.description}</p>
)}
<div>
{section.fields.map(fieldId => {
const field = template.fields[fieldId];
return field ? renderField(field) : null;
})}
</div>
</div>
)
))}
</div>
</div>
</div>
);
};
🔗 Integration beider Komponenten
tsx
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// App.tsx
import { ReviewQueue } from './components/ReviewQueue';
import { TaxFormRenderer } from './components/TaxFormRenderer';
import { EUeR2024Template } from './templates/euer2024';
export default function App() {
// Daten aus ReviewQueue fließen automatisch in TaxForm:
// Wenn User in Review "einnahmen_4_8" bestätigt,
// wird es in den initialData der EÜR übernommen
return (
<div className="min-h-screen bg-gray-100">
<nav className="bg-blue-800 text-white p-4">
<h1 className="text-xl font-bold">SteuerFlow Pro</h1>
</nav>
<div className="flex h-screen">
<div className="w-1/2 border-r border-gray-300 overflow-auto">
<ReviewQueue />
</div>
<div className="w-1/2 overflow-auto">
<TaxFormRenderer
template={EUeR2024Template}
onSave={(data, errors) => console.log('Saved:', data, errors)}
onExport={(format) => console.log('Export:', format)}
/>
</div>
</div>
</div>
);
}
Soll ich noch den Export-Generator (ELSTER-XML Format) oder die API-Endpoints für die Redis-Queue implementieren?
Wenn ich später solche Anfragen erhalten, wie teuer kann man so eine Applikation realistisch verkaufen?
profile
kimi-k2.5:cloud
Nachgedacht für 20 Sekunden
Das ist eine hochspezialisierte Business-Software mit Enterprise-Features. Hier ist eine realistische Markt- und Preisanalyse:
💰 Preismodelle (Endkunden)
A) SaaS-Modell (Multi-Tenant)
Zielgruppe: Selbstständige, Vermieter, kleine Hausverwaltungen
Tier
Preis/Monat
Features
Starter
19-29€
1-2 Objekte, Basis-Buchhaltung, keine KI
Pro
49-79€
10 Objekte, Ollama-Integration, EÜR, Nebenkosten
Business
149-199€
Unlimitierte Objekte, Multi-User, API, Priority-Support
Enterprise
499€+
Self-Hosted Option, White-Label, dedizierter Server
Vergleichsbasis:
SevDesk: 9-29€/Monat (nur Buchhaltung, keine Immobilien)
ImmoTool (Desktop): 399€ Einmalkauf + 99€/Jahr Update
FastBill: 11-44€/Monat (nur Gewerbe)
Dein USP: Kombination Immobilien + Gewerbe + Self-Hosted + Datenschutz (Ollama)
B) Self-Hosted Lizenzmodell (Empfohlen!)
Da Docker/On-Premise ein Hauptfeature ist:
Single License: 2.999€ Einmalzahlung + 599€/Jahr Support & Updates
Agentur-Lizenz: 9.999€ (für Steuerberater/Hausverwaltungen mit bis zu 50 Mandanten)
White-Label: 25.000€ + Revenue Share
Warum das funktioniert: Datenschutz-bewusste Kunden zahlen premium für On-Premise (keine Cloud).
🏗️ Entwicklungskosten (falls als Auftrag)
Wenn du eine Agentur beauftragen würdest:
Komponente
Stunden
Kosten (100€/h)
Backend (Node.js/Python, DB, API)
300-400h
30.000-40.000€
Frontend (React, Formulare, Dashboard)
250-350h
25.000-35.000€
OCR/KI-Integration (Ollama, Privacy-Layer)
150-200h
15.000-20.000€
Docker/DevOps/Security
100-150h
10.000-15.000€
Testing & Steuerrechtliche Validierung
100-150h
10.000-15.000€
Gesamt
900-1.250h
90.000-125.000€
Realistische Bandbreite für MV: 60.000-80.000€ (MVP mit reduziertem Feature-Set)
🎯 Wertargumente für den Preis
Warum Kunden 5.000-10.000€ zahlen (statt 20€/Monat für SevDesk):
Datenhoheit: Self-Hosted = keine Cloud-Sensiblen Steuerdaten bei US-Anbieter
Kombination: Immobilien-Nebenkosten + Gewerbe-Buchhaltung in einem Tool (sonst 2-3 Tools nötig)
Ollama-Integration: Keine laufenden KI-Kosten (OpenAI-API wäre teuer bei vielen Dokumenten)
Elster-Kompatibilität: Deutsche Steuerformulare out-of-the-box
Anonymisierung: Privacy-Guard für paranoid Datenschützer
⚠️ Haftungskosten (wichtig!)
Achtung: Steuerliche Beratungssoftware hat Haftungsrisiken!
Compliance-Check: 5.000-10.000€ (Steuerberater prüft Berechnungslogik)
Rechtsschutzversicherung: 2.000-5.000€/Jahr (für Software-Haftung)
Disclaimer & AGB: 1.000-2.000€ (Anwalt für IT-Recht)
Das rechtfertigt höhere Preise, da du Haftungsrisiko trägst.
📊 Preisempfehlung für dich
Wenn du verkaufen möchtest:
Freemium: 1 Objekt, 50 Dokumente/Jahr, kein Ollama → Lead-Generierung
Pro: 59€/Monat oder 599€/Jahr (spart 169€) → Für Vermieter mit 3-10 Wohnungen
Unlimited: 149€/Monat → Für Gewerbe mit Immobilienholdings
On-Premise: 4.999€ + 20% jährlich → Für Steuerberater/Kanzleien
Zusatz-Einnahmen:
Setup-Service: 500-1.500€ (Docker-Installation beim Kunden)
Schulungen: 200€/Stunde
Custom-Ollama-Modelle: 2.000€ (Feintuning auf spezifische Dokumente)
🚀 Marktpositionierung
Positioniere dich nicht als "billige Buchhaltungssoftware", sondern als:
"Die einzige DSGVO-konforme Immobilien- und Gewerbe-Buchhaltung mit lokaler KI für deutsche Steuerformulare."
Wettbewerbsvorteil, der den Preis rechtfertigt:
Konkurrenz (Lexware, WISO) = Cloud-only, altmodisches UI
Du = Modern, Self-Hosted, KI-gestützt, Privacy-by-Design
💡 Tipp: Value-Based Pricing
Rechne vor dem Kunden vor, was er spart:
Steuerberater für Nebenkostenabrechnung: 150-300€/Objekt/Jahr
Bei 5 Objekten: 750-1.500€/Jahr Kosten
Deine Software: 599€/Jahr → Einsparung 150-900€/Jahr
Plus Zeitersparnis durch Ollama-Automatisierung: 10-20h/Jahr à 50€ = 500-1.000€ Wert.
Argument: "Die Software amortisiert sich in 3-6 Monaten."
Soll ich ein Business-Plan-Template oder eine Investoren-Pitch-Struktur für diese App erstellen?
Und wie viel prozentual haben wir von der Software Stehen?
profile
kimi-k2.5:cloud
Nachgedacht für 20 Sekunden
Das kommt drauf an, was du meinst hier sind die drei wahrscheinlichsten Interpretationen:
1. 📊 Entwicklungsfortschritt (Architektur vs. Implementation)
Realistische Einschätzung: ~25-35% eines MVP (Minimum Viable Product)
Was wir konzeptuell haben (70%):
✅ Architektur & Docker-Setup
✅ Datenbank-Schema
✅ Privacy-Layer Konzept (Anonymisierung)
✅ UI/UX Design (React Components skizziert)
✅ OCR-Workflow mit Ollama
Was noch fehlt (65-75% der Arbeit):
❌ Produktions-Code (nicht nur Pseudocode)
❌ Fehlerbehandlung & Edge Cases (was wenn Ollama offline?)
❌ Steuerliche Validierung (testen ob Berechnungen wirklich korrekt)
❌ Import/Export (DATEV-CSV, ELSTER-XML)
❌ Multi-User & Berechtigungen
❌ Backup & Update-Mechanismus für Docker
❌ Mobile Responsiveness
❌ Onboarding/Dokumentation
Fazit: Die schwere Architekturarbeit ist gemacht, aber die "letzten 20%" (Polish, Testing, Steuer-Compliance) sind oft 50% der Zeit.
2. 💻 Code-Status (tatsächlich geschriebene Zeilen)
~10-15% Wir haben bisher hauptsächlich Pseudocode, Schemas und Konzepte besprochen.
Für ein release-fähiges Produkt fehlen noch:
~15.000 Zeilen TypeScript/React
~8.000 Zeilen Backend-API
~2.000 Zeilen Tests
Docker-Compose Production-Ready
Error Handling & Logging
3. 💰 Gewinnmarge (wenn du verkaufst)
Bei Self-Hosted (4.999€):
Entwicklungskosten: ~80.000€
Du müsstest ~16 Lizenzen verkaufen, um break-even zu sein
Danach: ~80-90% Marge (nur Support-Kosten)
Bei SaaS (59€/Monat):
Serverkosten: ~5€/User/Monat (Strom, Storage)
Support-Aufwand: ~20€/User/Monat (zeitanteilig)
Netto-Marge: ~60% (34€/Monat Gewinn pro User)
Welche Prozentzahl meinst du genau?
Wenn du "wie weit sind wir" meinst: ~30% der Planung ist fertig, aber nur ~10% des Codes existiert wirklich
Wenn du "wie viel Gewinn bleibt hängen" meinst: 60-90% je nach Geschäftsmodell
Sollen wir priorisieren, welche 20% Features du als erstes MVP bauen solltest, um schnellsten Umsatz zu machen?
Ich meinte tatsächlich den Fortschritt des Projektes. Und so hat es sich auch agefühlt. Viel ist noch nicht. Aber es ist echt weniger Arbeit mit einer ki im Rücken
profile
kimi-k2.5:cloud
Nachgedacht für 13 Sekunden
Genau das ist der Klassische Architektur-Trap:
Die ersten 30% fühlen sich mit KI (GitHub Copilot, ChatGPT, Cursor) wie 70% an, weil du in 2 Wochen mehr Struktur hast als früher in 2 Monaten. Aber die letzten 20% (Testing, Edge Cases, Steuer-Compliance) fressen trotzdem 80% der Zeit da hilft KI weniger, weil sie spezifische deutsche Steuergesetze oft falsch interpretiert.
🚀 Realistischer Fahrplan MIT KI-Unterstützung
Zeitersparnis durch KI: ca. 40-50% bei Coding, aber nur 10-20% bei Steuerrecht/Testing
Phase 1: "Der schnelle Win" (Wochen 1-4)
Ziel: Einfacher Upload → OCR → manuelle Korrektur → CSV-Export
Docker-Setup mit Ollama (du hast das schon konzeptuell)
Basis-Upload + Tesseract-OCR (ohne komplexe Anonymisierung erstmal)
Simple Tabelle statt fancy UI
Nutzen: Du kannst schon deine eigenen Dokumente verarbeiten
KI-Boost: Copilot schreibt die API-Endpoints und React-Tables fast allein.
Phase 2: "Der Privacy-Layer" (Wochen 5-8)
Anonymisierung vor Ollama
Redis-Queue für Review-Prozess
Hürde: Testen, dass Zahlen korrekt ersetzt und zurückübersetzt werden
KI-Boost: Weniger hilfreich Regex-Testing braucht Präzision, keine Halluzination.
Phase 3: "Das Steuer-Monster" (Wochen 9-16) ⚠️
Hier wird's teuer (zeitlich):
EÜR-Berechnungslogik
Plausibilitätsprüfungen (Kann Verlust >50% des Umsatzes sein?)
DATEV-Export-Format (muss 100% korrekt sein)
KI-Warnung: ChatGPT wird dir falsche Steuersätze oder veraltete Regeln vorschlagen. Hier brauchst du einen Steuerberater als Sparringspartner (Kosten: 1.500-3.000€), sonst haftest du später.
Phase 4: "Production Polish" (Wochen 17-20)
Backup-Strategien für Docker-Volumes
Update-Mechanismus (wie kommen neue Ollama-Modelle zum Nutzer?)
Multi-User & Berechtigungen
🎯 KI-Optimale Strategie für dich
Was du JETZT mit KI machst (80% Ertrag):
Scaffold: Lass Copilot/Cursor das komplette Backend-CRUD generieren
UI-Komponenten: Alle Formularfelder, Tables, Buttons von KI bauen lassen
OCR-Preprocessing: Die Regex-Patterns für deutsche Rechnungen
Was du OHNE KI/Experten machen musst (20% Aufwand, 80% Risiko):
Steuerliche Logik: EÜR-Formeln von Hand prüfen (oder Steuerberater)
Datenbank-Constraints: Welche Felder dürfen nie NULL sein?
Security: Die Anonymisierung wirklich wasserdicht testen (Penetration-Testing)
💡 Tipp: "MVP auf Diät"
Bau erstmal nur einen Workflow komplett fertig:
Nur Wohngeldabrechnungen (keine EÜR, keine Gewerbe-Buchhaltung)
Nur PDF-Upload → Extraktion → CSV-Export
Kein schickes Review-UI erstmal, sondern einfach JSON-Editor
Warum? Weil du dann in 6-8 Wochen (mit KI: 3-4 Wochen) etwas Verkaufbares hast, statt in 6 Monaten ein halbfertiges Monster.
Wenn das läuft:
Du hast Einnahmen für die weitere Entwicklung
Du lernst aus echten Nutzer-Feedback, welche Features wirklich wichtig sind (vielleicht wollen alle nur den DATEV-Export und niemand braucht das Elster-UI)
Soll ich dir einen "Sprint 0" Plan für die nächsten 2 Wochen machen, um das erste funktionierende OCR-Upload-Demo zu haben? Dann siehst du sofort, wie gut dein Ollama-Setup wirklich funktioniert.
Das Problem ist ja auch Steuer Themen zu testen. Überhaupt so eine Software zu testen. Egal, die Entwickeln wir eh nur für mich, und gerade zu Beginn werde ich ganz genau prüfen was da passiert. Langfristig nimmt mir diese App dann aber einen Haufen Buchhaltung ab.
profile
kimi-k2.5:cloud
Nachgedacht für 13 Sekunden
Perfekt! Das ist der "Eat Your Own Dog Food" Ansatz und der ist für solche Software goldrichtig. Du bist gleichzeitig Entwickler, Product Owner und strengster Endnutzer. Das ändert die Strategie komplett:
🎯 Dogfooding-Strategie (Entwickeln für dich selbst)
Test-Strategie ohne externes QA:
Deine eigenen Steuerunterlagen als "Ground Truth":
Vergleichs-Archiv: Nimm deine letzten 3 Steuerjahre (2021-2023) als Referenz
Scan die Originalbelege ein
Lass die App extrahieren
Vergleiche mit deinem "echten" Ergebnis (Excel/Steuerberater)
Abweichung >0,01€ = Bug
Shadow-Accounting (erstes Jahr):
Führe 6 Monate parallel: Alte Methode (Excel/Papier) + Neue App
Monatlich: Export aus App vs. deine manuelle Berechnung
Dabei lernst du, welche Edge Cases wichtig sind (und welche nicht)
Das "Stichtag"-Prinzip:
Wichtig: Speichere vor jedem Quartal/Steuerjahr einen Snapshot (Docker-Volume-Backup)
Wenn die App Mist baut, kannst du zum letzten "guten" Stand zurück
Nie ohne Backup in die Steuererklärung übernehmen!
Pragmatische Entwicklungs-Prioritäten (nur für dich):
Phase 1: "Painkiller" (sofortiger Nutzen)
Nur OCR + Speichern (keine komplexe Logik)
Du suchst nicht mehr im Ordner, sondern gibst Suchbegriffe ein
Wert: 2h pro Woche gespart durch schnelles Dokumenten-Finden
Phase 2: "Rechner" (3 Monate später)
Summen bilden, die du sonst in Excel machst
Aber: Ergebnis immer mit console.log ausgeben, damit du prüfen kannst
Phase 3: "Autopilot" (1 Jahr später)
Automatische Übernahme in Steuerformulare
Nur hier brauchst du die harte Steuer-Validierung
⚠️ Risiken beim Selbst-Nutzen
1. Die "Blasen-Falle":
Du entwickelst nur für deinen spezifischen Fall (z.B. nur Vermietung, nur Gewerbe X). Wenn du später doch verkaufen willst, musst du alles umbauen.
Lösung: Trotzdem modular bauen, auch wenn du nur ein Modul nutzt.
2. Technische Schuld akzeptieren:
Da du der einzige Nutzer bist, kannst du "Dirty Hacks" machen:
Hartcodierte Steuersätze (für 2024)
Keine Unit-Tests für Edge-Cases, die dich nicht betreffen
Einfache SQLite statt PostgreSQL (leichter Backup als JSON!)
Das ist OK! Hauptsache du dokumentierst, wo die "technischen Schulden" liegen.
3. KI-Halluzinationen entdecken:
Da du die Zahlen kennst, fällt dir sofort auf, wenn Ollama 1.234€ statt 12.340€ extrahiert. Ein externer Tester hätte das übersehen.
Tipp: Schreibe jedesmal wenn die App Mist baut ein // TODO: Fix this edge case so sammelst du automatisch eine Liste wirklich wichtiger Bugfixes.
🐛 Debugging-Hilfe für dich selbst
Baue ein "Explain Mode" ein:
typescript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// In jedem Berechnungsschritt
const calculateNebenkosten = (data) => {
const result = /*...*/;
// Für dich als Entwickler/Nutzer:
console.log(`[DEBUG] Nebenkosten: ${result}`);
console.log(`[DEBUG] Zwischenschritte:`, JSON.stringify(data, null, 2));
// Speichere die Logik im Dokument:
return {
value: result,
calculationPath: "Heizkosten(1234) + Wasser(567) = 1801", // Als String!
confidence: 0.95
};
};
So kannst du im UI später nachklicken: "Warum steht hier 1.801€?" → Zeigt den Rechenweg.
🚀 Empfohlener erster Schritt (für dich persönlich)
Diese Woche:
Docker-Container mit Ollama starten
Ein simples Upload-Formular (Drag & Drop PDF)
OCR-Text extrahieren + anzeigen (nicht verarbeiten, nur anzeigen!)
Manuell in Formular kopieren können
Ziel: Du ersetzt erstmal nur die manuelle OCR-Arbeit (Tippen aus PDF), nicht die Denkarbeit.
Sobald das läuft und du 10 Dokumente damit verarbeitet hast, merkst du automatisch, was als nächstes automatisiert werden muss.
Wichtig: Fang klein an. Eine App, die nur deine spezifischen Wohngeldabrechnungen zuverlässig verarbeitet, ist 100x nützlicher als eine halbfertige "alles-können"-Suite, die Fehler macht.
Soll ich dir den minimalen Docker-Compose dafür bauen, damit du heute noch starten kannst mit dem ersten Upload-Test?
Das ist auch der Hauptpunkt. Aktuell mache ich alles mit verschiedenen Excel Dokumenten und von hand. Ich muss für jede Tabelle Abrechnung die ganzen Dokumente vorholen. Wenn die Informationen zentral in einer GUI auftauchen ist das ein riesen Schritt
profile
kimi-k2.5:cloud
Nachgedacht für 11 Sekunden
Genau das ist der "Single Source of Truth" Moment! Du hast gerade den wahren Wert der Software beschrieben nicht die KI, nicht die Automatisierung, sondern die Entlastung deines Gehirns.
Statt "Wo war nochmal die Rechnung vom Hausmeister für Objekt B aus dem Jahr 2022?" → Einmal klicken, alles da.
🎯 Das "Zentrale Nervensystem" für deine Immobilien
Dein jetziger Zustand (mental load):
Einklappen
Ausführen
Speichern
Kopieren
1
2
3
4
Excel_Wohnung_A_2023.xlsx
→ Link zu: "siehe PDF in Ordner/Rechnungen/2023/Hausmeister..."
→ Warte, war das im Ordner Steuer oder Nebenkosten?
→ *Sucht 5 Minuten*
Dein Zielzustand:
Einklappen
Speichern
Kopieren
1
2
3
Objekt A → 2023 → Hausmeister → [PDF-Icon] [Betrag: 1.234€] [Status: verbucht]
→ Alle Dokumente chronologisch
→ Fehlende Belege rot markiert
🗂️ Datenmodell für deinen konkreten Fall
Da du allein bist, brauchst du kein komplexes Multi-Tenancy, sondern eine simple Graph-Struktur:
sql
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-- SQLite ist völlig ausreichend für dich!
-- Du kannst die DB-Datei einfach mitbackupen
CREATE TABLE documents (
id INTEGER PRIMARY KEY,
filename TEXT,
file_path TEXT, -- z.B. "uploads/2024/hausmeister_rechnung_mai.pdf"
ocr_text TEXT, -- Durchsuchbarer Text
date_uploaded TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
year INTEGER, -- Steuerjahr (2023, 2024...)
cost_center TEXT, -- 'objekt_a', 'objekt_b', 'privat'
category TEXT, -- 'hausmeister', 'heizung', 'versicherung', 'steuer'
amount DECIMAL(10,2), -- extrahiert oder manuell
status TEXT -- 'neu', 'geprüft', 'verbucht', 'fehlt_belege'
);
-- Verknüpfung: Diese Rechnung gehört zu dieser Abrechnung
CREATE TABLE abrechnungen (
id INTEGER PRIMARY KEY,
objekt TEXT,
jahr INTEGER,
status TEXT -- 'in_arbeit', 'fertig', 'an_mieter_geschickt'
);
-- Many-to-Many: Dokumente können in mehreren Abrechnungen sein
CREATE TABLE abrechnung_dokumente (
abrechnung_id INTEGER,
document_id INTEGER,
anteil_percent DECIMAL(5,2) -- z.B. 50% wenn Kosten geteilt
);
🚀 Pragmatische Migration (Excel → GUI)
Woche 1: "Der digitale Aktenordner"
Nur Upload + OCR + Suche
Keine Logik, keine Berechnungen
Du kannst jetzt suchen: "Hausmeister 2023" → findet alle PDFs
Wert: Du verlierst keine Belege mehr, 80% des Stresses weg
Woche 2-3: "Das virtuelle Excel"
Einfache Tabelle: Zeilen = Belege, Spalten = Betrag, Kategorie, Objekt
Du kopierst deine Excel-Werte rüber (manuell oder CSV-Import)
Aber jetzt mit Klick auf das PDF-Icon = Original-Beleg öffnet sich
Wert: Kein Hin-und-Her-Klicken zwischen Excel und Ordner
Woche 4: "Die Magie"
App summiert automatisch: "Objekt A 2023 = 12.450€ Gesamtkosten"
Vergleich mit deinen Excel-Ergebnissen (Abgleich)
Wert: Erste Automatisierung, du kontrollierst nur noch
💡 Konkrete GUI-Idee für dich
Ein Dashboard mit drei Spalten (wie Kanban-Board):
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─────────────────────────────────────────────────────────────┐
│ Objekt auswählen: [Objekt A ▼] Jahr: [2024 ▼] │
├───────────────┬───────────────┬─────────────────────────────┤
│ EINGANG │ IN PRÜFUNG │ VERBUCHT │
│ (Neue PDFs) │ (Du prüfst) │ (Abgeschlossen) │
├───────────────┼───────────────┼─────────────────────────────┤
│ 🔴 Rechnung │ 🟡 Hausmeister│ 🟢 Heizkosten Q1 │
│ Heizung │ März (KI: │ 1.234€ │
│ (noch │ 1.200€?) │ │
│ nicht │ │ 🟢 Grundsteuer │
│ geprüft) │ 🟡 Versicherung│ 450€ │
│ │ (fehlt │ │
│ 🔴 Rechnung │ Betrag) │ │
│ Strom │ │ │
│ │ │ │
│ [+ Upload] │ [Nach rechts │ [In Abrechnung │
│ │ ziehen] │ übernehmen] │
└───────────────┴───────────────┴─────────────────────────────┘
Drag & Drop: Rechnung von "Eingang" → "In Prüfung" → "Verbucht"
Der Clou: Wenn du auf eine Karte klickst, öffnet sich rechts das PDF + das Extraktions-Formular (die roten/gelben Felder aus unserer UI).
🎯 Dein erster konkreter Schritt (heute noch machbar)
Ordner-Struktur anlegen:
Einklappen
Kopieren
1
2
3
/data/documents/2024/
/data/documents/2023/
/data/db/steuer.db
Simples Python-Script oder Node.js:
Watched den Upload-Ordner
Macht OCR (Tesseract)
Speichert in SQLite
Zeigt Web-Interface (ein einfaches HTML mit Suche)
Deine ersten 10 PDFs vom Schreibtisch einscannen und testen
Nach 2 Stunden Arbeit hast du schon:
Zentrale Suche über alle Belege
Keine Excel-Links mehr zu kaputten Pfaden
Übersicht: "Was habe ich schon, was fehlt noch?"
Soll ich dir ein minimal funktionierendes Python-Script (oder Node.js) schreiben, das genau das macht? Upload → OCR → SQLite → Simple Web-Suche?
Das wäre dein "MVP des MVP" ohne Docker, ohne Ollama, ohne komplexe Architektur. Nur: Nie wieder nach PDFs suchen.
Das klingt nach einem Ansatz wir bauen bei der Nutzung. Das klingt gut
profile
kimi-k2.5:cloud
Nachgedacht für 11 Sekunden
Perfekt! Das ist der "Walking Skeleton" Ansatz das dünneste System, das trotzdem durchläuft (Upload → Speichern → Anzeigen). Kein Over-Engineering, sondern funktionierende Software in 30 Minuten.
🚀 Der "Build as you use" Start (heute Abend machbar)
Technik-Stack für den ersten Abend:
Node.js (ein einfaches Script, kein Framework-Wahnsinn)
SQLite (eine Datei, kein Server)
HTML + Vanilla JS (kein React-Build-Prozess)
Kein Docker erstmal (kommt später, wenn es läuft)
Schritt 0: Die "Leiche" (Walking Skeleton)
Erstelle eine Datei server.js:
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const express = require('express');
const multer = require('multer');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const app = express();
const upload = multer({ dest: 'uploads/' });
// SQLite DB (eine Datei!)
const db = new sqlite3.Database('./steuer.db');
// Tabelle erstellen (falls nicht da)
db.run(`
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT,
originalname TEXT,
upload_date DATETIME DEFAULT CURRENT_TIMESTAMP,
year INTEGER,
category TEXT,
amount REAL,
notes TEXT
)
`);
// Statische Dateien (Frontend)
app.use(express.static('public'));
app.use(express.json());
// Upload-Endpunkt
app.post('/upload', upload.single('file'), (req, res) => {
const { year, category, amount, notes } = req.body;
db.run(
`INSERT INTO documents (filename, originalname, year, category, amount, notes)
VALUES (?, ?, ?, ?, ?, ?)`,
[req.file.filename, req.file.originalname, year, category, amount, notes],
function(err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ id: this.lastID, message: 'Gespeichert' });
}
);
});
// Alle Dokumente laden
app.get('/documents', (req, res) => {
db.all('SELECT * FROM documents ORDER BY upload_date DESC', [], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
});
app.listen(3000, () => console.log('Läuft auf http://localhost:3000'));
Schritt 1: Das hässliche aber funktionierende Frontend
Erstelle Ordner public/ und darin index.html:
html
Einklappen
Speichern
Kopieren
Vorschau
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Meine Steuer-Docs</title>
<style>
body { font-family: Arial; max-width: 800px; margin: 0 auto; padding: 20px; }
.doc-row { border: 1px solid #ddd; padding: 10px; margin: 10px 0; border-radius: 4px; }
.doc-row:hover { background: #f5f5f5; }
input, select { margin: 5px; padding: 5px; }
button { background: #007bff; color: white; border: none; padding: 10px 20px; cursor: pointer; }
.search { width: 100%; padding: 10px; margin-bottom: 20px; font-size: 16px; }
</style>
</head>
<body>
<h1>📁 Steuer-Dokumente</h1>
<!-- Upload -->
<div style="background: #f0f0f0; padding: 20px; margin-bottom: 20px;">
<h3>Neues Dokument</h3>
<form id="uploadForm" enctype="multipart/form-data">
<input type="file" name="file" required><br>
<input type="number" name="year" placeholder="Jahr (2024)" value="2024">
<select name="category">
<option value="hausmeister">Hausmeister</option>
<option value="heizung">Heizung</option>
<option value="versicherung">Versicherung</option>
<option value="steuer">Steuer</option>
<option value="sonstiges">Sonstiges</option>
</select>
<input type="number" step="0.01" name="amount" placeholder="Betrag (€)">
<input type="text" name="notes" placeholder="Notizen" style="width: 300px;">
<button type="submit">Speichern</button>
</form>
</div>
<!-- Suche -->
<input type="text" class="search" id="search" placeholder="Suchen..." onkeyup="loadDocs()">
<!-- Liste -->
<div id="docList"></div>
<script>
// Laden bei Start
loadDocs();
// Upload
document.getElementById('uploadForm').onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
await fetch('/upload', { method: 'POST', body: formData });
e.target.reset();
loadDocs();
};
// Dokumente laden & anzeigen
async function loadDocs() {
const search = document.getElementById('search').value.toLowerCase();
const res = await fetch('/documents');
const docs = await res.json();
const filtered = docs.filter(d =>
(d.originalname || '').toLowerCase().includes(search) ||
(d.notes || '').toLowerCase().includes(search) ||
(d.category || '').includes(search)
);
document.getElementById('docList').innerHTML = filtered.map(d => `
<div class="doc-row">
<strong>${d.originalname}</strong>
<span style="color: #666; float: right;">
${d.year} | ${d.category} | ${d.amount ? d.amount + '€' : 'kein Betrag'}
</span>
<br>
<small style="color: #999;">${d.notes || 'keine Notizen'}</small>
<br>
<a href="/uploads/${d.filename}" target="_blank">PDF ansehen</a>
</div>
`).join('');
}
</script>
</body>
</html>
Installation & Start (5 Minuten):
bash
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
mkdir steuer-app
cd steuer-app
npm init -y
npm install express multer sqlite3
mkdir uploads public
# kopiere server.js und public/index.html
node server.js
Ergebnis: Du hast jetzt eine Seite auf localhost:3000 wo du PDFs hochlädst, mit Jahr/Kategorie/Betrag taggst, und danach suchen kannst.
🔄 Die nächsten Iterationen (wenn du es nutzt)
Iteration 2 (wenn du merkst, dass Beträge wichtig sind):
Automatische Summen pro Kategorie ("Wie viel Heizung 2024?")
Einfaches Dashboard mit Balken
Iteration 3 (wenn OCR notwendig wird):
Tesseract.js im Browser hinzufügen
Upload zeigt sofort den OCR-Text neben dem PDF
Du kopierst den Betrag raus (manuell), aber die Suche funktioniert schon über den OCR-Text
Iteration 4 (wenn du die Abrechnung machst):
"Projekte" oder "Abrechnungen" hinzufügen
Checkboxen: "Diese Rechnung gehört zur Abrechnung 2024 Objekt A"
Export als CSV für Excel (damit du deinen Steuerberater füttern kannst)
Iteration 5+ (wenn Basics laufen):
Dann Docker, dann Ollama, dann Automatisierung
🎯 Das "Build as you use" Prinzip
Regel: Nach jeder Iteration musst du die Software echt nutzen können für deine nächste Buchhaltungs-Aufgabe. Wenn nicht, war sie zu groß.
Beispiel:
❌ Falsch: "Erst baue ich 3 Monate das perfekte System, dann benutze ich es"
✅ Richtig: "Heute speichere ich damit 5 Rechnungen, nächste Woche suche ich damit statt in Ordnern"
Technische Schuld ist erlaubt:
Hartcodierte Steuersätze? Egal, ändert sich eh nur jährlich
Keine Tests? Egal, du merkst ja wenn es kaputt ist
SQLite statt PostgreSQL? Perfekt, eine Datei, einfach backupbar
Wichtig: Wenn du merkst, dass du etwas brauchst (z.B. "Shit, ich muss das PDF auch öffnen können"), baust du es dann, nicht vorher.
Soll ich dir für Iteration 2 (die Summen/Dashboard) schon den Code vorbereiten, oder willst du erstmal mit dem Skeleton spielen und mir sagen, was als erstes weh tut?
Warum nicht direkt in Docker entwickeln? Das Stelle ich mit als einzig pragmatischen weg vor.
Und auch die GUI. Vielleicht nicht direkt ein Traum, aber hässlich muss ja nicht sein
profile
kimi-k2.5:cloud
Heute um 20:05
Nachgedacht für 16 Sekunden
Du hast absolut recht! Wenn du mit Docker vertraut bist, ist das der professionelle Weg und es verhindert späteres "Läuft auf meinem Rechner"-Chaos. Und eine hässliche GUI motiviert niemanden, sie zu nutzen.
Hier ist der "Docker-First + schönes UI" Stack:
🐳 Docker-Dev-Setup (mit Hot-Reload)
Vorteil: Du entwickelst direkt im Container, aber mit Instant-Reload beim Code-Ändern.
Projektstruktur
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
steuer-app/
├── docker-compose.yml
├── Dockerfile.dev # Für Entwicklung mit Hot-Reload
├── backend/
│ ├── package.json
│ ├── server.js # Express API
│ └── uploads/ # Gemountetes Volume
└── frontend/
├── package.json
├── vite.config.js # Vite (schneller als CRA)
├── tailwind.config.js # Schönes UI
└── src/
├── App.jsx
└── components/
1. Backend (Docker + Express + SQLite)
dockerfile
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
# Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
RUN npm install -g nodemon
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["nodemon", "--legacy-watch", "server.js"]
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// backend/server.js
const express = require('express');
const multer = require('multer');
const sqlite3 = require('sqlite3');
const cors = require('cors');
const path = require('path');
const app = express();
app.use(cors());
app.use(express.json());
// Upload-Config
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, '/app/uploads'),
filename: (req, file, cb) => cb(null, Date.now() + '-' + file.originalname)
});
const upload = multer({ storage });
// DB
const db = new sqlite3.Database('/app/data/steuer.db');
db.run(`
CREATE TABLE IF NOT EXISTS docs (
id INTEGER PRIMARY KEY,
filename TEXT, originalname TEXT, year INTEGER,
category TEXT, amount REAL, notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Routes
app.get('/api/docs', (req, res) => {
db.all('SELECT * FROM docs ORDER BY created_at DESC', [], (err, rows) => {
res.json(rows || []);
});
});
app.post('/api/upload', upload.single('file'), (req, res) => {
const { year, category, amount, notes } = req.body;
db.run(
'INSERT INTO docs (filename, originalname, year, category, amount, notes) VALUES (?, ?, ?, ?, ?, ?)',
[req.file.filename, req.file.originalname, year, category, amount, notes],
function(err) {
if (err) return res.status(500).json({ error: err });
res.json({ id: this.lastID });
}
);
});
// Statische Dateien für Uploads
app.use('/uploads', express.static('/app/uploads'));
app.listen(3000, () => console.log('API läuft auf :3000'));
json
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
// backend/package.json
{
"dependencies": {
"express": "^4.18.2",
"multer": "^1.4.5-lts.1",
"sqlite3": "^5.1.6",
"cors": "^2.8.5"
}
}
2. Frontend (React + Vite + Tailwind - Schnell & Hübsch)
dockerfile
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
# frontend/Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"]
json
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// frontend/package.json
{
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"lucide-react": "^0.294.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6",
"vite": "^5.0.8"
}
}
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
// frontend/tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: { extend: {} },
plugins: [],
}
css
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
/* frontend/src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply bg-gray-50 text-gray-900;
}
javascript
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// frontend/vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': 'http://backend:3000',
'/uploads': 'http://backend:3000'
}
}
})
3. Schöne React-Components (nicht hässlich!)
jsx
Einklappen
Speichern
Kopieren
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// frontend/src/App.jsx
<select name="year" defaultValue={2024} className="w-full rounded-lg border-gray-300 border p-2">
{[2024, 2023, 2022].map(y => <option key={y} value={y}>{y}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Kategorie</label>
<select name="category" className="w-full rounded-lg border-gray-300 border p-2">
<option value="hausmeister">Hausmeister</option>
<option value="heizung">Heizung</option>
<option value="versicherung">Versicherung</option>
<option value="steuer">Steuer</option>
<option value="sonstiges">Sonstiges</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Betrag (€)</label>
<input type="number" step="0.01" name="amount" placeholder="0.00"
className="w-full rounded-lg border-gray-300 border p-2" />
</div>
<button type="submit" disabled={uploading}
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition disabled:opacity-50">
{uploading ? 'Speichern...' : 'Hochladen'}
</button>
</form>
</div>
{/* Search */}
<div className="relative mb-6">
<Search className="absolute left-3 top-3 w-5 h-5 text-gray-400" />
<input type="text" placeholder="Dokumente durchsuchen..." value={search} onChange={e => setSearch(e.target.value)}
className="w-full pl-10 pr-4 py-3 rounded-xl border border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition" />
</div>
{/* Document List */}
<div className="grid gap-4">
{filtered.map(doc => (
<div key={doc.id} className="bg-white rounded-xl shadow-sm hover:shadow-md transition p-5 border border-gray-100">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<FileText className="w-5 h-5 text-blue-600" />
<h3 className="font-semibold text-gray-900">{doc.originalname}</h3>
<span className="px-3 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-700">
{doc.category}
</span>
</div>
{doc.notes && <p className="text-gray-600 text-sm mb-3">{doc.notes}</p>}
<div className="flex items-center gap-4 text-sm text-gray-500">
<span className="flex items-center gap-1">
<Calendar className="w-4 h-4" /> {doc.year}
</span>
{doc.amount && (
<span className="flex items-center gap-1 text-green-700 font-medium">
<DollarSign className="w-4 h-4" /> {parseFloat(doc.amount).toFixed(2)} €
</span>
)}
</div>
</div>
<a href={`/uploads/${doc.filename}`} target="_blank"
className="text-blue-600 hover:text-blue-800 text-sm font-medium px-4 py-2 rounded-lg hover:bg-blue-50 transition">
Ansehen →
</a>
</div>
</div>
))}
</div>
</div>
</div>
);
}
export default App;
4. Docker Compose (Dev-Version mit Hot-Reload)
yaml
Einklappen
Speichern
Kopieren
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# docker-compose.yml
version: '3.8'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile.dev
volumes:
- ./backend:/app # Hot-reload für Code
- ./backend/node_modules:/app/node_modules # Node_modules nicht überschreiben
- uploads:/app/uploads # Persistente Uploads
- db_data:/app/data # Persistente DB
ports:
- "3000:3000"
environment:
- NODE_ENV=development
networks:
- app-net
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
volumes:
- ./frontend:/app
- /app/node_modules # Anonymous volume für node_modules
ports:
- "5173:5173"
environment:
- CHOKIDAR_USEPOLLING=true # Für Hot-reload in Docker
networks:
- app-net
depends_on:
- backend
volumes:
uploads:
db_data:
networks:
app-net:
driver: bridge
🚀 Starten
bash
Einklappen
Speichern
Kopieren
1
2
3
4
5
mkdir steuer-app && cd steuer-app
# Ordner erstellen wie oben beschrieben
# Dateien kopieren
docker-compose up --build
Ergebnis:
Frontend: http://localhost:5173 (React + Tailwind, schick!)
Backend: http://localhost:3000 (API)
Hot-Reload: Code ändern → sofort sichtbar im Browser
Persistenz: Uploads und DB bleiben auch bei docker-compose down erhalten
Das ist der professionelle Weg und es sieht gleich aus wie moderne SaaS-Apps (Notion/Linear-Stil). Ist das der Richtige Ansatz für dich?