#!/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"""
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""" # 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()