Files
ordinaceprojekt/Faktury/trash/FakturyRenameClaude.py
T
2026-06-05 15:48:09 +02:00

366 lines
13 KiB
Python

# FakturyRenameClaude.py
# Verze: 1.0
# Datum: 05JUN2026
# Autor: Claude (Anthropic)
#
# Popis:
# Obdoba FakturyRenameOpenAI.py — místo OpenAI Responses API používá
# Anthropic Messages API (Claude). PDF se posílá jako base64 document blok.
# 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
# NamedInvoicesbyClaude, aby další běh zpracovával jen nové dokumenty.
import os
import base64
import json
import re
import time
from pathlib import Path
import anthropic
# =========================
# CENA API
# =========================
USD_TO_CZK = 25.0
MODEL = "claude-haiku-4-5"
PRICE_INPUT_USD_PER_1M = 1.00
PRICE_OUTPUT_USD_PER_1M = 5.00
# =========================
# NASTAVENÍ
# =========================
FOLDER = Path(
r"u:\Dropbox\Ordinace\!!MUDr. Michaela Buzalková s.r.o\Prosek\#040 Faktury přijaté"
)
PROCESSED_FOLDER = FOLDER / "NamedInvoicesbyClaude"
# 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
PDF_PATTERN = "*.pdf"
LOG_FILE = FOLDER / "_rename_log_invoices_claude.txt"
ENV_FILE = Path(__file__).resolve().parent.parent / "Medevio" / ".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 — různé typy dokladů:
2026-01-22 Faktura MEDIPOS 101827406 [materiál do ordinace] [9620.80 CZK].pdf
2026-01-16 Faktura MEDEVIO 2600616 JAN2026 [1999.00 CZK].pdf
2026-01-07 Faktura Ptáček 202600168 [vakcíny] [6070.00 CZK].pdf
2026-01-15 Faktura Poliklinika Prosek 91251957 [telefon a sterilizace] [827.28 CZK].pdf
2026-01-29 Faktura QuickSeal 120600292 [kontrola kvality QSK 1. cyklus 2026] [6970.00 CZK].pdf
2026-01-20 Faktura Microsoft G136228996 [licence] [942.95 CZK].pdf
2026-01-04 Faktura OpenAI 3OXD6KWG-0006 [ChatGPT plus subscription] [558.88 CZK].pdf
2026-01-31 Faktura Mediately 80fae [předplatné] [175.12 CZK].pdf
2026-01-16 Faktura MEDATRON 2261100086 [coagucheck 2x] [5677.40 CZK].pdf
2026-02-11 Opravný doklad Alza 3260384509 [vratka faktury 4009941955] [-12941.00 CZK].pdf
2026-04-28 Faktura CLIMPROFI 900260026 [servis a čištění klimatizace] [2178.00 CZK].pdf
2026-01-19 Paragon [parkování Poliklinika Prosek 2026] [4800.00 CZK].pdf
2026-03-18 Paragon [papír do tiskárny] [180.00 CZK].pdf
2026-01-13 Mzdy MUDr. Buzalkové 202512 [Jarmila Kusinová].pdf
2025-03-31 Platba Michaela Buzalkové ČLK 2025 [4000.00 CZK].pdf
2025-01-24 Zálohová faktura Stormware 2512805657 [program Pohoda mini].pdf
2025-11-11 Faktura Avenier 425160437 [vakcíny] [28180.00 CZK].pdf
2025-04-03 Faktura CLIMPROFI 900250020 [servis a čištění klimatizace] [2057.00 CZK].pdf
2026-01-22 Dodatek Poliklinika Prosek [nájemní smlouva č.3].pdf
2025-12-31 Smlouva Kooperativa 8604142932 [profesní pojištění odpovědnosti 2026].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
- Opravný doklad ← pro storno/vrátky (ne Dobropis, pokud dokument říká "Opravný daňový doklad")
- Paragon
- Dodací list
- Zálohová faktura
- Smlouva
- Dodatek ← pro dodatky ke smlouvám
- Platba ← pro členské příspěvky a podobné platby bez faktury
- Poplatek
- Mzdy ← pro výplatní / mzdové dokumenty
- 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ě podle tohoto seznamu — použij přesně tato jména:
- MEDIPOS (Medipos, MEDIPOS s.r.o.)
- MEDEVIO (Medevio)
- MEDATRON (MEDATRON s.r.o., Medatron)
- ASKER (Asker)
- QuickSeal (QuickSeal International s.r.o.)
- Poliklinika Prosek (i pro "Lékárna Poliklinika Prosek a.s." — viz pravidlo 14)
- Alza
- Microsoft
- OpenAI
- Ptáček (i pro "Distribuce CZ" — viz pravidlo 6)
- Avenier
- Stormware (STORMWARE s.r.o., Pohoda software)
- CompuGroup (CompuGroup Medical, Medicus software)
- CLIMPROFI (CLIMPROFI s.r.o.)
- SEIVA (SEIVA s.r.o.)
- DrMAX
- Mediately (číslo je krátký hash, např. 80fae, bcd33)
- Kooperativa
- ICA (ICA a.s., První certifikační autorita — certifikáty)
- Česká pošta
- SOLDIERBOY
- OMNIPRAX
- Medicross (MediCross s.r.o.)
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. SPECIÁLNÍ PRAVIDLO: u faktur MEDEVIO přidej za číslo faktury i měsíční kód, například "2600616 JAN2026".
9. Částku piš vždy s desetinnou tečkou a měnou, například [5578.97 CZK]. Pokud je částka záporná (dobropis/storno), piš ji jako [-12941.00 CZK].
10. Pokud je částka v EUR, měna je EUR, pokud v Kč/CZK, měna je CZK.
11. Popis drž krátký, praktický a česky.
12. Popis dávej do hranatých závorek [popis].
13. Nepoužívej dvojtečky, lomítka, uvozovky ani znaky nevhodné pro Windows názvy souborů.
14. Pokud je dodavatel "Lékárna Poliklinika Prosek", "Lékárna Prosek" nebo podobně, použij jako dodavatele "Poliklinika Prosek" a jako popis [lékárna] nebo [léky do ordinace].
15. Paragon: pokud dokument nemá číslo dokladu, vynech ho. Popis musí popisovat co bylo nakoupeno.
16. Pokud jde jen o dodací list bez daňového dokladu, částku můžeš uvést, ale typ musí zůstat Dodací list.
17. Pokud si nejsi jistý popisem, použij obecný popis: [materiál do ordinace], [lékárna], [vakcíny], [testy], [licence], [předplatné].
18. 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 load_env() -> None:
if ENV_FILE.exists():
for line in ENV_FILE.read_text(encoding="utf-8").splitlines():
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
os.environ[k.strip()] = v.strip()
def pdf_to_base64(path: Path) -> str:
return base64.standard_b64encode(path.read_bytes()).decode("utf-8")
def sanitize_windows_filename(name: str) -> str:
name = re.sub(r'[<>:"/\\|?*]', " ", name)
name = re.sub(r"\s+", " ", name).strip()
name = name.rstrip(" .")
if not name.lower().endswith(".pdf"):
name += ".pdf"
return name
def unique_path(target: Path) -> Path:
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:
text = text.strip()
# Odstraň markdown code block (```json ... ``` nebo ``` ... ```)
text = re.sub(r"^```(?:json)?\s*", "", text)
text = re.sub(r"\s*```$", "", text.strip()).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 calculate_cost(input_tokens: int, output_tokens: int) -> dict:
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
return {
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"total_tokens": input_tokens + output_tokens,
"input_cost_usd": input_cost_usd,
"output_cost_usd": output_cost_usd,
"total_cost_usd": total_cost_usd,
"total_cost_czk": total_cost_usd * USD_TO_CZK,
}
def ask_claude_for_filename(client: anthropic.Anthropic, pdf_path: Path) -> tuple[str, dict]:
pdf_b64 = pdf_to_base64(pdf_path)
response = client.messages.create(
model=MODEL,
max_tokens=256,
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": pdf_b64,
},
},
{
"type": "text",
"text": NAMING_RULES,
},
],
}
],
)
text = next(
(block.text for block in response.content if block.type == "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(response.usage.input_tokens, response.usage.output_tokens)
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}")
load_env()
if not os.getenv("ANTHROPIC_API_KEY"):
raise RuntimeError(
f"Chybí ANTHROPIC_API_KEY. Zkontroluj soubor {ENV_FILE}"
)
client = anthropic.Anthropic()
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_claude_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}"
)
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}")
log_line("=" * 80)
log_line("\nHOTOVO")
if __name__ == "__main__":
main()