Files
medicus/MedicusWithClaudePN/pn_report.py
T
2026-03-31 07:47:17 +02:00

321 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
pn_report.py Report aktivních pracovních neschopností
Generuje PDF a posílá na výchozí tiskárnu.
Spuštění:
python pn_report.py # vytvoří PDF a vytiskne
python pn_report.py --no-print # jen vytvoří PDF (pro testování)
Požadavky:
pip install reportlab pywin32
"""
import fdb
import sys
import os
import tempfile
from datetime import date, datetime
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
# ---------------------------------------------------------------------------
# Konfigurace
# ---------------------------------------------------------------------------
DB_DSN = r'localhost:c:\medicus 3\data\medicus.fdb'
DB_USER = 'SYSDBA'
DB_PASS = 'masterkey'
DB_CHARSET = 'win1250'
# Pokud chcete soubor uložit trvale, nastavte výstupní adresář:
OUTPUT_DIR = None # None = dočasný soubor, smaže se po tisku
#OUTPUT_DIR = r'u:\Dropbox\!!!Days\Downloads Z230'
# ---------------------------------------------------------------------------
# Dotaz aktivní PN (konnes IS NULL nebo v budoucnosti, storno='F')
# ---------------------------------------------------------------------------
SQL = """
SELECT
nes.id AS idnes,
nes.idpac,
TRIM(kar.prijmeni) || ', ' || TRIM(kar.jmeno) AS jmeno,
kar.rodcis,
nes.zacnes,
nes.konnes,
nes.diagno,
COALESCE(nes.ecn, nes.cisnes) AS cisnes,
(SELECT MAX(h.datum) FROM hpn h
WHERE h.idnes = nes.id AND h.typ = 'P' AND h.storno = 'F') AS posl_potvrzeni
FROM nes, kar
WHERE nes.zacnes <= current_date
AND nes.konnes IS NULL
AND nes.idpac = kar.idpac
AND nes.pracne = 'A'
AND nes.storno <> 'T'
AND (
NOT EXISTS (SELECT id FROM nesd WHERE nesd.idnes = nes.id)
OR (SELECT FIRST 1 kam FROM nesd WHERE nesd.idnes = nes.id
ORDER BY nesd.datum DESC, nesd.id DESC) = 'N'
)
ORDER BY kar.prijmeni ASC, kar.jmeno ASC
"""
# ---------------------------------------------------------------------------
# Pomocné funkce
# ---------------------------------------------------------------------------
def fmt_date(val):
"""Datum → DD.MM.YYYY nebo prázdný řetězec."""
if val is None:
return ''
if isinstance(val, (date, datetime)):
return val.strftime('%d.%m.%Y')
return str(val)
def fmt_str(val):
if val is None:
return ''
return str(val).strip()
def delka_pn(zacnes, konnes):
"""Počet dnů PN (od začátku do dnes / do konce)."""
if zacnes is None:
return ''
end = konnes if konnes else date.today()
if isinstance(zacnes, datetime):
zacnes = zacnes.date()
if isinstance(end, datetime):
end = end.date()
if isinstance(zacnes, date) and isinstance(end, date):
days = (end - zacnes).days + 1
return str(days)
return ''
# ---------------------------------------------------------------------------
# Načtení fontu s českou diakritikou
# ---------------------------------------------------------------------------
def register_font():
"""
Zkusí zaregistrovat DejaVuSans (umí win1250 znaky).
Fallback: Helvetica (bez diakritiky nouzové řešení).
"""
font_paths = [
r'C:\Windows\Fonts\DejaVuSans.ttf',
r'C:\Windows\Fonts\arial.ttf',
r'C:\Windows\Fonts\segoeui.ttf',
]
for path in font_paths:
if os.path.exists(path):
name = os.path.splitext(os.path.basename(path))[0]
try:
pdfmetrics.registerFont(TTFont(name, path))
pdfmetrics.registerFont(TTFont(name + '-Bold',
path.replace('.ttf', 'bd.ttf') if 'arial' in path.lower()
else path.replace('.ttf', '-Bold.ttf')
if os.path.exists(path.replace('.ttf', '-Bold.ttf'))
else path
))
return name, name + '-Bold'
except Exception:
continue
return 'Helvetica', 'Helvetica-Bold'
# ---------------------------------------------------------------------------
# Generování PDF
# ---------------------------------------------------------------------------
def build_pdf(rows, output_path, font_name, font_bold):
today_str = date.today().strftime('%d.%m.%Y')
weekday_cs = ['pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota', 'neděle']
weekday = weekday_cs[date.today().weekday()]
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
topMargin=1.5*cm,
bottomMargin=1.5*cm,
leftMargin=1.5*cm,
rightMargin=1.5*cm,
)
styles = getSampleStyleSheet()
def para(text, size=10, bold=False, align='LEFT', color=colors.black):
fn = font_bold if bold else font_name
al = {'LEFT': 0, 'CENTER': 1, 'RIGHT': 2}.get(align, 0)
from reportlab.platypus import Paragraph as P
from reportlab.lib.styles import ParagraphStyle
st = ParagraphStyle('x', fontName=fn, fontSize=size,
textColor=color, alignment=al, leading=size*1.3)
return P(text, st)
story = []
# Záhlaví
story.append(para('MUDr. Buzalková Michaela ordinace praktického lékaře',
size=9, color=colors.grey))
story.append(para(f'Aktivní pracovní neschopnosti',
size=16, bold=True))
story.append(para(f'Vytištěno: {weekday} {today_str} | Počet záznamů: {len(rows)}',
size=9, color=colors.grey))
story.append(Spacer(1, 0.4*cm))
if not rows:
story.append(para('Žádné aktivní pracovní neschopnosti.', size=12))
doc.build(story)
return
# Záhlaví tabulky
headers = ['#', 'Příjmení a jméno', 'Rod. číslo', 'Začátek PN',
'Dnů', 'Diagnóza', 'Posl. potvrzení', 'Dní od potvr.']
col_widths = [0.7*cm, 5.5*cm, 2.8*cm, 2.4*cm, 1.4*cm, 2.2*cm, 3.0*cm, 2.2*cm]
table_data = [headers]
overdue_rows = [] # indexy řádků kde je potvrzení po splatnosti
for idx, row in enumerate(rows, start=1):
idnes, idpac, jmeno, rodcis, zacnes, konnes, diagno, cisnes, posl_potvrzeni = row
# Počet dní od posledního potvrzení
if posl_potvrzeni is not None:
pp = posl_potvrzeni.date() if isinstance(posl_potvrzeni, datetime) else posl_potvrzeni
dni_od = (date.today() - pp).days
dni_od_str = str(dni_od)
if dni_od > 14:
overdue_rows.append(idx + 1) # +1 kvůli záhlaví
else:
# Žádné potvrzení počítáme od začátku PN
zac = zacnes.date() if isinstance(zacnes, datetime) else zacnes
if zac:
dni_od = (date.today() - zac).days
dni_od_str = str(dni_od) + ' (!)'
if dni_od > 14:
overdue_rows.append(idx + 1)
else:
dni_od_str = ''
table_data.append([
str(idx),
fmt_str(jmeno),
fmt_str(rodcis),
fmt_date(zacnes),
delka_pn(zacnes, konnes),
fmt_str(diagno),
fmt_date(posl_potvrzeni) if posl_potvrzeni else '',
dni_od_str,
])
tbl = Table(table_data, colWidths=col_widths, repeatRows=1)
style = TableStyle([
# Záhlaví
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2F5496')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTNAME', (0, 0), (-1, 0), font_bold),
('FONTSIZE', (0, 0), (-1, 0), 8),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('BOTTOMPADDING',(0, 0), (-1, 0), 5),
('TOPPADDING', (0, 0), (-1, 0), 5),
# Data
('FONTNAME', (0, 1), (-1, -1), font_name),
('FONTSIZE', (0, 1), (-1, -1), 8),
('ALIGN', (0, 1), (0, -1), 'CENTER'), # #
('ALIGN', (5, 1), (5, -1), 'RIGHT'), # Dnů
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('TOPPADDING', (0, 1), (-1, -1), 3),
('BOTTOMPADDING',(0, 1), (-1, -1), 3),
# Mřížka
('GRID', (0, 0), (-1, -1), 0.3, colors.HexColor('#AAAAAA')),
('LINEBELOW', (0, 0), (-1, 0), 1, colors.HexColor('#2F5496')),
# Zebra
*[('BACKGROUND', (0, i), (-1, i), colors.HexColor('#DCE6F1'))
for i in range(2, len(table_data), 2)],
# Červené zvýraznění potvrzení po splatnosti (> 14 dní)
*[('BACKGROUND', (6, i), (7, i), colors.HexColor('#F4CCCC'))
for i in overdue_rows],
*[('TEXTCOLOR', (6, i), (7, i), colors.HexColor('#CC0000'))
for i in overdue_rows],
*[('FONTNAME', (6, i), (7, i), font_bold)
for i in overdue_rows],
])
tbl.setStyle(style)
story.append(tbl)
# Patička
story.append(Spacer(1, 0.5*cm))
story.append(para(f'--- konec reportu ({len(rows)} záznamů) ---',
size=8, color=colors.grey, align='CENTER'))
doc.build(story)
# ---------------------------------------------------------------------------
# Tisk
# ---------------------------------------------------------------------------
def print_pdf(path):
"""Pošle PDF na výchozí tiskárnu přes Windows ShellExecute."""
try:
import win32api
win32api.ShellExecute(0, 'print', path, None, '.', 0)
print(f'Odesláno na tiskárnu: {path}')
except ImportError:
# Fallback otevře soubor v PDF prohlížeči (ruční tisk)
print('pywin32 není nainstalován, otevírám PDF...')
os.startfile(path)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
no_print = '--no-print' in sys.argv
# Připojení k DB
print('Připojuji se k DB...')
conn = fdb.connect(dsn=DB_DSN, user=DB_USER, password=DB_PASS, charset=DB_CHARSET)
cur = conn.cursor()
print('Načítám aktivní PN...')
cur.execute(SQL)
rows = cur.fetchall()
conn.close()
print(f'Nalezeno {len(rows)} aktivních PN.')
# Font
font_name, font_bold = register_font()
print(f'Font: {font_name}')
# Výstupní soubor
if OUTPUT_DIR:
os.makedirs(OUTPUT_DIR, exist_ok=True)
out_path = os.path.join(OUTPUT_DIR,
date.today().strftime('%Y-%m-%d') + '_pn_report.pdf')
else:
fd, out_path = tempfile.mkstemp(suffix='_pn_report.pdf')
os.close(fd)
print(f'Generuji PDF: {out_path}')
build_pdf(rows, out_path, font_name, font_bold)
print('PDF hotovo.')
if no_print:
print('(tisk přeskočen --no-print)')
# Otevřeme pro náhled
os.startfile(out_path)
else:
print_pdf(out_path)
# Dočasný soubor necháme tiskárna ho potřebuje přečíst
# Windows ho smaže sám po zpracování tisku (temp adresář)
if __name__ == '__main__':
main()