feat(telegram): bot bonuri fiscale — OCR → preview → Oracle write
- US-001: mută queue_client.py în data_entry/services/ocr/ - US-002/003/004: oracle_receipt_writer + oracle_server_id în DB - US-005: receipt_handlers.py (PDF/photo/callback flow) - US-006: wire handlers în main.py, per-schema connect, seq_cod.nextval - US-007: .gitignore secrets/*.oracle_pass - US-008/009/010: teste unit + integration + E2E - setup-secrets.sh helper + template - docs/telegram/README.md actualizat cu arhitectura nouă Testat E2E pe DB live (MARIUSM_AUTO). COD din seq_cod.nextval. pypdfium2 fallback pentru PDF decode (fără poppler). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,108 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Client pentru OCR API roa2web - adaugă job direct în SQLite queue.
|
||||
Folosește aceeași coadă ca backend-ul, fără HTTP auth.
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# Paths
|
||||
QUEUE_DIR = Path("/workspace/roa2web/backend/data/ocr_queue")
|
||||
DB_PATH = QUEUE_DIR / "ocr_jobs.db"
|
||||
FILES_DIR = QUEUE_DIR / "files"
|
||||
|
||||
async def submit_ocr_job(file_path: Path, engine: str = "doctr_plus") -> str:
|
||||
"""Submit OCR job to queue, return job_id."""
|
||||
import aiosqlite
|
||||
|
||||
job_id = str(uuid.uuid4())
|
||||
|
||||
# Copy file to queue
|
||||
FILES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
dest_path = FILES_DIR / f"{job_id}_{file_path.name}"
|
||||
shutil.copy(file_path, dest_path)
|
||||
|
||||
# Determine mime type
|
||||
mime_type = "application/pdf" if file_path.suffix.lower() == ".pdf" else "image/jpeg"
|
||||
|
||||
async with aiosqlite.connect(str(DB_PATH), timeout=5.0) as db:
|
||||
await db.execute("""
|
||||
INSERT INTO ocr_jobs (
|
||||
id, status, file_path, mime_type, engine,
|
||||
created_at, original_filename, expires_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
job_id, "pending", str(dest_path), mime_type, engine,
|
||||
datetime.now().isoformat(), file_path.name,
|
||||
(datetime.now() + timedelta(hours=24)).isoformat()
|
||||
))
|
||||
await db.commit()
|
||||
|
||||
return job_id
|
||||
|
||||
async def wait_for_result(job_id: str, timeout: int = 120) -> dict:
|
||||
"""Wait for job completion and return result."""
|
||||
import aiosqlite
|
||||
|
||||
start = datetime.now()
|
||||
while (datetime.now() - start).seconds < timeout:
|
||||
async with aiosqlite.connect(str(DB_PATH), timeout=5.0) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
async with db.execute(
|
||||
"SELECT status, result_json, error_message, processing_time_ms FROM ocr_jobs WHERE id = ?",
|
||||
(job_id,)
|
||||
) as cursor:
|
||||
row = await cursor.fetchone()
|
||||
if row:
|
||||
if row["status"] == "completed":
|
||||
return {
|
||||
"success": True,
|
||||
"result": json.loads(row["result_json"]) if row["result_json"] else None,
|
||||
"time_ms": row["processing_time_ms"]
|
||||
}
|
||||
elif row["status"] == "failed":
|
||||
return {
|
||||
"success": False,
|
||||
"error": row["error_message"]
|
||||
}
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
return {"success": False, "error": "Timeout"}
|
||||
|
||||
async def process_file(file_path: Path):
|
||||
"""Process file through OCR queue."""
|
||||
print(f"[OCR Queue] Submitting: {file_path.name}")
|
||||
job_id = await submit_ocr_job(file_path)
|
||||
print(f"[OCR Queue] Job ID: {job_id}")
|
||||
print(f"[OCR Queue] Waiting for result...")
|
||||
|
||||
result = await wait_for_result(job_id)
|
||||
|
||||
if result["success"]:
|
||||
r = result["result"]
|
||||
print(f"\n✅ OCR Complete ({result['time_ms']}ms)")
|
||||
print(f" CUI: {r.get('cui')}")
|
||||
print(f" Data: {r.get('receipt_date')}")
|
||||
print(f" Total: {r.get('amount')}")
|
||||
print(f" TVA: {r.get('tva_total')}")
|
||||
return r
|
||||
else:
|
||||
print(f"\n❌ Error: {result['error']}")
|
||||
return None
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python roa2web_api_client.py <file_path>")
|
||||
sys.exit(1)
|
||||
|
||||
file_path = Path(sys.argv[1])
|
||||
if not file_path.exists():
|
||||
print(f"File not found: {file_path}")
|
||||
sys.exit(1)
|
||||
|
||||
asyncio.run(process_file(file_path))
|
||||
@@ -7,28 +7,17 @@ import sys
|
||||
sys.path.insert(0, "/workspace/roa2web")
|
||||
|
||||
import asyncio
|
||||
import oracledb
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
from backend.modules.data_entry.services.oracle_receipt_writer import write_receipt
|
||||
|
||||
ORACLE_CONFIG = {
|
||||
"user": "MARIUSM_AUTO",
|
||||
"password": "ROMFASTSOFT",
|
||||
"dsn": "10.0.20.121:1521/ROA"
|
||||
}
|
||||
|
||||
CUI_TO_CONT = {
|
||||
"11201891": "6022", # MOL
|
||||
"1590082": "6022", # OMV Petrom
|
||||
"14991381": "6022", # MOL Romania
|
||||
"10562600": "6021", # Dedeman
|
||||
}
|
||||
|
||||
def get_cont_cheltuiala(cui: str) -> str:
|
||||
cui_clean = cui.upper().replace("RO", "").strip()
|
||||
return CUI_TO_CONT.get(cui_clean, "6028")
|
||||
|
||||
async def process_pdf(pdf_path: Path):
|
||||
from backend.modules.data_entry.services.ocr_service import ocr_service
|
||||
|
||||
@@ -57,109 +46,27 @@ def save_to_oracle_with_pack(result, do_commit: bool = False):
|
||||
mode = "SAVE" if do_commit else "DRY RUN"
|
||||
print(f"\n[Oracle + PACK_CONTAFIN] {mode}")
|
||||
print("-" * 50)
|
||||
|
||||
conn = oracledb.connect(**ORACLE_CONFIG)
|
||||
cursor = conn.cursor()
|
||||
|
||||
|
||||
receipt_dict = {
|
||||
"partner_name": result.partner_name,
|
||||
"cui": result.cui,
|
||||
"receipt_date": result.receipt_date,
|
||||
"receipt_number": result.receipt_number,
|
||||
"amount": result.amount,
|
||||
"tva_total": result.tva_total,
|
||||
}
|
||||
|
||||
try:
|
||||
an = result.receipt_date.year if result.receipt_date else datetime.now().year
|
||||
luna = result.receipt_date.month if result.receipt_date else datetime.now().month
|
||||
receipt_date = result.receipt_date or datetime.now().date()
|
||||
|
||||
# Parametri
|
||||
id_util = 0 # ID utilizator implicit
|
||||
id_sucursala = 0
|
||||
|
||||
# 1. INITIALIZEAZA
|
||||
print("[1] INITIALIZEAZA_SCRIERE_ACT_RUL...")
|
||||
cursor.callproc('PACK_CONTAFIN.INITIALIZEAZA_SCRIERE_ACT_RUL', [
|
||||
id_util,
|
||||
datetime.now(),
|
||||
an,
|
||||
luna,
|
||||
0, # suprascriere_cod
|
||||
0, # suprascriere_anluna
|
||||
0, # scrie_sterge (0=scrie)
|
||||
id_sucursala
|
||||
])
|
||||
print(" OK")
|
||||
|
||||
# Obține COD din secvență sau calculează
|
||||
cursor.execute("SELECT NVL(MAX(COD), 0) + 1 FROM ACT WHERE AN = :an AND LUNA = :luna", an=an, luna=luna)
|
||||
cod = cursor.fetchone()[0]
|
||||
|
||||
# Partner
|
||||
cui_clean = (result.cui or "").upper().replace("RO", "").strip()
|
||||
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()
|
||||
id_part = row[0] if row else 0
|
||||
|
||||
total = float(result.amount or 0)
|
||||
tva = float(result.tva_total or 0)
|
||||
fara_tva = total - tva
|
||||
nract = int(result.receipt_number) if result.receipt_number and result.receipt_number.isdigit() else 0
|
||||
cont = get_cont_cheltuiala(result.cui or "")
|
||||
expl = f"OCR: {result.partner_name or 'N/A'}"
|
||||
|
||||
print(f" COD: {cod}, Partner ID: {id_part}, Cont: {cont}")
|
||||
|
||||
# 2. INSERT în ACT_TEMP
|
||||
print("[2] INSERT ACT_TEMP...")
|
||||
|
||||
lines = [
|
||||
(cont, "401", fara_tva, expl, id_part, 0), # cheltuială - partener pe credit
|
||||
("401", "5311", total, f"Plata {expl}", 0, id_part), # plată - partener pe debit
|
||||
]
|
||||
if tva > 0:
|
||||
lines.insert(1, ("4426", "401", tva, f"TVA {expl}", id_part, 0)) # TVA - partener pe credit
|
||||
|
||||
for scd, scc, suma, e, id_partc, id_partd in lines:
|
||||
cursor.execute("""
|
||||
INSERT INTO ACT_TEMP (
|
||||
LUNA, AN, COD, DATAIREG, DATAACT, NRACT,
|
||||
EXPLICATIA, SCD, SCC, SUMA,
|
||||
ID_PARTC, ID_PARTD, ID_UTIL, DATAORA
|
||||
) VALUES (
|
||||
:luna, :an, :cod, TRUNC(SYSDATE), :dataact, :nract,
|
||||
:expl, :scd, :scc, :suma,
|
||||
:id_partc, :id_partd, :id_util, SYSDATE
|
||||
)
|
||||
""", luna=luna, an=an, cod=cod, dataact=receipt_date, nract=nract,
|
||||
expl=e, scd=scd, scc=scc, suma=suma,
|
||||
id_partc=id_partc, id_partd=id_partd, id_util=id_util)
|
||||
print(f" {scd} = {scc}: {suma:.2f}")
|
||||
|
||||
# 3. FINALIZEAZA
|
||||
print("[3] FINALIZEAZA_SCRIERE_ACT_RUL...")
|
||||
mesaj = cursor.var(oracledb.STRING, 4000)
|
||||
cursor.callproc('PACK_CONTAFIN.FINALIZEAZA_SCRIERE_ACT_RUL', [
|
||||
id_util,
|
||||
cod,
|
||||
0, # scrie_sterge
|
||||
0, # modificare_nota
|
||||
0, # scrie_cump_vanz
|
||||
mesaj
|
||||
])
|
||||
result_msg = mesaj.getvalue()
|
||||
print(f" Mesaj: {result_msg}")
|
||||
|
||||
cod, mesaj = write_receipt(receipt_dict, ORACLE_CONFIG, commit=do_commit)
|
||||
print(f" Mesaj: {mesaj}")
|
||||
if do_commit:
|
||||
conn.commit()
|
||||
print(f"\n✅ SALVAT în Oracle (COD={cod})")
|
||||
else:
|
||||
conn.rollback()
|
||||
print(f"\n⚠️ DRY RUN - rollback (COD ar fi fost {cod})")
|
||||
|
||||
return cod, result_msg
|
||||
|
||||
return cod, mesaj
|
||||
except Exception as e:
|
||||
print(f"❌ Eroare: {e}")
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
async def main():
|
||||
if len(sys.argv) < 2:
|
||||
|
||||
@@ -8,10 +8,10 @@ import json
|
||||
import shutil
|
||||
import sys
|
||||
import uuid
|
||||
import oracledb
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from decimal import Decimal
|
||||
|
||||
from backend.modules.data_entry.services.oracle_receipt_writer import write_receipt
|
||||
|
||||
# OCR Queue paths
|
||||
QUEUE_DIR = Path("/workspace/roa2web/backend/data/ocr_queue")
|
||||
@@ -25,16 +25,6 @@ ORACLE_CONFIG = {
|
||||
"dsn": "10.0.20.121:1521/ROA"
|
||||
}
|
||||
|
||||
CUI_TO_CONT = {
|
||||
"11201891": "6022",
|
||||
"1590082": "6022",
|
||||
"14991381": "6022",
|
||||
"10562600": "6021",
|
||||
}
|
||||
|
||||
def get_cont(cui: str) -> str:
|
||||
return CUI_TO_CONT.get(cui.upper().replace("RO", "").strip(), "6028")
|
||||
|
||||
async def submit_ocr_job(file_path: Path) -> str:
|
||||
import aiosqlite
|
||||
job_id = str(uuid.uuid4())
|
||||
@@ -43,13 +33,14 @@ async def submit_ocr_job(file_path: Path) -> str:
|
||||
shutil.copy(file_path, dest_path)
|
||||
mime_type = "application/pdf" if file_path.suffix.lower() == ".pdf" else "image/jpeg"
|
||||
|
||||
now = datetime.now()
|
||||
async with aiosqlite.connect(str(DB_PATH), timeout=5.0) as db:
|
||||
await db.execute("""
|
||||
INSERT INTO ocr_jobs (id, status, file_path, mime_type, engine, created_at, original_filename, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (job_id, "pending", str(dest_path), mime_type, "doctr_plus",
|
||||
datetime.now().isoformat(), file_path.name,
|
||||
(datetime.now() + timedelta(hours=24)).isoformat()))
|
||||
now.isoformat(), file_path.name,
|
||||
(now + timedelta(hours=24)).isoformat()))
|
||||
await db.commit()
|
||||
return job_id
|
||||
|
||||
@@ -72,71 +63,20 @@ async def wait_for_result(job_id: str, timeout: int = 120) -> dict:
|
||||
return {"success": False, "error": "Timeout"}
|
||||
|
||||
def save_to_oracle(ocr_result: dict, do_commit: bool = False) -> dict:
|
||||
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
|
||||
|
||||
# Init
|
||||
cursor.callproc('PACK_CONTAFIN.INITIALIZEAZA_SCRIERE_ACT_RUL', [0, datetime.now(), an, luna, 0, 0, 0, 0])
|
||||
|
||||
# Get 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]
|
||||
|
||||
# Partner
|
||||
cui_clean = (ocr_result.get("cui") or "").upper().replace("RO", "").strip()
|
||||
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()
|
||||
id_part = row[0] if row else 0
|
||||
|
||||
# Amounts
|
||||
total = float(ocr_result.get("amount") or 0)
|
||||
tva = float(ocr_result.get("tva_total") or 0)
|
||||
fara_tva = total - tva
|
||||
nract = int(ocr_result.get("receipt_number") or 0) if str(ocr_result.get("receipt_number", "")).isdigit() else 0
|
||||
cont = get_cont(ocr_result.get("cui") or "")
|
||||
expl = f"OCR: {ocr_result.get('partner_name') or 'N/A'}"
|
||||
|
||||
# Insert lines
|
||||
lines = [
|
||||
(cont, "401", fara_tva, expl, id_part, 0),
|
||||
("401", "5311", total, f"Plata {expl}", 0, id_part),
|
||||
]
|
||||
if tva > 0:
|
||||
lines.insert(1, ("4426", "401", tva, f"TVA {expl}", id_part, 0))
|
||||
|
||||
for scd, scc, suma, e, id_partc, id_partd in lines:
|
||||
cursor.execute("""
|
||||
INSERT INTO ACT_TEMP (LUNA, AN, COD, DATAIREG, DATAACT, NRACT, EXPLICATIA, SCD, SCC, SUMA, ID_PARTC, ID_PARTD, ID_UTIL, DATAORA)
|
||||
VALUES (:luna, :an, :cod, TRUNC(SYSDATE), :dataact, :nract, :expl, :scd, :scc, :suma, :id_partc, :id_partd, 0, SYSDATE)
|
||||
""", luna=luna, an=an, cod=cod, dataact=receipt_date, nract=nract, expl=e, scd=scd, scc=scc, suma=suma, id_partc=id_partc, id_partd=id_partd)
|
||||
|
||||
# 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}
|
||||
else:
|
||||
conn.rollback()
|
||||
return {"success": True, "cod": cod, "luna": luna, "an": an, "saved": False}
|
||||
receipt_date = (
|
||||
datetime.strptime(date_str[:10], "%Y-%m-%d").date() if date_str else None
|
||||
)
|
||||
effective_date = receipt_date or datetime.now().date()
|
||||
an, luna = effective_date.year, effective_date.month
|
||||
|
||||
receipt_dict = {**ocr_result, "receipt_date": receipt_date}
|
||||
|
||||
cod, _mesaj = write_receipt(receipt_dict, ORACLE_CONFIG, commit=do_commit)
|
||||
return {"success": True, "cod": cod, "luna": luna, "an": an, "saved": do_commit}
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
return {"success": False, "error": str(e)}
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
async def process_whatsapp_file(file_path: Path, do_save: bool = False):
|
||||
print(f"📄 Procesez: {file_path.name}")
|
||||
|
||||
Reference in New Issue
Block a user