15 KiB
Plan: Sincronizare Nomenclatoare Oracle + Auth SSO + OCR Furnizori
Obiective
- Sincronizare nomenclatoare din Oracle în SQLite (furnizori, casa/banca)
- Auth pentru data-entry-app cu SSO (frontend-uri separate pe path)
- OCR: căutare furnizor după CUI + creare locală dacă nu există
- 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/- Rapoarteroa2web.romfast.ro/data/- Introducere date (bonuri fiscale)roa2web.romfast.ro/api/reports/- API rapoarteroa2web.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.pydata-entry-app/backend/app/routers/receipts.pydata-entry-app/backend/app/core/config.py
Acțiuni:
# 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", "/"]
)
# 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/loginde 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
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
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
- La startup app: Sync automat (background task)
- Periodic: Task programat la 4h
- On-demand: Căutare live în Oracle când CUI nu există local
Fișier: data-entry-app/backend/app/main.py
@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
@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
// 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/):
<!-- 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):
# /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
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)
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)
#!/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 syncapp/db/models/nomenclature.py- noi modele (CREARE)app/services/sync_service.py- sync Oracle (CREARE)app/services/nomenclature_service.py- refactorizareapp/routers/nomenclature.py- endpoint-uri noi (CREARE)app/routers/receipts.py- auth dependenciesmigrations/versions/xxx_nomenclature.py- migrare (CREARE)
Frontend data-entry-app:
src/stores/auth.js- copiat din reports-appsrc/views/LoginView.vue- copiat + adaptatsrc/router/index.js- auth guardsrc/services/api.js- axios configsrc/views/ReceiptCreateView.vue- OCR + supplier flow
Deploy (structură unitară):
deployment/windows/config/web.config- reguli noi + actualizatedeployment/windows/scripts/Install-ROA2WEB.ps1- ACTUALIZAT pentru structura unitarădeployment/windows/scripts/Install-DataEntry.ps1- NOUdeployment/windows/scripts/Build-ROA2WEB.ps1- ACTUALIZAT pentru ambele appsdeployment/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/ |