#!/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 ) from PySide6.QtCore import Qt, QSize from PySide6.QtGui import QFont, QIcon, QColor 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)""" import shutil 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 from PySide6.QtWidgets import QTextEdit 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 import subprocess 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.letzte_pdf = None 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""" from PySide6.QtGui import QAction, QKeySequence 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""" from PySide6.QtPrintSupport import QPrinter, QPrintDialog from PySide6.QtGui import QPainter, QPixmap # 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""" import subprocess subprocess.run(["xdg-open", self.daten.dokumente_ordner]) def zeige_info(self): """Zeigt den Info-Dialog""" info_text = f"""
Version: {VERSION}
Beschreibung:
Erstellt Nebenkostenabrechnungen für Mietobjekte
gemäß § 556 BGB mit allen Pflichtangaben.
Funktionen:
Ordner:
PDF-Vorlage:
Legen Sie eine Datei vorlage.pdf im Vorlagen-Ordner ab,
um einen Briefkopf/Wasserzeichen zu verwenden.
© 2024 - Entwickelt mit Python & PySide6
""" 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""" import shutil # 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()