diff --git a/memory/kb/projects/roa2web-telegram-import/README.md b/memory/kb/projects/roa2web-telegram-import/README.md new file mode 100644 index 0000000..cb95c40 --- /dev/null +++ b/memory/kb/projects/roa2web-telegram-import/README.md @@ -0,0 +1,386 @@ +# Proiect: Import Bonuri Fiscale via Telegram/WhatsApp → ROA + +**Data:** 2026-02-03 +**Status:** Analiză completă +**Tags:** @work @project + +--- + +## TL;DR + +Sistem pentru importul bonurilor fiscale de achiziție din Telegram/WhatsApp în contabilitatea ROA. OCR prin Doctr (cost zero, local). Integrare cu Oracle prin proceduri existente (PACK_CONTAFIN). + +--- + +## 1. Obiective + +- **Input:** Bonuri fiscale trimise pe Telegram/WhatsApp (PDF/imagine) +- **Procesare:** OCR local (Doctr) - cost zero +- **Output:** Note contabile în Oracle (ACT, BAL, etc.) +- **Aprobare:** Opțional - contabil verifică înainte de salvare finală + +--- + +## 2. Arhitectură + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Telegram │ │ │ │ │ │ │ +│ WhatsApp │────▶│ Clawdbot │────▶│ roa2web │────▶│ ROA Oracle │ +└─────────────┘ │ (Bridge) │ │ (OCR+API) │ │ (PACK_ │ + └─────────────┘ └─────────────┘ │ CONTAFIN) │ + │ │ └─────────────┘ + ▼ ▼ │ + • Primește fișier • Doctr OCR • ACT_TEMP + • Forward la API • PDF text layer • ACT, BAL + • Răspuns user • JSON extrase • IREG_PART +``` + +--- + +## 3. Componente Existente + +### 3.1 roa2web (LXC 171 - claude-agent) + +| Component | Status | Locație | +|-----------|--------|---------| +| **OCR Engine** | ✅ Complet | `/workspace/roa2web/backend/modules/data_entry/services/ocr_engine.py` | +| **Doctr** | ✅ Instalat | PyTorch backend, db_resnet50 + crnn_vgg16_bn | +| **PaddleOCR** | ✅ Instalat | Backup engine | +| **API OCR** | ✅ Funcțional | `POST /api/data-entry/ocr/extract` | +| **Job Queue** | ✅ Complet | Async processing cu worker pool | +| **Bot Telegram** | ⚠️ Parțial | Dashboard/sold, NU handler fișiere | + +### 3.2 Schema Date OCR (ExtractionData) + +```python +ExtractionData: + partner_name: str # nume furnizor + cui: str # CUI furnizor + receipt_date: date # data bon + receipt_number: str # număr bon + receipt_series: str # serie bon + amount: Decimal # total + tva_entries: List[TvaEntry] # TVA pe cote (A=19%, B=9%, etc.) + tva_total: Decimal # total TVA + payment_methods: List # CARD/NUMERAR + address: str # adresă furnizor + confidence_*: float # scoruri încredere +``` + +### 3.3 Oracle (MARIUSM_AUTO) + +**Tabele principale:** + +| Tabel | Scop | +|-------|------| +| **ACT** | Note contabile | +| **ACT_TEMP** | Staging pentru note | +| **RUL** | Rulaje gestiuni | +| **STOC** | Solduri stoc | +| **BAL** | Balanță verificare | +| **IREG_PARTENERI** | Evidență parteneri | +| **BALANTA_PARTENERI** | Balanță parteneri | +| **ATAS_ATASAMENTE** | Fișiere atașate (BLOB) | +| **ATAS_REFERINTE** | Legături atașamente | +| **NOM_FDOC** | Nomenclator tipuri documente | + +**Package-uri relevante:** + +| Package | Scop | +|---------|------| +| **PACK_CONTAFIN** | Salvare note contabile (principal) | +| **PACK_FACTURARE** | Exemple de salvare (pentru vânzări) | +| **PACK_PARTENERI** | Gestiune parteneri | + +--- + +## 4. Formula Contabilă - Bon Fiscal Achiziție + +### 4.1 Bon simplu (plătit cash) + +``` +Cheltuială = Furnizor: 6xx = 401 (valoare fără TVA) +TVA deductibil = Furn: 4426 = 401 (TVA) +Plată cash: 401 = 5311 (total cu TVA) +``` + +**Exemplu bon benzină 119 RON (100 + 19 TVA):** +```sql +-- Linia 1: Cheltuială combustibil +SCD='6022', SCC='401', SUMA=100.00 + +-- Linia 2: TVA deductibil +SCD='4426', SCC='401', SUMA=19.00, PROC_TVA=1.19 + +-- Linia 3: Plată din casă +SCD='401', SCC='5311', SUMA=119.00 +``` + +### 4.2 Bon cu TVA la încasare + +``` +Cheltuială = Furnizor: 6xx = 401 +TVA neexigibil = Furn: 4428 = 401 +Plată cash: 401 = 5311 +Exigibilizare TVA: 4426 = 4428 +``` + +### 4.3 Conturi cheltuieli uzuale + +| Tip | Cont | Descriere | +|-----|------|-----------| +| Combustibil | 6022 | Combustibili | +| Materiale | 6021 | Materiale auxiliare | +| Rechizite | 6021 | Materiale auxiliare | +| Piese | 6024 | Piese schimb | +| Întreținere | 611 | Cheltuieli întreținere | +| Transport | 624 | Cheltuieli transport | + +--- + +## 5. Flux Oracle - Salvare Note Contabile + +### 5.1 Proceduri PACK_CONTAFIN + +```sql +-- 1. INIȚIALIZARE SESIUNE +PACK_CONTAFIN.INITIALIZEAZA_SCRIERE_ACT_RUL( + TNIDUTIL NUMBER, -- ID utilizator + TDDATAORA DATE, -- Data și ora + TNAN NUMBER, -- An + TNLUNA NUMBER, -- Luna + TNSUPRASCRIERECOD NUMBER, -- 0/1 + TNSUPRASCRIEREANLUNA NUMBER, -- 0/1 + TNSCRIESTERGE NUMBER, -- 0=scrie, 2=șterge + TNIDSUCURSALA NUMBER -- ID sucursală +); + +-- 2. INSERT ÎN ACT_TEMP (pentru fiecare linie) +INSERT INTO ACT_TEMP ( + LUNA, AN, COD, + DATAIREG, DATAACT, DATASCAD, + NRACT, SERIE_ACT, + EXPLICATIA, + SCD, ASCD, -- cont debit, analitic + SCC, ASCC, -- cont credit, analitic + SUMA, + PROC_TVA, + ID_FDOC, -- tip document + ID_PARTD, ID_PARTC, -- parteneri + ID_FACT, + ID_VALUTA, + ID_SUCURSALA +) VALUES (...); + +-- 3. FINALIZARE (transferă în ACT, actualizează BAL, etc.) +PACK_CONTAFIN.FINALIZEAZA_SCRIERE_ACT_RUL( + TNIDUTIL NUMBER, -- ID utilizator + TNCOD NUMBER, -- Cod document + TNSCRIESTERGE NUMBER, -- 0=scrie + TNMODIFICARENOTA NUMBER, -- 0/1 + TNSCRIECUMPVANZ NUMBER, -- 0/1 + TCMESAJREFACERENOTA OUT VARCHAR2 -- Mesaj rezultat +); +``` + +### 5.2 Ce face FINALIZEAZA intern + +```sql +-- La linia 8059 în PACK_CONTAFIN: +pack_contafin.SCRIE_IN_ACT(user); -- ACT_TEMP → ACT + BAL +pack_contafin.SCRIE_IN_RUL(user); -- RUL_TEMP → RUL (dacă există) +pack_contafin.SCRIE_IN_RUL_OBINV(user); -- pentru obiecte inventar +``` + +--- + +## 6. Structura ACT_TEMP + +```sql +ACT_TEMP ( + LUNA NUMBER, + AN NUMBER, + COD NUMBER, + DATAIREG DATE, + NRACT NUMBER, + DATAACT DATE, + EXPLICATIA VARCHAR2(100), + SCD VARCHAR2(10), -- Cont DEBIT + ASCD VARCHAR2(20), -- Analitic debit + SCC VARCHAR2(10), -- Cont CREDIT + ASCC VARCHAR2(20), -- Analitic credit + SUMA NUMBER, + PROC_TVA NUMBER, -- Procent TVA (1.19, 1.09, etc.) + ID_FDOC NUMBER, -- Tip document (din NOM_FDOC) + ID_PARTD NUMBER, -- Partner debit + ID_PARTC NUMBER, -- Partner credit + ID_FACT NUMBER, -- ID factură (opțional) + ID_VALUTA NUMBER, + ID_SUCURSALA NUMBER, + SERIE_ACT VARCHAR2(10), + TVA_INCASARE NUMBER, -- 0/1 pentru TVA la încasare + ... +) +``` + +--- + +## 7. Optimizări Propuse + +### 7.1 PDF Text Layer Extraction + +Adobe Scan creează PDF-uri cu text layer. Verificăm înainte de OCR: + +```python +import pdfplumber + +def extract_text_or_ocr(pdf_path): + with pdfplumber.open(pdf_path) as pdf: + text = "" + for page in pdf.pages: + page_text = page.extract_text() + if page_text: + text += page_text + + if text.strip(): + # PDF are text layer - folosește-l direct + return {"source": "pdf_layer", "text": text} + else: + # Nu are text - fallback la Doctr OCR + return doctr_ocr(pdf_path) +``` + +**Beneficii:** +- Instant vs. 2-5 sec OCR +- 100% acuratețe vs. ~95% OCR +- Zero procesare GPU + +### 7.2 Mapare CUI → Cont + +Tabel pentru auto-detectare tip cheltuială: + +```python +CUI_CONT_MAP = { + # Benzinării + "RO1234567": "6022", # OMV + "RO2345678": "6022", # Petrom + "RO3456789": "6022", # Mol + + # Magazine bricolaj + "RO10562600": "6021", # Dedeman + + # Default + "DEFAULT": "6028" # Alte cheltuieli +} +``` + +--- + +## 8. Status Implementare (2026-02-03) + +| Component | Status | Note | +|-----------|--------|------| +| OCR (Doctr/PaddleOCR) | ✅ Testat | 34 sec per PDF | +| Script OCR standalone | ✅ Funcțional | `/workspace/roa2web-telegram-test/` | +| Python: INSERT ACT_TEMP | ✅ Testat | 3 linii: cheltuială + TVA + plată | +| Mapare CUI → cont | ✅ Bază | 6022=combustibil, 6021=materiale | +| Găsire partener | ✅ Funcțional | Caută în NOM_PARTENERI | +| Conexiune Oracle | ✅ Testat | Server 10.0.20.121 | +| Handler WhatsApp | 🔄 În pregătire | Clawdbot poate primi fișiere | +| CALL PACK_CONTAFIN | ⏳ TODO | Finalizare cu procedurile Oracle | +| UI aprobare | ⏳ TODO | Opțional | + +### Scripturi create: +- `/workspace/roa2web-telegram-test/process_and_save.py` - OCR → Oracle +- `/workspace/roa2web-telegram-test/save_to_oracle.py` - test salvare Oracle + +### Test efectuat: +``` +PDF "benzina 07 aug. 2024.pdf" → OCR → Oracle ACT_TEMP + CUI: RO11201891 + Data: 2024-08-01 + Total: 263.28 RON + TVA: 42.04 (19%) + Note: 6022=401, 4426=401, 401=5311 + Status: DRY RUN OK +``` + +--- + +## 9. Plan Implementare + +### Faza 1: Handler Telegram (3-4h) +- [ ] MessageHandler pentru imagini/PDF în bot +- [ ] Forward la `/api/data-entry/ocr/extract` +- [ ] Răspuns cu datele extrase (preview) + +### Faza 2: PDF Optimization (1-2h) +- [ ] Verificare text layer în PDF +- [ ] Extracție directă dacă există +- [ ] Fallback la Doctr + +### Faza 3: Integrare Oracle (5-7h) +- [ ] Conexiune Oracle din roa2web +- [ ] Găsire/creare partener după CUI +- [ ] INSERT în ACT_TEMP +- [ ] Apel PACK_CONTAFIN proceduri +- [ ] Salvare atașament (ATAS_ATASAMENTE) + +### Faza 4: WhatsApp Bridge (4-6h) +- [ ] Clawdbot handler pentru fișiere +- [ ] Forward la roa2web API +- [ ] Răspuns pe canal + +### Faza 5: Aprobare & Polish (3-5h) +- [ ] Workflow aprobare (opțional) +- [ ] Mapare automată CUI → cont +- [ ] Logging și error handling + +--- + +## 10. Credențiale Test + +**Oracle Test Server:** +``` +Host: 10.0.20.121 +Port: 1521 +SID: ROA +User: CONTAFIN_ORACLE +Pass: (în secrets/test.oracle_pass) +Schema: MARIUSM_AUTO +``` + +**roa2web:** +``` +LXC: 171 (claude-agent) +Path: /workspace/roa2web +Start: ./start.sh test +``` + +--- + +## 11. Fișiere Proiect + +``` +roa2web-telegram-import/ +├── README.md # Acest fișier +├── schema-oracle.md # Detalii schema Oracle +├── flux-contabil.md # Formule contabile detaliate +├── api-spec.md # Specificații API (de creat) +└── implementation-notes.md # Note implementare (de creat) +``` + +--- + +## 12. Întrebări Deschise + +1. **Workflow aprobare:** E necesar ca un contabil să aprobe înainte de salvare? +2. **Tip document (ID_FDOC):** Ce valoare pentru bonuri fiscale achiziție? +3. **Analitice:** Sunt necesare analitice pe conturi (ASCD, ASCC)? +4. **Sucursală:** Valoare default pentru ID_SUCURSALA? +5. **Atașamente:** Unde se face legătura ATAS_REFERINTE (ID_ENTITATE = ID_ACT)? + +--- + +*Analiză generată de Echo • 2026-02-03* diff --git a/memory/kb/projects/roa2web-telegram-import/flux-contabil.md b/memory/kb/projects/roa2web-telegram-import/flux-contabil.md new file mode 100644 index 0000000..c540fbf --- /dev/null +++ b/memory/kb/projects/roa2web-telegram-import/flux-contabil.md @@ -0,0 +1,229 @@ +# Flux Contabil - Import Bonuri Fiscale Achiziție + +**Actualizat:** 2026-02-03 + +--- + +## 1. Formula Contabilă Generală + +### Bon fiscal achiziție (plătit cash) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ DEBIT │ CREDIT │ SUMA │ DESCRIERE │ +├──────────────────┼─────────────────┼──────────┼─────────────┤ +│ 6xx (cheltuială)│ 401 (furnizor) │ fără TVA│ Cheltuială │ +│ 4426 (TVA ded.) │ 401 (furnizor) │ TVA │ TVA deduct.│ +│ 401 (furnizor) │ 5311 (casa) │ cu TVA │ Plată cash │ +└──────────────────┴─────────────────┴──────────┴─────────────┘ +``` + +### Bon fiscal cu TVA la încasare + +``` +┌─────────────────────────────────────────────────────────────┐ +│ DEBIT │ CREDIT │ SUMA │ DESCRIERE │ +├──────────────────┼─────────────────┼──────────┼─────────────┤ +│ 6xx (cheltuială)│ 401 (furnizor) │ fără TVA│ Cheltuială │ +│ 4428 (TVA neex.)│ 401 (furnizor) │ TVA │ TVA neexig.│ +│ 401 (furnizor) │ 5311 (casa) │ cu TVA │ Plată cash │ +│ 4426 (TVA ded.) │ 4428 (TVA neex)│ TVA │ Exigibiliz.│ +└──────────────────┴─────────────────┴──────────┴─────────────┘ +``` + +--- + +## 2. Conturi Cheltuieli (6xx) + +| Cont | Denumire | Utilizare | +|------|----------|-----------| +| 6021 | Cheltuieli cu materialele auxiliare | Rechizite, materiale consumabile | +| 6022 | Cheltuieli privind combustibilii | Benzină, motorină, GPL | +| 6024 | Cheltuieli privind piesele de schimb | Piese auto, componente | +| 6028 | Cheltuieli privind alte materiale consumabile | Diverse materiale | +| 604 | Cheltuieli privind materialele nestocate | Materiale directe în consum | +| 611 | Cheltuieli cu întreținerea și reparațiile | Service, reparații | +| 612 | Cheltuieli cu redevențele | Licențe, abonamente | +| 613 | Cheltuieli cu primele de asigurare | Asigurări | +| 614 | Cheltuieli cu studiile și cercetările | Consultanță | +| 622 | Cheltuieli privind comisioanele | Comisioane bancare | +| 623 | Cheltuieli de protocol, reclamă | Protocol, publicitate | +| 624 | Cheltuieli cu transportul | Transport bunuri/persoane | +| 625 | Cheltuieli cu deplasările | Diurnă, cazare, transport | +| 626 | Cheltuieli poștale și telecomunicații | Telefon, internet | +| 627 | Cheltuieli cu serviciile bancare | Comisioane bancare | +| 628 | Alte cheltuieli cu serviciile | Diverse servicii | + +--- + +## 3. Mapare Automată CUI → Cont + +### Benzinării + +| CUI | Denumire | Cont | +|-----|----------|------| +| RO1590082 | OMV PETROM | 6022 | +| RO1025tried | ROMPETROL | 6022 | +| RO14991381 | MOL ROMANIA | 6022 | +| RO524186 | LUKOIL | 6022 | + +### Magazine Bricolaj + +| CUI | Denumire | Cont | +|-----|----------|------| +| RO10562600 | DEDEMAN | 6021/6028 | +| RO6323780 | HORNBACH | 6021/6028 | +| RO14688790 | LEROY MERLIN | 6021/6028 | + +### Supermarket-uri + +| CUI | Denumire | Cont | +|-----|----------|------| +| RO15tried | MEGA IMAGE | 6028 | +| RO5765323 | KAUFLAND | 6028 | +| RO3891449 | CARREFOUR | 6028 | +| RO6334171 | LIDL | 6028 | + +--- + +## 4. Exemplu Complet - Bon Benzină + +**Bon:** +- Furnizor: OMV PETROM (CUI: RO1590082) +- Data: 2026-02-03 +- Număr bon: 12345 +- Total: 250.00 RON (210.08 + 39.92 TVA 19%) + +### 4.1 Date extrase OCR + +```json +{ + "partner_name": "OMV PETROM MARKETING SRL", + "cui": "RO1590082", + "receipt_date": "2026-02-03", + "receipt_number": "12345", + "amount": 250.00, + "tva_entries": [ + {"code": "A", "percent": 19, "amount": 39.92} + ], + "tva_total": 39.92, + "payment_methods": [{"method": "NUMERAR", "amount": 250.00}] +} +``` + +### 4.2 Note Contabile Generate + +```sql +-- LINIA 1: Cheltuială combustibil +INSERT INTO ACT_TEMP ( + LUNA, AN, COD, DATAIREG, DATAACT, NRACT, + EXPLICATIA, + SCD, SCC, SUMA, + ID_PARTC, ID_FDOC +) VALUES ( + 2, 2026, :cod, SYSDATE, TO_DATE('2026-02-03', 'YYYY-MM-DD'), 12345, + 'Combustibil OMV bon 12345', + '6022', '401', 210.08, + :id_part_omv, :id_fdoc_bon +); + +-- LINIA 2: TVA deductibil +INSERT INTO ACT_TEMP ( + LUNA, AN, COD, DATAIREG, DATAACT, NRACT, + EXPLICATIA, + SCD, SCC, SUMA, PROC_TVA, + ID_PARTC, ID_FDOC +) VALUES ( + 2, 2026, :cod, SYSDATE, TO_DATE('2026-02-03', 'YYYY-MM-DD'), 12345, + 'TVA combustibil OMV bon 12345', + '4426', '401', 39.92, 1.19, + :id_part_omv, :id_fdoc_bon +); + +-- LINIA 3: Plată din casă +INSERT INTO ACT_TEMP ( + LUNA, AN, COD, DATAIREG, DATAACT, NRACT, + EXPLICATIA, + SCD, SCC, SUMA, + ID_PARTD, ID_FDOC +) VALUES ( + 2, 2026, :cod, SYSDATE, TO_DATE('2026-02-03', 'YYYY-MM-DD'), 12345, + 'Plata bon OMV 12345', + '401', '5311', 250.00, + :id_part_omv, :id_fdoc_bon +); +``` + +### 4.3 Flux Complet Python + +```python +import oracledb + +def import_bon_achizitie(extraction_data, conn): + cursor = conn.cursor() + + # 1. Găsește sau creează partenerul + id_part = find_or_create_partner(cursor, extraction_data['cui'], extraction_data['partner_name']) + + # 2. Determină contul de cheltuială + cont_cheltuiala = get_cont_by_cui(extraction_data['cui']) # ex: '6022' + + # 3. Calculează sumele + total_cu_tva = extraction_data['amount'] + tva = extraction_data['tva_total'] + total_fara_tva = total_cu_tva - tva + + # 4. Inițializare + cursor.callproc('MARIUSM_AUTO.PACK_CONTAFIN.INITIALIZEAZA_SCRIERE_ACT_RUL', [ + id_util, # ID utilizator + datetime.now(), + an, + luna, + 0, # suprascriere_cod + 0, # suprascriere_anluna + 0, # scrie_sterge (0=scrie) + id_sucursala + ]) + + # 5. INSERT în ACT_TEMP (cele 3 linii) + # ... INSERT-uri ca mai sus ... + + # 6. Finalizare + mesaj = cursor.var(oracledb.STRING) + cursor.callproc('MARIUSM_AUTO.PACK_CONTAFIN.FINALIZEAZA_SCRIERE_ACT_RUL', [ + id_util, + cod, + 0, # scrie_sterge + 0, # modificare_nota + 0, # scrie_cump_vanz + mesaj + ]) + + conn.commit() + return mesaj.getvalue() +``` + +--- + +## 5. Cote TVA România + +| Cotă | Procent | PROC_TVA | Aplicare | +|------|---------|----------|----------| +| Standard | 19% | 1.19 | Majoritatea bunurilor/serviciilor | +| Redus 1 | 9% | 1.09 | Alimente, medicamente, cărți | +| Redus 2 | 5% | 1.05 | Locuințe sociale | +| Zero | 0% | 1.00 | Export, anumite servicii | + +--- + +## 6. Validări Necesare + +1. **CUI valid** - verificare checksum +2. **Data bon** - nu în viitor, nu mai veche de 1 an +3. **Sumă** - pozitivă, format corect +4. **TVA** - să corespundă cu cotele legale +5. **Balanță** - SUMA(debit) = SUMA(credit) + +--- + +*Flux documentat de Echo • 2026-02-03* diff --git a/memory/kb/projects/roa2web-telegram-import/schema-oracle.md b/memory/kb/projects/roa2web-telegram-import/schema-oracle.md new file mode 100644 index 0000000..f95478c --- /dev/null +++ b/memory/kb/projects/roa2web-telegram-import/schema-oracle.md @@ -0,0 +1,275 @@ +# Schema Oracle - ROA (MARIUSM_AUTO) + +**Actualizat:** 2026-02-03 + +--- + +## Tabele Principale + +### ACT - Note Contabile + +```sql +ACT ( + ID_ACT NUMBER PRIMARY KEY, + LUNA NUMBER, + AN NUMBER, + COD NUMBER, -- Cod document unic + DATAIREG DATE, -- Data înregistrare + NRACT NUMBER, -- Număr act + DATAACT DATE, -- Data act + EXPLICATIA VARCHAR2(100), -- Descriere + SCD VARCHAR2(10), -- Cont DEBIT + ASCD VARCHAR2(20), -- Analitic debit + SCC VARCHAR2(10), -- Cont CREDIT + ASCC VARCHAR2(20), -- Analitic credit + SUMA NUMBER, -- Sumă + ID_FACTD NUMBER, -- Factură debit + ID_FACTC NUMBER, -- Factură credit + PERECHED NUMBER, + PERECHEC NUMBER, + SUMA_VAL NUMBER, -- Sumă valută + CURS NUMBER, -- Curs valutar + DATASCAD DATE, -- Data scadență + ID_UTIL NUMBER, -- Utilizator creare + DATAORA DATE, -- Data/ora creare + ID_UTILS NUMBER, -- Utilizator modificare + DATAORAS DATE, -- Data/ora modificare + TVA_INCASARE NUMBER, -- Flag TVA la încasare + SERIE_ACT VARCHAR2(10), -- Serie act + ID_PART NUMBER, -- ID partener + ID_CTR NUMBER, -- ID contract + CONT VARCHAR2(10), + ACONT VARCHAR2(20), + ID_VALUTA NUMBER, + ID_SUCURSALA NUMBER, + ID_SET NUMBER, + TIP_SAFT NUMBER +) +``` + +### ACT_TEMP - Staging Note Contabile + +Aceeași structură ca ACT, folosită pentru inserare temporară înainte de transfer. + +### RUL - Rulaje Gestiuni + +```sql +RUL ( + ID_RUL NUMBER PRIMARY KEY, + AN NUMBER, + LUNA NUMBER, + COD NUMBER, + NNIR NUMBER, -- Număr NIR + ID_ARTICOL NUMBER, + ID_GESTIUNE NUMBER, + PRET NUMBER, + PRETV NUMBER, -- Preț vânzare + TVA NUMBER, + TVAV NUMBER, + CANT NUMBER, -- Cantitate + CANTE NUMBER, + CONT VARCHAR2(10), + ACONT VARCHAR2(20), + ADAOS NUMBER, + ID_LUCRARE NUMBER, + ID_RESPONSABIL NUMBER, + DATAACT DATE, + PROCENT NUMBER, + CURS NUMBER, + PRETD NUMBER, + ID_UTIL NUMBER, + DATAORA DATE, + ID_SECTIE NUMBER, + PROC_TVA NUMBER, + ID_FACT NUMBER, + STERS NUMBER, + ID_TIP_RULAJ NUMBER, -- 0=intrare/ieșire reală + ID_SET NUMBER, + ID_SUCURSALA NUMBER, + NRACT NUMBER, + SERIE_ACT VARCHAR2(10), + ID_VALUTA NUMBER, + LOT VARCHAR2(50), + ADATA_EXPIRARE DATE, + ... +) +``` + +### NOM_TIP_RULAJ - Tipuri Rulaj + +```sql +(0, 'INTRARE/IESIRE REALA') +(1, 'TRANSFER') +(2, 'RULAJ CMP') +(6, 'INVENTARIERE') +``` + +### STOC - Stocuri + +```sql +STOC ( + ID_STOC NUMBER PRIMARY KEY, + AN NUMBER, + LUNA NUMBER, + ID_ARTICOL NUMBER, + PRET NUMBER, + PRETV NUMBER, + TVA NUMBER, + TVAV NUMBER, + CANTS NUMBER, -- Cantitate sold + CANT NUMBER, + CANTE NUMBER, + CONT VARCHAR2(10), + ID_GESTIUNE NUMBER, + ... +) +``` + +### BAL - Balanță Verificare + +```sql +BAL ( + ID_BAL NUMBER PRIMARY KEY, + CONT VARCHAR2(10), + PRECDEB1 NUMBER, -- Precedent debit (an) + PRECCRED1 NUMBER, + PRECDEB NUMBER, -- Precedent debit (lună) + PRECCRED NUMBER, + RULDEB NUMBER, -- Rulaj debit + RULCRED NUMBER, -- Rulaj credit + AN NUMBER, + LUNA NUMBER, + ID_SUCURSALA NUMBER +) +``` + +### IREG_PARTENERI - Evidență Parteneri + +```sql +IREG_PARTENERI ( + ID_IREG_PART NUMBER PRIMARY KEY, + AN NUMBER, + LUNA NUMBER, + ID_PART NUMBER, + CONT VARCHAR2(10), + ACONT VARCHAR2(20), + DEBIT NUMBER, + CREDIT NUMBER, + VALDEBIT NUMBER, + VALCREDIT NUMBER, + CURS NUMBER, + ID_FDOC NUMBER, -- Tip document + DATAACT DATE, + DATAIREG DATE, + DATASCAD DATE, + NRACT NUMBER, + ID_RESPONSABIL NUMBER, + PRECDEB NUMBER, + PRECCRED NUMBER, + ... +) +``` + +### ATAS_ATASAMENTE - Fișiere Atașate + +```sql +ATAS_ATASAMENTE ( + ID_ATAS NUMBER PRIMARY KEY, + NUME_FISIER VARCHAR2(200), + FISIER BLOB, -- Conținut fișier + ID_UTIL NUMBER, + DATAORA DATE, + STERS NUMBER, + ID_UTILS NUMBER, + DATAORAS DATE, + DESCRIERE VARCHAR2(500), + ID_LINK NUMBER, + ID_TIP_ENT_PREL NUMBER +) +``` + +### ATAS_REFERINTE - Legături Atașamente + +```sql +ATAS_REFERINTE ( + ID_REFERINTA NUMBER PRIMARY KEY, + ID_ATAS NUMBER, -- FK la ATAS_ATASAMENTE + ID_ENTITATE NUMBER, -- ID din tabela referită + ID_UTIL NUMBER, + DATAORA DATE, + STERS NUMBER, + ID_UTILS NUMBER, + DATAORAS DATE, + ID_PROGRAM NUMBER, -- Identificator tabel/modul + ORDINE NUMBER +) +``` + +### NOM_FDOC - Nomenclator Tipuri Documente + +```sql +NOM_FDOC ( + ID_FDOC NUMBER PRIMARY KEY, + FEL_DOCUMENT VARCHAR2(50), + STERS NUMBER, + ID_MOD NUMBER, + INACTIV NUMBER, + TIP_DOC VARCHAR2(5), + PAYMENTCODE VARCHAR2(10) +) + +-- Exemple: +(1, 'ABONAMENT') +(3, 'AVANSURI') +(5, 'AVIZ EXP') +(6, 'BILET') +... +``` + +--- + +## Package-uri Oracle + +### PACK_CONTAFIN (principal) + +**Proceduri principale:** +- `INITIALIZEAZA_SCRIERE_ACT_RUL` - Inițializare sesiune +- `SCRIE_IN_ACT` - Transfer ACT_TEMP → ACT +- `SCRIE_IN_BALANTA` - Actualizare balanță +- `SCRIE_IN_RUL` - Transfer RUL_TEMP → RUL +- `SCRIE_IN_STOC` - Actualizare stoc +- `SCRIE_IN_IREG_PARTENERI` - Actualizare evidență parteneri +- `FINALIZEAZA_SCRIERE_ACT_RUL` - Finalizare (apelează toate cele de mai sus) + +### PACK_FACTURARE (referință) + +**Exemple INSERT ACT_TEMP:** +- Liniile 1000-1200 în package body +- Model pentru structura datelor +- NU pentru achiziții, dar util ca referință + +### PACK_PARTENERI + +- Gestiune parteneri (furnizori, clienți) +- Căutare/creare partener după CUI + +--- + +## Conexiune Python + +```python +import oracledb + +conn = oracledb.connect( + user="CONTAFIN_ORACLE", + password="ROMFASTSOFT", + dsn="10.0.20.121:1521/ROA" +) + +# Toate operațiile folosesc schema MARIUSM_AUTO +cursor.execute("SELECT * FROM MARIUSM_AUTO.ACT WHERE ROWNUM <= 1") +``` + +--- + +*Schema documentată de Echo • 2026-02-03* diff --git a/tools/process_bon.py b/tools/process_bon.py new file mode 100644 index 0000000..77f4c88 --- /dev/null +++ b/tools/process_bon.py @@ -0,0 +1,588 @@ +#!/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()