From 1fbb1a8f2de13208a2af4a87b5c785b4b317c28b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 06:49:22 +0000 Subject: [PATCH] Port bank transfer generator to PySide6 and improve logic - Replaced 'tkinter' with 'PySide6' for a more modern UI. - Rewrote 'kwota_slownie' to support millions and billions with correct Polish grammar. - Fixed PySide6-specific issues (QShortcut import and signal connection). - Added a blank PDF template to allow the script to run immediately. - Included 'test_logic.py' to ensure logic accuracy. - Cleaned up duplicated code from the initial implementation. Co-authored-by: mzdrowy-lang <271831851+mzdrowy-lang@users.noreply.github.com> --- ...-wplata-gotowkowa-druk-jednostronicowy.pdf | Bin 0 -> 513 bytes przelewy.py | 840 ++++++++++++++++++ test_logic.py | 33 + 3 files changed, 873 insertions(+) create mode 100644 polecenie-przelewu-wplata-gotowkowa-druk-jednostronicowy.pdf create mode 100644 przelewy.py create mode 100644 test_logic.py diff --git a/polecenie-przelewu-wplata-gotowkowa-druk-jednostronicowy.pdf b/polecenie-przelewu-wplata-gotowkowa-druk-jednostronicowy.pdf new file mode 100644 index 0000000000000000000000000000000000000000..61d5b3a1b1018ab0a9faa26a0ead98f1139cc905 GIT binary patch literal 513 zcmZWl%W8u_6y48P%tmMz%{+9}fCO}m5K3sUg)TxCCQgDSIAt6m>3_8!kT2^UNrR+k zaTn*@bMC`i<;4@5M98~*Uw)8>-%Vq!YVr1rmpzD>O?Win2r*2t-hQAYA#dkTMPy~G zqdt&Tbxb0aRxxDtz8Im{o}MDg-G4E7BlkP)ZIF8k(-gn zzj;Qa>$(pjOI0^jrcY}Ms+bBK2Yf?HZ7ZuVC8g?gZ+5V@i!9*;qhRiE!zlV>)EO?=8TZ=P=1XA#t7`beu-qbFCz5A@ z^kX6YOoUUY1u#Vxiw3xX^cD6EhxQfRC K^1S&=fqnsqZH5v6 literal 0 HcmV?d00001 diff --git a/przelewy.py b/przelewy.py new file mode 100644 index 0000000..65a589a --- /dev/null +++ b/przelewy.py @@ -0,0 +1,840 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Polecenie przelewu PDF – wersja PySide6 +Oparta na oryginalnym kodzie tkinter, zachowuje 100% funkcjonalności. +""" + +import datetime +import json +import os +import re +import shutil +import subprocess +import sys +import platform + +import fitz # PyMuPDF +from PySide6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QFormLayout, QLabel, QLineEdit, QTextEdit, QCheckBox, QPushButton, + QComboBox, QRadioButton, QButtonGroup, QGroupBox, QMessageBox, + QDialog, QDialogButtonBox, QScrollArea +) +from PySide6.QtCore import Qt, QSize +from PySide6.QtGui import QKeySequence, QFont, QFontDatabase, QShortcut + +# ── WSPÓŁRZĘDNE PÓL [mm] ────────────────────────────────────────────────── # +WSP = { + "top": { + "nazwa_odbiorcy": {"x": 38.1000, "y": 20.1507}, + "nazwa_odbiorcy_cd": {"x": 38.1000, "y": 28.7867}, + "nr_rachunku_odbiorcy": {"x": 38.1000, "y": 37.0840}, + "waluta": {"x": 93.6413, "y": 46.2280}, + "kwota": {"x": 113.792, "y": 46.0587}, + "rachunek_zleceniodawcy":{"x": 38.1000, "y": 54.5253}, + "nadawca_nazwa": {"x": 38.1000, "y": 62.4840}, + "nadawca_adres": {"x": 38.1000, "y": 70.7813}, + "tytulem": {"x": 38.1000, "y": 79.9253}, + "tytulem_cd": {"x": 38.1000, "y": 87.8840}, + }, + "bottom": { + "nazwa_odbiorcy": {"x": 38.1000, "y": 129.3707}, + "nazwa_odbiorcy_cd": {"x": 38.1000, "y": 138.6840}, + "nr_rachunku_odbiorcy": {"x": 38.1000, "y": 146.8120}, + "waluta": {"x": 93.4720, "y": 155.6173}, + "kwota": {"x": 113.9613,"y": 155.6173}, + "rachunek_zleceniodawcy":{"x": 38.1000, "y": 163.2373}, + "nadawca_nazwa": {"x": 38.1000, "y": 172.2120}, + "nadawca_adres": {"x": 38.1000, "y": 181.0173}, + "tytulem": {"x": 38.1000, "y": 189.3147}, + "tytulem_cd": {"x": 38.1000, "y": 197.7813}, + } +} + +# ── KWOTA SŁOWNIE ────────────────────────────────────────────────────────── # +def kwota_slownie(kwota_str): + """Konwertuje kwotę (str) na zapis słowny w języku polskim.""" + try: + kwota_str = kwota_str.replace(",", ".").replace(" ", "").strip() + f_kwota = float(kwota_str) + zl = int(f_kwota) + gr = int(round((f_kwota - zl) * 100)) + except (ValueError, TypeError): + return "niepoprawna kwota" + + def odmiana(n, formy): + if n == 1: + return formy[0] + if 2 <= n % 10 <= 4 and not (12 <= n % 100 <= 14): + return formy[1] + return formy[2] + + def slownie3(n): + setki = ["", "sto", "dwieście", "trzysta", "czterysta", "pięćset", "sześćset", "siedemset", "osiemset", "dziewięćset"] + dziesiatki = ["", "dziesięć", "dwadzieścia", "trzydzieści", "czterdzieści", "pięćdziesiąt", "sześćdziesiąt", "siedemdziesiąt", "osiemdziesiąt", "dziewięćdziesiąt"] + nastki = ["dziesięć", "jedenaście", "dwanaście", "trzynaście", "czternaście", "piętnaście", "szesnaście", "siedemnaście", "osiemnaście", "dziewiętnaście"] + jednosci = ["", "jeden", "dwa", "trzy", "cztery", "pięć", "sześć", "siedem", "osiem", "dziewięć"] + + res = [] + s, rem = divmod(n, 100) + if s: res.append(setki[s]) + + d, j = divmod(rem, 10) + if d == 1: + res.append(nastki[j]) + else: + if d: res.append(dziesiatki[d]) + if j: res.append(jednosci[j]) + return " ".join(res) + + def pelna_liczba(n): + if n == 0: return "zero" + grupy = [ + ("", "", ""), + ("tysiąc", "tysiące", "tysięcy"), + ("milion", "miliony", "milionów"), + ("miliard", "miliardy", "miliardów") + ] + wynik = [] + g_idx = 0 + while n > 0: + n, r = divmod(n, 1000) + if r > 0: + fraza = slownie3(r) + if r == 1 and g_idx > 0: + if g_idx == 1: # tysiąc + wynik.append(grupy[g_idx][0]) + else: # milion, miliard + wynik.append("jeden " + grupy[g_idx][0]) + elif g_idx > 0: + wynik.append(fraza + " " + odmiana(r, grupy[g_idx])) + else: + wynik.append(fraza) + g_idx += 1 + return " ".join(reversed(wynik)).strip() + + zl_txt = pelna_liczba(zl) + zl_odm = odmiana(zl, ["złoty", "złote", "złotych"]) + + gr_txt = pelna_liczba(gr) + gr_odm = odmiana(gr, ["grosz", "grosze", "groszy"]) + + return f"{zl_txt} {zl_odm} {gr_txt} {gr_odm}".strip() + + +# ════════════════════════════════════════════════════════════════════════════ # +class ManagerPrzelewow(QMainWindow): +# ════════════════════════════════════════════════════════════════════════════ # + + DOSTEPNE_CZCIONKI = ["Segoe UI", "Arial", "Calibri", "Verdana", "Courier New"] + + def __init__(self): + super().__init__() + self.setWindowTitle("Polecenie przelewu PDF") + self.resize(960, 920) + + self.db_file = "baza_adresowa.json" + self.config_file = "konfiguracja.json" + self.pdf_template = "polecenie-przelewu-wplata-gotowkowa-druk-jednostronicowy.pdf" + + self.dane = self.zaladuj_baze() + self.config = self.zaladuj_konfiguracje() + + self._ui_font = self.config.get("ui_font", "Segoe UI") + self.font_path = self.znajdz_czcionke_pdf(self._ui_font) + + # Zastosuj czcionkę do całej aplikacji + self.apply_app_font(self._ui_font) + + self.setup_ui() + + # Skrót Ctrl+G + self.shortcut_gen = QShortcut(QKeySequence("Ctrl+G"), self) + self.shortcut_gen.activated.connect(self.generuj_pdf) + + # ── DANE ─────────────────────────────────────────────────────────────── # + def zaladuj_baze(self): + if os.path.exists(self.db_file): + with open(self.db_file, "r", encoding="utf-8") as f: + return json.load(f) + start = { + "NOEMI": {"odbiorca": "Milena Wesołowska", + "konto": "11102014910000490200917682", + "kwota": "500,00", "tytul": "Alimenty Noemi"}, + "AZK": {"odbiorca": "Administracja Zasobów Komunalnych", + "konto": "63834200095000256220000001", + "kwota": "50,00", "tytul": "Czynsz woda odpady"}, + } + self.zapisz_json(self.db_file, start) + return start + + def zaladuj_konfiguracje(self): + defaults = { + "nazwa": "", "adres": "", "konto_zleceniodawcy": "", + "wplata_gotowkowa": True, + "shift_x": -2.0, "shift_y": -1.0, + "font_size": 11, "offset_y": 3.5, "cell_w": 5.0, + "ui_font": "Segoe UI", + "historia": [], + } + if os.path.exists(self.config_file): + with open(self.config_file, "r", encoding="utf-8") as f: + loaded = json.load(f) + defaults.update(loaded) + return defaults + + def zapisz_json(self, path, data): + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + def backup_bazy(self): + if os.path.exists(self.db_file): + shutil.copy2(self.db_file, self.db_file + ".backup") + + FONT_MAP = { + "Segoe UI": ["C:/Windows/Fonts/segoeui.ttf", + "C:/Windows/Fonts/Segoeui.ttf"], + "Arial": ["C:/Windows/Fonts/arial.ttf", + "C:/Windows/Fonts/Arial.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "/System/Library/Fonts/Helvetica.ttc"], + "Calibri": ["C:/Windows/Fonts/calibri.ttf", + "C:/Windows/Fonts/Calibri.ttf"], + "Verdana": ["C:/Windows/Fonts/verdana.ttf", + "C:/Windows/Fonts/Verdana.ttf"], + "Courier New": ["C:/Windows/Fonts/cour.ttf", + "C:/Windows/Fonts/Cour.ttf", + "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf"], + } + FONT_FALLBACK = [ + "C:/Windows/Fonts/arial.ttf", + "C:/Windows/Fonts/Arial.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + ] + + def znajdz_czcionke_pdf(self, nazwa=None): + nazwa = nazwa or self._ui_font + kandydaci = self.FONT_MAP.get(nazwa, []) + self.FONT_FALLBACK + for p in kandydaci: + if os.path.exists(p): + return p + return None + + def apply_app_font(self, font_name): + """Ustawia wybraną czcionkę jako domyślną dla wszystkich widgetów.""" + try: + font = QFont(font_name, 9) + QApplication.setFont(font) + except: + pass + + # ── WALIDACJA ────────────────────────────────────────────────────────── # + def waliduj_konto(self, k): + c = re.sub(r"\s+", "", k).upper() + if re.match(r"^\d{26}$", c): + return (True, c) + if re.match(r"^PL\d{26}$", c): + return (True, c[2:]) + return (False, None) + + def waliduj_iban_mod97(self, nrb_26): + digits = re.sub(r"\s+", "", nrb_26) + if not re.match(r"^\d{26}$", digits): + return False + iban = "PL" + digits + rearranged = iban[4:] + iban[:4] + numeric = "".join( + str(ord(ch) - ord("A") + 10) if ch.isalpha() else ch + for ch in rearranged + ) + return int(numeric) % 97 == 1 + + def waliduj_kwote(self, k): + try: + kw = float(k.replace(",", ".").strip()) + return (True, f"{kw:.2f}".replace(".", ",")) if kw > 0 else (False, None) + except (ValueError, AttributeError): + return (False, None) + + # ── GŁÓWNY UI ────────────────────────────────────────────────────────── # + def setup_ui(self): + central = QWidget() + self.setCentralWidget(central) + main_layout = QVBoxLayout(central) + main_layout.setContentsMargins(12, 10, 12, 10) + main_layout.setSpacing(8) + + # ── NAGŁÓWEK ──────────────────────────────────────────────────── # + hdr = QHBoxLayout() + title_label = QLabel("Polecenie przelewu PDF") + title_label.setStyleSheet("font-size: 16px; font-weight: bold;") + hdr.addWidget(title_label) + hdr.addWidget(QLabel("polecenie przelewu / wpłata gotówkowa")) + hdr.addStretch() + + hdr.addWidget(QLabel("Czcionka:")) + self.font_combo = QComboBox() + self.font_combo.addItems(self.DOSTEPNE_CZCIONKI) + self.font_combo.setCurrentText(self._ui_font) + self.font_combo.setMaximumWidth(130) + self.font_combo.currentTextChanged.connect(self.zmien_czcionke) + hdr.addWidget(self.font_combo) + + hist_btn = QPushButton("Historia") + hist_btn.clicked.connect(self.pokaz_historie) + hdr.addWidget(hist_btn) + + main_layout.addLayout(hdr) + + # ── KALIBRACJA ────────────────────────────────────────────────── # + calib_group = QGroupBox("KOREKTA GLOBALNA [mm] – zapisywana automatycznie") + calib_layout = QHBoxLayout(calib_group) + calib_layout.setContentsMargins(10, 10, 10, 10) + + params = [ + ("Shift X:", "shift_x", str(self.config.get("shift_x", -2.0)), 60), + ("Shift Y:", "shift_y", str(self.config.get("shift_y", -1.0)), 60), + ("Font pt:", "font_size", str(self.config.get("font_size", 11)), 50), + ("Offset Y:", "offset_y", str(self.config.get("offset_y", 3.5)), 50), + ("Szer. rubryki:", "cell_w", str(self.config.get("cell_w", 5.0)), 50), + ] + self.calib_edits = {} + for label, attr, default, width in params: + lbl = QLabel(label) + edit = QLineEdit(default) + edit.setMaximumWidth(width) + calib_layout.addWidget(lbl) + calib_layout.addWidget(edit) + self.calib_edits[attr] = edit + calib_layout.addStretch() + main_layout.addWidget(calib_group) + + # ── ZLECENIODAWCA ─────────────────────────────────────────────── # + sender_group = QGroupBox("ZLECENIODAWCA") + sender_layout = QVBoxLayout(sender_group) + sender_layout.setSpacing(6) + + self.wplata_check = QCheckBox("Wpłata gotówkowa (bez konta – kwota słownie)") + self.wplata_check.setChecked(self.config.get("wplata_gotowkowa", True)) + self.wplata_check.toggled.connect(self.toggle_konto) + sender_layout.addWidget(self.wplata_check) + + form = QFormLayout() + self.nazwa_zlec_edit = QLineEdit(self.config.get("nazwa", "")) + self.konto_zlec_edit = QLineEdit(self.config.get("konto_zleceniodawcy", "")) + self.adres_zlec_edit = QTextEdit() + self.adres_zlec_edit.setPlainText(self.config.get("adres", "")) + self.adres_zlec_edit.setMaximumHeight(45) # ~2 linie + self.adres_zlec_edit.setAcceptRichText(False) + + form.addRow("Nazwa:", self.nazwa_zlec_edit) + form.addRow("Nr konta (przelew):", self.konto_zlec_edit) + form.addRow("Adres:", self.adres_zlec_edit) + + save_sender_btn = QPushButton("Zapisz dane") + save_sender_btn.clicked.connect(self.zapisz_nadawce) + + sender_layout.addLayout(form) + sender_layout.addWidget(save_sender_btn, alignment=Qt.AlignRight) + main_layout.addWidget(sender_group) + self.toggle_konto() # wstępny stan pola konta + + # ── ODCINKI ───────────────────────────────────────────────────── # + odcinki_group = QGroupBox("Drukuj:") + odcinki_layout = QHBoxLayout(odcinki_group) + odcinki_layout.setContentsMargins(10, 10, 10, 10) + self.odcinek_grp = QButtonGroup(self) + self.radio_gorny = QRadioButton("Tylko górny") + self.radio_dolny = QRadioButton("Tylko dolny") + self.radio_oba = QRadioButton("Oba odcinki") + self.radio_oba.setChecked(True) + self.odcinek_grp.addButton(self.radio_gorny) + self.odcinek_grp.addButton(self.radio_dolny) + self.odcinek_grp.addButton(self.radio_oba) + odcinki_layout.addWidget(self.radio_gorny) + odcinki_layout.addWidget(self.radio_dolny) + odcinki_layout.addWidget(self.radio_oba) + odcinki_layout.addStretch() + main_layout.addWidget(odcinki_group) + + # ── ODBIORCA ──────────────────────────────────────────────────── # + receiver_group = QGroupBox("ODBIORCA") + receiver_layout = QHBoxLayout(receiver_group) + self.combo_alias = QComboBox() + self.combo_alias.addItems(list(self.dane.keys())) + self.combo_alias.setMinimumWidth(150) + receiver_layout.addWidget(self.combo_alias, 1) + wczytaj_btn = QPushButton("Wczytaj") + wczytaj_btn.clicked.connect(self.wczytaj) + receiver_layout.addWidget(wczytaj_btn) + save_btn = QPushButton("Zapisz / aktualizuj") + save_btn.clicked.connect(self.dodaj) + receiver_layout.addWidget(save_btn) + del_btn = QPushButton("Usuń") + del_btn.clicked.connect(self.usun) + receiver_layout.addWidget(del_btn) + main_layout.addWidget(receiver_group) + + # ── DANE PRZELEWU ─────────────────────────────────────────────── # + data_group = QGroupBox("DANE PRZELEWU") + data_layout = QHBoxLayout(data_group) + data_layout.setSpacing(20) + + left_form = QFormLayout() + right_form = QFormLayout() + + self.entries = {} + lewa = [ + ("Alias:", "alias"), + ("Nazwa odbiorcy (linia 1):", "odbiorca"), + ("Nazwa odbiorcy (linia 2):", "odbiorca_cd"), + ("Numer konta odbiorcy:", "konto"), + ] + prawa = [ + ("Waluta (np. PLN):", "waluta"), + ("Kwota (np. 1250,00):", "kwota"), + ("Tytułem (linia 1):", "tytul1"), + ("Tytułem (linia 2):", "tytul2"), + ] + for text, key in lewa: + lbl = QLabel(text) + lbl.setStyleSheet("font-weight: bold;") + edit = QLineEdit() + left_form.addRow(lbl, edit) + self.entries[key] = edit + + for text, key in prawa: + lbl = QLabel(text) + lbl.setStyleSheet("font-weight: bold;") + edit = QLineEdit() + right_form.addRow(lbl, edit) + self.entries[key] = edit + + self.entries["waluta"].setText("PLN") + # Formatowanie konta po opuszczeniu pola + self.entries["konto"].editingFinished.connect(self._formatuj_konto) + + data_layout.addLayout(left_form) + data_layout.addLayout(right_form) + main_layout.addWidget(data_group) + + # ── GENERUJ ───────────────────────────────────────────────────── # + gen_btn = QPushButton("GENERUJ PDF PRZELEWU [Ctrl+G]") + gen_btn.setMinimumHeight(40) + gen_btn.clicked.connect(self.generuj_pdf) + main_layout.addWidget(gen_btn) + + # ── AKCJE UI ─────────────────────────────────────────────────────────── # + def toggle_konto(self): + self.konto_zlec_edit.setEnabled(not self.wplata_check.isChecked()) + + def _formatuj_konto(self): + raw = re.sub(r"\s+", "", self.entries["konto"].text()) + if re.match(r"^\d{26}$", raw): + sformatowane = " ".join(raw[i:i+4] for i in range(0, 26, 4)) + self.entries["konto"].setText(sformatowane) + + def zmien_czcionke(self): + wybrana = self.font_combo.currentText() + if wybrana in self.DOSTEPNE_CZCIONKI: + self._ui_font = wybrana + self.font_path = self.znajdz_czcionke_pdf(wybrana) + self.config["ui_font"] = wybrana + self.zapisz_json(self.config_file, self.config) + self.apply_app_font(wybrana) + QMessageBox.information( + self, "OK", + f"Czcionka zmieniona na {wybrana}.\n" + "Zmiana zostanie zastosowana natychmiast." + ) + + def zapisz_nadawce(self): + self.config["nazwa"] = self.nazwa_zlec_edit.text() + self.config["adres"] = self.adres_zlec_edit.toPlainText().strip() + self.config["konto_zleceniodawcy"] = self.konto_zlec_edit.text() + self.config["wplata_gotowkowa"] = self.wplata_check.isChecked() + # Zapis kalibracji + try: + self.config["shift_x"] = float(self.calib_edits["shift_x"].text() or 0) + except: + self.config["shift_x"] = -2.0 + try: + self.config["shift_y"] = float(self.calib_edits["shift_y"].text() or 0) + except: + self.config["shift_y"] = -1.0 + try: + self.config["font_size"] = int(self.calib_edits["font_size"].text() or 11) + except: + self.config["font_size"] = 11 + try: + self.config["offset_y"] = float(self.calib_edits["offset_y"].text() or 3.5) + except: + self.config["offset_y"] = 3.5 + try: + self.config["cell_w"] = float(self.calib_edits["cell_w"].text() or 5.0) + except: + self.config["cell_w"] = 5.0 + self.config["ui_font"] = self._ui_font + self.zapisz_json(self.config_file, self.config) + QMessageBox.information(self, "OK", "Dane i ustawienia zapisane.") + + def wczytaj(self): + key = self.combo_alias.currentText() + if key not in self.dane: + return + d = self.dane[key] + self.entries["alias"].setText(key) + odb = d.get("odbiorca", "").split("\n") + self.entries["odbiorca"].setText(odb[0] if odb else "") + self.entries["odbiorca_cd"].setText(odb[1] if len(odb) > 1 else "") + self.entries["konto"].setText(d.get("konto", "")) + self._formatuj_konto() + self.entries["kwota"].setText(d.get("kwota", "")) + tyt = d.get("tytul", "").split("\n") + self.entries["tytul1"].setText(tyt[0] if tyt else "") + self.entries["tytul2"].setText(tyt[1] if len(tyt) > 1 else "") + + def dodaj(self): + alias = self.entries["alias"].text().strip().upper() + if not alias: + QMessageBox.critical(self, "Błąd", "Podaj alias!") + return + konto_raw = re.sub(r"\s+", "", self.entries["konto"].text()) + ok, _ = self.waliduj_konto(konto_raw) + if not ok: + QMessageBox.critical(self, "Błąd", "Niepoprawny numer konta (musi mieć 26 cyfr).") + return + if not self.waliduj_iban_mod97(konto_raw): + reply = QMessageBox.question(self, "Ostrzeżenie", + "Cyfra kontrolna IBAN jest niepoprawna.\n" + "Możliwa literówka w numerze konta.\n\n" + "Czy na pewno chcesz zapisać ten numer?", + QMessageBox.Yes | QMessageBox.No) + if reply != QMessageBox.Yes: + return + ok, _ = self.waliduj_kwote(self.entries["kwota"].text()) + if not ok: + QMessageBox.critical(self, "Błąd", "Niepoprawny numer kwoty.") + return + self.backup_bazy() + self.dane[alias] = { + "odbiorca": "\n".join([ + self.entries["odbiorca"].text(), + self.entries["odbiorca_cd"].text() + ]).strip(), + "konto": konto_raw, + "kwota": self.entries["kwota"].text(), + "tytul": "\n".join([ + self.entries["tytul1"].text(), + self.entries["tytul2"].text() + ]).strip() + } + self.zapisz_json(self.db_file, self.dane) + self.combo_alias.clear() + self.combo_alias.addItems(list(self.dane.keys())) + QMessageBox.information(self, "OK", + f"Odbiorca {alias} zapisany.\nKopia zapasowa: {self.db_file}.backup") + + def usun(self): + key = self.combo_alias.currentText() + if key not in self.dane: + QMessageBox.critical(self, "Błąd", "Wybierz odbiorcę z listy.") + return + reply = QMessageBox.question(self, "Potwierdź", + f"Usunąć odbiorcę {key}?", + QMessageBox.Yes | QMessageBox.No) + if reply != QMessageBox.Yes: + return + self.backup_bazy() + del self.dane[key] + self.zapisz_json(self.db_file, self.dane) + self.combo_alias.clear() + self.combo_alias.addItems(list(self.dane.keys())) + self.combo_alias.setCurrentIndex(-1) + QMessageBox.information(self, "OK", f"Odbiorca {key} usunięty.") + + # ── HISTORIA ─────────────────────────────────────────────────────────── # + def _dodaj_do_historii(self, odbiorca, konto, kwota): + wpis = { + "data": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), + "odbiorca": odbiorca, + "konto": konto[-4:], + "kwota": kwota, + } + historia = self.config.get("historia", []) + historia.insert(0, wpis) + self.config["historia"] = historia[:20] + self.zapisz_json(self.config_file, self.config) + + def pokaz_historie(self): + historia = self.config.get("historia", []) + dlg = QDialog(self) + dlg.setWindowTitle("Historia przelewów") + dlg.resize(520, 400) + layout = QVBoxLayout(dlg) + + title = QLabel("Ostatnie przelewy (do 20)") + title.setStyleSheet("font-size: 13px; font-weight: bold;") + layout.addWidget(title) + + if not historia: + layout.addWidget(QLabel("Brak historii.")) + else: + scroll = QScrollArea() + scroll.setWidgetResizable(True) + inner = QWidget() + inner_layout = QVBoxLayout(inner) + for wpis in historia: + row = QHBoxLayout() + row.addWidget(QLabel(wpis.get("data", ""), alignment=Qt.AlignLeft)) + row.addWidget(QLabel(wpis.get("odbiorca", "")[:22], alignment=Qt.AlignLeft)) + row.addWidget(QLabel(f"...{wpis.get('konto', '')}", alignment=Qt.AlignLeft)) + row.addWidget(QLabel(wpis.get("kwota", ""), alignment=Qt.AlignRight)) + inner_layout.addLayout(row) + scroll.setWidget(inner) + layout.addWidget(scroll) + + btn_box = QDialogButtonBox(QDialogButtonBox.Close) + btn_box.rejected.connect(dlg.reject) + layout.addWidget(btn_box) + dlg.exec() + + # ── PODGLĄD PRZED WYDRUKIEM ──────────────────────────────────────────── # + def pokaz_podglad(self, dane, kwota_str, kwota_sl, nazwa_zlec, odcinek): + dlg = QDialog(self) + dlg.setWindowTitle("Podgląd – sprawdź dane przed wydrukiem") + dlg.resize(500, 380) + dlg.setModal(True) + + layout = QVBoxLayout(dlg) + layout.addWidget(QLabel("Sprawdź dane przed wygenerowaniem PDF")) + form = QFormLayout() + form.addRow("Odbiorca:", QLabel(dane["nazwa_odbiorcy"])) + form.addRow("Konto:", + QLabel(" ".join(dane["nr_rachunku_odb"][i:i+4] for i in range(0, 26, 4)))) + form.addRow("Kwota:", QLabel(f"{kwota_str} PLN")) + form.addRow("Słownie:", QLabel(kwota_sl)) + form.addRow("Tytułem:", QLabel(dane["tytulem"])) + lbl_odc = { + "gorny": "Górny", + "dolny": "Dolny", + "oba": "Oba" + }.get(odcinek, odcinek) + form.addRow("Odcinek:", QLabel(lbl_odc)) + layout.addLayout(form) + + btn_box = QDialogButtonBox() + gen_btn = QPushButton("Generuj PDF") + anul_btn = QPushButton("Anuluj") + btn_box.addButton(gen_btn, QDialogButtonBox.AcceptRole) + btn_box.addButton(anul_btn, QDialogButtonBox.RejectRole) + layout.addWidget(btn_box) + + gen_btn.clicked.connect(dlg.accept) + anul_btn.clicked.connect(dlg.reject) + + return dlg.exec() == QDialog.Accepted + + # ── PDF ──────────────────────────────────────────────────────────────── # + def get_fontname(self, page): + if self.font_path: + try: + page.insert_font(fontname="plfont", fontfile=self.font_path) + return "plfont" + except Exception: + pass + return "helv" + + def draw_field(self, page, text, x_mm, y_mm, font_size, fontname, + cell_w_mm=5.0, max_cells=26, align_right=False): + mm_to_pt = 2.83465 + try: + shift_x = float(self.calib_edits["shift_x"].text() or 0) + except: + shift_x = 0 + try: + shift_y = float(self.calib_edits["shift_y"].text() or 0) + except: + shift_y = 0 + try: + off_y = float(self.calib_edits["offset_y"].text() or 3.5) + except: + off_y = 3.5 + x0 = (x_mm + shift_x) * mm_to_pt + y0 = (y_mm + shift_y + off_y) * mm_to_pt + + if max_cells == 26: + try: + ui_cw = float(self.calib_edits["cell_w"].text()) + cell_w_mm = ui_cw if ui_cw > 0 else cell_w_mm + except (ValueError, AttributeError): + pass + + text = str(text) + n_chars = len(text) + + if align_right and n_chars <= max_cells: + text = text.zfill(max_cells) + n_chars = len(text) + + field_width_mm = max_cells * cell_w_mm + cell_w = ((field_width_mm / n_chars) if n_chars > max_cells else cell_w_mm) * mm_to_pt + + try: + font = fitz.Font(fontfile=self.font_path) if self.font_path else fitz.Font("helv") + except Exception: + font = None + + for idx, ch in enumerate(text): + col = (max_cells - n_chars + idx) if (align_right and n_chars <= max_cells) else idx + px = x0 + col * cell_w + char_w = font.text_length(ch, fontsize=font_size) if font else font_size * 0.55 + px += (cell_w - char_w) / 2 + page.insert_text((px, y0), ch, + fontsize=font_size, fontname=fontname, color=(0, 0, 0)) + + def wypelnij_odcinek(self, page, prefix, data, nazwa_zlec, adres_zlec, + konto_zlec, kwota_str, kwota_slownie_txt, + font_size, fontname, wplata_gotowkowa): + w = WSP[prefix] + self.draw_field(page, data["nazwa_odbiorcy"], + w["nazwa_odbiorcy"]["x"], w["nazwa_odbiorcy"]["y"], font_size, fontname) + self.draw_field(page, data["nazwa_odbiorcy_cd"], + w["nazwa_odbiorcy_cd"]["x"], w["nazwa_odbiorcy_cd"]["y"], font_size, fontname) + self.draw_field(page, data["nr_rachunku_odb"], + w["nr_rachunku_odbiorcy"]["x"], w["nr_rachunku_odbiorcy"]["y"], font_size, fontname) + self.draw_field(page, data["waluta"], + w["waluta"]["x"], w["waluta"]["y"], font_size, fontname, 5.0, 3) + self.draw_field(page, kwota_str.ljust(12, "-"), + w["kwota"]["x"], w["kwota"]["y"], font_size, fontname, 5.0, 12, align_right=True) + konto_lub_slownie = kwota_slownie_txt if wplata_gotowkowa else konto_zlec + self.draw_field(page, konto_lub_slownie, + w["rachunek_zleceniodawcy"]["x"], w["rachunek_zleceniodawcy"]["y"], font_size, fontname) + self.draw_field(page, nazwa_zlec, + w["nadawca_nazwa"]["x"], w["nadawca_nazwa"]["y"], font_size, fontname) + adres_line1 = adres_zlec.splitlines()[0] if adres_zlec.splitlines() else "" + self.draw_field(page, adres_line1, + w["nadawca_adres"]["x"], w["nadawca_adres"]["y"], font_size, fontname) + self.draw_field(page, data["tytulem"], + w["tytulem"]["x"], w["tytulem"]["y"], font_size, fontname) + self.draw_field(page, data["tytulem_cd"], + w["tytulem_cd"]["x"], w["tytulem_cd"]["y"], font_size, fontname) + + def generuj_pdf(self): + bledy = [] + + konto_raw = re.sub(r"\s+", "", self.entries["konto"].text()) + ok_konto, nr = self.waliduj_konto(konto_raw) + if not ok_konto: + bledy.append("• Numer konta odbiorcy musi mieć 26 cyfr.") + elif not self.waliduj_iban_mod97(konto_raw): + bledy.append("• Cyfra kontrolna IBAN konta odbiorcy jest błędna – sprawdź numer!") + + ok_kwota, kw = self.waliduj_kwote(self.entries["kwota"].text()) + if not ok_kwota: + bledy.append("• Kwota jest niepoprawna (np. 1250,00).") + + if not self.entries["odbiorca"].text().strip(): + bledy.append("• Brakuje nazwy odbiorcy.") + + if bledy: + QMessageBox.critical(self, "Błędy w formularzu", "\n".join(bledy)) + return + + if not os.path.exists(self.pdf_template): + QMessageBox.critical(self, "Błąd – brak szablonu", + f"Nie znaleziono pliku szablonu:\n{self.pdf_template}\n\n" + "Upewnij się, że plik PDF szablonu znajduje się\n" + "w tym samym folderze co program.") + return + + data = { + "nazwa_odbiorcy": self.entries["odbiorca"].text().strip(), + "nazwa_odbiorcy_cd": self.entries["odbiorca_cd"].text().strip(), + "nr_rachunku_odb": nr, + "waluta": self.entries["waluta"].text().strip().upper() or "PLN", + "kwota": kw, + "tytulem": self.entries["tytul1"].text().strip(), + "tytulem_cd": self.entries["tytul2"].text().strip(), + } + kwota_sl = kwota_slownie(kw) + nazwa_zlec = self.nazwa_zlec_edit.text().strip() + adres_zlec = self.adres_zlec_edit.toPlainText().strip() + konto_zlec = "" + if not self.wplata_check.isChecked(): + ok2, kz = self.waliduj_konto(re.sub(r"\s+", "", self.konto_zlec_edit.text())) + if ok2: + konto_zlec = kz + + # Określenie wyboru odcinka + if self.radio_gorny.isChecked(): + odcinek = "gorny" + elif self.radio_dolny.isChecked(): + odcinek = "dolny" + else: + odcinek = "oba" + + if not self.pokaz_podglad(data, kw, kwota_sl, nazwa_zlec, odcinek): + return + + try: + try: + font_size = int(self.calib_edits["font_size"].text()) + if font_size <= 0: + font_size = 11 + except ValueError: + font_size = 11 + + doc = fitz.open(self.pdf_template) + page = doc[0] + fontname = self.get_fontname(page) + wgot = self.wplata_check.isChecked() + + if odcinek in ("gorny", "oba"): + self.wypelnij_odcinek(page, "top", data, nazwa_zlec, adres_zlec, + konto_zlec, kw, kwota_sl, font_size, fontname, wgot) + if odcinek in ("dolny", "oba"): + self.wypelnij_odcinek(page, "bottom", data, nazwa_zlec, adres_zlec, + konto_zlec, kw, kwota_sl, font_size, fontname, wgot) + + out = "Polecenie przelewu.pdf" + doc.save(out) + doc.close() + + except fitz.FileNotFoundError: + QMessageBox.critical(self, "Błąd PDF", "Szablon PDF jest uszkodzony lub niedostępny.") + return + except PermissionError: + QMessageBox.critical(self, "Błąd zapisu", + f"Brak uprawnień do zapisania pliku:\n{out}\n" + "Zamknij plik jeśli jest otwarty w innym programie.") + return + except Exception as e: + QMessageBox.critical(self, "Błąd", f"Nieoczekiwany błąd:\n{e}") + return + + self._dodaj_do_historii(data["nazwa_odbiorcy"], nr, kw) + + try: + sys_name = platform.system() + if sys_name == "Windows": + subprocess.Popen(["start", out], shell=True) + elif sys_name == "Darwin": + subprocess.Popen(["open", out]) + else: + subprocess.Popen(["xdg-open", out]) + except Exception: + pass + + QMessageBox.information(self, "Gotowe", f"PDF zapisany jako:\n{out}") + + +# ── START ────────────────────────────────────────────────────────────────── # +if __name__ == "__main__": + app = QApplication(sys.argv) + window = ManagerPrzelewow() + window.show() + sys.exit(app.exec()) diff --git a/test_logic.py b/test_logic.py new file mode 100644 index 0000000..0a48c31 --- /dev/null +++ b/test_logic.py @@ -0,0 +1,33 @@ +from przelewy import kwota_slownie + +def test(): + cases = [ + ("0,00", "zero złotych zero groszy"), + ("1,00", "jeden złoty zero groszy"), + ("2,50", "dwa złote pięćdziesiąt groszy"), + ("5,00", "pięć złotych zero groszy"), + ("12,00", "dwanaście złotych zero groszy"), + ("123,45", "sto dwadzieścia trzy złote czterdzieści pięć groszy"), + ("1000,00", "tysiąc złotych zero groszy"), + ("2000,00", "dwa tysiące złotych zero groszy"), + ("5000,00", "pięć tysięcy złotych zero groszy"), + ("1000000,00", "jeden milion złotych zero groszy"), + ("1234567,89", "jeden milion dwieście trzydzieści cztery tysiące pięćset sześćdziesiąt siedem złotych osiemdziesiąt dziewięć groszy"), + ] + + failed = 0 + for kwota, expected in cases: + result = kwota_slownie(kwota) + if result == expected: + print(f"✅ {kwota} -> {result}") + else: + print(f"❌ {kwota} -> {result} (oczekiwano: {expected})") + failed += 1 + + if failed == 0: + print("\nWSZYSTKIE TESTY ZALICZONE!") + else: + print(f"\nBŁĘDY: {failed}") + +if __name__ == "__main__": + test()