#!/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 json import time import argparse from pathlib import Path from datetime import datetime from decimal import Decimal import requests import oracledb # === CONFIG === API_BASE = "http://10.0.20.171:8000" API_USER = "MARIUS M" API_PASS = "123" SERVER_ID = "central" COMPANY_ID = 110 # MARIUSM AUTO ORACLE_CONFIG = { "user": "MARIUSM_AUTO", "password": "ROMFASTSOFT", "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()