diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9cbb5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo + +# Virtual environment +.venv/ + +# PyInstaller build artifacts +build/ +dist/ +*.spec + +# IDE +.idea/ + +# Claude +.claude/ + +# Output folders +QRPlatby/ diff --git a/QRPlatba/QRPlatbaApp.py b/QRPlatba/QRPlatbaApp.py new file mode 100644 index 0000000..9168ecf --- /dev/null +++ b/QRPlatba/QRPlatbaApp.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import urllib.parse +import qrcode +import fdb +from pathlib import Path +from datetime import datetime +from PIL import Image, ImageTk +import customtkinter as ctk +from tkinter import messagebox + + +# ================================ +# ⚙️ Default Configuration +# ================================ +ACCOUNTS = { + "2100046291/2010": "CZ1720100000002100046291", + "2800046620/2010": "CZ7520100000002800046620", +} +CURRENCY = "CZK" +# sys.executable = cesta k EXE (i po přesunutí), __file__ by v onefile EXE ukazoval do temp +OUTPUT_DIR = Path(sys.executable).parent / "QRPlatby" +OUTPUT_DIR.mkdir(exist_ok=True) + +# Firebird připojení +DB_DSN = r'localhost:c:\medicus 3\data\medicus.fdb' +DB_USER = 'SYSDBA' +DB_PASSWORD = 'masterkey' +DB_CHARSET = 'win1250' + +# Default values (can be overridden by arguments) +PRIJMENI = "Buzalka" +JMENO = "Vladimír" +RODCIS = "730928104" + +# ================================ +# 💬 Argument Handling +# ================================ +if len(sys.argv) >= 4: + JMENO = sys.argv[1] + PRIJMENI = sys.argv[2] + RODCIS = sys.argv[3] +elif len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"): + print("Usage: QRPlatbaApp.py JMENO PRIJMENI RODCIS") + sys.exit(0) + +# ================================ +# 💉 Items to Pay – načteno z Medicusu +# ================================ +def nacti_polozky(): + """Načte ceník z Medicusu seřazený podle KOD (pořadového čísla).""" + try: + conn = fdb.connect(dsn=DB_DSN, user=DB_USER, password=DB_PASSWORD, charset=DB_CHARSET) + cur = conn.cursor() + cur.execute("SELECT V.KOD, V.NAZEV, V.CENA FROM VLV_SEL(NULL, NULL, NULL) V ORDER BY V.KOD") + rows = cur.fetchall() + conn.close() + # Vrátí dict: název -> cena (float), seřazený podle KOD + return {row[1].strip(): float(row[2]) for row in rows if row[2] is not None} + except Exception as e: + messagebox.showerror("Chyba databáze", f"Nepodařilo se načíst ceník z Medicusu:\n{e}") + return {} + +ITEMS = nacti_polozky() + +# ================================ +# 🧩 Helper – sestavení SPAYD řetězce +# ================================ +def create_spayd(iban, amount, vs, msg, currency="CZK"): + msg_encoded = urllib.parse.quote(msg, safe="$%*+-.:/") + return f"SPD*1.0*ACC:{iban}*AM:{amount:.2f}*CC:{currency}*X-VS:{vs}*MSG:{msg_encoded}" + + +# ================================ +# 🪟 GUI Class +# ================================ +class QRPlatbaApp(ctk.CTk): + def __init__(self): + super().__init__() + self.title("QR Platba – Ordinace MUDr. Buzalková") + self.geometry("520x680") + self.minsize(480, 480) + self.resizable(True, True) + + ctk.set_appearance_mode("light") + ctk.set_default_color_theme("blue") + + frame = ctk.CTkFrame(self, corner_radius=10) + frame.pack(expand=True, fill="both", padx=20, pady=20) + + ctk.CTkLabel(frame, text="Generátor QR Platby", + font=("Arial", 20, "bold")).pack(pady=(8, 10)) + + # 👤 Informace o pacientovi + patient = ctk.CTkFrame(frame, corner_radius=8) + patient.pack(fill="x", pady=(0, 8), padx=10) + for text in [f"Příjmení: {PRIJMENI}", + f"Jméno: {JMENO}", + f"Rodné číslo: {RODCIS}"]: + ctk.CTkLabel(patient, text=text, font=("Arial", 12)).pack(anchor="w", padx=10, pady=1) + + # 💰 Výběr položky + pay = ctk.CTkFrame(frame, corner_radius=8) + pay.pack(fill="x", pady=(0, 8), padx=10) + ctk.CTkLabel(pay, text="Vyberte položku k úhradě:", + font=("Arial", 12, "bold")).pack(anchor="w", padx=10, pady=(6, 3)) + + self.display_items = [f"{name} ({price:.0f} Kč)" for name, price in ITEMS.items()] + self.item_map = {f"{name} ({price:.0f} Kč)": name for name, price in ITEMS.items()} + + self.selected_item = ctk.StringVar(value=self.display_items[0]) + self.combo = ctk.CTkOptionMenu( + pay, + variable=self.selected_item, + values=self.display_items, + font=("Arial", 12), + command=self.on_change + ) + self.combo.pack(fill="x", padx=10) + + self.amount_label = ctk.CTkLabel(pay, text="", font=("Arial", 12, "italic")) + self.amount_label.pack(anchor="e", padx=10, pady=(3, 6)) + + # 🏦 Výběr účtu + acc = ctk.CTkFrame(frame, corner_radius=8) + acc.pack(fill="x", pady=(0, 8), padx=10) + ctk.CTkLabel(acc, text="Číslo účtu:", font=("Arial", 12, "bold")).pack(anchor="w", padx=10, pady=(6, 3)) + self.selected_account = ctk.StringVar(value=list(ACCOUNTS.keys())[0]) + ctk.CTkOptionMenu( + acc, + variable=self.selected_account, + values=list(ACCOUNTS.keys()), + font=("Arial", 12), + command=self.on_change + ).pack(fill="x", padx=10, pady=(0, 6)) + + ctk.CTkButton(frame, text="Uložit QR kód", + font=("Arial", 13, "bold"), + height=40, + command=self.ulozit_qr).pack(pady=6) + + self.qr_label = ctk.CTkLabel(frame, text="") + self.qr_label.pack(pady=6) + + ctk.CTkLabel(frame, + text="© Ordinace MUDr. Buzalková | QR Platba dle ČBA v1.2", + font=("Arial", 10), + text_color="#666").pack(side="bottom", pady=(10, 0)) + + self.center_window() + # QR se vygeneruje automaticky po načtení okna + self.after(100, self.refresh_qr) + + # ================================ + # 🪟 Vycentrování okna + # ================================ + def center_window(self): + self.update_idletasks() + width = self.winfo_width() + height = self.winfo_height() + screen_width = self.winfo_screenwidth() + screen_height = self.winfo_screenheight() + x = int((screen_width / 2) - (width / 2)) + y = int((screen_height / 2) - (height / 2)) + self.geometry(f"{width}x{height}+{x}+{y}") + + # ================================ + # 💸 QR logika + # ================================ + def _get_current(self): + """Vrátí (item, iban, spayd) pro aktuální výběr.""" + display_item = self.selected_item.get() + item = self.item_map[display_item] + iban = ACCOUNTS[self.selected_account.get()] + spayd = create_spayd(iban, ITEMS[item], RODCIS, f"{PRIJMENI} {JMENO} – {item}", CURRENCY) + return item, iban, spayd + + def on_change(self, _=None): + """Při změně dropdownu – obnoví částku i QR kód.""" + self.refresh_qr() + + def refresh_qr(self): + """Zobrazí QR kód pro aktuální výběr (bez uložení na disk).""" + item, _, spayd = self._get_current() + self.amount_label.configure(text=f"Částka: {ITEMS[item]:.2f} Kč") + img = qrcode.make(spayd) + img_resized = img.resize((200, 200), Image.LANCZOS) + qr_tk = ImageTk.PhotoImage(img_resized) + self.qr_label.configure(image=qr_tk) + self.qr_label.image = qr_tk + + def ulozit_qr(self): + """Uloží QR kód do souboru PNG a zobrazí potvrzení.""" + item, _, spayd = self._get_current() + img = qrcode.make(spayd) + filename = f"{PRIJMENI}_{JMENO}_{datetime.now():%Y%m%d_%H%M%S}.png" + out_path = OUTPUT_DIR / filename + img.save(out_path) + messagebox.showinfo("Uloženo", f"QR kód uložen:\n{out_path}") + + +# ================================ +# 🚀 Main +# ================================ +if __name__ == "__main__": + app = QRPlatbaApp() + app.mainloop() diff --git a/QRPlatba/README.md b/QRPlatba/README.md new file mode 100644 index 0000000..82a61dd --- /dev/null +++ b/QRPlatba/README.md @@ -0,0 +1,116 @@ +# QRPlatbaApp – Generátor QR plateb pro ordinaci MUDr. Buzalková + +Aplikace pro generování QR kódů plateb dle standardu **SPAYD / ČBA** přímo z prostředí Medicus 3. +Ceník se načítá živě z databáze Medicusu, takže jakákoliv změna cen v Medicusu se okamžitě projeví. + +--- + +## Funkce + +- Načítá ceník přímo z databáze Medicus (Firebird) – vždy aktuální ceny +- Pacientské údaje (jméno, příjmení, rodné číslo) přijímá jako argumenty z tlačítka Medicusu +- QR kód se zobrazí automaticky po otevření okna +- QR kód se obnoví při každé změně položky nebo čísla účtu +- Výběr ze dvou bankovních účtů (dropdown) +- Tlačítko **Uložit QR kód** uloží PNG soubor do složky `QRPlatby/` +- Variabilní symbol = rodné číslo pacienta +- Zpráva příjemci = `Příjmení Jméno – Název položky` + +--- + +## Spuštění z Medicusu + +Příkaz pro tlačítko v Medicusu: + +``` +"U:\PycharmProjects\FIO\.venv\Scripts\pythonw.exe" "U:\PycharmProjects\FIO\QRPlatba\QRPlatbaApp.py" "%JMENO%" "%PRIJMENI%" "%RODCISN%" +``` + +> `pythonw.exe` se používá místo `python.exe`, aby se nezobrazovalo černé konzolové okno. + +--- + +## Struktura projektu + +``` +QRPlatba/ +├── QRPlatbaApp.py # Hlavní aplikace +├── README.md # Tato dokumentace +└── QRPlatby/ # Výstupní složka pro uložené QR kódy (PNG) +``` + +--- + +## Závislosti + +Nainstalovat přes pip do projektového venv: + +```bash +pip install customtkinter pillow qrcode fdb +``` + +| Balíček | Účel | +|-----------------|-------------------------------------------| +| `customtkinter` | Moderní GUI (tmavý/světlý režim) | +| `pillow` | Práce s obrázky (zobrazení QR v okně) | +| `qrcode` | Generování QR kódů | +| `fdb` | Připojení k Firebird databázi (Medicus) | + +--- + +## Databázové připojení + +Aplikace se připojuje k lokální Firebird databázi Medicusu: + +| Parametr | Hodnota | +|-----------|------------------------------------------| +| DSN | `localhost:c:\medicus 3\data\medicus.fdb`| +| Uživatel | `SYSDBA` | +| Charset | `win1250` | +| View | `VLV_SEL` – ceník výkonů | + +Položky jsou seřazeny podle pole `KOD` (pořadové číslo v ceníku Medicusu). + +--- + +## Formát QR kódu (SPAYD) + +QR kód odpovídá standardu **Short Payment Descriptor (SPAYD)** dle specifikace České bankovní asociace. + +Příklad vygenerovaného řetězce: +``` +SPD*1.0*ACC:CZ1720100000002100046291*AM:600.00*CC:CZK*X-VS:486122443*MSG:Abohamda%20Horia%20%E2%80%93%20O%C4%8Dkov%C3%A1n%C3%AD%20ch%C5%99ipka%20(VAXIGRIP) +``` + +| Pole | Obsah | +|---------|------------------------------------| +| `ACC` | IBAN cílového účtu | +| `AM` | Částka v Kč | +| `CC` | Měna (vždy CZK) | +| `X-VS` | Variabilní symbol = rodné číslo | +| `MSG` | Příjmení Jméno – Název položky | + +--- + +## Bankovní účty + +| Číslo účtu | IBAN | +|------------------|----------------------------| +| 2100046291/2010 | CZ1720100000002100046291 | +| 2800046620/2010 | CZ7520100000002800046620 | + +--- + +## Uložené QR kódy + +Soubory se ukládají do složky `QRPlatby/` ve formátu: + +``` +Příjmení_Jméno_YYYYMMDD_HHMMSS.png +``` + +Příklad: `Abohamda_Horia_20260325_143022.png` + +--- + +*© Ordinace MUDr. Buzalková | QR Platba dle ČBA v1.2* diff --git a/QRPlatbaApp.py b/QRPlatbaApp.py index 5756265..1690e24 100644 --- a/QRPlatbaApp.py +++ b/QRPlatbaApp.py @@ -4,6 +4,7 @@ import sys import urllib.parse import qrcode +import fdb from pathlib import Path from datetime import datetime from PIL import Image, ImageTk @@ -14,11 +15,20 @@ from tkinter import messagebox # ================================ # ⚙️ Default Configuration # ================================ -IBAN = "CZ7520100000002800046620" +ACCOUNTS = { + "2100046291/2010": "CZ1720100000002100046291", + "2800046620/2010": "CZ7520100000002800046620", +} CURRENCY = "CZK" OUTPUT_DIR = Path("QRPlatby") OUTPUT_DIR.mkdir(exist_ok=True) +# Firebird připojení +DB_DSN = r'localhost:c:\medicus 3\data\medicus.fdb' +DB_USER = 'SYSDBA' +DB_PASSWORD = 'masterkey' +DB_CHARSET = 'win1250' + # Default values (can be overridden by arguments) PRIJMENI = "Buzalka" JMENO = "Vladimír" @@ -36,12 +46,23 @@ elif len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"): sys.exit(0) # ================================ -# 💉 Items to Pay +# 💉 Items to Pay – načteno z Medicusu # ================================ -ITEMS = { - "Očkování chřipka Vaxigrip": 600.00, - "Očkování chřipka Efluelda": 1300.00, -} +def nacti_polozky(): + """Načte ceník z Medicusu seřazený podle KOD (pořadového čísla).""" + try: + conn = fdb.connect(dsn=DB_DSN, user=DB_USER, password=DB_PASSWORD, charset=DB_CHARSET) + cur = conn.cursor() + cur.execute("SELECT V.KOD, V.NAZEV, V.CENA FROM VLV_SEL(NULL, NULL, NULL) V ORDER BY V.KOD") + rows = cur.fetchall() + conn.close() + # Vrátí OrderedDict: název -> cena (float) + return {row[1].strip(): float(row[2]) for row in rows if row[2] is not None} + except Exception as e: + messagebox.showerror("Chyba databáze", f"Nepodařilo se načíst ceník z Medicusu:\n{e}") + return {} + +ITEMS = nacti_polozky() # ================================ # 🧩 Helper @@ -58,7 +79,7 @@ class QRPlatbaApp(ctk.CTk): def __init__(self): super().__init__() self.title("QR Platba – Ordinace MUDr. Buzalková") - self.geometry("520x520") + self.geometry("520x680") self.minsize(480, 480) self.resizable(True, True) @@ -69,21 +90,21 @@ class QRPlatbaApp(ctk.CTk): frame.pack(expand=True, fill="both", padx=20, pady=20) ctk.CTkLabel(frame, text="Generátor QR Platby", - font=("Arial", 20, "bold")).pack(pady=(10, 20)) + font=("Arial", 20, "bold")).pack(pady=(8, 10)) # 👤 Patient Info patient = ctk.CTkFrame(frame, corner_radius=8) - patient.pack(fill="x", pady=(0, 20), padx=10) + patient.pack(fill="x", pady=(0, 8), padx=10) for text in [f"Příjmení: {PRIJMENI}", f"Jméno: {JMENO}", f"Rodné číslo: {RODCIS}"]: - ctk.CTkLabel(patient, text=text, font=("Arial", 12)).pack(anchor="w", padx=10, pady=2) + ctk.CTkLabel(patient, text=text, font=("Arial", 12)).pack(anchor="w", padx=10, pady=1) # 💰 Payment Section pay = ctk.CTkFrame(frame, corner_radius=8) - pay.pack(fill="x", pady=(0, 20), padx=10) + pay.pack(fill="x", pady=(0, 8), padx=10) ctk.CTkLabel(pay, text="Vyberte položku k úhradě:", - font=("Arial", 12, "bold")).pack(anchor="w", padx=10, pady=(10, 5)) + font=("Arial", 12, "bold")).pack(anchor="w", padx=10, pady=(6, 3)) self.display_items = [f"{name} ({price:.0f} Kč)" for name, price in ITEMS.items()] self.item_map = {f"{name} ({price:.0f} Kč)": name for name, price in ITEMS.items()} @@ -94,21 +115,33 @@ class QRPlatbaApp(ctk.CTk): variable=self.selected_item, values=self.display_items, font=("Arial", 12), - command=self.update_amount + command=self.on_change ) self.combo.pack(fill="x", padx=10) self.amount_label = ctk.CTkLabel(pay, text="", font=("Arial", 12, "italic")) - self.amount_label.pack(anchor="e", padx=10, pady=(5, 10)) - self.update_amount() + self.amount_label.pack(anchor="e", padx=10, pady=(3, 6)) - ctk.CTkButton(frame, text="Vytvořit QR Platbu", + # 🏦 Account Selection + acc = ctk.CTkFrame(frame, corner_radius=8) + acc.pack(fill="x", pady=(0, 8), padx=10) + ctk.CTkLabel(acc, text="Číslo účtu:", font=("Arial", 12, "bold")).pack(anchor="w", padx=10, pady=(6, 3)) + self.selected_account = ctk.StringVar(value=list(ACCOUNTS.keys())[0]) + ctk.CTkOptionMenu( + acc, + variable=self.selected_account, + values=list(ACCOUNTS.keys()), + font=("Arial", 12), + command=self.on_change + ).pack(fill="x", padx=10, pady=(0, 6)) + + ctk.CTkButton(frame, text="Uložit QR kód", font=("Arial", 13, "bold"), height=40, - command=self.generate_qr).pack(pady=10) + command=self.ulozit_qr).pack(pady=6) self.qr_label = ctk.CTkLabel(frame, text="") - self.qr_label.pack(pady=15) + self.qr_label.pack(pady=6) ctk.CTkLabel(frame, text="© Ordinace MUDr. Buzalková | QR Platba dle ČBA v1.2", @@ -116,6 +149,8 @@ class QRPlatbaApp(ctk.CTk): text_color="#666").pack(side="bottom", pady=(10, 0)) self.center_window() + # QR automaticky při startu + self.after(100, self.refresh_qr) # ================================ # 🪟 Center Window @@ -133,28 +168,36 @@ class QRPlatbaApp(ctk.CTk): # ================================ # 💸 Update and Generate # ================================ - def update_amount(self, _=None): + def _get_current(self): + """Vrátí (item, iban, spayd) pro aktuální výběr.""" display_item = self.selected_item.get() item = self.item_map[display_item] + iban = ACCOUNTS[self.selected_account.get()] + spayd = create_spayd(iban, ITEMS[item], RODCIS, f"{PRIJMENI} {JMENO} – {item}", CURRENCY) + return item, iban, spayd + + def on_change(self, _=None): + """Při změně dropdownu – aktualizuj částku i QR.""" + self.refresh_qr() + + def refresh_qr(self): + """Zobrazí QR kód pro aktuální výběr (bez uložení).""" + item, _, spayd = self._get_current() self.amount_label.configure(text=f"Částka: {ITEMS[item]:.2f} Kč") - - def generate_qr(self): - display_item = self.selected_item.get() - item = self.item_map[display_item] - spayd = create_spayd(IBAN, ITEMS[item], RODCIS, f"{PRIJMENI} {JMENO} – {item}", CURRENCY) img = qrcode.make(spayd) - filename = f"{PRIJMENI}_{JMENO}_{datetime.now():%Y%m%d_%H%M%S}.png" - out_path = OUTPUT_DIR / filename - img.save(out_path) - - img_resized = img.resize((300, 300)) + img_resized = img.resize((200, 200), Image.LANCZOS) qr_tk = ImageTk.PhotoImage(img_resized) self.qr_label.configure(image=qr_tk) self.qr_label.image = qr_tk - self.update_idletasks() - self.geometry(f"{self.winfo_reqwidth()}x{self.winfo_reqheight()}") - self.center_window() + def ulozit_qr(self): + """Uloží QR kód do souboru a informuje uživatele.""" + item, _, spayd = self._get_current() + img = qrcode.make(spayd) + filename = f"{PRIJMENI}_{JMENO}_{datetime.now():%Y%m%d_%H%M%S}.png" + out_path = OUTPUT_DIR / filename + img.save(out_path) + messagebox.showinfo("Uloženo", f"QR kód uložen:\n{out_path}") # ================================