Add QR payment generator with live Medicus price list

- New QRPlatba/ module: standalone app for generating SPAYD QR payment codes
- Live price list loaded directly from Medicus Firebird DB (VLV_SEL), sorted by KOD
- Patient data (name, surname, RC) passed as arguments from Medicus button
- Auto-generates QR on load and refreshes on every dropdown change
- Account selector dropdown (two FIO accounts with IBAN)
- Save button exports QR as PNG next to the EXE (sys.executable-relative path)
- SPAYD format: ACC, AM, CC, X-VS (rodné číslo), MSG (patient + item)
- README.md with full documentation, Medicus button command, DB schema
- Add .gitignore (excludes build/, dist/, .venv/, .idea/, .claude/)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 20:07:10 +01:00
parent 532fe5ab41
commit 89511c500f
4 changed files with 421 additions and 32 deletions
+21
View File
@@ -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/
+209
View File
@@ -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}")
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()
+116
View File
@@ -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*
+75 -32
View File
@@ -4,6 +4,7 @@
import sys import sys
import urllib.parse import urllib.parse
import qrcode import qrcode
import fdb
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from PIL import Image, ImageTk from PIL import Image, ImageTk
@@ -14,11 +15,20 @@ from tkinter import messagebox
# ================================ # ================================
# ⚙️ Default Configuration # ⚙️ Default Configuration
# ================================ # ================================
IBAN = "CZ7520100000002800046620" ACCOUNTS = {
"2100046291/2010": "CZ1720100000002100046291",
"2800046620/2010": "CZ7520100000002800046620",
}
CURRENCY = "CZK" CURRENCY = "CZK"
OUTPUT_DIR = Path("QRPlatby") OUTPUT_DIR = Path("QRPlatby")
OUTPUT_DIR.mkdir(exist_ok=True) 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) # Default values (can be overridden by arguments)
PRIJMENI = "Buzalka" PRIJMENI = "Buzalka"
JMENO = "Vladimír" JMENO = "Vladimír"
@@ -36,12 +46,23 @@ elif len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"):
sys.exit(0) sys.exit(0)
# ================================ # ================================
# 💉 Items to Pay # 💉 Items to Pay načteno z Medicusu
# ================================ # ================================
ITEMS = { def nacti_polozky():
"Očkování chřipka Vaxigrip": 600.00, """Načte ceník z Medicusu seřazený podle KOD (pořadového čísla)."""
"Očkování chřipka Efluelda": 1300.00, 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 # 🧩 Helper
@@ -58,7 +79,7 @@ class QRPlatbaApp(ctk.CTk):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.title("QR Platba Ordinace MUDr. Buzalková") self.title("QR Platba Ordinace MUDr. Buzalková")
self.geometry("520x520") self.geometry("520x680")
self.minsize(480, 480) self.minsize(480, 480)
self.resizable(True, True) self.resizable(True, True)
@@ -69,21 +90,21 @@ class QRPlatbaApp(ctk.CTk):
frame.pack(expand=True, fill="both", padx=20, pady=20) frame.pack(expand=True, fill="both", padx=20, pady=20)
ctk.CTkLabel(frame, text="Generátor QR Platby", 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 Info
patient = ctk.CTkFrame(frame, corner_radius=8) 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}", for text in [f"Příjmení: {PRIJMENI}",
f"Jméno: {JMENO}", f"Jméno: {JMENO}",
f"Rodné číslo: {RODCIS}"]: 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 # 💰 Payment Section
pay = ctk.CTkFrame(frame, corner_radius=8) 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ě:", 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.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.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, variable=self.selected_item,
values=self.display_items, values=self.display_items,
font=("Arial", 12), font=("Arial", 12),
command=self.update_amount command=self.on_change
) )
self.combo.pack(fill="x", padx=10) self.combo.pack(fill="x", padx=10)
self.amount_label = ctk.CTkLabel(pay, text="", font=("Arial", 12, "italic")) self.amount_label = ctk.CTkLabel(pay, text="", font=("Arial", 12, "italic"))
self.amount_label.pack(anchor="e", padx=10, pady=(5, 10)) self.amount_label.pack(anchor="e", padx=10, pady=(3, 6))
self.update_amount()
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"), font=("Arial", 13, "bold"),
height=40, 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 = ctk.CTkLabel(frame, text="")
self.qr_label.pack(pady=15) self.qr_label.pack(pady=6)
ctk.CTkLabel(frame, ctk.CTkLabel(frame,
text="© Ordinace MUDr. Buzalková | QR Platba dle ČBA v1.2", 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)) text_color="#666").pack(side="bottom", pady=(10, 0))
self.center_window() self.center_window()
# QR automaticky při startu
self.after(100, self.refresh_qr)
# ================================ # ================================
# 🪟 Center Window # 🪟 Center Window
@@ -133,28 +168,36 @@ class QRPlatbaApp(ctk.CTk):
# ================================ # ================================
# 💸 Update and Generate # 💸 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() display_item = self.selected_item.get()
item = self.item_map[display_item] 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}") self.amount_label.configure(text=f"Částka: {ITEMS[item]:.2f}")
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) img = qrcode.make(spayd)
filename = f"{PRIJMENI}_{JMENO}_{datetime.now():%Y%m%d_%H%M%S}.png" img_resized = img.resize((200, 200), Image.LANCZOS)
out_path = OUTPUT_DIR / filename
img.save(out_path)
img_resized = img.resize((300, 300))
qr_tk = ImageTk.PhotoImage(img_resized) qr_tk = ImageTk.PhotoImage(img_resized)
self.qr_label.configure(image=qr_tk) self.qr_label.configure(image=qr_tk)
self.qr_label.image = qr_tk self.qr_label.image = qr_tk
self.update_idletasks() def ulozit_qr(self):
self.geometry(f"{self.winfo_reqwidth()}x{self.winfo_reqheight()}") """Uloží QR kód do souboru a informuje uživatele."""
self.center_window() 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}")
# ================================ # ================================