321 lines
11 KiB
Python
321 lines
11 KiB
Python
"""
|
||
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()
|