- Ungenutzte Variable `self.letzte_pdf` entfernt - Inline-Imports nach oben verschoben (QTextEdit, QAction, etc.) - `shutil` und `subprocess` zentral importiert - Keine funktionalen Änderungen Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1590 lines
58 KiB
Python
1590 lines
58 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Nebenkostenabrechnung - PySide6 GUI-Anwendung
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from io import BytesIO
|
|
|
|
from PySide6.QtWidgets import (
|
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QFormLayout, QGridLayout, QStackedWidget, QListWidget, QListWidgetItem,
|
|
QLineEdit, QDoubleSpinBox, QSpinBox, QPushButton, QLabel, QGroupBox,
|
|
QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox, QFileDialog,
|
|
QComboBox, QFrame, QSplitter, QScrollArea, QSizePolicy, QTextEdit
|
|
)
|
|
from PySide6.QtCore import Qt, QSize
|
|
from PySide6.QtGui import QFont, QIcon, QColor, QAction, QKeySequence
|
|
from PySide6.QtPrintSupport import QPrinter, QPrintDialog
|
|
|
|
import shutil
|
|
import subprocess
|
|
|
|
from reportlab.lib.pagesizes import A4
|
|
from reportlab.lib.units import cm
|
|
from reportlab.pdfgen import canvas
|
|
from reportlab.lib import colors
|
|
|
|
try:
|
|
from PyPDF2 import PdfReader, PdfWriter
|
|
PYPDF2_AVAILABLE = True
|
|
except ImportError:
|
|
PYPDF2_AVAILABLE = False
|
|
|
|
|
|
class DatenManager:
|
|
"""Verwaltet Stammdaten und Historie"""
|
|
|
|
# Konfigurations-Datei im Home-Verzeichnis
|
|
CONFIG_DATEI = Path.home() / ".config" / "nebenkostenabrechnung" / "config.json"
|
|
|
|
def __init__(self):
|
|
# Arbeitsverzeichnis setzen
|
|
self.basis_pfad = Path(__file__).parent
|
|
os.chdir(self.basis_pfad)
|
|
|
|
# Standard-Pfade
|
|
self.daten_ordner = str(self.basis_pfad / "daten")
|
|
self.dokumente_ordner = str(self.basis_pfad / "dokumente")
|
|
self.vorlagen_ordner = str(self.basis_pfad / "vorlagen")
|
|
|
|
# Konfig laden (überschreibt Standard-Pfade)
|
|
self.lade_config()
|
|
|
|
# Ordner erstellen
|
|
Path(self.daten_ordner).mkdir(parents=True, exist_ok=True)
|
|
Path(self.dokumente_ordner).mkdir(parents=True, exist_ok=True)
|
|
Path(self.vorlagen_ordner).mkdir(parents=True, exist_ok=True)
|
|
|
|
# Dynamische Pfade
|
|
self.stammdaten_datei = Path(self.daten_ordner) / "stammdaten.json"
|
|
self.historie_datei = Path(self.daten_ordner) / "historie.json"
|
|
self.vorlage_datei = Path(self.vorlagen_ordner) / "vorlage.pdf"
|
|
|
|
# Standard-Daten
|
|
self.vermieter = {
|
|
"name": "", "adresse": "", "iban": "", "bic": "", "bank": ""
|
|
}
|
|
self.mieter = {
|
|
"name": "", "adresse": "", "iban": "", "bic": "", "bank": "", "email": ""
|
|
}
|
|
self.objekt_adresse = ""
|
|
self.vorauszahlung_monat = 50.0
|
|
self.kostenarten = ["Wasser/Abwasser", "Müllabfuhr", "Schornsteinfeger"]
|
|
self.briefkopf_hoehe = 4.5
|
|
self.historie = {}
|
|
|
|
self.lade_stammdaten()
|
|
self.lade_historie()
|
|
|
|
def lade_config(self):
|
|
"""Lädt Pfad-Konfiguration"""
|
|
if self.CONFIG_DATEI.exists():
|
|
try:
|
|
with open(self.CONFIG_DATEI, "r", encoding="utf-8") as f:
|
|
config = json.load(f)
|
|
self.daten_ordner = config.get("daten_ordner", self.daten_ordner)
|
|
self.dokumente_ordner = config.get("dokumente_ordner", self.dokumente_ordner)
|
|
self.vorlagen_ordner = config.get("vorlagen_ordner", self.vorlagen_ordner)
|
|
except:
|
|
pass
|
|
|
|
def speichere_config(self):
|
|
"""Speichert Pfad-Konfiguration"""
|
|
self.CONFIG_DATEI.parent.mkdir(parents=True, exist_ok=True)
|
|
config = {
|
|
"daten_ordner": self.daten_ordner,
|
|
"dokumente_ordner": self.dokumente_ordner,
|
|
"vorlagen_ordner": self.vorlagen_ordner,
|
|
}
|
|
with open(self.CONFIG_DATEI, "w", encoding="utf-8") as f:
|
|
json.dump(config, f, ensure_ascii=False, indent=2)
|
|
|
|
def aktualisiere_pfade(self):
|
|
"""Aktualisiert die Dateipfade nach Änderung der Ordner"""
|
|
Path(self.daten_ordner).mkdir(parents=True, exist_ok=True)
|
|
Path(self.dokumente_ordner).mkdir(parents=True, exist_ok=True)
|
|
Path(self.vorlagen_ordner).mkdir(parents=True, exist_ok=True)
|
|
self.stammdaten_datei = Path(self.daten_ordner) / "stammdaten.json"
|
|
self.historie_datei = Path(self.daten_ordner) / "historie.json"
|
|
self.vorlage_datei = Path(self.vorlagen_ordner) / "vorlage.pdf"
|
|
|
|
def lade_stammdaten(self):
|
|
if self.stammdaten_datei.exists():
|
|
try:
|
|
with open(self.stammdaten_datei, "r", encoding="utf-8") as f:
|
|
daten = json.load(f)
|
|
self.vermieter = daten.get("vermieter", self.vermieter)
|
|
self.mieter = daten.get("mieter", self.mieter)
|
|
self.objekt_adresse = daten.get("objekt_adresse", "")
|
|
self.vorauszahlung_monat = daten.get("vorauszahlung_monat", 50.0)
|
|
self.kostenarten = daten.get("kostenarten", self.kostenarten)
|
|
self.briefkopf_hoehe = daten.get("briefkopf_hoehe", 4.5)
|
|
except Exception as e:
|
|
print(f"Fehler beim Laden: {e}")
|
|
|
|
def speichere_stammdaten(self):
|
|
daten = {
|
|
"vermieter": self.vermieter,
|
|
"mieter": self.mieter,
|
|
"objekt_adresse": self.objekt_adresse,
|
|
"vorauszahlung_monat": self.vorauszahlung_monat,
|
|
"kostenarten": self.kostenarten,
|
|
"briefkopf_hoehe": self.briefkopf_hoehe,
|
|
}
|
|
with open(self.stammdaten_datei, "w", encoding="utf-8") as f:
|
|
json.dump(daten, f, ensure_ascii=False, indent=2)
|
|
|
|
def lade_historie(self):
|
|
if self.historie_datei.exists():
|
|
try:
|
|
with open(self.historie_datei, "r", encoding="utf-8") as f:
|
|
self.historie = json.load(f)
|
|
except:
|
|
pass
|
|
|
|
def get_abrechnung_ordner(self, jahr):
|
|
"""Gibt den Ordner für eine Abrechnung zurück"""
|
|
ordner = Path(self.dokumente_ordner) / str(jahr)
|
|
ordner.mkdir(parents=True, exist_ok=True)
|
|
return ordner
|
|
|
|
def get_belege_ordner(self, jahr):
|
|
"""Gibt den Belege-Ordner für eine Abrechnung zurück"""
|
|
ordner = self.get_abrechnung_ordner(jahr) / "belege"
|
|
ordner.mkdir(parents=True, exist_ok=True)
|
|
return ordner
|
|
|
|
def speichere_entwurf(self, jahr, kosten, belege, anzahl_monate, bemerkungen=""):
|
|
"""Speichert einen Entwurf (ohne PDF zu erstellen)"""
|
|
belege_ordner = self.get_belege_ordner(jahr)
|
|
gespeicherte_belege = {}
|
|
|
|
# Belege kopieren
|
|
for kostenart, dateien in belege.items():
|
|
gespeicherte_belege[kostenart] = []
|
|
for i, datei in enumerate(dateien):
|
|
if datei and Path(datei).exists():
|
|
# Dateiname: Kostenart_Nr.pdf
|
|
ext = Path(datei).suffix
|
|
ziel_name = f"{kostenart.replace('/', '_')}_{i+1}{ext}"
|
|
ziel = belege_ordner / ziel_name
|
|
if str(datei) != str(ziel): # Nur kopieren wenn nicht gleich
|
|
shutil.copy2(datei, ziel)
|
|
gespeicherte_belege[kostenart].append(str(ziel))
|
|
|
|
summe_kosten = sum(kosten.values())
|
|
summe_vz = self.vorauszahlung_monat * anzahl_monate
|
|
|
|
self.historie[str(jahr)] = {
|
|
"jahr": jahr,
|
|
"mieter": self.mieter["name"],
|
|
"kosten": kosten,
|
|
"belege": gespeicherte_belege,
|
|
"bemerkungen": bemerkungen,
|
|
"summe_kosten": summe_kosten,
|
|
"vorauszahlung_monat": self.vorauszahlung_monat,
|
|
"anzahl_monate": anzahl_monate,
|
|
"summe_vorauszahlung": summe_vz,
|
|
"differenz": summe_kosten - summe_vz,
|
|
"status": "entwurf",
|
|
"datum": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
|
}
|
|
|
|
with open(self.historie_datei, "w", encoding="utf-8") as f:
|
|
json.dump(self.historie, f, ensure_ascii=False, indent=2)
|
|
|
|
return gespeicherte_belege
|
|
|
|
def lade_entwurf(self, jahr):
|
|
"""Lädt einen gespeicherten Entwurf"""
|
|
if str(jahr) in self.historie:
|
|
return self.historie[str(jahr)]
|
|
return None
|
|
|
|
def speichere_abrechnung(self, jahr, kosten, belege, anzahl_monate, bemerkungen=""):
|
|
"""Speichert eine fertige Abrechnung"""
|
|
# Erst als Entwurf speichern (kopiert Belege)
|
|
gespeicherte_belege = self.speichere_entwurf(jahr, kosten, belege, anzahl_monate, bemerkungen)
|
|
|
|
summe_kosten = sum(kosten.values())
|
|
summe_vz = self.vorauszahlung_monat * anzahl_monate
|
|
|
|
self.historie[str(jahr)] = {
|
|
"jahr": jahr,
|
|
"mieter": self.mieter["name"],
|
|
"kosten": kosten,
|
|
"belege": gespeicherte_belege,
|
|
"summe_kosten": summe_kosten,
|
|
"vorauszahlung_monat": self.vorauszahlung_monat,
|
|
"anzahl_monate": anzahl_monate,
|
|
"summe_vorauszahlung": summe_vz,
|
|
"differenz": summe_kosten - summe_vz,
|
|
"status": "fertig",
|
|
"datum": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
|
}
|
|
|
|
with open(self.historie_datei, "w", encoding="utf-8") as f:
|
|
json.dump(self.historie, f, ensure_ascii=False, indent=2)
|
|
|
|
return gespeicherte_belege
|
|
|
|
def get_vorjahr_kosten(self, jahr):
|
|
vorjahr = str(jahr - 1)
|
|
if vorjahr in self.historie:
|
|
return self.historie[vorjahr].get("kosten", {})
|
|
return {}
|
|
|
|
|
|
class StammdatenSeite(QWidget):
|
|
"""Seite für Stammdaten-Eingabe"""
|
|
|
|
def __init__(self, daten: DatenManager):
|
|
super().__init__()
|
|
self.daten = daten
|
|
self.setup_ui()
|
|
self.lade_daten()
|
|
|
|
def setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setSpacing(20)
|
|
|
|
# Titel
|
|
titel = QLabel("Stammdaten")
|
|
titel.setFont(QFont("", 18, QFont.Bold))
|
|
layout.addWidget(titel)
|
|
|
|
# Scroll-Bereich
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setFrameShape(QFrame.NoFrame)
|
|
|
|
content = QWidget()
|
|
content_layout = QVBoxLayout(content)
|
|
content_layout.setSpacing(15)
|
|
|
|
# Vermieter
|
|
self.vermieter_group = QGroupBox("Vermieter")
|
|
vl = QFormLayout(self.vermieter_group)
|
|
self.v_name = QLineEdit()
|
|
self.v_adresse = QLineEdit()
|
|
self.v_iban = QLineEdit()
|
|
self.v_bic = QLineEdit()
|
|
self.v_bank = QLineEdit()
|
|
vl.addRow("Name:", self.v_name)
|
|
vl.addRow("Adresse:", self.v_adresse)
|
|
vl.addRow("IBAN:", self.v_iban)
|
|
vl.addRow("BIC:", self.v_bic)
|
|
vl.addRow("Bank:", self.v_bank)
|
|
content_layout.addWidget(self.vermieter_group)
|
|
|
|
# Mieter
|
|
self.mieter_group = QGroupBox("Mieter")
|
|
ml = QFormLayout(self.mieter_group)
|
|
self.m_name = QLineEdit()
|
|
self.m_adresse = QLineEdit()
|
|
self.m_email = QLineEdit()
|
|
self.m_email.setPlaceholderText("Nur intern - nicht im PDF")
|
|
self.m_iban = QLineEdit()
|
|
self.m_iban.setPlaceholderText("Für Rückzahlung bei Guthaben")
|
|
self.m_bic = QLineEdit()
|
|
self.m_bank = QLineEdit()
|
|
ml.addRow("Name:", self.m_name)
|
|
ml.addRow("Adresse:", self.m_adresse)
|
|
ml.addRow("E-Mail:", self.m_email)
|
|
ml.addRow("IBAN:", self.m_iban)
|
|
ml.addRow("BIC:", self.m_bic)
|
|
ml.addRow("Bank:", self.m_bank)
|
|
content_layout.addWidget(self.mieter_group)
|
|
|
|
# Objekt
|
|
self.objekt_group = QGroupBox("Mietobjekt")
|
|
ol = QFormLayout(self.objekt_group)
|
|
self.objekt_adresse = QLineEdit()
|
|
self.vorauszahlung = QDoubleSpinBox()
|
|
self.vorauszahlung.setRange(0, 10000)
|
|
self.vorauszahlung.setSuffix(" €/Monat")
|
|
self.vorauszahlung.setDecimals(2)
|
|
ol.addRow("Adresse:", self.objekt_adresse)
|
|
ol.addRow("Vorauszahlung:", self.vorauszahlung)
|
|
content_layout.addWidget(self.objekt_group)
|
|
|
|
content_layout.addStretch()
|
|
scroll.setWidget(content)
|
|
layout.addWidget(scroll)
|
|
|
|
# Speichern-Button
|
|
btn_layout = QHBoxLayout()
|
|
btn_layout.addStretch()
|
|
self.btn_speichern = QPushButton("Speichern")
|
|
self.btn_speichern.setMinimumWidth(150)
|
|
self.btn_speichern.clicked.connect(self.speichern)
|
|
btn_layout.addWidget(self.btn_speichern)
|
|
layout.addLayout(btn_layout)
|
|
|
|
def lade_daten(self):
|
|
v = self.daten.vermieter
|
|
self.v_name.setText(v.get("name", ""))
|
|
self.v_adresse.setText(v.get("adresse", ""))
|
|
self.v_iban.setText(v.get("iban", ""))
|
|
self.v_bic.setText(v.get("bic", ""))
|
|
self.v_bank.setText(v.get("bank", ""))
|
|
|
|
m = self.daten.mieter
|
|
self.m_name.setText(m.get("name", ""))
|
|
self.m_adresse.setText(m.get("adresse", ""))
|
|
self.m_email.setText(m.get("email", ""))
|
|
self.m_iban.setText(m.get("iban", ""))
|
|
self.m_bic.setText(m.get("bic", ""))
|
|
self.m_bank.setText(m.get("bank", ""))
|
|
|
|
self.objekt_adresse.setText(self.daten.objekt_adresse)
|
|
self.vorauszahlung.setValue(self.daten.vorauszahlung_monat)
|
|
|
|
def speichern(self):
|
|
self.daten.vermieter = {
|
|
"name": self.v_name.text(),
|
|
"adresse": self.v_adresse.text(),
|
|
"iban": self.v_iban.text(),
|
|
"bic": self.v_bic.text(),
|
|
"bank": self.v_bank.text(),
|
|
}
|
|
self.daten.mieter = {
|
|
"name": self.m_name.text(),
|
|
"adresse": self.m_adresse.text(),
|
|
"email": self.m_email.text(),
|
|
"iban": self.m_iban.text(),
|
|
"bic": self.m_bic.text(),
|
|
"bank": self.m_bank.text(),
|
|
}
|
|
self.daten.objekt_adresse = self.objekt_adresse.text()
|
|
self.daten.vorauszahlung_monat = self.vorauszahlung.value()
|
|
self.daten.speichere_stammdaten()
|
|
|
|
QMessageBox.information(self, "Gespeichert", "Stammdaten wurden gespeichert.")
|
|
|
|
|
|
class KostenartenSeite(QWidget):
|
|
"""Seite für Kostenarten-Verwaltung"""
|
|
|
|
def __init__(self, daten: DatenManager):
|
|
super().__init__()
|
|
self.daten = daten
|
|
self.setup_ui()
|
|
self.lade_daten()
|
|
|
|
def setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setSpacing(20)
|
|
|
|
titel = QLabel("Kostenarten verwalten")
|
|
titel.setFont(QFont("", 18, QFont.Bold))
|
|
layout.addWidget(titel)
|
|
|
|
info = QLabel("Diese Kostenarten werden bei jeder Abrechnung abgefragt.")
|
|
info.setStyleSheet("color: gray;")
|
|
layout.addWidget(info)
|
|
|
|
# Tabelle
|
|
self.tabelle = QTableWidget()
|
|
self.tabelle.setColumnCount(1)
|
|
self.tabelle.setHorizontalHeaderLabels(["Kostenart"])
|
|
self.tabelle.horizontalHeader().setStretchLastSection(True)
|
|
self.tabelle.setSelectionBehavior(QTableWidget.SelectRows)
|
|
layout.addWidget(self.tabelle)
|
|
|
|
# Buttons
|
|
btn_layout = QHBoxLayout()
|
|
|
|
self.input_neu = QLineEdit()
|
|
self.input_neu.setPlaceholderText("Neue Kostenart eingeben...")
|
|
self.input_neu.returnPressed.connect(self.hinzufuegen)
|
|
btn_layout.addWidget(self.input_neu)
|
|
|
|
self.btn_add = QPushButton("Hinzufügen")
|
|
self.btn_add.clicked.connect(self.hinzufuegen)
|
|
btn_layout.addWidget(self.btn_add)
|
|
|
|
self.btn_del = QPushButton("Löschen")
|
|
self.btn_del.clicked.connect(self.loeschen)
|
|
btn_layout.addWidget(self.btn_del)
|
|
|
|
layout.addLayout(btn_layout)
|
|
|
|
def lade_daten(self):
|
|
self.tabelle.setRowCount(len(self.daten.kostenarten))
|
|
for i, art in enumerate(self.daten.kostenarten):
|
|
self.tabelle.setItem(i, 0, QTableWidgetItem(art))
|
|
|
|
def hinzufuegen(self):
|
|
text = self.input_neu.text().strip()
|
|
if text and text not in self.daten.kostenarten:
|
|
self.daten.kostenarten.append(text)
|
|
self.daten.speichere_stammdaten()
|
|
self.lade_daten()
|
|
self.input_neu.clear()
|
|
|
|
def loeschen(self):
|
|
row = self.tabelle.currentRow()
|
|
if row >= 0:
|
|
art = self.daten.kostenarten[row]
|
|
self.daten.kostenarten.remove(art)
|
|
self.daten.speichere_stammdaten()
|
|
self.lade_daten()
|
|
|
|
|
|
class KostenZeile(QWidget):
|
|
"""Widget für eine Kostenart mit Betrag und Belegen"""
|
|
|
|
def __init__(self, kostenart, vorjahr_wert=0, parent=None):
|
|
super().__init__(parent)
|
|
self.kostenart = kostenart
|
|
self.belege = [] # Liste der Beleg-Pfade
|
|
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(0, 2, 0, 2)
|
|
|
|
# Label
|
|
label = kostenart
|
|
if vorjahr_wert > 0:
|
|
label = f"{kostenart} (VJ: {vorjahr_wert:.2f} €)"
|
|
self.lbl = QLabel(label)
|
|
self.lbl.setMinimumWidth(200)
|
|
layout.addWidget(self.lbl)
|
|
|
|
# Betrag
|
|
self.betrag = QDoubleSpinBox()
|
|
self.betrag.setRange(0, 100000)
|
|
self.betrag.setDecimals(2)
|
|
self.betrag.setSuffix(" €")
|
|
self.betrag.setMinimumWidth(120)
|
|
layout.addWidget(self.betrag)
|
|
|
|
# Beleg-Button
|
|
self.btn_beleg = QPushButton("📎 Beleg...")
|
|
self.btn_beleg.setMinimumWidth(100)
|
|
self.btn_beleg.clicked.connect(self.waehle_beleg)
|
|
layout.addWidget(self.btn_beleg)
|
|
|
|
# Beleg-Anzeige
|
|
self.lbl_beleg = QLabel("")
|
|
self.lbl_beleg.setStyleSheet("color: gray; font-size: 9px;")
|
|
layout.addWidget(self.lbl_beleg, 1)
|
|
|
|
def waehle_beleg(self):
|
|
dateien, _ = QFileDialog.getOpenFileNames(
|
|
self, f"Belege für {self.kostenart}",
|
|
"", "PDF/Bilder (*.pdf *.jpg *.jpeg *.png);;Alle (*.*)"
|
|
)
|
|
if dateien:
|
|
self.belege = dateien
|
|
self.aktualisiere_beleg_anzeige()
|
|
|
|
def aktualisiere_beleg_anzeige(self):
|
|
if self.belege:
|
|
namen = [Path(b).name for b in self.belege]
|
|
text = ", ".join(namen)
|
|
if len(text) > 40:
|
|
text = f"{len(self.belege)} Belege"
|
|
self.lbl_beleg.setText(f"✓ {text}")
|
|
self.lbl_beleg.setStyleSheet("color: green; font-size: 9px;")
|
|
else:
|
|
self.lbl_beleg.setText("")
|
|
|
|
def setze_belege(self, belege_liste):
|
|
"""Setzt Belege aus gespeichertem Entwurf"""
|
|
self.belege = [b for b in belege_liste if Path(b).exists()]
|
|
self.aktualisiere_beleg_anzeige()
|
|
|
|
|
|
class AbrechnungSeite(QWidget):
|
|
"""Seite für neue Abrechnung mit Beleg-Upload"""
|
|
|
|
def __init__(self, daten: DatenManager):
|
|
super().__init__()
|
|
self.daten = daten
|
|
self.kosten_zeilen = {} # kostenart -> KostenZeile
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setSpacing(15)
|
|
|
|
# Titel und Status
|
|
titel_layout = QHBoxLayout()
|
|
titel = QLabel("Abrechnung")
|
|
titel.setFont(QFont("", 18, QFont.Bold))
|
|
titel_layout.addWidget(titel)
|
|
|
|
self.lbl_status = QLabel("")
|
|
self.lbl_status.setStyleSheet("color: gray;")
|
|
titel_layout.addWidget(self.lbl_status)
|
|
titel_layout.addStretch()
|
|
layout.addLayout(titel_layout)
|
|
|
|
# Jahr, Monate und Laden
|
|
periode_layout = QHBoxLayout()
|
|
|
|
periode_layout.addWidget(QLabel("Jahr:"))
|
|
self.jahr = QSpinBox()
|
|
self.jahr.setRange(2000, 2100)
|
|
self.jahr.setValue(datetime.now().year - 1)
|
|
self.jahr.valueChanged.connect(self.jahr_geaendert)
|
|
periode_layout.addWidget(self.jahr)
|
|
|
|
periode_layout.addWidget(QLabel("Monate:"))
|
|
self.monate = QSpinBox()
|
|
self.monate.setRange(1, 12)
|
|
self.monate.setValue(12)
|
|
self.monate.valueChanged.connect(self.berechne)
|
|
periode_layout.addWidget(self.monate)
|
|
|
|
self.btn_laden = QPushButton("Entwurf laden")
|
|
self.btn_laden.clicked.connect(self.lade_entwurf)
|
|
periode_layout.addWidget(self.btn_laden)
|
|
|
|
periode_layout.addStretch()
|
|
layout.addLayout(periode_layout)
|
|
|
|
# Scroll-Bereich für Kosten
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setFrameShape(QFrame.NoFrame)
|
|
scroll.setMaximumHeight(300)
|
|
|
|
self.kosten_widget = QWidget()
|
|
self.kosten_layout = QVBoxLayout(self.kosten_widget)
|
|
self.kosten_layout.setSpacing(5)
|
|
scroll.setWidget(self.kosten_widget)
|
|
|
|
kosten_group = QGroupBox("Nebenkosten (Jahressummen) - mit Belegen")
|
|
kosten_group_layout = QVBoxLayout(kosten_group)
|
|
kosten_group_layout.addWidget(scroll)
|
|
layout.addWidget(kosten_group)
|
|
|
|
# Zusammenfassung
|
|
summen_group = QGroupBox("Zusammenfassung")
|
|
sl = QFormLayout(summen_group)
|
|
|
|
self.lbl_summe_kosten = QLabel("0,00 €")
|
|
self.lbl_summe_kosten.setFont(QFont("", 11))
|
|
sl.addRow("Summe Nebenkosten:", self.lbl_summe_kosten)
|
|
|
|
self.lbl_vorauszahlung = QLabel("0,00 €")
|
|
self.lbl_vorauszahlung.setFont(QFont("", 11))
|
|
sl.addRow("Vorauszahlungen:", self.lbl_vorauszahlung)
|
|
|
|
self.lbl_differenz = QLabel("0,00 €")
|
|
self.lbl_differenz.setFont(QFont("", 14, QFont.Bold))
|
|
sl.addRow("Ergebnis:", self.lbl_differenz)
|
|
|
|
layout.addWidget(summen_group)
|
|
|
|
# Bemerkungen
|
|
bemerk_group = QGroupBox("Bemerkungen (erscheint im PDF)")
|
|
bemerk_layout = QVBoxLayout(bemerk_group)
|
|
self.bemerkungen = QTextEdit()
|
|
self.bemerkungen.setMaximumHeight(80)
|
|
self.bemerkungen.setPlaceholderText("Optionale Anmerkungen zur Abrechnung...")
|
|
bemerk_layout.addWidget(self.bemerkungen)
|
|
layout.addWidget(bemerk_group)
|
|
|
|
# Buttons
|
|
btn_layout = QHBoxLayout()
|
|
|
|
self.btn_entwurf = QPushButton("💾 Entwurf speichern")
|
|
self.btn_entwurf.clicked.connect(self.speichere_entwurf)
|
|
btn_layout.addWidget(self.btn_entwurf)
|
|
|
|
btn_layout.addStretch()
|
|
|
|
self.btn_pdf = QPushButton("📄 PDF erstellen")
|
|
self.btn_pdf.setMinimumWidth(150)
|
|
self.btn_pdf.clicked.connect(self.erstelle_pdf)
|
|
btn_layout.addWidget(self.btn_pdf)
|
|
|
|
layout.addLayout(btn_layout)
|
|
layout.addStretch()
|
|
|
|
# Initial laden (inkl. vorhandener Daten)
|
|
self.jahr_geaendert()
|
|
|
|
def jahr_geaendert(self):
|
|
"""Wird aufgerufen wenn das Jahr geändert wird - lädt automatisch vorhandene Daten"""
|
|
self.aktualisiere_kosten()
|
|
|
|
# Prüfen ob Daten existieren und automatisch laden
|
|
entwurf = self.daten.lade_entwurf(self.jahr.value())
|
|
if entwurf:
|
|
# Automatisch laden
|
|
self.monate.setValue(entwurf.get("anzahl_monate", 12))
|
|
|
|
kosten = entwurf.get("kosten", {})
|
|
belege = entwurf.get("belege", {})
|
|
|
|
for art, zeile in self.kosten_zeilen.items():
|
|
if art in kosten:
|
|
zeile.betrag.setValue(kosten[art])
|
|
if art in belege:
|
|
zeile.setze_belege(belege[art])
|
|
|
|
# Bemerkungen laden
|
|
self.bemerkungen.setPlainText(entwurf.get("bemerkungen", ""))
|
|
|
|
status = entwurf.get("status", "entwurf")
|
|
status_text = "Abrechnung" if status == "fertig" else "Entwurf"
|
|
self.lbl_status.setText(f"[{status_text} vom {entwurf.get('datum', '?')}]")
|
|
self.btn_laden.setEnabled(True)
|
|
self.berechne()
|
|
else:
|
|
self.lbl_status.setText("")
|
|
self.btn_laden.setEnabled(False)
|
|
|
|
def aktualisiere_kosten(self):
|
|
"""Erstellt die Kosten-Zeilen neu"""
|
|
# Alte Widgets entfernen
|
|
while self.kosten_layout.count():
|
|
item = self.kosten_layout.takeAt(0)
|
|
if item.widget():
|
|
item.widget().deleteLater()
|
|
|
|
self.kosten_zeilen = {}
|
|
vorjahr = self.daten.get_vorjahr_kosten(self.jahr.value())
|
|
|
|
for art in self.daten.kostenarten:
|
|
vj_wert = vorjahr.get(art, 0)
|
|
zeile = KostenZeile(art, vj_wert)
|
|
zeile.betrag.valueChanged.connect(self.berechne)
|
|
self.kosten_layout.addWidget(zeile)
|
|
self.kosten_zeilen[art] = zeile
|
|
|
|
self.kosten_layout.addStretch()
|
|
self.berechne()
|
|
|
|
def lade_entwurf(self):
|
|
"""Lädt einen gespeicherten Entwurf"""
|
|
entwurf = self.daten.lade_entwurf(self.jahr.value())
|
|
if not entwurf:
|
|
QMessageBox.information(self, "Kein Entwurf", "Kein Entwurf für dieses Jahr gefunden.")
|
|
return
|
|
|
|
# Monate setzen
|
|
self.monate.setValue(entwurf.get("anzahl_monate", 12))
|
|
|
|
# Kosten und Belege setzen
|
|
kosten = entwurf.get("kosten", {})
|
|
belege = entwurf.get("belege", {})
|
|
|
|
for art, zeile in self.kosten_zeilen.items():
|
|
if art in kosten:
|
|
zeile.betrag.setValue(kosten[art])
|
|
if art in belege:
|
|
zeile.setze_belege(belege[art])
|
|
|
|
self.lbl_status.setText(f"[Geladen: {entwurf.get('datum', '?')}]")
|
|
self.berechne()
|
|
|
|
def speichere_entwurf(self):
|
|
"""Speichert den aktuellen Stand als Entwurf"""
|
|
kosten = {art: zeile.betrag.value() for art, zeile in self.kosten_zeilen.items()}
|
|
belege = {art: zeile.belege for art, zeile in self.kosten_zeilen.items()}
|
|
bemerkungen = self.bemerkungen.toPlainText()
|
|
|
|
self.daten.speichere_entwurf(
|
|
self.jahr.value(), kosten, belege, self.monate.value(), bemerkungen
|
|
)
|
|
|
|
self.lbl_status.setText(f"[Entwurf gespeichert: {datetime.now().strftime('%H:%M')}]")
|
|
QMessageBox.information(self, "Gespeichert", "Entwurf wurde gespeichert.\nBelege wurden kopiert.")
|
|
|
|
def berechne(self):
|
|
summe = sum(zeile.betrag.value() for zeile in self.kosten_zeilen.values())
|
|
vz = self.daten.vorauszahlung_monat * self.monate.value()
|
|
diff = summe - vz
|
|
|
|
self.lbl_summe_kosten.setText(f"{summe:,.2f} €".replace(",", "X").replace(".", ",").replace("X", "."))
|
|
self.lbl_vorauszahlung.setText(f"{vz:,.2f} €".replace(",", "X").replace(".", ",").replace("X", "."))
|
|
|
|
if diff > 0:
|
|
self.lbl_differenz.setText(f"Nachzahlung: {diff:,.2f} €".replace(",", "X").replace(".", ",").replace("X", "."))
|
|
self.lbl_differenz.setStyleSheet("color: #CC0000;")
|
|
else:
|
|
self.lbl_differenz.setText(f"Guthaben: {abs(diff):,.2f} €".replace(",", "X").replace(".", ",").replace("X", "."))
|
|
self.lbl_differenz.setStyleSheet("color: #006600;")
|
|
|
|
def erstelle_pdf(self):
|
|
if not self.daten.vermieter.get("name"):
|
|
QMessageBox.warning(self, "Fehler", "Bitte zuerst Stammdaten eingeben!")
|
|
return
|
|
|
|
# Kosten und Belege aus den Zeilen sammeln
|
|
kosten = {art: zeile.betrag.value() for art, zeile in self.kosten_zeilen.items()}
|
|
belege = {art: zeile.belege for art, zeile in self.kosten_zeilen.items()}
|
|
jahr = self.jahr.value()
|
|
anzahl_monate = self.monate.value()
|
|
|
|
bemerkungen = self.bemerkungen.toPlainText()
|
|
|
|
# Busy-Cursor anzeigen während PDF erstellt wird
|
|
QApplication.setOverrideCursor(Qt.WaitCursor)
|
|
self.lbl_status.setText("PDF wird erstellt...")
|
|
QApplication.processEvents()
|
|
|
|
try:
|
|
# Speichern (kopiert auch Belege)
|
|
gespeicherte_belege = self.daten.speichere_abrechnung(jahr, kosten, belege, anzahl_monate, bemerkungen)
|
|
|
|
# PDF erstellen (mit angehängten Belegen)
|
|
pdf_pfad = self._erstelle_pdf(jahr, kosten, anzahl_monate, gespeicherte_belege, bemerkungen)
|
|
finally:
|
|
QApplication.restoreOverrideCursor()
|
|
|
|
QMessageBox.information(self, "PDF erstellt", f"PDF gespeichert:\n{pdf_pfad}")
|
|
|
|
self.lbl_status.setText(f"[PDF erstellt: {datetime.now().strftime('%H:%M')}]")
|
|
|
|
# PDF öffnen
|
|
subprocess.run(["xdg-open", pdf_pfad])
|
|
|
|
def _erstelle_pdf(self, jahr, kosten, anzahl_monate, belege=None, bemerkungen=""):
|
|
# Absoluten Pfad sicherstellen
|
|
dok_ordner = Path(self.daten.dokumente_ordner).resolve()
|
|
jahr_ordner = dok_ordner / str(jahr)
|
|
jahr_ordner.mkdir(parents=True, exist_ok=True)
|
|
print(f"PDF-Ordner: {jahr_ordner}")
|
|
|
|
# Dateiname: Datum - Nebenkostenabrechnung - Objekt.pdf
|
|
datum_str = datetime.now().strftime("%Y-%m-%d")
|
|
objekt_safe = self.daten.objekt_adresse.replace('/', '-').replace('\\', '-').replace(':', '-')
|
|
dateiname = jahr_ordner / f"{datum_str} - Nebenkostenabrechnung - {objekt_safe}.pdf"
|
|
|
|
summe_kosten = sum(kosten.values())
|
|
summe_vz = self.daten.vorauszahlung_monat * anzahl_monate
|
|
differenz = summe_kosten - summe_vz
|
|
|
|
# Hack-Schrift für PDF registrieren
|
|
from reportlab.pdfbase import pdfmetrics
|
|
from reportlab.pdfbase.ttfonts import TTFont
|
|
try:
|
|
pdfmetrics.registerFont(TTFont('Hack', '/usr/share/fonts/TTF/Hack-Regular.ttf'))
|
|
pdfmetrics.registerFont(TTFont('Hack-Bold', '/usr/share/fonts/TTF/Hack-Bold.ttf'))
|
|
FONT = 'Hack'
|
|
FONT_BOLD = 'Hack-Bold'
|
|
except:
|
|
FONT = 'Helvetica'
|
|
FONT_BOLD = 'Helvetica-Bold'
|
|
|
|
# PDF erstellen
|
|
buffer = BytesIO()
|
|
c = canvas.Canvas(buffer, pagesize=A4)
|
|
breite, hoehe = A4
|
|
|
|
def format_eur(betrag):
|
|
return f"{betrag:,.2f} EUR".replace(",", "X").replace(".", ",").replace("X", ".")
|
|
|
|
def format_iban(iban):
|
|
iban = iban.replace(" ", "")
|
|
return " ".join([iban[i:i+4] for i in range(0, len(iban), 4)])
|
|
|
|
# ============ BRIEFKOPF ============
|
|
# Absender kommt aus der Vorlage (Briefkopf)
|
|
|
|
# Absenderzeile klein (direkt über Empfänger mit 1 Leerzeile)
|
|
y = hoehe - 4.5*cm
|
|
c.setFont(FONT, 7)
|
|
absender_zeile = f"{self.daten.vermieter['name']} · {self.daten.vermieter['adresse']}"
|
|
c.drawString(2*cm, y, absender_zeile)
|
|
|
|
# Leerzeile
|
|
y -= 0.4*cm
|
|
|
|
# Empfänger (Mieter) - "An:"
|
|
y -= 0.4*cm
|
|
c.setFont(FONT_BOLD, 10)
|
|
c.drawString(2*cm, y, "An:")
|
|
y -= 0.5*cm
|
|
c.setFont(FONT, 11)
|
|
c.drawString(2*cm, y, self.daten.mieter["name"])
|
|
y -= 0.45*cm
|
|
# Adresse aufteilen falls mit Komma
|
|
adresse_teile = self.daten.mieter["adresse"].split(",")
|
|
for teil in adresse_teile:
|
|
c.drawString(2*cm, y, teil.strip())
|
|
y -= 0.45*cm
|
|
|
|
# Datum rechts
|
|
c.setFont(FONT, 10)
|
|
c.drawRightString(breite - 2*cm, y + 0.9*cm, f"Datum: {datetime.now().strftime('%d.%m.%Y')}")
|
|
|
|
# ============ BETREFF / TITEL ============
|
|
# 6 Zeilen tiefer (ca. 2.5cm)
|
|
y -= 3.3*cm
|
|
c.setFont(FONT_BOLD, 12)
|
|
c.drawString(2*cm, y, f"Nebenkostenabrechnung {jahr}")
|
|
y -= 0.5*cm
|
|
c.setFont(FONT, 10)
|
|
c.drawString(2*cm, y, f"Abrechnungszeitraum: 01.01.{jahr} - 31.12.{jahr}")
|
|
|
|
# Objekt und Umlageschlüssel
|
|
y -= 0.6*cm
|
|
c.setFont(FONT, 9)
|
|
c.drawString(2*cm, y, f"Mietobjekt: {self.daten.objekt_adresse}")
|
|
y -= 0.4*cm
|
|
c.drawString(2*cm, y, "Umlageschlüssel: 100%")
|
|
|
|
# Trennlinie
|
|
y -= 0.5*cm
|
|
c.line(2*cm, y, breite - 2*cm, y)
|
|
|
|
# Kosten
|
|
y -= 0.8*cm
|
|
c.setFont(FONT_BOLD, 11)
|
|
c.drawString(2*cm, y, "Kostenaufstellung")
|
|
|
|
y -= 0.5*cm
|
|
c.setFont(FONT_BOLD, 9)
|
|
c.drawString(2*cm, y, "Kostenart")
|
|
c.drawRightString(breite - 2*cm, y, "Betrag")
|
|
y -= 0.15*cm
|
|
c.line(2*cm, y, breite - 2*cm, y)
|
|
|
|
c.setFont(FONT, 9)
|
|
for art, betrag in kosten.items():
|
|
if betrag > 0:
|
|
y -= 0.45*cm
|
|
c.drawString(2*cm, y, art)
|
|
c.drawRightString(breite - 2*cm, y, format_eur(betrag))
|
|
|
|
y -= 0.15*cm
|
|
c.line(2*cm, y, breite - 2*cm, y)
|
|
y -= 0.45*cm
|
|
c.setFont(FONT_BOLD, 9)
|
|
c.drawString(2*cm, y, "Summe Nebenkosten")
|
|
c.drawRightString(breite - 2*cm, y, format_eur(summe_kosten))
|
|
|
|
# Vorauszahlung
|
|
y -= 0.8*cm
|
|
c.setFont(FONT_BOLD, 11)
|
|
c.drawString(2*cm, y, "Vorauszahlungen")
|
|
y -= 0.5*cm
|
|
c.setFont(FONT, 9)
|
|
c.drawString(2*cm, y, f"{anzahl_monate} Monate x {format_eur(self.daten.vorauszahlung_monat)}")
|
|
c.drawRightString(breite - 2*cm, y, format_eur(summe_vz))
|
|
|
|
# Ergebnis
|
|
y -= 0.7*cm
|
|
c.setLineWidth(1.5)
|
|
c.line(2*cm, y, breite - 2*cm, y)
|
|
y -= 0.6*cm
|
|
c.setFont(FONT_BOLD, 11)
|
|
|
|
if differenz > 0:
|
|
c.drawString(2*cm, y, "Nachzahlung durch Mieter")
|
|
c.setFillColor(colors.HexColor("#CC0000"))
|
|
else:
|
|
c.drawString(2*cm, y, "Guthaben für Mieter")
|
|
c.setFillColor(colors.HexColor("#006600"))
|
|
|
|
c.drawRightString(breite - 2*cm, y, format_eur(abs(differenz)))
|
|
c.setFillColor(colors.black)
|
|
|
|
# Zahlungshinweis
|
|
y -= 1*cm
|
|
c.setFont(FONT_BOLD, 9)
|
|
c.drawString(2*cm, y, "Zahlungshinweis:")
|
|
y -= 0.4*cm
|
|
c.setFont(FONT, 8)
|
|
|
|
if differenz > 0:
|
|
c.drawString(2*cm, y, f"Bitte überweisen Sie {format_eur(abs(differenz))} innerhalb von 30 Tagen an:")
|
|
y -= 0.4*cm
|
|
if self.daten.vermieter["iban"]:
|
|
c.drawString(2*cm, y, f"IBAN: {format_iban(self.daten.vermieter['iban'])}")
|
|
if self.daten.vermieter["bank"]:
|
|
c.drawString(10*cm, y, f"Bank: {self.daten.vermieter['bank']}")
|
|
y -= 0.35*cm
|
|
c.drawString(2*cm, y, f"Verwendungszweck: Nebenkostennachzahlung {jahr}")
|
|
else:
|
|
c.drawString(2*cm, y, f"Das Guthaben von {format_eur(abs(differenz))} wird überwiesen an:")
|
|
y -= 0.4*cm
|
|
c.drawString(2*cm, y, f"Empfänger: {self.daten.mieter['name']}")
|
|
if self.daten.mieter["iban"]:
|
|
y -= 0.35*cm
|
|
c.drawString(2*cm, y, f"IBAN: {format_iban(self.daten.mieter['iban'])}")
|
|
|
|
# Bemerkungen (falls vorhanden)
|
|
if bemerkungen and bemerkungen.strip():
|
|
y -= 0.8*cm
|
|
c.setFont(FONT_BOLD, 9)
|
|
c.drawString(2*cm, y, "Bemerkungen:")
|
|
y -= 0.4*cm
|
|
c.setFont(FONT, 8)
|
|
for zeile in bemerkungen.strip().split('\n'):
|
|
c.drawString(2*cm, y, zeile)
|
|
y -= 0.3*cm
|
|
|
|
# Anlagen-Verzeichnis (falls Belege vorhanden)
|
|
anlagen_liste = []
|
|
if belege:
|
|
for kostenart, dateien in belege.items():
|
|
for datei in dateien:
|
|
if datei and Path(datei).exists():
|
|
anlagen_liste.append((kostenart, Path(datei).name))
|
|
|
|
if anlagen_liste:
|
|
y -= 0.8*cm
|
|
c.setFont(FONT_BOLD, 9)
|
|
c.drawString(2*cm, y, f"Anlagen ({len(anlagen_liste)} Beleg{'e' if len(anlagen_liste) > 1 else ''}):")
|
|
y -= 0.4*cm
|
|
c.setFont(FONT, 8)
|
|
for i, (kostenart, anlage_name) in enumerate(anlagen_liste, 1):
|
|
c.drawString(2*cm, y, f" {i}. {kostenart}: {anlage_name}")
|
|
y -= 0.3*cm
|
|
|
|
# Rechtliche Hinweise (Pflichtangaben gem. § 556 BGB)
|
|
y -= 0.5*cm
|
|
c.setFont(FONT_BOLD, 8)
|
|
c.drawString(2*cm, y, "Rechtliche Hinweise:")
|
|
y -= 0.35*cm
|
|
c.setFont(FONT, 7)
|
|
c.drawString(2*cm, y, "• Sie haben das Recht, die dieser Abrechnung zugrunde liegenden Belege einzusehen (§ 556 Abs. 4 BGB).")
|
|
y -= 0.3*cm
|
|
c.drawString(2*cm, y, "• Einwendungen gegen diese Abrechnung sind innerhalb von 12 Monaten nach Zugang schriftlich geltend zu machen (§ 556 Abs. 3 BGB).")
|
|
y -= 0.3*cm
|
|
c.drawString(2*cm, y, "• Nach Ablauf dieser Frist können Einwendungen nicht mehr geltend gemacht werden, es sei denn, der Mieter hat die")
|
|
y -= 0.3*cm
|
|
c.drawString(2*cm, y, " verspätete Geltendmachung nicht zu vertreten.")
|
|
|
|
# Unterschrift
|
|
y -= 0.8*cm
|
|
c.setLineWidth(0.5)
|
|
c.line(2*cm, y, 6*cm, y)
|
|
y -= 0.3*cm
|
|
c.setFont(FONT, 8)
|
|
c.drawString(2*cm, y, self.daten.vermieter["name"])
|
|
|
|
c.save()
|
|
buffer.seek(0)
|
|
|
|
# PDF zusammenstellen
|
|
writer = PdfWriter()
|
|
|
|
# Mit Vorlage zusammenführen oder direkt verwenden
|
|
vorlage_pfad = self.daten.vorlage_datei
|
|
if vorlage_pfad.exists() and PYPDF2_AVAILABLE:
|
|
try:
|
|
vorlage = PdfReader(vorlage_pfad)
|
|
vorlage_seite = vorlage.pages[0]
|
|
inhalt = PdfReader(buffer)
|
|
vorlage_seite.merge_page(inhalt.pages[0])
|
|
writer.add_page(vorlage_seite)
|
|
except:
|
|
buffer.seek(0)
|
|
inhalt = PdfReader(buffer)
|
|
writer.add_page(inhalt.pages[0])
|
|
elif PYPDF2_AVAILABLE:
|
|
inhalt = PdfReader(buffer)
|
|
writer.add_page(inhalt.pages[0])
|
|
else:
|
|
# Ohne PyPDF2 - nur Abrechnung, keine Belege
|
|
with open(str(dateiname), "wb") as f:
|
|
f.write(buffer.getvalue())
|
|
return str(dateiname)
|
|
|
|
# Hilfsfunktion: Prüft ob Seite leer ist (mit OCR)
|
|
def ist_seite_leer(seite):
|
|
"""Prüft ob eine PDF-Seite leer/ohne sinnvollen Inhalt ist"""
|
|
try:
|
|
# Basis-Prüfungen
|
|
if '/Contents' not in seite:
|
|
return True
|
|
contents = seite['/Contents']
|
|
if contents is None:
|
|
return True
|
|
if '/MediaBox' in seite:
|
|
box = seite['/MediaBox']
|
|
if box[2] < 50 or box[3] < 50:
|
|
return True
|
|
|
|
# OCR-Prüfung: Hat die Seite lesbaren Text?
|
|
try:
|
|
import pytesseract
|
|
from pdf2image import convert_from_bytes
|
|
from PyPDF2 import PdfWriter as TempWriter
|
|
|
|
temp_writer = TempWriter()
|
|
temp_writer.add_page(seite)
|
|
temp_buffer = BytesIO()
|
|
temp_writer.write(temp_buffer)
|
|
temp_buffer.seek(0)
|
|
|
|
images = convert_from_bytes(temp_buffer.read(), dpi=100)
|
|
if not images:
|
|
return True
|
|
|
|
# Text extrahieren
|
|
text = pytesseract.image_to_string(images[0], lang='deu')
|
|
# Nur alphanumerische Zeichen zählen
|
|
alnum_chars = sum(1 for c in text if c.isalnum())
|
|
|
|
# Weniger als 50 Zeichen = wahrscheinlich leer
|
|
if alnum_chars < 50:
|
|
print(f"Seite gefiltert: nur {alnum_chars} Zeichen erkannt")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"OCR-Prüfung fehlgeschlagen: {e}")
|
|
|
|
return False
|
|
except:
|
|
return False
|
|
|
|
# Orientierung erkennen und korrigieren
|
|
def korrigiere_seite(seite):
|
|
"""Erkennt Textausrichtung mit OCR und dreht Seite falls nötig"""
|
|
try:
|
|
import pytesseract
|
|
from pdf2image import convert_from_bytes
|
|
from PyPDF2 import PdfWriter as TempWriter
|
|
|
|
# Seite temporär als PDF speichern
|
|
temp_writer = TempWriter()
|
|
temp_writer.add_page(seite)
|
|
temp_buffer = BytesIO()
|
|
temp_writer.write(temp_buffer)
|
|
temp_buffer.seek(0)
|
|
|
|
# PDF in Bild konvertieren
|
|
images = convert_from_bytes(temp_buffer.read(), dpi=150)
|
|
if not images:
|
|
return seite
|
|
|
|
# Orientierung mit Tesseract erkennen
|
|
osd = pytesseract.image_to_osd(images[0], output_type=pytesseract.Output.DICT)
|
|
rotation_needed = osd.get('rotate', 0)
|
|
|
|
print(f"OCR erkannt: Rotation {rotation_needed}°")
|
|
|
|
# Seite drehen falls nötig
|
|
if rotation_needed != 0:
|
|
seite.rotate(rotation_needed)
|
|
seite.transfer_rotation_to_content()
|
|
|
|
return seite
|
|
except Exception as e:
|
|
print(f"Orientierungserkennung fehlgeschlagen: {e}")
|
|
return seite
|
|
|
|
# Belege anhängen
|
|
beleg_seiten = [] # Sammelt alle Beleg-Seiten
|
|
if belege and PYPDF2_AVAILABLE:
|
|
for kostenart, dateien in belege.items():
|
|
for datei in dateien:
|
|
if datei and Path(datei).exists():
|
|
datei_pfad = Path(datei)
|
|
|
|
# Leere/sehr kleine Dateien überspringen
|
|
if datei_pfad.stat().st_size < 500: # < 500 Bytes
|
|
print(f"Überspringe leere Datei: {datei}")
|
|
continue
|
|
|
|
# PDFs direkt anhängen
|
|
if datei_pfad.suffix.lower() == '.pdf':
|
|
try:
|
|
beleg_pdf = PdfReader(str(datei_pfad))
|
|
for seite in beleg_pdf.pages:
|
|
# Leere Seiten überspringen
|
|
if ist_seite_leer(seite):
|
|
print(f"Überspringe leere Seite in: {datei}")
|
|
continue
|
|
# ERST rotieren
|
|
seite = korrigiere_seite(seite)
|
|
beleg_seiten.append((seite, kostenart, datei_pfad.name))
|
|
except Exception as e:
|
|
print(f"Fehler beim Anhängen von {datei}: {e}")
|
|
# Bilder in PDF konvertieren
|
|
elif datei_pfad.suffix.lower() in ['.jpg', '.jpeg', '.png']:
|
|
try:
|
|
from reportlab.lib.utils import ImageReader
|
|
bild_buffer = BytesIO()
|
|
bild_c = canvas.Canvas(bild_buffer, pagesize=A4)
|
|
bild_breite, bild_hoehe = A4
|
|
|
|
img = ImageReader(str(datei_pfad))
|
|
img_w, img_h = img.getSize()
|
|
|
|
# Skalierung (max 85% wegen Fußzeile)
|
|
max_w = bild_breite * 0.85
|
|
max_h = (bild_hoehe - 2*cm) * 0.85
|
|
scale = min(max_w / img_w, max_h / img_h)
|
|
new_w = img_w * scale
|
|
new_h = img_h * scale
|
|
|
|
# Zentriert zeichnen (etwas höher wegen Fußzeile)
|
|
x = (bild_breite - new_w) / 2
|
|
y = (bild_hoehe - new_h) / 2 + 0.5*cm
|
|
|
|
bild_c.drawImage(str(datei_pfad), x, y, new_w, new_h)
|
|
bild_c.save()
|
|
bild_buffer.seek(0)
|
|
|
|
bild_pdf = PdfReader(bild_buffer)
|
|
beleg_seiten.append((bild_pdf.pages[0], kostenart, datei_pfad.name))
|
|
except Exception as e:
|
|
print(f"Fehler beim Konvertieren von {datei}: {e}")
|
|
|
|
# Alle Beleg-Seiten zum Writer hinzufügen
|
|
for seite, _, _ in beleg_seiten:
|
|
writer.add_page(seite)
|
|
|
|
# Gesamtseitenzahl ermitteln
|
|
gesamt_seiten = len(writer.pages)
|
|
|
|
# Seitenzahlen als Overlay hinzufügen
|
|
final_writer = PdfWriter()
|
|
for i, seite in enumerate(writer.pages):
|
|
seiten_nr = i + 1
|
|
|
|
# Text für Fußzeile
|
|
seiten_text = f"Seite {seiten_nr} von {gesamt_seiten}"
|
|
anlage_text = ""
|
|
if seiten_nr > 1 and (seiten_nr - 2) < len(beleg_seiten):
|
|
_, kostenart, dateiname_beleg = beleg_seiten[seiten_nr - 2]
|
|
anlage_text = f"Anlage: {kostenart} - {dateiname_beleg}"
|
|
|
|
# Overlay erstellen - Text links am Rand vertikal
|
|
overlay_buffer = BytesIO()
|
|
overlay_c = canvas.Canvas(overlay_buffer, pagesize=A4)
|
|
overlay_c.setFont(FONT, 8)
|
|
|
|
# Beide Texte links am Rand (vertikal, von unten nach oben)
|
|
overlay_c.saveState()
|
|
overlay_c.translate(0.8*cm, 2*cm)
|
|
overlay_c.rotate(90)
|
|
# Anlage-Text und Seitenzahl kombinieren
|
|
if anlage_text:
|
|
overlay_c.drawString(0, 0, f"{anlage_text} - {seiten_text}")
|
|
else:
|
|
overlay_c.drawString(0, 0, seiten_text)
|
|
overlay_c.restoreState()
|
|
overlay_c.save()
|
|
overlay_buffer.seek(0)
|
|
|
|
# Overlay auf Seite legen
|
|
overlay_pdf = PdfReader(overlay_buffer)
|
|
seite.merge_page(overlay_pdf.pages[0])
|
|
|
|
final_writer.add_page(seite)
|
|
|
|
# Finales PDF speichern
|
|
with open(str(dateiname), "wb") as f:
|
|
final_writer.write(f)
|
|
|
|
return str(dateiname)
|
|
|
|
|
|
class StatistikSeite(QWidget):
|
|
"""Seite für Statistik/Historie"""
|
|
|
|
def __init__(self, daten: DatenManager):
|
|
super().__init__()
|
|
self.daten = daten
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setSpacing(20)
|
|
|
|
titel = QLabel("Statistik / Historie")
|
|
titel.setFont(QFont("", 18, QFont.Bold))
|
|
layout.addWidget(titel)
|
|
|
|
# Übersicht
|
|
self.tabelle = QTableWidget()
|
|
self.tabelle.setColumnCount(4)
|
|
self.tabelle.setHorizontalHeaderLabels(["Jahr", "Kosten", "Vorauszahlung", "Differenz"])
|
|
self.tabelle.horizontalHeader().setStretchLastSection(True)
|
|
self.tabelle.setSelectionBehavior(QTableWidget.SelectRows)
|
|
layout.addWidget(self.tabelle)
|
|
|
|
# Refresh Button
|
|
btn = QPushButton("Aktualisieren")
|
|
btn.clicked.connect(self.lade_daten)
|
|
layout.addWidget(btn)
|
|
|
|
self.lade_daten()
|
|
|
|
def lade_daten(self):
|
|
self.daten.lade_historie()
|
|
jahre = sorted(self.daten.historie.keys(), reverse=True)
|
|
|
|
self.tabelle.setRowCount(len(jahre))
|
|
|
|
for i, jahr in enumerate(jahre):
|
|
eintrag = self.daten.historie[jahr]
|
|
|
|
self.tabelle.setItem(i, 0, QTableWidgetItem(str(jahr)))
|
|
self.tabelle.setItem(i, 1, QTableWidgetItem(f"{eintrag.get('summe_kosten', 0):.2f} €"))
|
|
self.tabelle.setItem(i, 2, QTableWidgetItem(f"{eintrag.get('summe_vorauszahlung', 0):.2f} €"))
|
|
|
|
diff = eintrag.get('differenz', 0)
|
|
diff_item = QTableWidgetItem(f"{diff:+.2f} €")
|
|
if diff > 0:
|
|
diff_item.setForeground(QColor("#CC0000"))
|
|
else:
|
|
diff_item.setForeground(QColor("#006600"))
|
|
self.tabelle.setItem(i, 3, diff_item)
|
|
|
|
|
|
class EinstellungenSeite(QWidget):
|
|
"""Seite für Einstellungen"""
|
|
|
|
def __init__(self, daten: DatenManager):
|
|
super().__init__()
|
|
self.daten = daten
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setSpacing(20)
|
|
|
|
titel = QLabel("Einstellungen")
|
|
titel.setFont(QFont("", 18, QFont.Bold))
|
|
layout.addWidget(titel)
|
|
|
|
# Ordner-Einstellungen
|
|
ordner_group = QGroupBox("Ordner")
|
|
ol = QFormLayout(ordner_group)
|
|
|
|
# Daten-Ordner
|
|
daten_layout = QHBoxLayout()
|
|
self.daten_pfad = QLineEdit(self.daten.daten_ordner)
|
|
daten_layout.addWidget(self.daten_pfad)
|
|
btn_daten = QPushButton("...")
|
|
btn_daten.setFixedWidth(40)
|
|
btn_daten.clicked.connect(lambda: self.waehle_ordner(self.daten_pfad))
|
|
daten_layout.addWidget(btn_daten)
|
|
ol.addRow("Daten:", daten_layout)
|
|
|
|
# Dokumente-Ordner
|
|
dok_layout = QHBoxLayout()
|
|
self.dok_pfad = QLineEdit(self.daten.dokumente_ordner)
|
|
dok_layout.addWidget(self.dok_pfad)
|
|
btn_dok = QPushButton("...")
|
|
btn_dok.setFixedWidth(40)
|
|
btn_dok.clicked.connect(lambda: self.waehle_ordner(self.dok_pfad))
|
|
dok_layout.addWidget(btn_dok)
|
|
ol.addRow("Dokumente:", dok_layout)
|
|
|
|
# Vorlagen-Ordner
|
|
vorl_layout = QHBoxLayout()
|
|
self.vorl_pfad = QLineEdit(self.daten.vorlagen_ordner)
|
|
vorl_layout.addWidget(self.vorl_pfad)
|
|
btn_vorl = QPushButton("...")
|
|
btn_vorl.setFixedWidth(40)
|
|
btn_vorl.clicked.connect(lambda: self.waehle_ordner(self.vorl_pfad))
|
|
vorl_layout.addWidget(btn_vorl)
|
|
ol.addRow("Vorlagen:", vorl_layout)
|
|
|
|
layout.addWidget(ordner_group)
|
|
|
|
# PDF-Einstellungen
|
|
pdf_group = QGroupBox("PDF-Vorlage")
|
|
pl = QFormLayout(pdf_group)
|
|
|
|
self.briefkopf = QDoubleSpinBox()
|
|
self.briefkopf.setRange(0, 15)
|
|
self.briefkopf.setDecimals(1)
|
|
self.briefkopf.setSuffix(" cm")
|
|
self.briefkopf.setValue(self.daten.briefkopf_hoehe)
|
|
pl.addRow("Briefkopf-Höhe:", self.briefkopf)
|
|
|
|
self.vorlage_label = QLabel(self._get_vorlage_status())
|
|
pl.addRow("Vorlage-Status:", self.vorlage_label)
|
|
|
|
layout.addWidget(pdf_group)
|
|
|
|
# Speichern
|
|
btn_layout = QHBoxLayout()
|
|
btn_layout.addStretch()
|
|
btn = QPushButton("Speichern")
|
|
btn.setMinimumWidth(150)
|
|
btn.clicked.connect(self.speichern)
|
|
btn_layout.addWidget(btn)
|
|
layout.addLayout(btn_layout)
|
|
|
|
layout.addStretch()
|
|
|
|
def waehle_ordner(self, line_edit):
|
|
ordner = QFileDialog.getExistingDirectory(self, "Ordner wählen", line_edit.text())
|
|
if ordner:
|
|
line_edit.setText(ordner)
|
|
|
|
def _get_vorlage_status(self):
|
|
if self.daten.vorlage_datei.exists():
|
|
return f"✓ {self.daten.vorlage_datei}"
|
|
return f"Keine Vorlage in {self.daten.vorlagen_ordner}"
|
|
|
|
def speichern(self):
|
|
# Ordner aktualisieren
|
|
self.daten.daten_ordner = self.daten_pfad.text()
|
|
self.daten.dokumente_ordner = self.dok_pfad.text()
|
|
self.daten.vorlagen_ordner = self.vorl_pfad.text()
|
|
self.daten.speichere_config()
|
|
self.daten.aktualisiere_pfade()
|
|
|
|
# PDF-Einstellungen
|
|
self.daten.briefkopf_hoehe = self.briefkopf.value()
|
|
self.daten.speichere_stammdaten()
|
|
|
|
# Status aktualisieren
|
|
self.vorlage_label.setText(self._get_vorlage_status())
|
|
|
|
QMessageBox.information(self, "Gespeichert", "Einstellungen gespeichert.")
|
|
|
|
|
|
VERSION = "1.0.0"
|
|
|
|
|
|
class HauptFenster(QMainWindow):
|
|
"""Hauptfenster der Anwendung"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.daten = DatenManager()
|
|
self.setup_ui()
|
|
self.setup_menu()
|
|
|
|
def setup_ui(self):
|
|
self.setWindowTitle("Nebenkostenabrechnung")
|
|
self.setMinimumSize(900, 650)
|
|
|
|
# Icon setzen
|
|
icon_pfad = Path(__file__).parent / "icon.svg"
|
|
if icon_pfad.exists():
|
|
self.setWindowIcon(QIcon(str(icon_pfad)))
|
|
|
|
# Zentrales Widget
|
|
central = QWidget()
|
|
self.setCentralWidget(central)
|
|
layout = QHBoxLayout(central)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(0)
|
|
|
|
# Navigation (links)
|
|
self.nav = QListWidget()
|
|
self.nav.setFixedWidth(180)
|
|
self.nav.setSpacing(2)
|
|
|
|
items = [
|
|
("📝 Neue Abrechnung", 0),
|
|
("👤 Stammdaten", 1),
|
|
("📋 Kostenarten", 2),
|
|
("📊 Statistik", 3),
|
|
("⚙️ Einstellungen", 4),
|
|
]
|
|
|
|
for text, _ in items:
|
|
item = QListWidgetItem(text)
|
|
item.setSizeHint(QSize(170, 45))
|
|
self.nav.addItem(item)
|
|
|
|
self.nav.currentRowChanged.connect(self.seite_wechseln)
|
|
layout.addWidget(self.nav)
|
|
|
|
# Trennlinie
|
|
line = QFrame()
|
|
line.setFrameShape(QFrame.VLine)
|
|
line.setStyleSheet("color: #ccc;")
|
|
layout.addWidget(line)
|
|
|
|
# Seiten (rechts)
|
|
self.stack = QStackedWidget()
|
|
|
|
self.abrechnung_seite = AbrechnungSeite(self.daten)
|
|
self.stammdaten_seite = StammdatenSeite(self.daten)
|
|
self.kostenarten_seite = KostenartenSeite(self.daten)
|
|
self.statistik_seite = StatistikSeite(self.daten)
|
|
self.einstellungen_seite = EinstellungenSeite(self.daten)
|
|
|
|
self.stack.addWidget(self.abrechnung_seite)
|
|
self.stack.addWidget(self.stammdaten_seite)
|
|
self.stack.addWidget(self.kostenarten_seite)
|
|
self.stack.addWidget(self.statistik_seite)
|
|
self.stack.addWidget(self.einstellungen_seite)
|
|
|
|
content_widget = QWidget()
|
|
content_layout = QVBoxLayout(content_widget)
|
|
content_layout.setContentsMargins(20, 20, 20, 20)
|
|
content_layout.addWidget(self.stack)
|
|
|
|
layout.addWidget(content_widget, 1)
|
|
|
|
# Erste Seite auswählen
|
|
self.nav.setCurrentRow(0)
|
|
|
|
def setup_menu(self):
|
|
"""Erstellt die Menüleiste"""
|
|
menubar = self.menuBar()
|
|
|
|
# Datei-Menü
|
|
datei_menu = menubar.addMenu("&Datei")
|
|
|
|
action_neu = QAction("&Neue Abrechnung", self)
|
|
action_neu.setShortcut(QKeySequence("Ctrl+N"))
|
|
action_neu.triggered.connect(lambda: self.nav.setCurrentRow(0))
|
|
datei_menu.addAction(action_neu)
|
|
|
|
action_drucken = QAction("&Drucken...", self)
|
|
action_drucken.setShortcut(QKeySequence("Ctrl+P"))
|
|
action_drucken.triggered.connect(self.drucken)
|
|
datei_menu.addAction(action_drucken)
|
|
|
|
action_ordner = QAction("Dokumente-&Ordner öffnen", self)
|
|
action_ordner.triggered.connect(self.ordner_oeffnen)
|
|
datei_menu.addAction(action_ordner)
|
|
|
|
datei_menu.addSeparator()
|
|
|
|
action_beenden = QAction("&Beenden", self)
|
|
action_beenden.setShortcut(QKeySequence("Ctrl+Q"))
|
|
action_beenden.triggered.connect(self.close)
|
|
datei_menu.addAction(action_beenden)
|
|
|
|
# Bearbeiten-Menü
|
|
bearbeiten_menu = menubar.addMenu("&Bearbeiten")
|
|
|
|
action_stamm = QAction("&Stammdaten", self)
|
|
action_stamm.triggered.connect(lambda: self.nav.setCurrentRow(1))
|
|
bearbeiten_menu.addAction(action_stamm)
|
|
|
|
action_kosten = QAction("&Kostenarten", self)
|
|
action_kosten.triggered.connect(lambda: self.nav.setCurrentRow(2))
|
|
bearbeiten_menu.addAction(action_kosten)
|
|
|
|
action_einst = QAction("&Einstellungen", self)
|
|
action_einst.triggered.connect(lambda: self.nav.setCurrentRow(4))
|
|
bearbeiten_menu.addAction(action_einst)
|
|
|
|
# Ansicht-Menü
|
|
ansicht_menu = menubar.addMenu("&Ansicht")
|
|
|
|
action_statistik = QAction("&Statistik / Historie", self)
|
|
action_statistik.triggered.connect(lambda: self.nav.setCurrentRow(3))
|
|
ansicht_menu.addAction(action_statistik)
|
|
|
|
# Hilfe-Menü
|
|
hilfe_menu = menubar.addMenu("&?")
|
|
|
|
action_info = QAction("&Info", self)
|
|
action_info.setShortcut(QKeySequence("F1"))
|
|
action_info.triggered.connect(self.zeige_info)
|
|
hilfe_menu.addAction(action_info)
|
|
|
|
def drucken(self):
|
|
"""Öffnet den Druckdialog für das letzte PDF"""
|
|
# Prüfen ob ein PDF existiert
|
|
jahr = self.abrechnung_seite.jahr.value()
|
|
mieter_name = self.daten.mieter.get("name", "").replace(" ", "_")
|
|
pdf_pfad = Path(self.daten.dokumente_ordner) / str(jahr) / f"Nebenkostenabrechnung_{jahr}_{mieter_name}.pdf"
|
|
|
|
if not pdf_pfad.exists():
|
|
QMessageBox.warning(self, "Kein PDF", "Bitte erst eine Abrechnung erstellen.")
|
|
return
|
|
|
|
# Druckdialog
|
|
printer = QPrinter(QPrinter.HighResolution)
|
|
dialog = QPrintDialog(printer, self)
|
|
|
|
if dialog.exec() == QPrintDialog.Accepted:
|
|
# PDF mit Systembefehl drucken
|
|
import subprocess
|
|
subprocess.run(["lpr", str(pdf_pfad)])
|
|
QMessageBox.information(self, "Gedruckt", f"Druckauftrag gesendet:\n{pdf_pfad.name}")
|
|
|
|
def ordner_oeffnen(self):
|
|
"""Öffnet den Dokumente-Ordner"""
|
|
subprocess.run(["xdg-open", self.daten.dokumente_ordner])
|
|
|
|
def zeige_info(self):
|
|
"""Zeigt den Info-Dialog"""
|
|
info_text = f"""<h2>Nebenkostenabrechnung</h2>
|
|
<p><b>Version:</b> {VERSION}</p>
|
|
<p><b>Beschreibung:</b><br>
|
|
Erstellt Nebenkostenabrechnungen für Mietobjekte
|
|
gemäß § 556 BGB mit allen Pflichtangaben.</p>
|
|
|
|
<p><b>Funktionen:</b></p>
|
|
<ul>
|
|
<li>Automatische Speicherung von Entwürfen</li>
|
|
<li>Belege anhängen (PDF, JPG, PNG)</li>
|
|
<li>PDF-Vorlage mit Briefkopf</li>
|
|
<li>Vorjahresvergleich</li>
|
|
<li>Historie aller Abrechnungen</li>
|
|
</ul>
|
|
|
|
<p><b>Ordner:</b></p>
|
|
<ul>
|
|
<li>Daten: {self.daten.daten_ordner}</li>
|
|
<li>Dokumente: {self.daten.dokumente_ordner}</li>
|
|
<li>Vorlagen: {self.daten.vorlagen_ordner}</li>
|
|
</ul>
|
|
|
|
<p><b>PDF-Vorlage:</b><br>
|
|
Legen Sie eine Datei <code>vorlage.pdf</code> im Vorlagen-Ordner ab,
|
|
um einen Briefkopf/Wasserzeichen zu verwenden.</p>
|
|
|
|
<hr>
|
|
<p style="color: gray;">© 2024 - Entwickelt mit Python & PySide6</p>
|
|
"""
|
|
|
|
QMessageBox.about(self, "Info", info_text)
|
|
|
|
def seite_wechseln(self, index):
|
|
self.stack.setCurrentIndex(index)
|
|
|
|
# Daten aktualisieren
|
|
if index == 0:
|
|
self.abrechnung_seite.aktualisiere_kosten()
|
|
elif index == 2:
|
|
self.kostenarten_seite.lade_daten()
|
|
elif index == 3:
|
|
self.statistik_seite.lade_daten()
|
|
|
|
|
|
def installiere_startmenu():
|
|
"""Installiert Startmenü-Eintrag beim ersten Start"""
|
|
# Pfade
|
|
programm_ordner = Path(__file__).parent.resolve()
|
|
desktop_quelle = programm_ordner / "nebenkostenabrechnung.desktop"
|
|
desktop_ziel = Path.home() / ".local" / "share" / "applications" / "nebenkostenabrechnung.desktop"
|
|
|
|
# Nur installieren wenn Quelldatei existiert und Ziel noch nicht
|
|
if desktop_quelle.exists() and not desktop_ziel.exists():
|
|
try:
|
|
desktop_ziel.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(desktop_quelle, desktop_ziel)
|
|
print(f"Startmenü-Eintrag installiert: {desktop_ziel}")
|
|
except Exception as e:
|
|
print(f"Startmenü-Installation fehlgeschlagen: {e}")
|
|
|
|
|
|
def main():
|
|
# Startmenü-Eintrag installieren (beim ersten Start)
|
|
installiere_startmenu()
|
|
|
|
app = QApplication(sys.argv)
|
|
app.setStyle("Fusion")
|
|
|
|
fenster = HauptFenster()
|
|
fenster.show()
|
|
|
|
sys.exit(app.exec())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|