# FakturyRename.py # Verze: 1.2 # Datum: 05JUN2026 # Autor: Vladimír Buzalka # # Popis: # Projde PDF faktury a doklady přímo ve vstupním adresáři, pošle je do # OpenAI Responses API k vytěžení údajů a navrhne jednotný název souboru. # Výsledný formát názvu: # YYYY-MM-DD Typ Dodavatel ČÍSLO [popis] [částka MĚNA].pdf # # Při DRY_RUN = False skript soubor přejmenuje a přesune do podadresáře # NamedInvoicesbyOpenAI, aby další běh zpracovával jen nové dokumenty. # Loguje původní název, návrh, tokeny, odhad ceny a stav zpracování. import os from dotenv import load_dotenv import base64 import json import re import time from pathlib import Path from openai import OpenAI # ========================= # CENA API # ========================= USD_TO_CZK = 25.0 # # Ceny nastav ručně podle modelu, který používáš. # # Zde počítáme: # # input = 5 USD / 1M tokenů # # output = 30 USD / 1M tokenů # PRICE_INPUT_USD_PER_1M = 5.00 # PRICE_OUTPUT_USD_PER_1M = 30.00 # # Model s podporou PDF / vision vstupu. # MODEL = "gpt-5.5" MODEL = "gpt-5.4-mini" PRICE_INPUT_USD_PER_1M = 0.75 PRICE_OUTPUT_USD_PER_1M = 4.50 # ========================= # NASTAVENÍ # ========================= FOLDER = Path( r"u:\Dropbox\Ordinace\!!MUDr. Michaela Buzalková s.r.o\Prosek\#040 Faktury přijaté" ) PROCESSED_FOLDER = FOLDER / "NamedInvoicesbyOpenAI" # Pro test na 3 fakturách nech DRY_RUN = True. # Skript jen vypíše a zaloguje návrhy názvů, ale soubory nepřejmenuje. DRY_RUN = False # Nepůjde do podadresářů, protože používáme glob(), ne rglob(). PDF_PATTERN = "*.pdf" LOG_FILE = FOLDER / "_rename_log_invoices.txt" ENV_FILE = Path(r"U:\ordinaceprojekt\.env") # ========================= # PRAVIDLA PRO POJMENOVÁNÍ # ========================= NAMING_RULES = """ Jsi pomocník pro pojmenování naskenovaných PDF dokladů MUDr. Michaely Buzalkové. ÚKOL: Z PDF faktury/dokladu vytěž datum, typ dokladu, dodavatele, číslo dokladu, stručný popis, částku a měnu. Vrať pouze JSON s polem "filename". CÍLOVÝ FORMÁT: YYYY-MM-DD Typ Dodavatel ČÍSLO [popis] [částka MĚNA].pdf PŘÍKLADY: 2026-06-01 Faktura ASKER 261103225 [kontejner Yannick 1.5 l] [339.00 CZK].pdf 2026-06-01 Faktura MEDIPOS 10195703 [CRP, kapiláry, písty, rukavice, nádoba] [5578.97 CZK].pdf 2026-05-29 Faktura Ptáček 202604570 [vakcíny Adacel, Vaqta, Havrix] [9235.20 CZK].pdf 2026-05-29 Faktura Poliklinika Prosek 91260763 [lékárna] [16165.40 CZK].pdf 2026-06-01 Dodací list QuickSeal 200609058 [VivaDiag Hydroxyvitamin D3] [2620.00 CZK].pdf DŮLEŽITÁ PRAVIDLA: 1. Prefix [POHODA] nikdy nepřidávej. 2. Používej datum vystavení dokladu, ne datum splatnosti. 3. Typ dokladu vyber podle dokumentu: - Faktura - Dobropis - Paragon - Dodací list - Zálohová faktura - Smlouva - Platba - Poplatek - Výdajový pokladní doklad 4. Pokud je v dokumentu napsáno "Dodací list není daňový doklad - nehraďte", typ musí být "Dodací list", ne "Faktura". 5. Dodavatel zapisuj krátce a konzistentně: - MEDIPOS - MEDEVIO - MEDATRON - ASKER - QuickSeal - Poliklinika Prosek - Alza - Microsoft - OpenAI - Ptáček 6. SPECIÁLNÍ PRAVIDLO: pokud je dodavatel/firma "Distribuce CZ", v názvu souboru použij dodavatele "Ptáček". 7. SPECIÁLNÍ PRAVIDLO: u faktur MEDIPOS použij jako číslo dokladu variabilní symbol nebo hlavní číslo faktury bez mezer, například 10195703. Nepoužívej interní evidenční číslo typu FV-5703/2026. 8. Částku piš vždy s desetinnou tečkou a měnou, například [5578.97 CZK]. 9. Když je částka v Kč, měna je CZK. 10. Popis drž krátký, praktický a česky. 11. Popis dávej do hranatých závorek. 12. Nepoužívej dvojtečky, lomítka, uvozovky ani znaky nevhodné pro Windows názvy souborů. 13. Pokud jde jen o dodací list bez daňového dokladu, částku můžeš uvést, ale typ musí zůstat Dodací list. 14. Pokud si nejsi jistý popisem, použij obecný popis typu [materiál do ordinace], [lékárna], [vakcíny], [testy]. 15. Výstup musí být pouze validní JSON, nic jiného. JSON FORMÁT: { "filename": "YYYY-MM-DD Faktura Dodavatel 123456 [popis] [123.45 CZK].pdf" } """ # ========================= # POMOCNÉ FUNKCE # ========================= def pdf_to_base64_data_url(path: Path) -> str: data = path.read_bytes() b64 = base64.b64encode(data).decode("utf-8") return f"data:application/pdf;base64,{b64}" def sanitize_windows_filename(name: str) -> str: """ Očistí název souboru pro Windows. """ # Zakázané znaky ve Windows: < > : " / \ | ? * name = re.sub(r'[<>:"/\\|?*]', " ", name) # Sjednocení mezer name = re.sub(r"\s+", " ", name).strip() # Windows nemá rád tečku nebo mezeru na konci name = name.rstrip(" .") if not name.lower().endswith(".pdf"): name += ".pdf" return name def unique_path(target: Path) -> Path: """ Pokud cílový soubor existuje, přidá (2), (3), ... """ if not target.exists(): return target stem = target.stem suffix = target.suffix parent = target.parent i = 2 while True: candidate = parent / f"{stem} ({i}){suffix}" if not candidate.exists(): return candidate i += 1 def extract_json_object(text: str) -> dict: """ Kdyby model náhodou vrátil něco kolem JSONu, zkusí vytáhnout první JSON objekt. """ text = text.strip() try: return json.loads(text) except json.JSONDecodeError: pass match = re.search(r"\{.*\}", text, flags=re.DOTALL) if not match: raise ValueError(f"Model nevrátil JSON:\n{text}") return json.loads(match.group(0)) def get_usage_value(usage, key: str) -> int: """ Bezpečně přečte usage hodnotu. Funguje pro objekt i dict. """ if usage is None: return 0 if isinstance(usage, dict): return usage.get(key, 0) or 0 return getattr(usage, key, 0) or 0 def calculate_cost_from_usage(usage) -> dict: """ Spočítá odhad ceny z response.usage. """ input_tokens = get_usage_value(usage, "input_tokens") output_tokens = get_usage_value(usage, "output_tokens") total_tokens = get_usage_value(usage, "total_tokens") if not total_tokens: total_tokens = input_tokens + output_tokens input_cost_usd = input_tokens / 1_000_000 * PRICE_INPUT_USD_PER_1M output_cost_usd = output_tokens / 1_000_000 * PRICE_OUTPUT_USD_PER_1M total_cost_usd = input_cost_usd + output_cost_usd total_cost_czk = total_cost_usd * USD_TO_CZK return { "input_tokens": input_tokens, "output_tokens": output_tokens, "total_tokens": total_tokens, "input_cost_usd": input_cost_usd, "output_cost_usd": output_cost_usd, "total_cost_usd": total_cost_usd, "total_cost_czk": total_cost_czk, } def ask_openai_for_filename(client: OpenAI, pdf_path: Path) -> tuple[str, dict]: file_data = pdf_to_base64_data_url(pdf_path) response = client.responses.create( model=MODEL, input=[ { "role": "user", "content": [ { "type": "input_file", "filename": pdf_path.name, "file_data": file_data, }, { "type": "input_text", "text": NAMING_RULES, }, ], } ], ) text = response.output_text.strip() obj = extract_json_object(text) filename = obj.get("filename", "").strip() if not filename: raise ValueError(f"JSON neobsahuje filename:\n{text}") cost = calculate_cost_from_usage(response.usage) return sanitize_windows_filename(filename), cost def log_line(text: str) -> None: print(text) with LOG_FILE.open("a", encoding="utf-8") as f: f.write(text + "\n") # ========================= # HLAVNÍ BĚH # ========================= def main() -> None: if not FOLDER.exists(): raise FileNotFoundError(f"Adresář neexistuje: {FOLDER}") # Načtení API klíče z .env load_dotenv(ENV_FILE) if not os.getenv("OPENAI_API_KEY"): raise RuntimeError( f"Chybí OPENAI_API_KEY. Zkontroluj soubor {ENV_FILE}" ) client = OpenAI() pdfs = sorted(FOLDER.glob(PDF_PATTERN)) pdfs = [p for p in pdfs if p.is_file() and p.suffix.lower() == ".pdf"] if not pdfs: print("Nenalezeno žádné PDF.") return total_input_tokens = 0 total_output_tokens = 0 total_tokens = 0 total_cost_usd = 0.0 total_cost_czk = 0.0 log_line("") log_line("=" * 80) log_line(f"START: {time.strftime('%Y-%m-%d %H:%M:%S')}") log_line(f"Adresář: {FOLDER}") log_line(f"Hotové faktury: {PROCESSED_FOLDER}") log_line(f"Počet PDF: {len(pdfs)}") log_line(f"DRY_RUN: {DRY_RUN}") log_line(f"MODEL: {MODEL}") log_line(f"Kurz: 1 USD = {USD_TO_CZK:.2f} CZK") log_line("=" * 80) for i, pdf in enumerate(pdfs, start=1): log_line(f"\n[{i}/{len(pdfs)}] Původní název: {pdf.name}") try: new_name, cost = ask_openai_for_filename(client, pdf) total_input_tokens += cost["input_tokens"] total_output_tokens += cost["output_tokens"] total_tokens += cost["total_tokens"] total_cost_usd += cost["total_cost_usd"] total_cost_czk += cost["total_cost_czk"] log_line(f" Návrh: {new_name}") log_line( f" Tokeny: input={cost['input_tokens']}, " f"output={cost['output_tokens']}, " f"total={cost['total_tokens']}" ) log_line( f" Cena volání: ${cost['total_cost_usd']:.6f} " f"≈ {cost['total_cost_czk']:.2f} Kč" ) target = unique_path(PROCESSED_FOLDER / new_name) if target.name != new_name: log_line(f" Cíl po vyřešení konfliktu: {target.name}") if DRY_RUN: log_line(f" Cíl: {target}") log_line(" Stav: DRY-RUN, nepřejmenováno/nepřesunuto") else: PROCESSED_FOLDER.mkdir(exist_ok=True) pdf.rename(target) if pdf.name == new_name: log_line(" Stav: PŘESUNUTO") else: log_line(" Stav: PŘEJMENOVÁNO A PŘESUNUTO") except Exception as e: log_line(f" CHYBA: {type(e).__name__}: {e}") log_line("") log_line("=" * 80) log_line("SOUHRN CENY") log_line(f"Tokeny celkem: input={total_input_tokens}, output={total_output_tokens}, total={total_tokens}") log_line(f"Cena celkem: ${total_cost_usd:.6f} ≈ {total_cost_czk:.2f} Kč") log_line("=" * 80) log_line("\nHOTOVO") if __name__ == "__main__": main()