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
+75 -32
View File
@@ -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}")
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}")
# ================================