docs
This commit is contained in:
484
data-entry-app/docs/IMPLEMENTATION_PLAN_AUTH_UNITAR.md
Normal file
484
data-entry-app/docs/IMPLEMENTATION_PLAN_AUTH_UNITAR.md
Normal file
@@ -0,0 +1,484 @@
|
||||
# Plan: Sincronizare Nomenclatoare Oracle + Auth SSO + OCR Furnizori
|
||||
|
||||
## Obiective
|
||||
1. **Sincronizare nomenclatoare din Oracle în SQLite** (furnizori, casa/banca)
|
||||
2. **Auth pentru data-entry-app** cu SSO (frontend-uri separate pe path)
|
||||
3. **OCR: căutare furnizor după CUI** + creare locală dacă nu există
|
||||
4. **Deploy Windows IIS** cu path routing
|
||||
|
||||
---
|
||||
|
||||
## Arhitectura Aleasă
|
||||
|
||||
```
|
||||
roa2web.romfast.ro (IIS + ARR)
|
||||
│
|
||||
├── /reports/ → reports-app/frontend/
|
||||
├── /data/ → data-entry-app/frontend/
|
||||
│
|
||||
├── /api/reports/* → reports-backend:8001
|
||||
├── /api/data/* → data-entry-backend:8003
|
||||
└── /api/auth/* → reports-backend (auth provider)
|
||||
```
|
||||
|
||||
**URL-uri compacte:**
|
||||
- `roa2web.romfast.ro/reports/` - Rapoarte
|
||||
- `roa2web.romfast.ro/data/` - Introducere date (bonuri fiscale)
|
||||
- `roa2web.romfast.ro/api/reports/` - API rapoarte
|
||||
- `roa2web.romfast.ro/api/data/` - API introducere date
|
||||
|
||||
**SSO**: Același domeniu = localStorage partajat = token JWT valid pentru ambele
|
||||
|
||||
---
|
||||
|
||||
## Faza 1: Auth pentru Data-Entry-App
|
||||
|
||||
### 1.1 Backend - Integrare shared/auth/
|
||||
|
||||
**Fișiere de modificat:**
|
||||
- `data-entry-app/backend/app/main.py`
|
||||
- `data-entry-app/backend/app/routers/receipts.py`
|
||||
- `data-entry-app/backend/app/core/config.py`
|
||||
|
||||
**Acțiuni:**
|
||||
```python
|
||||
# main.py - Adăugare middleware
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent / "shared"))
|
||||
|
||||
from auth.middleware import AuthenticationMiddleware
|
||||
from auth.dependencies import get_current_user
|
||||
|
||||
app.add_middleware(
|
||||
AuthenticationMiddleware,
|
||||
excluded_paths=["/docs", "/redoc", "/openapi.json", "/health", "/"]
|
||||
)
|
||||
```
|
||||
|
||||
```python
|
||||
# receipts.py - Înlocuire placeholder
|
||||
from auth.dependencies import get_current_user
|
||||
from auth.models import CurrentUser
|
||||
|
||||
@router.get("/")
|
||||
async def list_receipts(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
# folosește current_user.username
|
||||
```
|
||||
|
||||
### 1.2 Frontend - Auth Store + Login Page
|
||||
|
||||
**Fișiere de creat/copiat din reports-app:**
|
||||
- `data-entry-app/frontend/src/stores/auth.js` (copiat)
|
||||
- `data-entry-app/frontend/src/views/LoginView.vue` (copiat)
|
||||
- `data-entry-app/frontend/src/router/index.js` (adăugat guard)
|
||||
- `data-entry-app/frontend/src/services/api.js` (axios interceptor)
|
||||
|
||||
**Decizie SSO:**
|
||||
- Frontend data-entry folosește `/api/auth/login` de pe reports-backend
|
||||
- Sau: redirect la `/login` (reports-app) care setează token în localStorage
|
||||
- Token valid pentru ambele (același JWT_SECRET_KEY)
|
||||
|
||||
---
|
||||
|
||||
## Faza 2: Sincronizare Nomenclatoare Oracle → SQLite
|
||||
|
||||
### 2.1 Noi Modele SQLModel
|
||||
|
||||
**Fișier:** `data-entry-app/backend/app/db/models/nomenclature.py`
|
||||
|
||||
```python
|
||||
class SyncedSupplier(SQLModel, table=True):
|
||||
"""Furnizori sincronizați din Oracle"""
|
||||
__tablename__ = "synced_suppliers"
|
||||
|
||||
id: int = Field(primary_key=True) # ID din Oracle (ID_PART)
|
||||
company_id: int = Field(index=True)
|
||||
name: str = Field(max_length=200) # DEN_PART
|
||||
fiscal_code: Optional[str] = Field(max_length=20, index=True) # COD_FISCAL
|
||||
address: Optional[str] = Field(max_length=500)
|
||||
synced_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class LocalSupplier(SQLModel, table=True):
|
||||
"""Furnizori creați local din OCR (neexistenți în Oracle)"""
|
||||
__tablename__ = "local_suppliers"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
company_id: int = Field(index=True)
|
||||
name: str = Field(max_length=200)
|
||||
fiscal_code: str = Field(max_length=20, unique=True, index=True)
|
||||
address: Optional[str] = Field(max_length=500)
|
||||
created_by: str = Field(max_length=100)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
oracle_synced: bool = Field(default=False) # True când e creat în Oracle
|
||||
|
||||
class SyncedCashRegister(SQLModel, table=True):
|
||||
"""Case/Bănci sincronizate din Oracle"""
|
||||
__tablename__ = "synced_cash_registers"
|
||||
|
||||
id: int = Field(primary_key=True) # ID din Oracle
|
||||
company_id: int = Field(index=True)
|
||||
name: str = Field(max_length=100)
|
||||
account_code: str = Field(max_length=20) # 5311, 5121 etc.
|
||||
register_type: str = Field(max_length=20) # CASA sau BANCA
|
||||
synced_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
```
|
||||
|
||||
### 2.2 Alembic Migration
|
||||
|
||||
**Fișier:** `data-entry-app/backend/migrations/versions/xxx_add_nomenclature_tables.py`
|
||||
|
||||
### 2.3 Sync Service
|
||||
|
||||
**Fișier:** `data-entry-app/backend/app/services/sync_service.py`
|
||||
|
||||
```python
|
||||
class NomenclatureSyncService:
|
||||
"""Sincronizare nomenclatoare din Oracle în SQLite"""
|
||||
|
||||
@staticmethod
|
||||
async def sync_suppliers(company_id: int, schema: str) -> int:
|
||||
"""Sincronizează furnizori pentru o companie"""
|
||||
async with oracle_pool.get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"""
|
||||
SELECT ID_PART, DEN_PART, COD_FISCAL, ADRESA
|
||||
FROM {schema}.NOM_PARTENERI
|
||||
WHERE TIP_PART IN ('F', 'A') -- Furnizori sau Ambele
|
||||
""")
|
||||
# Upsert în SQLite
|
||||
|
||||
@staticmethod
|
||||
async def sync_cash_registers(company_id: int, schema: str) -> int:
|
||||
"""Sincronizează case și bănci"""
|
||||
# Similar pentru NOM_CASE și NOM_BANCI
|
||||
|
||||
@staticmethod
|
||||
async def get_schema_for_company(company_id: int) -> str:
|
||||
"""Obține schema Oracle pentru o companie"""
|
||||
# Folosește cache din shared sau query V_NOM_FIRME
|
||||
```
|
||||
|
||||
### 2.4 Strategia de Sync Hibrid
|
||||
|
||||
1. **La startup app**: Sync automat (background task)
|
||||
2. **Periodic**: Task programat la 4h
|
||||
3. **On-demand**: Căutare live în Oracle când CUI nu există local
|
||||
|
||||
**Fișier:** `data-entry-app/backend/app/main.py`
|
||||
```python
|
||||
@app.on_event("startup")
|
||||
async def startup_sync():
|
||||
# Background sync pentru company-urile active
|
||||
asyncio.create_task(sync_nomenclatures_background())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Faza 3: OCR + Căutare Furnizor după CUI
|
||||
|
||||
### 3.1 Flow Căutare Furnizor
|
||||
|
||||
```
|
||||
OCR extrage CUI
|
||||
↓
|
||||
Căutare în SyncedSupplier (SQLite)
|
||||
↓ (nu găsit)
|
||||
Căutare în LocalSupplier (SQLite)
|
||||
↓ (nu găsit)
|
||||
Căutare LIVE în Oracle (NOM_PARTENERI)
|
||||
↓ (nu găsit)
|
||||
Creare LocalSupplier cu date OCR
|
||||
↓
|
||||
Utilizator poate edita înainte de submit
|
||||
```
|
||||
|
||||
### 3.2 Endpoint Căutare Furnizor
|
||||
|
||||
**Fișier:** `data-entry-app/backend/app/routers/nomenclature.py`
|
||||
|
||||
```python
|
||||
@router.get("/suppliers/search")
|
||||
async def search_supplier(
|
||||
company_id: int,
|
||||
fiscal_code: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> SupplierSearchResult:
|
||||
"""
|
||||
Caută furnizor:
|
||||
1. În SQLite (synced + local)
|
||||
2. Live în Oracle dacă nu găsit
|
||||
3. Returnează sugestie creare dacă nu există
|
||||
"""
|
||||
|
||||
@router.post("/suppliers/local")
|
||||
async def create_local_supplier(
|
||||
supplier: LocalSupplierCreate,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> LocalSupplier:
|
||||
"""Crează furnizor local din date OCR"""
|
||||
```
|
||||
|
||||
### 3.3 Modificare OCR Flow în Frontend
|
||||
|
||||
**Fișier:** `data-entry-app/frontend/src/views/ReceiptCreateView.vue`
|
||||
|
||||
```javascript
|
||||
// După OCR, caută automat furnizor
|
||||
async function handleOCRResult(ocrData) {
|
||||
if (ocrData.cui) {
|
||||
const result = await api.get('/api/data-entry/suppliers/search', {
|
||||
params: { company_id: selectedCompany.id, fiscal_code: ocrData.cui }
|
||||
});
|
||||
|
||||
if (result.found) {
|
||||
form.partner_id = result.supplier.id;
|
||||
form.partner_name = result.supplier.name;
|
||||
} else {
|
||||
// Afișează opțiune creare locală
|
||||
showCreateSupplierDialog(ocrData);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Faza 4: Deploy Windows IIS
|
||||
|
||||
### 4.1 Serviciu Windows pentru data-entry-backend
|
||||
|
||||
**Fișier:** `deployment/windows/scripts/Install-DataEntry.ps1`
|
||||
|
||||
Similar cu Install-ROA2WEB.ps1 dar:
|
||||
- ServiceName: `ROA2WEB-DataEntry`
|
||||
- Port: 8003
|
||||
- BackendPath: `C:\inetpub\wwwroot\roa2web\data-entry-app\backend`
|
||||
- FrontendPath: `C:\inetpub\wwwroot\roa2web\data-entry-app\frontend`
|
||||
|
||||
**Actualizare Install-ROA2WEB.ps1** pentru structura unitară:
|
||||
- BackendPath: `C:\inetpub\wwwroot\roa2web\reports-app\backend`
|
||||
- FrontendPath: `C:\inetpub\wwwroot\roa2web\reports-app\frontend`
|
||||
|
||||
### 4.2 Actualizare web.config
|
||||
|
||||
**Fișier:** `deployment/windows/config/web.config`
|
||||
|
||||
Reguli URL compacte (`/reports/`, `/data/`, `/api/reports/`, `/api/data/`):
|
||||
|
||||
```xml
|
||||
<!-- API Auth (comun) -->
|
||||
<rule name="Auth API" stopProcessing="true">
|
||||
<match url="^api/auth/(.*)" />
|
||||
<action type="Rewrite" url="http://localhost:8001/api/auth/{R:1}" />
|
||||
</rule>
|
||||
|
||||
<!-- API Data Entry -->
|
||||
<rule name="Data Entry API" stopProcessing="true">
|
||||
<match url="^api/data/(.*)" />
|
||||
<action type="Rewrite" url="http://localhost:8003/api/{R:1}" />
|
||||
</rule>
|
||||
|
||||
<!-- API Reports -->
|
||||
<rule name="Reports API" stopProcessing="true">
|
||||
<match url="^api/reports/(.*)" />
|
||||
<action type="Rewrite" url="http://localhost:8001/api/{R:1}" />
|
||||
</rule>
|
||||
|
||||
<!-- Frontend Data Entry SPA (/data/) -->
|
||||
<rule name="Data Entry SPA" stopProcessing="true">
|
||||
<match url="^data($|/.*)" />
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="/data/index.html" />
|
||||
</rule>
|
||||
|
||||
<!-- Frontend Reports SPA (/reports/) -->
|
||||
<rule name="Reports SPA" stopProcessing="true">
|
||||
<match url="^reports($|/.*)" />
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="/reports/index.html" />
|
||||
</rule>
|
||||
|
||||
<!-- Root redirect la /reports/ -->
|
||||
<rule name="Root Redirect" stopProcessing="true">
|
||||
<match url="^$" />
|
||||
<action type="Redirect" url="/reports/" redirectType="Found" />
|
||||
</rule>
|
||||
```
|
||||
|
||||
**IIS Virtual Directories (pentru URL-uri compacte):**
|
||||
```powershell
|
||||
# /reports/ → reports-app/frontend/
|
||||
New-WebVirtualDirectory -Site "Default Web Site" -Name "reports" `
|
||||
-PhysicalPath "C:\inetpub\wwwroot\roa2web\reports-app\frontend"
|
||||
|
||||
# /data/ → data-entry-app/frontend/
|
||||
New-WebVirtualDirectory -Site "Default Web Site" -Name "data" `
|
||||
-PhysicalPath "C:\inetpub\wwwroot\roa2web\data-entry-app\frontend"
|
||||
```
|
||||
|
||||
### 4.3 Structura Foldere (UNITARĂ - identică dev/prod)
|
||||
|
||||
**În development (git repo):**
|
||||
```
|
||||
roa2web/
|
||||
├── reports-app/
|
||||
│ ├── backend/ # FastAPI port 8001
|
||||
│ ├── frontend/ # Vue.js port 3000
|
||||
│ └── telegram-bot/ # Bot Telegram
|
||||
├── data-entry-app/
|
||||
│ ├── backend/ # FastAPI port 8003
|
||||
│ └── frontend/ # Vue.js port 3010
|
||||
└── shared/ # Cod partajat (auth, database)
|
||||
```
|
||||
|
||||
**În producție (Windows IIS) - IDENTIC:**
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\
|
||||
├── reports-app/
|
||||
│ ├── backend/ # Serviciu Windows port 8001
|
||||
│ └── frontend/ # Servit de IIS pe /
|
||||
├── data-entry-app/
|
||||
│ ├── backend/ # Serviciu Windows port 8003
|
||||
│ └── frontend/ # Servit de IIS pe /data-entry/
|
||||
├── telegram-bot/ # Serviciu Windows port 8002
|
||||
└── shared/ # Cod partajat
|
||||
```
|
||||
|
||||
**Avantaje structură unitară:**
|
||||
- Deploy simplu: `xcopy /E /Y source\reports-app dest\reports-app`
|
||||
- Path-uri identice în cod (no surprises)
|
||||
- Un singur script de deploy pentru ambele medii
|
||||
|
||||
---
|
||||
|
||||
## Faza 5: Configurare Dev (identic cu prod)
|
||||
|
||||
### 5.1 Vite Config pentru URL-uri Compacte
|
||||
|
||||
**Fișier:** `data-entry-app/frontend/vite.config.js`
|
||||
```javascript
|
||||
export default defineConfig({
|
||||
base: '/data/', // URL compact în producție
|
||||
server: {
|
||||
proxy: {
|
||||
'/api/auth': 'http://localhost:8001',
|
||||
'/api/data': 'http://localhost:8003'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Fișier:** `reports-app/frontend/vite.config.js` (ACTUALIZAT)
|
||||
```javascript
|
||||
export default defineConfig({
|
||||
base: '/reports/', // URL compact în producție (era '/')
|
||||
server: {
|
||||
proxy: {
|
||||
'/api/auth': 'http://localhost:8001',
|
||||
'/api/reports': 'http://localhost:8001'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**IMPORTANT:** Actualizare API calls în frontend:
|
||||
- Reports: `/api/reports/companies`, `/api/reports/invoices`, etc.
|
||||
- Data Entry: `/api/data/receipts`, `/api/data/suppliers`, etc.
|
||||
- Auth (comun): `/api/auth/login`, `/api/auth/refresh`
|
||||
|
||||
### 5.2 Script Start Unificat
|
||||
|
||||
**Fișier:** `start-all.sh` (nou)
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Pornește toate serviciile pentru dev
|
||||
|
||||
# SSH tunnel
|
||||
./ssh_tunnel.sh start
|
||||
|
||||
# Reports backend
|
||||
cd reports-app/backend && uvicorn app.main:app --port 8001 &
|
||||
|
||||
# Data entry backend
|
||||
cd data-entry-app/backend && uvicorn app.main:app --port 8003 &
|
||||
|
||||
# Reports frontend
|
||||
cd reports-app/frontend && npm run dev -- --port 3000 &
|
||||
|
||||
# Data entry frontend
|
||||
cd data-entry-app/frontend && npm run dev -- --port 3010 &
|
||||
|
||||
wait
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ordine Implementare
|
||||
|
||||
| # | Task | Efort | Dependențe |
|
||||
|---|------|-------|------------|
|
||||
| 1 | Modele SQLModel nomenclatoare | 30 min | - |
|
||||
| 2 | Alembic migration | 15 min | #1 |
|
||||
| 3 | Sync service (Oracle → SQLite) | 2h | #2 |
|
||||
| 4 | Auth middleware în data-entry-backend | 1h | - |
|
||||
| 5 | Auth store + login în data-entry-frontend | 1h | #4 |
|
||||
| 6 | Endpoint căutare furnizor | 1h | #3 |
|
||||
| 7 | Frontend OCR + furnizor flow | 1.5h | #6 |
|
||||
| 8 | web.config IIS actualizat | 30 min | - |
|
||||
| 9 | Script deploy data-entry Windows | 1h | #8 |
|
||||
| 10 | Testare end-to-end | 1h | all |
|
||||
|
||||
**Total estimat: ~10h**
|
||||
|
||||
---
|
||||
|
||||
## Fișiere Critice de Modificat/Creat
|
||||
|
||||
### Backend data-entry-app:
|
||||
- `app/main.py` - middleware auth + startup sync
|
||||
- `app/db/models/nomenclature.py` - noi modele (CREARE)
|
||||
- `app/services/sync_service.py` - sync Oracle (CREARE)
|
||||
- `app/services/nomenclature_service.py` - refactorizare
|
||||
- `app/routers/nomenclature.py` - endpoint-uri noi (CREARE)
|
||||
- `app/routers/receipts.py` - auth dependencies
|
||||
- `migrations/versions/xxx_nomenclature.py` - migrare (CREARE)
|
||||
|
||||
### Frontend data-entry-app:
|
||||
- `src/stores/auth.js` - copiat din reports-app
|
||||
- `src/views/LoginView.vue` - copiat + adaptat
|
||||
- `src/router/index.js` - auth guard
|
||||
- `src/services/api.js` - axios config
|
||||
- `src/views/ReceiptCreateView.vue` - OCR + supplier flow
|
||||
|
||||
### Deploy (structură unitară):
|
||||
- `deployment/windows/config/web.config` - reguli noi + actualizate
|
||||
- `deployment/windows/scripts/Install-ROA2WEB.ps1` - ACTUALIZAT pentru structura unitară
|
||||
- `deployment/windows/scripts/Install-DataEntry.ps1` - NOU
|
||||
- `deployment/windows/scripts/Build-ROA2WEB.ps1` - ACTUALIZAT pentru ambele apps
|
||||
- `deployment/windows/docs/WINDOWS_DEPLOYMENT.md` - ACTUALIZAT cu noua structură
|
||||
|
||||
### Shared:
|
||||
- Nu necesită modificări (refolosim exact ce există)
|
||||
|
||||
---
|
||||
|
||||
## Întrebări Rezolvate
|
||||
|
||||
| Întrebare | Răspuns |
|
||||
|-----------|---------|
|
||||
| Furnizor nou din OCR? | Creare automată în SQLite (LocalSupplier) |
|
||||
| Sync strategy? | Hibrid: startup + periodic 4h + on-demand |
|
||||
| Auth sharing? | Frontend-uri separate pe path, același token JWT (SSO via localStorage) |
|
||||
| Deployment? | IIS path routing, servicii Windows separate |
|
||||
| Structura directoare? | **UNITARĂ** - grupat pe app (`{app}/backend`, `{app}/frontend`) identic dev/prod |
|
||||
| SSO cum funcționează? | Același domeniu IIS → localStorage partajat → token valid pentru ambele API-uri |
|
||||
| URL-uri? | **COMPACTE**: `/reports/`, `/data/`, `/api/reports/`, `/api/data/` |
|
||||
| Root (/)? | Redirect automat la `/reports/` |
|
||||
254
data-entry-app/docs/IMPLEMENTATION_PLAN_OCR.md
Normal file
254
data-entry-app/docs/IMPLEMENTATION_PLAN_OCR.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# Plan: OCR Inteligent cu Early Exit
|
||||
|
||||
> **Context Handover Document** - Plan de implementare pentru următoarea sesiune
|
||||
|
||||
## Obiectiv
|
||||
Optimizare proces OCR - dacă PaddleOCR pe light preprocessing dă rezultate bune, să NU mai ruleze heavy preprocessing și Tesseract.
|
||||
|
||||
---
|
||||
|
||||
## Criterii Early Exit (TOATE trebuie îndeplinite)
|
||||
|
||||
**Continuă cu alte încercări DACĂ:**
|
||||
- Confidență < **85%** SAU
|
||||
- Lipsește ORICARE din câmpurile critice:
|
||||
- ✗ Număr bon (`receipt_number`)
|
||||
- ✗ Dată (`receipt_date`)
|
||||
- ✗ Valoare totală (`amount`)
|
||||
- ✗ Valoare TVA (`tva_total` sau `tva_entries`)
|
||||
- ✗ Cod fiscal (`cui`)
|
||||
|
||||
**Early exit DOAR când:**
|
||||
- Confidență >= 85% **ȘI**
|
||||
- TOATE 5 câmpurile sunt extrase
|
||||
|
||||
---
|
||||
|
||||
## Flow Propus: Adaptive OCR Pipeline
|
||||
|
||||
```
|
||||
1. PaddleOCR + Light Preprocessing (cel mai rapid, cel mai bun pentru PDF-uri clare)
|
||||
↓
|
||||
Verifică: conf >= 85% AND toate 5 câmpurile extrase?
|
||||
├─ DA → STOP, returnează rezultat
|
||||
└─ NU → continuă la pasul 2
|
||||
|
||||
2. PaddleOCR + Heavy Preprocessing (pentru bonuri termice șterse)
|
||||
↓
|
||||
Combină cu rezultatul anterior (merge)
|
||||
Verifică: toate câmpurile extrase acum?
|
||||
├─ DA → STOP
|
||||
└─ NU → continuă la pasul 3
|
||||
|
||||
3. Tesseract + Light (fallback pentru cazuri dificile)
|
||||
↓
|
||||
Combină toate rezultatele
|
||||
Returnează cel mai bun rezultat combinat
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Beneficii Estimate
|
||||
|
||||
| Tip document | OCR calls | Timp estimat |
|
||||
|--------------|-----------|--------------|
|
||||
| PDF clar (Kineterra) | 1 | ~2-3 sec |
|
||||
| PDF mediu | 2 | ~5 sec |
|
||||
| Bon termic șters | 3 | ~8-10 sec |
|
||||
|
||||
**Comparație cu acum:** Totdeauna 4 calls → maxim 3, de obicei 1-2
|
||||
|
||||
---
|
||||
|
||||
## Fișier de Modificat
|
||||
|
||||
**`data-entry-app/backend/app/services/ocr_service.py`**
|
||||
|
||||
### Înlocuire completă `_process_sync()`:
|
||||
|
||||
```python
|
||||
def _process_sync(
|
||||
self,
|
||||
image_path: Path,
|
||||
mime_type: str
|
||||
) -> Tuple[bool, str, Optional[ExtractionResult]]:
|
||||
"""Synchronous processing with ADAPTIVE OCR pipeline."""
|
||||
|
||||
logger.info(f"[OCR Service] Starting processing: {image_path}, mime: {mime_type}")
|
||||
|
||||
# Load image
|
||||
if mime_type == 'application/pdf':
|
||||
try:
|
||||
images = self.preprocessor.pdf_to_images(image_path)
|
||||
if not images:
|
||||
return False, "Failed to extract images from PDF", None
|
||||
image = images[0]
|
||||
except RuntimeError as e:
|
||||
return False, str(e), None
|
||||
else:
|
||||
try:
|
||||
image = self.preprocessor.load_image(image_path)
|
||||
except ValueError as e:
|
||||
return False, str(e), None
|
||||
|
||||
raw_texts = []
|
||||
extraction = None
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# STEP 1: PaddleOCR + Light (fastest, best for clear PDFs)
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
logger.info("[OCR] Step 1: PaddleOCR + Light preprocessing")
|
||||
light_img = self.preprocessor.preprocess_light(image)
|
||||
|
||||
try:
|
||||
paddle_light = self.ocr_engine._paddle_recognize(light_img)
|
||||
if paddle_light and paddle_light.text:
|
||||
extraction = self.extractor.extract(paddle_light.text)
|
||||
extraction.ocr_engine = "paddle-light"
|
||||
raw_texts.append(f"═══ PaddleOCR (light, conf: {paddle_light.confidence:.0%}) ═══\n{paddle_light.text}")
|
||||
|
||||
# Early exit if complete
|
||||
if self._is_extraction_complete(extraction):
|
||||
extraction.raw_text = "\n\n".join(raw_texts)
|
||||
logger.info("[OCR] ✓ Early exit: complete extraction from paddle-light")
|
||||
return True, "OCR complete (fast mode)", extraction
|
||||
except Exception as e:
|
||||
logger.warning(f"[OCR] PaddleOCR light failed: {e}")
|
||||
extraction = ExtractionResult()
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# STEP 2: PaddleOCR + Heavy (for faded thermal receipts)
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
logger.info("[OCR] Step 2: PaddleOCR + Heavy preprocessing")
|
||||
heavy_img = self.preprocessor.preprocess_heavy(image)
|
||||
|
||||
try:
|
||||
paddle_heavy = self.ocr_engine._paddle_recognize(heavy_img)
|
||||
if paddle_heavy and paddle_heavy.text:
|
||||
extraction_heavy = self.extractor.extract(paddle_heavy.text)
|
||||
extraction_heavy.ocr_engine = "paddle-heavy"
|
||||
raw_texts.append(f"═══ PaddleOCR (heavy, conf: {paddle_heavy.confidence:.0%}) ═══\n{paddle_heavy.text}")
|
||||
|
||||
# Merge with previous
|
||||
extraction = self._merge_extractions(extraction, extraction_heavy)
|
||||
|
||||
if self._is_extraction_complete(extraction):
|
||||
extraction.raw_text = "\n\n".join(raw_texts)
|
||||
extraction.ocr_engine = "paddle-adaptive"
|
||||
logger.info("[OCR] ✓ Early exit: complete extraction after paddle-heavy")
|
||||
return True, "OCR complete (paddle dual)", extraction
|
||||
except Exception as e:
|
||||
logger.warning(f"[OCR] PaddleOCR heavy failed: {e}")
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# STEP 3: Tesseract fallback
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
logger.info("[OCR] Step 3: Tesseract fallback")
|
||||
|
||||
try:
|
||||
tesseract_result = self.ocr_engine._tesseract_recognize(light_img)
|
||||
if tesseract_result and tesseract_result.text:
|
||||
extraction_tess = self.extractor.extract(tesseract_result.text)
|
||||
extraction_tess.ocr_engine = "tesseract"
|
||||
raw_texts.append(f"═══ Tesseract (conf: {tesseract_result.confidence:.0%}) ═══\n{tesseract_result.text}")
|
||||
|
||||
extraction = self._merge_extractions(extraction, extraction_tess)
|
||||
except Exception as e:
|
||||
logger.warning(f"[OCR] Tesseract failed: {e}")
|
||||
|
||||
# Final result
|
||||
if extraction is None:
|
||||
return False, "No text detected", None
|
||||
|
||||
extraction.raw_text = "\n\n".join(raw_texts)
|
||||
extraction.ocr_engine = "adaptive-full"
|
||||
|
||||
# Build result message
|
||||
fields_found = []
|
||||
if extraction.amount: fields_found.append("amount")
|
||||
if extraction.receipt_date: fields_found.append("date")
|
||||
if extraction.receipt_number: fields_found.append("number")
|
||||
if extraction.cui: fields_found.append("CUI")
|
||||
if extraction.tva_total or extraction.tva_entries: fields_found.append("TVA")
|
||||
|
||||
message = f"OCR complete (full pipeline). Found: {', '.join(fields_found) or 'no fields'}"
|
||||
logger.info(f"[OCR] Final result: {message}")
|
||||
|
||||
return True, message, extraction
|
||||
```
|
||||
|
||||
### Adăugare metodă `_is_extraction_complete()`:
|
||||
|
||||
```python
|
||||
def _is_extraction_complete(self, ext: ExtractionResult, min_confidence: float = 0.85) -> bool:
|
||||
"""
|
||||
Check if extraction has ALL required fields to skip further processing.
|
||||
|
||||
Required for early exit (ALL must be true):
|
||||
- Overall confidence >= 85%
|
||||
- ALL 5 critical fields present: number, date, amount, TVA, CUI
|
||||
"""
|
||||
# Must have high confidence
|
||||
if ext.overall_confidence < min_confidence:
|
||||
logger.info(f"[OCR] Confidence {ext.overall_confidence:.0%} < {min_confidence:.0%} - continuing")
|
||||
return False
|
||||
|
||||
# Check all required fields
|
||||
has_number = bool(ext.receipt_number)
|
||||
has_date = bool(ext.receipt_date)
|
||||
has_amount = bool(ext.amount)
|
||||
has_tva = bool(ext.tva_total) or bool(ext.tva_entries)
|
||||
has_cui = bool(ext.cui)
|
||||
|
||||
missing = []
|
||||
if not has_number: missing.append("number")
|
||||
if not has_date: missing.append("date")
|
||||
if not has_amount: missing.append("amount")
|
||||
if not has_tva: missing.append("TVA")
|
||||
if not has_cui: missing.append("CUI")
|
||||
|
||||
if missing:
|
||||
logger.info(f"[OCR] Missing: {', '.join(missing)} - continuing")
|
||||
return False
|
||||
|
||||
logger.info(f"[OCR] ✓ All 5 fields found with {ext.overall_confidence:.0%} confidence")
|
||||
return True
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cod de Șters
|
||||
|
||||
După implementare, poți șterge:
|
||||
- `_merge_all_extractions()` - înlocuită de flow secvențial
|
||||
- `_format_dual_raw_text()` - nefolosită
|
||||
- Bucla `for i, processed in enumerate(variants):` - înlocuită complet
|
||||
|
||||
---
|
||||
|
||||
## Context: Rezultate OCR Kineterra
|
||||
|
||||
Din testele anterioare, **PaddleOCR + Light** a dat cele mai bune rezultate:
|
||||
|
||||
| Variantă | Conf | CUI | Adresa |
|
||||
|----------|------|-----|--------|
|
||||
| **PaddleOCR Light** | **83%** | **31180432** ✓ | MUN.CONSTANTA ✓ |
|
||||
| PaddleOCR Heavy | 83% | 31189432 ✗ | CONSTANTN ✗ |
|
||||
| Tesseract Light | 50% | 31100400 ✗ | corupt |
|
||||
| Tesseract Heavy | 42% | - | corupt |
|
||||
|
||||
---
|
||||
|
||||
## Testare
|
||||
|
||||
După implementare, testează cu toate PDF-urile:
|
||||
|
||||
1. **`abonament kineterra.pdf`** - ar trebui să facă early exit la Step 1
|
||||
2. **`benzina 27 octombrie.pdf`** - verifică extracție completă
|
||||
3. **`igiena 11 octombrie.pdf`** - verifică extracție completă
|
||||
4. **`benzina 14 august.pdf`** - verifică extracție completă
|
||||
|
||||
---
|
||||
|
||||
*Generat: 2024-12-12*
|
||||
*Pentru continuare în următoarea sesiune Claude*
|
||||
667
data-entry-app/docs/PLAN_DATA_ENTRY_RECEIPTS.md
Normal file
667
data-entry-app/docs/PLAN_DATA_ENTRY_RECEIPTS.md
Normal file
@@ -0,0 +1,667 @@
|
||||
# Plan: Introducere Bonuri Fiscale - Faza 1 (MVP SQLite)
|
||||
|
||||
> **Plan Handover** - Acest document conține planul complet pentru implementare.
|
||||
> Creat: 2025-12-11 | Status: Ready for implementation
|
||||
|
||||
## Obiectiv
|
||||
Sistem de introducere bonuri fiscale cu:
|
||||
- **Upload poze** bonuri de la utilizatori
|
||||
- **Generare automată** note contabile (staging area)
|
||||
- **Aprobare de contabil** înainte de finalizare
|
||||
- SQLite + ORM (SQLModel) + Migrări (Alembic)
|
||||
- Pregătit pentru integrare Oracle în Faza 2
|
||||
|
||||
---
|
||||
|
||||
## Setup Proiect
|
||||
|
||||
### Branch de dezvoltare
|
||||
```bash
|
||||
git checkout -b feature/data-entry-receipts
|
||||
```
|
||||
|
||||
### Structură Directoare (SEPARAT de reports-app)
|
||||
```
|
||||
.
|
||||
├── reports-app/ # EXISTENT - Raportări (read-only din Oracle)
|
||||
│ ├── backend/
|
||||
│ ├── frontend/
|
||||
│ └── telegram-bot/
|
||||
│
|
||||
├── data-entry-app/ # NOU - Introduceri date (write în SQLite → Oracle)
|
||||
│ ├── backend/ # FastAPI pentru introduceri
|
||||
│ ├── frontend/ # Vue.js pentru introduceri
|
||||
│ └── docs/ # Documentație și cerințe
|
||||
│
|
||||
├── shared/ # EXISTENT - Componente partajate
|
||||
│ ├── database/
|
||||
│ └── auth/
|
||||
│
|
||||
└── docs/ # Documentație generală proiect
|
||||
└── data-entry/ # Documentație specifică data-entry
|
||||
├── REQUIREMENTS.md # Cerințe inițiale (acest plan)
|
||||
└── ARCHITECTURE.md # Decizii arhitecturale
|
||||
```
|
||||
|
||||
### Documentație Salvată
|
||||
La finalizarea planului, se vor crea:
|
||||
1. `docs/data-entry/REQUIREMENTS.md` - Cerințe funcționale și tehnice
|
||||
2. `docs/data-entry/ARCHITECTURE.md` - Decizii arhitecturale (ORM, workflow)
|
||||
3. `data-entry-app/README.md` - Quick start pentru dezvoltare
|
||||
|
||||
---
|
||||
|
||||
## Workflow Principal
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. UTILIZATOR uploadează bon │
|
||||
│ ├─ Poză bon fiscal / chitanță │
|
||||
│ ├─ Date de bază: sumă, dată, furnizor │
|
||||
│ └─ Status: DRAFT │
|
||||
└──────────────────────┬──────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 2. SISTEM generează propunere note contabile │
|
||||
│ ├─ Debit: Cont cheltuială (6022, 6024, etc.) │
|
||||
│ ├─ Credit: Casă (5311) sau Bancă (5121) │
|
||||
│ └─ Status: PENDING_REVIEW │
|
||||
└──────────────────────┬──────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 3. CONTABIL revizuiește │
|
||||
│ ├─ Verifică poza + datele │
|
||||
│ ├─ Ajustează conturi dacă e nevoie │
|
||||
│ ├─ APROBĂ → Status: APPROVED │
|
||||
│ └─ RESPINGE → Status: REJECTED (cu motiv) │
|
||||
└──────────────────────┬──────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 4. FAZA 2: Sync în Oracle │
|
||||
│ ├─ INSERT ACT_TEMP │
|
||||
│ ├─ pack_contafin.finalizeaza_scriere_act_rul() │
|
||||
│ └─ Status: SYNCED │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decizie Tehnică: ORM + Migrări
|
||||
|
||||
### Recomandare: **SQLModel + Alembic**
|
||||
|
||||
**Motivație:**
|
||||
1. **Creat de autorul FastAPI** (Sebastian Ramirez) - integrare perfectă
|
||||
2. **Un model = Pydantic + SQLAlchemy** - nu duplici definiții
|
||||
3. **Async support** nativ
|
||||
4. **Alembic** - standard industrial pentru migrări
|
||||
5. **Validare automată** - Pydantic validează input, SQLAlchemy gestionează DB
|
||||
|
||||
---
|
||||
|
||||
## Arhitectură Propusă
|
||||
|
||||
### Backend Structure (`data-entry-app/backend/`)
|
||||
```
|
||||
data-entry-app/backend/
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # FastAPI app entry point
|
||||
│ ├── config.py # Settings & env vars
|
||||
│ │
|
||||
│ ├── db/ # Database layer (SQLModel)
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── database.py # Engine, SessionLocal, init
|
||||
│ │ ├── models/
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── receipt.py # Receipt, ReceiptAttachment
|
||||
│ │ │ └── accounting_entry.py # AccountingEntry
|
||||
│ │ └── crud/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── receipt.py
|
||||
│ │ ├── attachment.py
|
||||
│ │ └── accounting_entry.py
|
||||
│ │
|
||||
│ ├── schemas/
|
||||
│ │ └── receipt.py # Request/Response Pydantic schemas
|
||||
│ │
|
||||
│ ├── services/
|
||||
│ │ ├── receipt_service.py # Business logic + workflow
|
||||
│ │ └── nomenclature_service.py # Nomenclatoare din Oracle
|
||||
│ │
|
||||
│ └── routers/
|
||||
│ └── receipts.py # API endpoints
|
||||
│
|
||||
├── migrations/ # Alembic migrations
|
||||
│ ├── env.py
|
||||
│ ├── alembic.ini
|
||||
│ └── versions/
|
||||
│ └── 001_initial_receipts.py
|
||||
│
|
||||
├── data/
|
||||
│ ├── receipts.db # SQLite database
|
||||
│ └── uploads/ # Poze bonuri
|
||||
│
|
||||
├── requirements.txt
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Frontend Structure (`data-entry-app/frontend/`)
|
||||
```
|
||||
data-entry-app/frontend/
|
||||
├── src/
|
||||
│ ├── views/receipts/
|
||||
│ │ ├── ReceiptsListView.vue
|
||||
│ │ ├── ReceiptCreateView.vue
|
||||
│ │ ├── ReceiptDetailView.vue
|
||||
│ │ └── ReceiptApprovalView.vue
|
||||
│ │
|
||||
│ ├── components/receipts/
|
||||
│ │ ├── ReceiptForm.vue
|
||||
│ │ ├── ReceiptImageUpload.vue
|
||||
│ │ └── AccountingEntriesTable.vue
|
||||
│ │
|
||||
│ ├── stores/
|
||||
│ │ └── receiptsStore.js
|
||||
│ │
|
||||
│ └── router/
|
||||
│ └── index.js
|
||||
│
|
||||
├── package.json
|
||||
└── vite.config.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modele de Date
|
||||
|
||||
### 1. Receipt (Bon Fiscal / Chitanță)
|
||||
|
||||
```python
|
||||
# app/db/models/receipt.py
|
||||
from sqlmodel import SQLModel, Field, Relationship
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import Optional, List
|
||||
|
||||
class ReceiptType(str, Enum):
|
||||
BON_FISCAL = "bon_fiscal"
|
||||
CHITANTA = "chitanta"
|
||||
|
||||
class ReceiptDirection(str, Enum):
|
||||
CHELTUIALA = "cheltuiala" # Plată (bon primit de la furnizor)
|
||||
INCASARE = "incasare" # Încasare (bon emis către client)
|
||||
|
||||
class ReceiptStatus(str, Enum):
|
||||
DRAFT = "draft" # Utilizator completează
|
||||
PENDING_REVIEW = "pending_review" # Așteaptă aprobare contabil
|
||||
APPROVED = "approved" # Aprobat de contabil
|
||||
REJECTED = "rejected" # Respins de contabil
|
||||
SYNCED = "synced" # Sincronizat în Oracle (Faza 2)
|
||||
|
||||
class Receipt(SQLModel, table=True):
|
||||
"""Bon fiscal sau chitanță cu workflow aprobare"""
|
||||
__tablename__ = "receipts"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
|
||||
# Identificare document
|
||||
receipt_type: ReceiptType
|
||||
direction: ReceiptDirection
|
||||
receipt_number: Optional[str] = None
|
||||
receipt_series: Optional[str] = None
|
||||
|
||||
# Date principale
|
||||
receipt_date: date
|
||||
amount: Decimal
|
||||
description: Optional[str] = None
|
||||
|
||||
# Referințe Oracle (nomenclatoare)
|
||||
company_id: int
|
||||
partner_id: Optional[int] = None
|
||||
partner_name: Optional[str] = None # Cache pentru display
|
||||
cash_register_id: Optional[int] = None # ID casă/bancă Oracle
|
||||
cash_register_name: Optional[str] = None # Cache pentru display
|
||||
|
||||
# Workflow
|
||||
status: ReceiptStatus = Field(default=ReceiptStatus.DRAFT)
|
||||
created_by: str # Username creator
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
submitted_at: Optional[datetime] = None # Când a fost trimis spre aprobare
|
||||
|
||||
# Aprobare
|
||||
reviewed_by: Optional[str] = None # Username contabil
|
||||
reviewed_at: Optional[datetime] = None
|
||||
rejection_reason: Optional[str] = None # Motiv respingere
|
||||
|
||||
# Faza 2 - Oracle sync
|
||||
oracle_synced_at: Optional[datetime] = None
|
||||
oracle_act_id: Optional[int] = None
|
||||
oracle_error: Optional[str] = None
|
||||
|
||||
# Relații
|
||||
attachments: List["ReceiptAttachment"] = Relationship(back_populates="receipt")
|
||||
entries: List["AccountingEntry"] = Relationship(back_populates="receipt")
|
||||
```
|
||||
|
||||
### 2. ReceiptAttachment (Poze bonuri - OBLIGATORIU)
|
||||
|
||||
```python
|
||||
class ReceiptAttachment(SQLModel, table=True):
|
||||
"""Poză sau PDF bon fiscal"""
|
||||
__tablename__ = "receipt_attachments"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
receipt_id: int = Field(foreign_key="receipts.id")
|
||||
|
||||
filename: str # Nume original fișier
|
||||
stored_filename: str # Nume pe disk (UUID)
|
||||
file_path: str # Cale relativă
|
||||
file_size: int # Bytes
|
||||
mime_type: str # image/jpeg, application/pdf
|
||||
uploaded_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
receipt: Optional["Receipt"] = Relationship(back_populates="attachments")
|
||||
```
|
||||
|
||||
### 3. AccountingEntry (Note Contabile - Staging)
|
||||
|
||||
```python
|
||||
class EntryType(str, Enum):
|
||||
DEBIT = "debit"
|
||||
CREDIT = "credit"
|
||||
|
||||
class AccountingEntry(SQLModel, table=True):
|
||||
"""Notă contabilă propusă pentru bon"""
|
||||
__tablename__ = "accounting_entries"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
receipt_id: int = Field(foreign_key="receipts.id")
|
||||
|
||||
# Cont
|
||||
entry_type: EntryType # Debit sau Credit
|
||||
account_code: str # Ex: 6022, 5311, 4426
|
||||
account_name: Optional[str] # Cache: "Cheltuieli combustibil"
|
||||
|
||||
# Valori
|
||||
amount: Decimal
|
||||
|
||||
# Analitice (opțional)
|
||||
partner_id: Optional[int] = None
|
||||
cost_center_id: Optional[int] = None
|
||||
|
||||
# Auto-generat sau modificat manual
|
||||
is_auto_generated: bool = True
|
||||
modified_by: Optional[str] = None
|
||||
modified_at: Optional[datetime] = None
|
||||
|
||||
receipt: Optional["Receipt"] = Relationship(back_populates="entries")
|
||||
```
|
||||
|
||||
### Exemplu Note Contabile Generate
|
||||
|
||||
```
|
||||
BON FISCAL BENZINĂ - 200 RON:
|
||||
┌──────────┬────────┬──────────────────────────┬─────────┐
|
||||
│ Tip │ Cont │ Denumire │ Sumă │
|
||||
├──────────┼────────┼──────────────────────────┼─────────┤
|
||||
│ DEBIT │ 6022 │ Cheltuieli combustibil │ 168.07 │
|
||||
│ DEBIT │ 4426 │ TVA deductibilă │ 31.93 │
|
||||
│ CREDIT │ 5311 │ Casă în lei │ 200.00 │
|
||||
└──────────┴────────┴──────────────────────────┴─────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Bonuri (CRUD + Workflow)
|
||||
```
|
||||
POST /api/receipts/ # Creare bon nou (cu upload poză)
|
||||
GET /api/receipts/ # Listă bonuri (filtrare, paginare)
|
||||
GET /api/receipts/{id} # Detalii bon + note contabile
|
||||
PUT /api/receipts/{id} # Modificare bon (doar DRAFT)
|
||||
DELETE /api/receipts/{id} # Ștergere bon (doar DRAFT)
|
||||
|
||||
# Workflow
|
||||
POST /api/receipts/{id}/submit # Trimite spre aprobare (DRAFT → PENDING)
|
||||
POST /api/receipts/{id}/approve # Aprobă (PENDING → APPROVED) [Contabil]
|
||||
POST /api/receipts/{id}/reject # Respinge (PENDING → REJECTED) [Contabil]
|
||||
POST /api/receipts/{id}/resubmit # Re-trimite după corecții (REJECTED → PENDING)
|
||||
```
|
||||
|
||||
### Note Contabile
|
||||
```
|
||||
GET /api/receipts/{id}/entries # Liste note contabile propuse
|
||||
PUT /api/receipts/{id}/entries # Modificare note (contabil ajustează conturi)
|
||||
POST /api/receipts/{id}/entries/regenerate # Re-generare automată
|
||||
```
|
||||
|
||||
### Atașamente
|
||||
```
|
||||
POST /api/receipts/{id}/attachments # Upload poză/PDF
|
||||
GET /api/receipts/{id}/attachments # Listă atașamente
|
||||
GET /api/attachments/{id}/download # Download fișier
|
||||
DELETE /api/attachments/{id} # Ștergere atașament
|
||||
```
|
||||
|
||||
### Nomenclatoare (din Oracle - read only)
|
||||
```
|
||||
GET /api/receipts/partners # Furnizori/Clienți pentru dropdown
|
||||
GET /api/receipts/accounts # Conturi sintetice (6xxx, 7xxx, etc.)
|
||||
GET /api/receipts/cash-registers # Case și bănci
|
||||
GET /api/receipts/expense-types # Tipuri cheltuieli predefinite (cu cont asociat)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Views
|
||||
|
||||
```
|
||||
frontend/src/views/receipts/
|
||||
├── ReceiptsListView.vue # Listă bonuri cu filtrare pe status
|
||||
├── ReceiptCreateView.vue # Form creare + upload poză
|
||||
├── ReceiptDetailView.vue # Vizualizare + editare note contabile
|
||||
└── ReceiptApprovalView.vue # View pentru contabil (aprobare în masă)
|
||||
```
|
||||
|
||||
### ReceiptCreateView - Form utilizator
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ UPLOAD POZĂ BON [Drag & Drop sau Click] │
|
||||
│ ┌────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ [Previzualizare imagine] │ │
|
||||
│ │ │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ Tip document: ○ Bon fiscal ○ Chitanță │
|
||||
│ Direcție: ○ Cheltuială ○ Încasare │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ Data: [DatePicker] │
|
||||
│ Suma: [InputNumber] RON │
|
||||
│ Furnizor: [Dropdown - din Oracle] │
|
||||
│ Tip cheltuială:[Dropdown - Benzină, Materiale...] │
|
||||
│ Casă/Bancă: [Dropdown - din Oracle] │
|
||||
│ Descriere: [Textarea] │
|
||||
├──────────────────────────────────────────────────────┤
|
||||
│ [Salvează Draft] [Trimite spre aprobare]│
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### ReceiptApprovalView - View Contabil
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ BONURI DE APROBAT (3) [Aprobă selectate] │
|
||||
├────────────────────────────────────────────────────────────────┤
|
||||
│ ☑ │ Data │ Furnizor │ Sumă │ Status │ Acțiuni │
|
||||
├───┼──────────┼───────────────┼─────────┼─────────┼───────────┤
|
||||
│ ☑ │ 10.12.24 │ OMV Petrom │ 200 RON │ PENDING │ [👁️][✓][✗]│
|
||||
│ ☑ │ 09.12.24 │ Dedeman │ 450 RON │ PENDING │ [👁️][✓][✗]│
|
||||
│ ☐ │ 08.12.24 │ Kaufland │ 85 RON │ PENDING │ [👁️][✓][✗]│
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
|
||||
[👁️] = Deschide detalii + poză + note contabile editabile
|
||||
[✓] = Aprobă
|
||||
[✗] = Respinge (cu motiv)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pași Implementare
|
||||
|
||||
### Etapa 0: Setup Proiect și Documentație
|
||||
1. [ ] Creare branch: `git checkout -b feature/data-entry-receipts`
|
||||
2. [ ] Creare structură directoare: `data-entry-app/{backend,frontend,docs}`
|
||||
3. [ ] Creare documentație:
|
||||
- `docs/data-entry/REQUIREMENTS.md` - Cerințe funcționale
|
||||
- `docs/data-entry/ARCHITECTURE.md` - Decizii tehnice (ORM, workflow)
|
||||
4. [ ] Creare `data-entry-app/README.md` - Quick start
|
||||
5. [ ] Creare `data-entry-app/CLAUDE.md` - Instrucțiuni pentru Claude Code
|
||||
|
||||
### Etapa 1: Setup Backend (SQLModel + Alembic)
|
||||
6. [ ] Creare `data-entry-app/backend/requirements.txt`:
|
||||
- `fastapi`, `uvicorn`, `sqlmodel`, `alembic`, `python-multipart`
|
||||
- `aiosqlite`, `pydantic`, `python-dotenv`
|
||||
7. [ ] Creare `app/main.py` - FastAPI app cu CORS, lifespan
|
||||
8. [ ] Creare `app/config.py` - Settings (DB path, upload path)
|
||||
9. [ ] Creare `app/db/database.py` - engine async, session factory
|
||||
10. [ ] Setup Alembic: `alembic init migrations`
|
||||
|
||||
### Etapa 2: Modele și Migrări
|
||||
11. [ ] Creare `app/db/models/receipt.py` - Receipt, ReceiptAttachment
|
||||
12. [ ] Creare `app/db/models/accounting_entry.py` - AccountingEntry
|
||||
13. [ ] Prima migrare: `001_initial_receipts.py`
|
||||
14. [ ] Creare folder `data/uploads/` pentru fișiere
|
||||
|
||||
### Etapa 3: Backend CRUD + Upload
|
||||
15. [ ] Creare `app/db/crud/receipt.py` - operații CRUD
|
||||
16. [ ] Creare `app/db/crud/attachment.py` - upload/download fișiere
|
||||
17. [ ] Creare `app/db/crud/accounting_entry.py` - note contabile
|
||||
18. [ ] Creare `app/schemas/receipt.py` - request/response Pydantic
|
||||
|
||||
### Etapa 4: Business Logic + Workflow
|
||||
19. [ ] Creare `app/services/receipt_service.py`:
|
||||
- `create_receipt()` - creare + upload poză
|
||||
- `generate_accounting_entries()` - generare automată note
|
||||
- `submit_for_review()` - DRAFT → PENDING
|
||||
- `approve_receipt()` - PENDING → APPROVED
|
||||
- `reject_receipt()` - PENDING → REJECTED
|
||||
|
||||
### Etapa 5: API Endpoints
|
||||
20. [ ] Creare `app/routers/receipts.py` - toate endpoint-urile
|
||||
21. [ ] Register router în `main.py`
|
||||
22. [ ] Middleware pentru upload fișiere
|
||||
|
||||
### Etapa 6: Nomenclatoare Oracle
|
||||
23. [ ] Creare `app/services/nomenclature_service.py`:
|
||||
- `get_partners()` - furnizori/clienți din Oracle
|
||||
- `get_expense_accounts()` - conturi 6xxx
|
||||
- `get_cash_registers()` - case/bănci
|
||||
- `get_expense_types()` - tipuri cheltuieli predefinite
|
||||
|
||||
### Etapa 7: Frontend Setup
|
||||
24. [ ] `npm create vite@latest frontend -- --template vue`
|
||||
25. [ ] Instalare dependențe: `pinia`, `vue-router`, `primevue`, `axios`
|
||||
26. [ ] Copy configurație PrimeVue din reports-app
|
||||
27. [ ] Copy CSS shared din reports-app (design tokens, patterns)
|
||||
|
||||
### Etapa 8: Frontend Views
|
||||
28. [ ] Creare `views/receipts/ReceiptsListView.vue` - listă cu filtre
|
||||
29. [ ] Creare `views/receipts/ReceiptCreateView.vue` - form + upload
|
||||
30. [ ] Creare `views/receipts/ReceiptDetailView.vue` - detalii + note
|
||||
31. [ ] Creare `views/receipts/ReceiptApprovalView.vue` - view contabil
|
||||
32. [ ] Creare `stores/receiptsStore.js` - Pinia store
|
||||
33. [ ] Configurare router și layout
|
||||
|
||||
### Etapa 9: Testing & Finalizare
|
||||
34. [ ] Unit tests pentru CRUD
|
||||
35. [ ] Integration tests pentru API
|
||||
36. [ ] Manual testing checklist
|
||||
37. [ ] Actualizare documentație
|
||||
38. [ ] Commit și push pe branch
|
||||
39. [ ] Creare PR către main
|
||||
|
||||
---
|
||||
|
||||
## Fișiere de Creat
|
||||
|
||||
### Documentație (Etapa 0):
|
||||
```
|
||||
docs/data-entry/
|
||||
├── REQUIREMENTS.md # Cerințe funcționale (din acest plan)
|
||||
└── ARCHITECTURE.md # Decizii tehnice (ORM, workflow)
|
||||
|
||||
data-entry-app/
|
||||
├── CLAUDE.md # Instrucțiuni pentru Claude Code
|
||||
└── README.md # Quick start pentru dezvoltare
|
||||
```
|
||||
|
||||
### Conținut `data-entry-app/CLAUDE.md`:
|
||||
```markdown
|
||||
# CLAUDE.md - Data Entry App
|
||||
|
||||
## Scop
|
||||
Aplicație pentru introducere date în ERP (bonuri fiscale, chitanțe) cu workflow de aprobare.
|
||||
|
||||
## Documentație de Referință
|
||||
- **Cerințe**: `docs/data-entry/REQUIREMENTS.md`
|
||||
- **Arhitectură**: `docs/data-entry/ARCHITECTURE.md`
|
||||
- **Quick Start**: `README.md`
|
||||
|
||||
## Decizii Tehnice
|
||||
- **ORM**: SQLModel (Pydantic + SQLAlchemy)
|
||||
- **Migrări**: Alembic
|
||||
- **Database**: SQLite (Faza 1) → Oracle (Faza 2)
|
||||
- **Frontend**: Vue.js 3 + PrimeVue (consistent cu reports-app)
|
||||
|
||||
## Workflow Bonuri
|
||||
1. DRAFT → utilizator completează + upload poză
|
||||
2. PENDING_REVIEW → sistem generează note contabile
|
||||
3. APPROVED/REJECTED → contabil aprobă sau respinge
|
||||
4. SYNCED → (Faza 2) date în Oracle
|
||||
|
||||
## Structură Directoare
|
||||
- `backend/` - FastAPI API (port 8003)
|
||||
- `frontend/` - Vue.js UI (port 3010)
|
||||
- `docs/` - Documentație specifică
|
||||
|
||||
## Componente Partajate
|
||||
- `shared/database/oracle_pool.py` - Conexiune Oracle
|
||||
- `shared/auth/` - JWT authentication
|
||||
|
||||
## Comenzi Dezvoltare
|
||||
```bash
|
||||
# Backend
|
||||
cd backend && pip install -r requirements.txt
|
||||
uvicorn app.main:app --reload --port 8003
|
||||
|
||||
# Frontend
|
||||
cd frontend && npm install && npm run dev
|
||||
|
||||
# Migrări
|
||||
cd backend && alembic upgrade head
|
||||
```
|
||||
|
||||
## Integrare Oracle (Faza 2)
|
||||
Vezi `docs/PACK_CONTAFIN.pck` pentru procedurile stocate:
|
||||
- `pack_contafin.init_scriere_act_rul_local()`
|
||||
- `INSERT INTO ACT_TEMP (...)`
|
||||
- `pack_contafin.finalizeaza_scriere_act_rul()`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tipuri Cheltuieli Predefinite
|
||||
|
||||
Pentru dropdown "Tip cheltuială" - mapare automată la conturi:
|
||||
|
||||
| Tip Cheltuială | Cont Debit | TVA | Descriere |
|
||||
|----------------|------------|-----|-----------|
|
||||
| Combustibil | 6022 | 4426 (19%) | Benzină, motorină |
|
||||
| Materiale consumabile | 6028 | 4426 (19%) | Diverse materiale |
|
||||
| Rechizite birou | 6024 | 4426 (19%) | Papetărie, toner |
|
||||
| Telefonie | 626 | 4426 (19%) | Telefon, internet |
|
||||
| Parcare | 6022 | 4426 (19%) | Taxe parcare |
|
||||
| Alimentație | 6028 | - | Fără TVA deductibilă |
|
||||
| Transport | 624 | 4426 (19%) | Taxi, transport |
|
||||
| Altele | 628 | 4426 (19%) | Alte cheltuieli |
|
||||
|
||||
**Logica generare note:**
|
||||
```python
|
||||
def generate_entries(receipt):
|
||||
expense_type = EXPENSE_TYPES[receipt.expense_type_code]
|
||||
|
||||
entries = []
|
||||
|
||||
if expense_type.has_vat:
|
||||
net_amount = receipt.amount / 1.19
|
||||
vat_amount = receipt.amount - net_amount
|
||||
|
||||
entries.append(AccountingEntry(
|
||||
entry_type=EntryType.DEBIT,
|
||||
account_code=expense_type.account_code, # ex: 6022
|
||||
amount=net_amount
|
||||
))
|
||||
entries.append(AccountingEntry(
|
||||
entry_type=EntryType.DEBIT,
|
||||
account_code="4426", # TVA deductibilă
|
||||
amount=vat_amount
|
||||
))
|
||||
else:
|
||||
entries.append(AccountingEntry(
|
||||
entry_type=EntryType.DEBIT,
|
||||
account_code=expense_type.account_code,
|
||||
amount=receipt.amount
|
||||
))
|
||||
|
||||
# Credit - casă sau bancă
|
||||
entries.append(AccountingEntry(
|
||||
entry_type=EntryType.CREDIT,
|
||||
account_code=receipt.cash_register_account, # 5311 sau 5121
|
||||
amount=receipt.amount
|
||||
))
|
||||
|
||||
return entries
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Faza 2 Preview (Oracle Integration)
|
||||
|
||||
După ce Faza 1 funcționează, Faza 2 va adăuga:
|
||||
|
||||
```python
|
||||
# receipt_service.py - metodă nouă
|
||||
async def sync_to_oracle(receipt_id: int):
|
||||
"""
|
||||
Sincronizează bon APPROVED în Oracle:
|
||||
|
||||
1. pack_contafin.init_scriere_act_rul_local()
|
||||
2. Pentru fiecare AccountingEntry:
|
||||
INSERT INTO ACT_TEMP (
|
||||
ID_ACT, DATAIREG, DATAACT, SCD, ASCD, SCC, ASCC,
|
||||
SUMA, ID_CTR, ID_PARTD, EXPLICATIA, ...
|
||||
)
|
||||
3. pack_contafin.finalizeaza_scriere_act_rul()
|
||||
→ SCRIE_IN_ACT()
|
||||
→ SCRIE_IN_RUL()
|
||||
→ Actualizare situații (BV, BP, TVA, etc.)
|
||||
4. Update receipt.status = SYNCED, oracle_act_id = ...
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Riscuri și Mitigări
|
||||
|
||||
| Risc | Impact | Mitigare |
|
||||
|------|--------|----------|
|
||||
| SQLModel e relativ nou | Mediu | Fallback la SQLAlchemy pur dacă e nevoie |
|
||||
| Upload fișiere mari | Mic | Limit 10MB, compresie imagini |
|
||||
| Workflow complex | Mediu | Începem cu workflow simplu, adăugăm features gradual |
|
||||
| Generare note greșite | Mare | Contabilul poate edita înainte de aprobare |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria (Faza 1)
|
||||
|
||||
- [ ] Utilizator poate uploada poză bon + date de bază
|
||||
- [ ] Sistem generează automat note contabile
|
||||
- [ ] Contabil poate vedea, edita și aproba note
|
||||
- [ ] Bonurile aprobate sunt vizibile în listă
|
||||
- [ ] Migrările Alembic funcționează corect
|
||||
- [ ] Poze bonuri se salvează și se afișează corect
|
||||
|
||||
---
|
||||
|
||||
## Context Handover
|
||||
|
||||
**Pentru sesiunea următoare:**
|
||||
1. Citește acest fișier `PLAN_DATA_ENTRY_RECEIPTS.md`
|
||||
2. Începe cu Etapa 0 - creare branch și structură directoare
|
||||
3. Referință pentru proceduri Oracle: `docs/PACK_CONTAFIN.pck`, `docs/PACK_FACTURARE.pck`
|
||||
4. Pattern-uri existente pentru SQLite: `reports-app/telegram-bot/app/db/`
|
||||
BIN
data-entry-app/docs/abonament kineterra.pdf
Normal file
BIN
data-entry-app/docs/abonament kineterra.pdf
Normal file
Binary file not shown.
BIN
data-entry-app/docs/benzina 14 august.pdf
Normal file
BIN
data-entry-app/docs/benzina 14 august.pdf
Normal file
Binary file not shown.
BIN
data-entry-app/docs/benzina 27 octombrie .pdf
Normal file
BIN
data-entry-app/docs/benzina 27 octombrie .pdf
Normal file
Binary file not shown.
2086
data-entry-app/docs/igiena 11 octombrie .pdf
Normal file
2086
data-entry-app/docs/igiena 11 octombrie .pdf
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user