#!/usr/bin/env python3 """ Procesare bon fiscal: PDF → OCR API → SQLite API → Oracle Usage: python process_bon.py [--save] --save Salvează efectiv în Oracle (altfel dry run) Fluxul: 1. OCR extract via API (http://10.0.20.171:8000/api/data-entry/ocr/extract) 2. Save receipt via API (http://10.0.20.171:8000/api/data-entry/receipts/) - TOATE datele 3. Save to Oracle: - Verifică/creează partener - Verifică TVA la încasare (CALENDAR.TVA_INCASARE) - Generează note contabile corecte """ import sys import os import json import time import argparse from pathlib import Path from datetime import datetime from decimal import Decimal import requests import oracledb from dotenv import load_dotenv # Load .env from parent directory load_dotenv(Path(__file__).parent.parent / ".env") # === CONFIG === API_BASE = "http://10.0.20.171:8000" API_USER = "MARIUS M" API_PASS = os.getenv("ROA_API_PASSWORD", "") SERVER_ID = "central" COMPANY_ID = 110 # MARIUSM AUTO ORACLE_CONFIG = { "user": "MARIUSM_AUTO", "password": os.getenv("ORACLE_PASSWORD", ""), "dsn": "10.0.20.121:1521/ROA" } # Mapare CUI → cont cheltuială CUI_TO_CONT = { "11201891": "6022", # MOL "1590082": "6022", # OMV Petrom "14991381": "6022", # Rompetrol "10562600": "6021", # Dedeman / Five Holding (Brick) "1879865": "6021", # Five Holding } # Mapare cotă TVA → (ID_JTVA baza, ID_JTVA tva, TAXCODE, TAXCODE_TVAI) # Pentru achiziții interne neexigibile (TVA la încasare) JTVA_NEEX = { 21: (210, 211, 301104, 301305), # ACH. INT. NEEX. 21% 19: (188, 189, 301101, 301301), # ACH. INT. NEEX. 19% 11: (214, 215, 301105, 301306), # ACH. INT. NEEX. 11% 9: (172, 173, 301102, 301302), # ACH. INT. NEEX. 9% 5: (174, 175, 301103, 301303), # ACH. INT. NEEX. 5% } # Pentru achiziții interne normale (fără TVA la încasare) JTVA_NORMAL = { 21: (208, 209, 301104, 301305), # ACH. INT. 21% 19: (None, None, 301101, 301301), 9: (None, None, 301102, 301302), } def get_cont(cui: str) -> str: """Mapare CUI → cont cheltuială.""" cui_clean = (cui or "").upper().replace("RO", "").strip() return CUI_TO_CONT.get(cui_clean, "6028") # 6028 = alte cheltuieli class APIClient: """Client pentru roa2web API.""" def __init__(self, base_url: str): self.base_url = base_url.rstrip("/") self.token = None self.session = requests.Session() def login(self, username: str, password: str, server_id: str) -> bool: """Login și obține token.""" r = self.session.post( f"{self.base_url}/api/auth/login", json={"username": username, "password": password, "server_id": server_id} ) if r.status_code == 200: data = r.json() self.token = data.get("access_token") self.session.headers["Authorization"] = f"Bearer {self.token}" return True print(f"Login failed: {r.status_code} - {r.text}") return False def ocr_extract(self, file_path: Path) -> dict: """Submit OCR job și așteaptă rezultatul.""" # Determine mime type suffix = file_path.suffix.lower() if suffix == ".pdf": mime_type = "application/pdf" elif suffix in (".jpg", ".jpeg"): mime_type = "image/jpeg" elif suffix == ".png": mime_type = "image/png" else: # Try to detect from content with open(file_path, "rb") as f: header = f.read(8) if header[:4] == b'%PDF': mime_type = "application/pdf" suffix = ".pdf" elif header[:3] == b'\xff\xd8\xff': mime_type = "image/jpeg" suffix = ".jpg" elif header[:8] == b'\x89PNG\r\n\x1a\n': mime_type = "image/png" suffix = ".png" else: mime_type = "application/pdf" # default suffix = ".pdf" # Use proper filename with extension filename = file_path.stem + suffix if not file_path.suffix else file_path.name # Submit with open(file_path, "rb") as f: r = self.session.post( f"{self.base_url}/api/data-entry/ocr/extract", files={"file": (filename, f, mime_type)} ) if r.status_code != 200: return {"success": False, "error": f"OCR submit failed: {r.text}"} job_id = r.json().get("job_id") print(f" OCR job: {job_id}") # Wait for result (max 60s per request, retry if pending) for _ in range(4): # Max 4 retries = ~240s total r = self.session.get( f"{self.base_url}/api/data-entry/ocr/jobs/{job_id}/wait", params={"timeout": 60, "wait_for_terminal": "true"}, timeout=70 ) if r.status_code != 200: return {"success": False, "error": f"OCR wait failed: {r.text}"} data = r.json() status = data.get("status") if status == "completed": return {"success": True, "result": data.get("result"), "time_ms": data.get("processing_time_ms")} elif status == "failed": return {"success": False, "error": data.get("error") or "OCR failed"} # Still pending/processing - will retry return {"success": False, "error": "OCR timeout"} def create_receipt(self, ocr_result: dict, company_id: int) -> dict: """Creează receipt în SQLite via API cu TOATE datele.""" # Parse date date_str = ocr_result.get("receipt_date") if date_str: receipt_date = date_str[:10] # YYYY-MM-DD else: receipt_date = datetime.now().strftime("%Y-%m-%d") # Build TVA breakdown from OCR tva_breakdown = [] for tva_entry in (ocr_result.get("tva_entries") or []): tva_breakdown.append({ "code": tva_entry.get("code"), "percent": tva_entry.get("percent"), "amount": float(tva_entry.get("amount") or 0) }) # Build payment methods from OCR payment_methods = [] for pm in (ocr_result.get("payment_methods") or []): payment_methods.append({ "method": pm.get("method"), "amount": float(pm.get("amount") or 0) }) # Determine payment mode payment_mode = ocr_result.get("suggested_payment_mode") or "casa" # If has CARD payment, it's "banca" if any(pm.get("method", "").upper() == "CARD" for pm in payment_methods): payment_mode = "banca" elif any(pm.get("method", "").upper() == "NUMERAR" for pm in payment_methods): payment_mode = "casa" payload = { "receipt_type": "bon_fiscal", "direction": "cheltuiala", "receipt_number": ocr_result.get("receipt_number"), "receipt_series": ocr_result.get("receipt_series"), "receipt_date": receipt_date, "amount": float(ocr_result.get("amount") or 0), "partner_name": ocr_result.get("partner_name"), "cui": ocr_result.get("cui"), "tva_total": float(ocr_result.get("tva_total") or 0), "tva_breakdown": tva_breakdown if tva_breakdown else None, "payment_methods": payment_methods if payment_methods else None, "payment_mode": payment_mode, "company_id": company_id, "vendor_address": ocr_result.get("address"), "items_count": ocr_result.get("items_count"), "ocr_raw_text": ocr_result.get("raw_text"), } # Remove None values payload = {k: v for k, v in payload.items() if v is not None} self.session.headers["X-Selected-Company"] = str(company_id) r = self.session.post( f"{self.base_url}/api/data-entry/receipts/", json=payload ) if r.status_code in (200, 201): return {"success": True, "receipt": r.json()} else: return {"success": False, "error": f"Create receipt failed: {r.text}"} def get_or_create_partner(cursor, cui: str, name: str, address: str = None) -> int: """Găsește sau creează partener în Oracle. Returnează ID_PART.""" cui_clean = (cui or "").upper().replace("RO", "").strip() if not cui_clean: return 0 # No CUI, no partner # Try to find existing partner cursor.execute(""" SELECT ID_PART FROM NOM_PARTENERI WHERE COD_FISCAL = :cui OR COD_FISCAL = :cui2 """, cui=cui_clean, cui2="RO" + cui_clean) row = cursor.fetchone() if row: return row[0] # Found existing partner # Create new partner cursor.execute("SELECT SEQ_NOM_PARTENERI.NEXTVAL FROM DUAL") new_id = cursor.fetchone()[0] # Clean name partner_name = (name or f"PARTENER {cui_clean}")[:100] partner_address = (address or "")[:200] cursor.execute(""" INSERT INTO NOM_PARTENERI (ID_PART, NUME, COD_FISCAL, ADRESA, STERS, INACTIV) VALUES (:id_part, :nume, :cui, :adresa, 0, 0) """, id_part=new_id, nume=partner_name, cui=cui_clean, adresa=partner_address) print(f" ➕ Partener nou creat: ID={new_id}, CUI={cui_clean}, Nume={partner_name}") return new_id def check_tva_incasare(cursor, an: int, luna: int) -> bool: """Verifică dacă firma e plătitoare de TVA la încasare în perioada dată.""" cursor.execute(""" SELECT NVL(TVA_INCASARE, 0) FROM CALENDAR WHERE AN = :an AND LUNA = :luna """, an=an, luna=luna) row = cursor.fetchone() return row[0] == 1 if row else False def save_to_oracle(ocr_result: dict, do_commit: bool = False) -> dict: """Salvează nota contabilă în Oracle cu toate regulile.""" conn = oracledb.connect(**ORACLE_CONFIG) cursor = conn.cursor() try: # Parse date date_str = ocr_result.get("receipt_date") if date_str: receipt_date = datetime.strptime(date_str[:10], "%Y-%m-%d").date() else: receipt_date = datetime.now().date() an, luna = receipt_date.year, receipt_date.month # 1. Get or create partner id_part = get_or_create_partner( cursor, ocr_result.get("cui"), ocr_result.get("partner_name"), ocr_result.get("address") ) print(f" Partner ID: {id_part}") # 2. Check TVA la încasare tva_incasare = check_tva_incasare(cursor, an, luna) cont_tva = "4428" if tva_incasare else "4426" print(f" TVA la încasare: {'DA (4428)' if tva_incasare else 'NU (4426)'}") # 3. Determine payment type payment_methods = ocr_result.get("payment_methods") or [] has_cash = any(pm.get("method", "").upper() == "NUMERAR" for pm in payment_methods) has_card = any(pm.get("method", "").upper() == "CARD" for pm in payment_methods) # If no payment info, assume cash if not payment_methods: has_cash = True print(f" Plată: {'NUMERAR' if has_cash else ''}{' + ' if has_cash and has_card else ''}{'CARD' if has_card else ''}") # 4. Init PACK_CONTAFIN cursor.callproc('PACK_CONTAFIN.INITIALIZEAZA_SCRIERE_ACT_RUL', [0, datetime.now(), an, luna, 0, 0, 0, 0]) # 5. Get next COD cursor.execute( "SELECT NVL(MAX(COD), 0) + 1 FROM ACT WHERE AN = :an AND LUNA = :luna", an=an, luna=luna ) cod = cursor.fetchone()[0] # 6. Calculate amounts total = float(ocr_result.get("amount") or 0) tva = float(ocr_result.get("tva_total") or 0) fara_tva = total - tva nract = ocr_result.get("receipt_number", "") nract = int(nract) if str(nract).isdigit() else 0 cont_cheltuiala = get_cont(ocr_result.get("cui") or "") expl = f"OCR: {ocr_result.get('partner_name') or 'N/A'}"[:100] print(f" COD: {cod}, Cont: {cont_cheltuiala}") print(f" Total: {total}, Bază: {fara_tva}, TVA: {tva}") # 7. Process TVA entries from OCR (pot fi mai multe cote TVA) tva_entries = ocr_result.get("tva_entries") or [] # 8. Build accounting lines lines = [] # Calculate base for each TVA rate if tva_entries: # Process each TVA entry separately for tva_entry in tva_entries: tva_rate = tva_entry.get("percent") or 21 tva_amount = float(tva_entry.get("amount") or 0) if tva_amount <= 0: continue # Calculate base for this TVA rate base_amount = tva_amount / (tva_rate / 100) # Get ID_JTVA_COLOANA and TAXCODE based on TVA rate and TVA la încasare if tva_incasare: jtva_data = JTVA_NEEX.get(tva_rate, (210, 211, 301104, 301305)) else: jtva_data = JTVA_NORMAL.get(tva_rate, (208, 209, 301104, 301305)) jtva_baza, jtva_tva, taxcode_normal, taxcode_tvai = jtva_data taxcode = taxcode_tvai if tva_incasare else taxcode_normal print(f" TVA {tva_rate}%: baza={base_amount:.2f}, tva={tva_amount:.2f}, JTVA=({jtva_baza},{jtva_tva}), TAXCODE={taxcode}") # Linia cheltuială pentru această cotă lines.append({ "scd": cont_cheltuiala, "scc": "401", "suma": base_amount, "expl": expl, "id_partc": id_part, "id_partd": 0, "id_jtva": jtva_baza, "taxcode": taxcode }) # Linia TVA pentru această cotă proc_tva = 1 + tva_rate / 100 # 1.21, 1.19, etc. lines.append({ "scd": cont_tva, "scc": "401", "suma": tva_amount, "expl": f"TVA {tva_rate}% {expl}"[:100], "id_partc": id_part, "id_partd": 0, "proc_tva": proc_tva, "id_jtva": jtva_tva, "taxcode": taxcode }) else: # Fallback: use total amounts if no tva_entries if fara_tva > 0: tva_rate = round(tva / fara_tva * 100) if fara_tva > 0 else 21 else: tva_rate = 21 if tva_incasare: jtva_data = JTVA_NEEX.get(tva_rate, (210, 211, 301104, 301305)) else: jtva_data = JTVA_NORMAL.get(tva_rate, (208, 209, 301104, 301305)) jtva_baza, jtva_tva, taxcode_normal, taxcode_tvai = jtva_data taxcode = taxcode_tvai if tva_incasare else taxcode_normal print(f" TVA {tva_rate}% (estimat): JTVA=({jtva_baza},{jtva_tva}), TAXCODE={taxcode}") lines.append({ "scd": cont_cheltuiala, "scc": "401", "suma": fara_tva, "expl": expl, "id_partc": id_part, "id_partd": 0, "id_jtva": jtva_baza, "taxcode": taxcode }) if tva > 0: proc_tva = 1 + tva_rate / 100 lines.append({ "scd": cont_tva, "scc": "401", "suma": tva, "expl": f"TVA {tva_rate}% {expl}"[:100], "id_partc": id_part, "id_partd": 0, "proc_tva": proc_tva, "id_jtva": jtva_tva, "taxcode": taxcode }) # Linia plată din casă (DOAR dacă plată numerar) if has_cash and not has_card: lines.append({ "scd": "401", "scc": "5311", "suma": total, "expl": f"Plata {expl}"[:100], "id_partc": 0, "id_partd": id_part, "id_jtva": None, # Nu are JTVA pentru plată "taxcode": None }) # Dacă plată CARD - nu se face nota 401=5311 (se face la extras bancar) # ID_FDOC = 17 pentru BON FISCAL id_fdoc = 17 # 9. Insert lines for line in lines: proc_tva = line.get("proc_tva") or 0 # Default 0 for non-TVA lines id_jtva = line.get("id_jtva") # Poate fi None pentru plăți taxcode = line.get("taxcode") # Poate fi None pentru plăți cursor.execute(""" INSERT INTO ACT_TEMP ( LUNA, AN, COD, DATAIREG, DATAACT, NRACT, EXPLICATIA, SCD, SCC, SUMA, PROC_TVA, ID_PARTC, ID_PARTD, ID_FDOC, ID_JTVA_COLOANA, TAXCODE, ID_UTIL, DATAORA ) VALUES ( :luna, :an, :cod, TRUNC(SYSDATE), :dataact, :nract, :expl, :scd, :scc, :suma, :proc_tva, :id_partc, :id_partd, :id_fdoc, :id_jtva, :taxcode, 0, SYSDATE ) """, luna=luna, an=an, cod=cod, dataact=receipt_date, nract=nract, expl=line["expl"], scd=line["scd"], scc=line["scc"], suma=line["suma"], proc_tva=proc_tva, id_partc=line["id_partc"], id_partd=line["id_partd"], id_fdoc=id_fdoc, id_jtva=id_jtva, taxcode=taxcode ) jtva_info = f" [JTVA={id_jtva}]" if id_jtva else "" taxcode_info = f" [TAX={taxcode}]" if taxcode else "" print(f" {line['scd']} = {line['scc']}: {line['suma']:.2f}{jtva_info}{taxcode_info}") # 9. Finalize mesaj = cursor.var(oracledb.STRING, 4000) cursor.callproc('PACK_CONTAFIN.FINALIZEAZA_SCRIERE_ACT_RUL', [0, cod, 0, 0, 0, mesaj]) if do_commit: conn.commit() return {"success": True, "cod": cod, "luna": luna, "an": an, "saved": True, "id_part": id_part, "tva_incasare": tva_incasare} else: conn.rollback() return {"success": True, "cod": cod, "luna": luna, "an": an, "saved": False, "id_part": id_part, "tva_incasare": tva_incasare} except Exception as e: conn.rollback() import traceback return {"success": False, "error": str(e), "traceback": traceback.format_exc()} finally: cursor.close() conn.close() def process_bon(file_path: Path, do_save: bool = False, company_id: int = COMPANY_ID, api_user: str = API_USER, api_pass: str = API_PASS): """Procesează un bon fiscal: OCR → SQLite → Oracle.""" print("=" * 60) print(f"📄 Procesez: {file_path.name}") print("=" * 60) # 1. Login print("\n🔑 Login API...") client = APIClient(API_BASE) if not client.login(api_user, api_pass, SERVER_ID): print("❌ Login failed!") return None print(" ✅ OK") # 2. OCR print("\n🔍 OCR extract...") ocr_result = client.ocr_extract(file_path) if not ocr_result["success"]: print(f" ❌ {ocr_result['error']}") return None ocr = ocr_result["result"] print(f" ✅ OK ({ocr_result.get('time_ms', '?')}ms)") print(f" CUI: {ocr.get('cui')}") print(f" Partner: {ocr.get('partner_name')}") print(f" Data: {ocr.get('receipt_date')}") print(f" Total: {ocr.get('amount')} RON") print(f" TVA: {ocr.get('tva_total')} RON") # Show payment methods payment_methods = ocr.get("payment_methods") or [] if payment_methods: pm_str = ", ".join(f"{pm.get('method')}: {pm.get('amount')}" for pm in payment_methods) print(f" Plăți: {pm_str}") # Show TVA breakdown tva_entries = ocr.get("tva_entries") or [] if tva_entries: tva_str = ", ".join(f"{t.get('code')}({t.get('percent')}%): {t.get('amount')}" for t in tva_entries) print(f" TVA detaliat: {tva_str}") # 3. SQLite (via API) - cu TOATE datele print("\n💾 Save SQLite (via API)...") sqlite_result = client.create_receipt(ocr, company_id) if not sqlite_result["success"]: print(f" ❌ {sqlite_result['error']}") return None receipt = sqlite_result["receipt"] print(f" ✅ Receipt ID: {receipt.get('id')}") print(f" Payment mode: {receipt.get('payment_mode')}") # 4. Oracle (direct) mode = "SAVE" if do_save else "DRY RUN" print(f"\n🗄️ Save Oracle ({mode})...") oracle_result = save_to_oracle(ocr, do_commit=do_save) if oracle_result["success"]: if oracle_result["saved"]: print(f" ✅ SALVAT: COD={oracle_result['cod']}, {oracle_result['luna']:02d}/{oracle_result['an']}") else: print(f" ⚠️ DRY RUN: ar fi COD={oracle_result['cod']}") else: print(f" ❌ {oracle_result.get('error')}") if oracle_result.get("traceback"): print(oracle_result["traceback"]) print("\n" + "=" * 60) return { "ocr": ocr, "sqlite_receipt_id": receipt.get("id"), "oracle": oracle_result } def main(): parser = argparse.ArgumentParser(description="Procesare bon fiscal: OCR → SQLite → Oracle") parser.add_argument("file", help="Path către PDF sau imagine") parser.add_argument("--save", action="store_true", help="Salvează efectiv în Oracle") parser.add_argument("--company", type=int, default=COMPANY_ID, help="Company ID") parser.add_argument("--user", default=API_USER, help="API username") parser.add_argument("--password", default=API_PASS, help="API password") args = parser.parse_args() file_path = Path(args.file) if not file_path.exists(): print(f"❌ File not found: {file_path}") sys.exit(1) result = process_bon(file_path, do_save=args.save, company_id=args.company, api_user=args.user, api_pass=args.password) if result: print("\n✅ Done!") else: print("\n❌ Failed!") sys.exit(1) if __name__ == "__main__": main()