+```
+
+### 4.2 Add CUI Field to Form State
+**File**: `data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue`
+
+Add to form ref initialization:
+```javascript
+cui: '',
+ocr_raw_text: '',
+```
+
+### 4.3 Add CUI Display Field
+Add after Furnizor dropdown (around line 210):
+```vue
+
+
+
+
+
+ CUI negăsit în nomenclator
+
+
+```
+
+### 4.4 Change Supplier Dialog to Warning Banner
+**Current behavior** (lines 555-563): When CUI not found, opens blocking dialog.
+
+**New behavior**: Show non-blocking warning message.
+
+Replace the `else` block in `applyOCRData()`:
+```javascript
+} else {
+ // Not found - show warning but allow continuing
+ supplierWarning.value = {
+ show: true,
+ cui: data.cui,
+ name: data.partner_name || ''
+ }
+ // Still set form values from OCR
+ form.value.cui = data.cui
+ form.value.partner_name = data.partner_name || ''
+
+ toast.add({
+ severity: 'warn',
+ summary: 'Furnizor negăsit',
+ detail: `CUI ${data.cui} nu a fost găsit în nomenclator`,
+ life: 5000
+ })
+}
+```
+
+Add ref for warning state:
+```javascript
+const supplierWarning = ref({ show: false, cui: '', name: '' })
+```
+
+### 4.5 Update `applyOCRData()` to Save Raw Text
+Add to the function:
+```javascript
+if (data.cui) form.value.cui = data.cui
+if (data.raw_text) form.value.ocr_raw_text = data.raw_text
+```
+
+### 4.6 Update `loadReceipt()` for Edit Mode
+Add to existing field mapping:
+```javascript
+cui: receipt.value.cui || '',
+ocr_raw_text: receipt.value.ocr_raw_text || '',
+```
+
+---
+
+## Part 5: Backend Approval Validation
+
+**File**: `data-entry-app/backend/app/services/receipt_service.py`
+
+In `approve_receipt()` method, add validation:
+```python
+if not receipt.cui:
+ return False, "Trebuie completat codul fiscal (CUI) pentru aprobare", None
+```
+
+**Note**: At approval, only `cui` (fiscal code) is required, NOT `partner_id`.
+The ROA ERP has a stored procedure that searches/creates suppliers based on `cui`.
+The `partner_id` is only populated later during Oracle import phase.
+
+---
+
+## Part 6: OCR Payment Methods Extraction
+
+### 6.1 Update ExtractionResult Dataclass
+**File**: `data-entry-app/backend/app/services/ocr_extractor.py`
+
+Add to `ExtractionResult` (after line 24, after `items_count`):
+```python
+payment_methods: List[dict] = field(default_factory=list) # [{"method":"CARD","amount":Decimal}]
+```
+
+### 6.2 Add Payment Method Patterns
+**File**: `data-entry-app/backend/app/services/ocr_extractor.py`
+
+Add new patterns (after TVA_PATTERNS ~line 184):
+```python
+# Payment method patterns - appears after TOTAL LEI, before TOTAL TVA
+# Format: "CARD: 50.00" or "NUMERAR 100.00" or "PLATA CARD: 50.00"
+PAYMENT_METHOD_PATTERNS = [
+ # CARD with amount
+ (r'(?:PLATA\s+)?CARD\s*:?\s*([\d\s.,]+)', 'CARD', 0.95),
+ # NUMERAR (cash) with amount
+ (r'NUMERAR\s*:?\s*([\d\s.,]+)', 'NUMERAR', 0.95),
+ # CASH alternative spelling
+ (r'CASH\s*:?\s*([\d\s.,]+)', 'NUMERAR', 0.90),
+]
+```
+
+### 6.3 Add Extraction Method
+**File**: `data-entry-app/backend/app/services/ocr_extractor.py`
+
+Add new method `_extract_payment_methods()` (after `_extract_address` ~line 996):
+```python
+def _extract_payment_methods(self, text: str) -> List[dict]:
+ """
+ Extract payment methods (CARD/NUMERAR) from receipt.
+ These appear after TOTAL LEI and before TOTAL TVA section.
+
+ Returns list of: {'method': 'CARD'/'NUMERAR', 'amount': Decimal}
+ """
+ payment_methods = []
+ seen_methods = set()
+
+ # Normalize spaces in numbers
+ normalized_text = re.sub(r'(\d+)[.,]\s+(\d{2})', r'\1.\2', text)
+
+ # Find the region between TOTAL LEI and TOTAL TVA
+ total_lei_match = re.search(r'TOTAL\s+LEI\s*([\d\s.,]+)', normalized_text, re.IGNORECASE)
+ total_tva_match = re.search(r'TOTAL\s+T[VU][AR]', normalized_text, re.IGNORECASE)
+
+ # Define search region (after TOTAL LEI, before TOTAL TVA if exists)
+ if total_lei_match:
+ start_pos = total_lei_match.end()
+ end_pos = total_tva_match.start() if total_tva_match else len(normalized_text)
+ search_region = normalized_text[start_pos:end_pos]
+ else:
+ search_region = normalized_text # Fallback to full text
+
+ for pattern, method, confidence in self.PAYMENT_METHOD_PATTERNS:
+ for match in re.finditer(pattern, search_region, re.IGNORECASE):
+ try:
+ amount_str = match.group(1).replace(' ', '')
+ amount_str = self._normalize_number(re.sub(r'[^\d.,]', '', amount_str))
+ amount = Decimal(amount_str)
+ if amount > 0 and method not in seen_methods:
+ payment_methods.append({
+ 'method': method,
+ 'amount': amount
+ })
+ seen_methods.add(method)
+ except (InvalidOperation, ValueError):
+ continue
+
+ return payment_methods
+```
+
+### 6.4 Call Extraction in `extract()` Method
+**File**: `data-entry-app/backend/app/services/ocr_extractor.py`
+
+Add to `extract()` method (after line 255, after `result.address = ...`):
+```python
+result.payment_methods = self._extract_payment_methods(text_upper)
+```
+
+### 6.5 Frontend - Add Payment Methods Display
+**File**: `data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue`
+
+Add to form ref:
+```javascript
+payment_methods: [],
+```
+
+Add to `applyOCRData()`:
+```javascript
+if (data.payment_methods) form.value.payment_methods = data.payment_methods
+```
+
+Add UI display (after TVA breakdown section):
+```vue
+
+
+
+
+
+
+
+```
+
+---
+
+## Implementation Order
+
+| Step | Task | Files |
+|------|------|-------|
+| 1 | Add `cui`, `ocr_raw_text`, `payment_methods` to model | `models/receipt.py` |
+| 2 | Create migration | `migrations/versions/...` |
+| 3 | Update schemas | `schemas/receipt.py` |
+| 4 | Fix image resize | `services/image_preprocessor.py` |
+| 5 | Add payment methods extraction to OCR | `services/ocr_extractor.py` |
+| 6 | Unify frontend form + add new fields | `views/receipts/ReceiptCreateView.vue` |
+| 7 | Add approval validation | `services/receipt_service.py` |
+| 8 | Test full workflow | Manual testing |
+
+---
+
+## Files to Modify
+
+### Backend
+- `data-entry-app/backend/app/db/models/receipt.py` - Add cui, ocr_raw_text, payment_methods fields
+- `data-entry-app/backend/app/schemas/receipt.py` - Add PaymentMethodSchema, update schemas
+- `data-entry-app/backend/app/services/image_preprocessor.py` - Fix resize bug (cap at 4000px)
+- `data-entry-app/backend/app/services/ocr_extractor.py` - Add payment methods extraction
+- `data-entry-app/backend/app/services/receipt_service.py` - Add approval validation
+- `data-entry-app/backend/migrations/versions/` - New migration
+
+### Frontend
+- `data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue` - Unify form, add CUI + payment methods fields, change dialog to warning
+
+---
+
+## Expected Behavior After Implementation
+
+1. **OCR Scan**: Extracts supplier name, CUI, raw text, payment methods → all saved to draft
+2. **Payment Methods**: CARD/NUMERAR amounts extracted (after TOTAL LEI, before TOTAL TVA)
+3. **CUI Match**: Auto-fills supplier name from ROA, user can edit
+4. **CUI No Match**: Shows warning toast, allows saving draft with OCR data
+5. **Edit Mode**: Can re-scan OCR to update extracted data
+6. **Approval**: Requires valid `cui` (fiscal code) - NOT partner_id
+7. **Oracle Import** (later): Uses `cui` to find/create supplier via ROA stored procedure
+8. **Large Images**: Automatically resized to max 4000px before OCR
+
+---
+
+## Romanian Receipt Structure Reference
+```
+NUME FIRMA S.R.L.
+CIF: RO12345678
+STR. EXEMPLU NR. 1
+
+[Product lines...]
+
+TOTAL LEI 150.00 ← Total amount
+CARD 50.00 ← Payment method 1 (NEW)
+NUMERAR 100.00 ← Payment method 2 (NEW)
+TOTAL TVA A-19% 23.95 ← TVA breakdown
+```
diff --git a/data-entry-app/CLAUDE.md b/data-entry-app/CLAUDE.md
index e1ff9f4..806a463 100644
--- a/data-entry-app/CLAUDE.md
+++ b/data-entry-app/CLAUDE.md
@@ -57,6 +57,89 @@ data-entry-app/
- `shared/frontend/stores/auth.js` - Pinia auth store factory
- `shared/frontend/styles/login.css` - Stiluri login
+## Servere Oracle (Producție vs Test)
+
+**IMPORTANT**: Există două servere Oracle separate. Verifică întotdeauna la care ești conectat!
+
+| Server | IP Oracle | Tunel SSH | Schema Verificare | Company ID |
+|--------|-----------|-----------|-------------------|------------|
+| **PRODUCȚIE** | `10.0.20.36` | `./ssh_tunnel.sh` | `ROMFAST` | 114 |
+| **TEST** | `10.0.20.121` | `./ssh-tunnel-test.sh` | `MARIUSM_AUTO` | 110 |
+
+### Scripturi de Pornire
+
+**IMPORTANT**: Folosește scriptul corespunzător mediului dorit!
+
+```bash
+# Pentru PRODUCȚIE (10.0.20.36)
+./start-data-entry-dev.sh # Pornește tot (tunel + backend + frontend)
+./start-data-entry-dev.sh stop # Oprește tot
+./start-data-entry-dev.sh status # Verifică status
+
+# Pentru TEST (10.0.20.121)
+./start-data-entry-test.sh # Pornește tot (tunel + backend + frontend)
+./start-data-entry-test.sh stop # Oprește tot
+./start-data-entry-test.sh status # Verifică status
+```
+
+Scripturile fac automat:
+1. Opresc tunelul celuilalt mediu (dacă rulează)
+2. Pornesc tunelul corect
+3. Copiază `.env.prod` sau `.env.test` în `.env`
+4. Rulează migrările pe baza de date corectă
+5. Pornesc frontend și backend
+
+### Fișiere .env
+
+| Fișier | Server | Database | Schema Test |
+|--------|--------|----------|-------------|
+| `.env.prod` | PRODUCȚIE (10.0.20.36) | `receipts_prod.db` | ROMFAST (id=114) |
+| `.env.test` | TEST (10.0.20.121) | `receipts_test.db` | MARIUSM_AUTO (id=110) |
+
+### Verificare conexiune
+
+```bash
+# Verifică la ce server ești conectat
+./ssh_tunnel.sh status # Dacă rulează = PRODUCȚIE
+./ssh-tunnel-test.sh status # Dacă rulează = TEST
+
+# Verifică schema disponibilă (din backend/)
+python3 -c "
+import asyncio
+from dotenv import load_dotenv
+load_dotenv()
+import sys; sys.path.insert(0, '../../shared')
+from database.oracle_pool import oracle_pool
+
+async def check():
+ await oracle_pool.initialize()
+ async with oracle_pool.get_connection() as conn:
+ with conn.cursor() as cur:
+ cur.execute('SELECT SCHEMA, NUME FROM CONTAFIN_ORACLE.V_NOM_FIRME ORDER BY NUME')
+ for row in cur.fetchall()[:10]:
+ print(f'{row[0]}: {row[1]}')
+asyncio.run(check())
+"
+```
+
+### Sincronizare Nomenclatoare
+
+Query-ul pentru furnizori folosește `CORESP_TIP_PART`:
+- `id_tip_part = 17` → Furnizori
+- `id_tip_part = 22` → Casa LEI
+- `id_tip_part = 23` → Casa Valută
+- `id_tip_part = 24` → Bancă LEI
+- `id_tip_part = 25` → Bancă Valută
+
+```sql
+-- Exemplu query furnizori pentru schema MARIUSM_AUTO (TEST)
+SELECT B.ID_PART, B.DENUMIRE, B.COD_FISCAL, B.ADRESA
+FROM MARIUSM_AUTO.CORESP_TIP_PART A
+INNER JOIN MARIUSM_AUTO.VNOM_PARTENERI B ON A.ID_PART = B.ID_PART
+WHERE A.ID_TIP_PART = 17 AND (B.INACTIV = 0 OR B.INACTIV IS NULL)
+ORDER BY B.DENUMIRE;
+```
+
## Comenzi Dezvoltare
```bash
@@ -135,6 +218,15 @@ cd frontend && npm run test
## Common Issues
+### Nomenclatoare goale / furnizori lipsă
+- Verifică la ce server Oracle ești conectat: `./ssh_tunnel.sh status`
+- Verifică dacă schema firmei selectate există pe acel server
+- Sincronizează manual: `POST /api/nomenclature/sync/all`
+
+### Conectat la serverul greșit
+- PRODUCȚIE are schema `ROMFAST`, TEST are schema `MARIUSM_AUTO`
+- Oprește tunelul curent și pornește cel corect (vezi secțiunea "Servere Oracle")
+
### SQLite locked
- Asigura-te ca nu ai multiple procese care acceseaza DB-ul
diff --git a/data-entry-app/backend/IMPLEMENTATION_SUMMARY_NOMENCLATURE_SYNC.md b/data-entry-app/backend/IMPLEMENTATION_SUMMARY_NOMENCLATURE_SYNC.md
deleted file mode 100644
index 7e29aa6..0000000
--- a/data-entry-app/backend/IMPLEMENTATION_SUMMARY_NOMENCLATURE_SYNC.md
+++ /dev/null
@@ -1,455 +0,0 @@
-# Implementation Summary: Nomenclature Sync (FAZA 3)
-
-**Date**: 2025-12-13
-**Status**: COMPLETED
-**Developer**: Claude Code
-
----
-
-## Overview
-
-Successfully implemented FAZA 3: Nomenclature Sync for the data-entry-app. This feature enables the application to maintain a local SQLite cache of nomenclatures (suppliers, cash registers) from Oracle, reducing latency and improving performance.
-
-## Files Created
-
-### 1. Models
-**File**: `/app/db/models/nomenclature.py`
-- `SyncedSupplier` - Suppliers synced from Oracle NOM_PARTENERI
-- `LocalSupplier` - Suppliers created locally from OCR (not yet in Oracle)
-- `SyncedCashRegister` - Cash registers and bank accounts synced from Oracle
-
-### 2. Service Layer
-**File**: `/app/services/sync_service.py`
-- `SyncService.sync_suppliers()` - Sync suppliers from Oracle to SQLite
-- `SyncService.sync_cash_registers()` - Sync cash registers from Oracle to SQLite
-- `SyncService.search_supplier()` - Search in synced + local suppliers
-- `SyncService.create_local_supplier()` - Create local supplier from OCR data
-- `SyncService.get_all_suppliers()` - Get all suppliers for dropdown
-- `SyncService.get_all_cash_registers()` - Get all cash registers for dropdown
-- `SyncService.get_schema_for_company()` - Map company ID to Oracle schema
-
-**Company-to-Schema Mapping**:
-```python
-COMPANY_SCHEMAS = {
- 1: "CONTAFIN",
- 2: "CONTAFIN2",
-}
-```
-> **TODO**: Move to config table or environment variable
-
-### 3. API Router
-**File**: `/app/routers/nomenclature.py`
-
-New endpoints:
-- `GET /api/nomenclature/suppliers` - Get all suppliers (synced + local)
-- `GET /api/nomenclature/suppliers/search` - Search supplier by fiscal code or name
-- `POST /api/nomenclature/suppliers/local` - Create local supplier from OCR
-- `GET /api/nomenclature/cash-registers` - Get all cash registers
-- `POST /api/nomenclature/sync/suppliers` - Manual supplier sync
-- `POST /api/nomenclature/sync/cash-registers` - Manual cash register sync
-- `POST /api/nomenclature/sync/all` - Sync all nomenclatures
-
-### 4. Database Migration
-**File**: `/migrations/versions/20251213_002805_add_nomenclature_tables.py`
-- Creates `synced_suppliers` table with indexes
-- Creates `local_suppliers` table with indexes
-- Creates `synced_cash_registers` table with indexes
-
-**Applied**: Yes (migration revision: 3a653da79002)
-
-### 5. Documentation
-**File**: `NOMENCLATURE_SYNC.md`
-- Complete implementation guide
-- Architecture overview
-- API reference
-- Usage examples
-- Troubleshooting guide
-
-**File**: `IMPLEMENTATION_SUMMARY_NOMENCLATURE_SYNC.md` (this file)
-- Implementation summary
-- Files changed
-- Testing checklist
-
-## Files Modified
-
-### 1. `/app/db/models/__init__.py`
-**Change**: Added imports for nomenclature models
-```python
-from .nomenclature import SyncedSupplier, LocalSupplier, SyncedCashRegister
-```
-
-### 2. `/app/services/nomenclature_service.py`
-**Changes**:
-- Updated `get_partners()` to accept optional `session` parameter
-- Added SQLite fallback: returns synced/local suppliers if available
-- Falls back to mock data if no synced data
-- Updated `get_cash_registers()` to accept optional `session` parameter
-- Added SQLite fallback for cash registers
-
-### 3. `/app/routers/receipts.py`
-**Changes**:
-- Updated `get_partners()` endpoint to pass `session` to service
-- Updated `get_cash_registers()` endpoint to pass `session` to service
-
-### 4. `/app/routers/__init__.py`
-**Change**: Added nomenclature router to exports
-```python
-from . import receipts, nomenclature
-__all__ = ["receipts", "nomenclature"]
-```
-
-### 5. `/app/main.py`
-**Change**: Registered nomenclature router
-```python
-from app.routers import receipts, ocr, nomenclature
-app.include_router(nomenclature.router, prefix="/api/nomenclature", tags=["nomenclature"])
-```
-
-## Database Schema
-
-### synced_suppliers
-```sql
-CREATE TABLE synced_suppliers (
- id INTEGER PRIMARY KEY,
- oracle_id INTEGER NOT NULL,
- company_id INTEGER NOT NULL,
- name VARCHAR(200) NOT NULL,
- fiscal_code VARCHAR(50),
- address VARCHAR(500),
- synced_at DATETIME NOT NULL
-);
-CREATE INDEX ix_synced_suppliers_oracle_id ON synced_suppliers(oracle_id);
-CREATE INDEX ix_synced_suppliers_company_id ON synced_suppliers(company_id);
-CREATE INDEX ix_synced_suppliers_fiscal_code ON synced_suppliers(fiscal_code);
-```
-
-### local_suppliers
-```sql
-CREATE TABLE local_suppliers (
- id INTEGER PRIMARY KEY,
- company_id INTEGER NOT NULL,
- name VARCHAR(200) NOT NULL,
- fiscal_code VARCHAR(50),
- address VARCHAR(500),
- created_by VARCHAR(100) NOT NULL,
- created_at DATETIME NOT NULL,
- pending_oracle_sync BOOLEAN NOT NULL
-);
-CREATE INDEX ix_local_suppliers_company_id ON local_suppliers(company_id);
-CREATE INDEX ix_local_suppliers_fiscal_code ON local_suppliers(fiscal_code);
-```
-
-### synced_cash_registers
-```sql
-CREATE TABLE synced_cash_registers (
- id INTEGER PRIMARY KEY,
- oracle_id INTEGER NOT NULL,
- company_id INTEGER NOT NULL,
- name VARCHAR(100) NOT NULL,
- account_code VARCHAR(20) NOT NULL,
- register_type VARCHAR(10) NOT NULL,
- synced_at DATETIME NOT NULL
-);
-CREATE INDEX ix_synced_cash_registers_oracle_id ON synced_cash_registers(oracle_id);
-CREATE INDEX ix_synced_cash_registers_company_id ON synced_cash_registers(company_id);
-```
-
-## API Endpoints Summary
-
-### Nomenclature Endpoints
-
-#### GET /api/nomenclature/suppliers
-Get all suppliers (synced + local) for dropdown/autocomplete.
-
-**Query Params**:
-- `search` (optional) - Filter by name or fiscal code
-- `company_id` (optional) - Company ID (defaults to user's first company)
-
-**Response**:
-```json
-[
- {
- "id": 1,
- "oracle_id": 123,
- "name": "OMV Petrom",
- "fiscal_code": "RO123456",
- "source": "synced"
- },
- {
- "id": 2,
- "name": "Local Supplier SRL",
- "fiscal_code": "RO789012",
- "source": "local"
- }
-]
-```
-
-#### GET /api/nomenclature/suppliers/search
-Search for supplier by fiscal code or name.
-
-**Query Params**:
-- `fiscal_code` (optional) - Fiscal code to search
-- `name` (optional) - Name to search (partial match)
-- `company_id` (optional) - Company ID
-
-**Response**:
-```json
-{
- "found": true,
- "supplier": {
- "id": 1,
- "oracle_id": 123,
- "name": "OMV Petrom",
- "fiscal_code": "RO123456",
- "address": "Str. Example 123"
- },
- "source": "synced"
-}
-```
-
-#### POST /api/nomenclature/suppliers/local
-Create a local supplier from OCR data.
-
-**Body**:
-```json
-{
- "name": "New Supplier SRL",
- "fiscal_code": "RO12345678",
- "address": "Str. Example 123"
-}
-```
-
-**Response**:
-```json
-{
- "id": 5,
- "name": "New Supplier SRL",
- "fiscal_code": "RO12345678",
- "address": "Str. Example 123",
- "is_local": true
-}
-```
-
-#### GET /api/nomenclature/cash-registers
-Get all cash registers for a company.
-
-**Query Params**:
-- `company_id` (optional) - Company ID
-
-**Response**:
-```json
-[
- {
- "id": 1,
- "oracle_id": 10,
- "name": "Casa principala",
- "account_code": "5311",
- "register_type": "cash"
- },
- {
- "id": 2,
- "oracle_id": 20,
- "name": "Cont BCR",
- "account_code": "5121",
- "register_type": "bank"
- }
-]
-```
-
-#### POST /api/nomenclature/sync/suppliers
-Manually trigger supplier sync from Oracle.
-
-**Response**:
-```json
-{
- "synced": 150,
- "errors": 0,
- "message": "Synced 150 suppliers with 0 errors"
-}
-```
-
-#### POST /api/nomenclature/sync/cash-registers
-Manually trigger cash register sync from Oracle.
-
-**Response**:
-```json
-{
- "synced": 5,
- "errors": 0,
- "message": "Synced 5 cash registers with 0 errors"
-}
-```
-
-#### POST /api/nomenclature/sync/all
-Sync all nomenclatures (suppliers + cash registers).
-
-**Response**:
-```json
-{
- "suppliers": {
- "synced": 150,
- "errors": 0
- },
- "cash_registers": {
- "synced": 5,
- "errors": 0
- },
- "total_synced": 155,
- "total_errors": 0,
- "message": "Synced 150 suppliers and 5 cash registers"
-}
-```
-
-## Testing Checklist
-
-### Unit Tests
-- [ ] Test `SyncService.sync_suppliers()` with mock Oracle data
-- [ ] Test `SyncService.sync_cash_registers()` with mock Oracle data
-- [ ] Test `SyncService.search_supplier()` for synced suppliers
-- [ ] Test `SyncService.search_supplier()` for local suppliers
-- [ ] Test `SyncService.create_local_supplier()`
-- [ ] Test upsert logic (update existing vs insert new)
-
-### Integration Tests
-- [ ] Test nomenclature router endpoints with authentication
-- [ ] Test `/api/nomenclature/suppliers` endpoint
-- [ ] Test `/api/nomenclature/suppliers/search` endpoint
-- [ ] Test `/api/nomenclature/suppliers/local` endpoint
-- [ ] Test `/api/nomenclature/cash-registers` endpoint
-- [ ] Test `/api/nomenclature/sync/suppliers` endpoint
-- [ ] Test `/api/nomenclature/sync/all` endpoint
-
-### Manual Testing
-- [ ] Start backend: `uvicorn app.main:app --reload --port 8003`
-- [ ] Verify `/docs` shows new nomenclature endpoints
-- [ ] Test sync endpoint (requires Oracle connection)
-- [ ] Test search endpoint with various queries
-- [ ] Test create local supplier endpoint
-- [ ] Verify existing `/api/receipts/nomenclature/partners` still works
-- [ ] Verify existing `/api/receipts/nomenclature/cash-registers` still works
-
-### Oracle Connection Testing
-- [ ] Verify SSH tunnel is running (dev/Linux)
-- [ ] Test Oracle connection via health endpoint
-- [ ] Verify company schema mapping is correct
-- [ ] Test sync with real Oracle data
-- [ ] Verify table names match actual Oracle schema
-
-### Error Handling Testing
-- [ ] Test sync with invalid company ID
-- [ ] Test sync with Oracle connection error
-- [ ] Test search with no results
-- [ ] Test create local supplier with duplicate fiscal code
-- [ ] Test endpoints with missing authentication token
-
-## Dependencies
-
-All required dependencies are already in `requirements.txt`:
-- `oracledb>=2.0.1` - Oracle database connection
-- `sqlmodel>=0.0.14` - ORM for SQLite
-- `alembic>=1.13.1` - Database migrations
-
-## Deployment Notes
-
-### Development
-1. Ensure SSH tunnel is running: `./ssh_tunnel.sh start`
-2. Apply migration: `alembic upgrade head`
-3. Run initial sync: `POST /api/nomenclature/sync/all`
-4. Start backend: `uvicorn app.main:app --reload --port 8003`
-
-### Production
-1. Update `COMPANY_SCHEMAS` in `sync_service.py` with production mappings
-2. Apply migration: `alembic upgrade head`
-3. Set up cron job for periodic sync (daily recommended)
-4. Configure Oracle connection (no SSH tunnel needed on Windows prod)
-
-### Migration Commands
-```bash
-# Check current version
-alembic current
-
-# Apply migration
-alembic upgrade head
-
-# Rollback migration
-alembic downgrade -1
-
-# View migration history
-alembic history
-```
-
-## Known Issues / TODOs
-
-1. **Company Schema Mapping**: Currently hardcoded in `sync_service.py`
- - TODO: Move to config table or environment variable
-
-2. **Oracle Table Names**: Assumes `NOM_PARTENERI` and `NOM_CASE` exist
- - TODO: Verify actual table names in production Oracle schema
- - TODO: Add error handling for missing tables
-
-3. **Sync Scheduling**: No automatic periodic sync implemented
- - TODO: Add background task or cron job for daily sync
-
-4. **Conflict Resolution**: No logic to handle local supplier matching synced supplier
- - TODO: Implement merge logic when OCR supplier matches Oracle supplier
-
-5. **Bidirectional Sync**: Local suppliers not pushed to Oracle
- - TODO: Implement sync from SQLite to Oracle for approved local suppliers
-
-6. **Performance**: Sync loads all records at once
- - TODO: Implement batch processing for large datasets
- - TODO: Add incremental sync (requires Oracle last_modified timestamp)
-
-7. **Validation**: No validation for duplicate fiscal codes
- - TODO: Add uniqueness constraint and conflict resolution
-
-8. **Testing**: No unit tests written yet
- - TODO: Add comprehensive test suite
-
-## Success Criteria
-
-✅ **Completed**:
-- SQLite tables created for synced nomenclatures
-- Sync service implemented with Oracle integration
-- API endpoints for sync and query operations
-- Updated existing nomenclature service to use synced data
-- Database migration created and applied
-- All files have correct Python syntax
-- Documentation created
-
-⏳ **Pending**:
-- Unit tests
-- Integration tests
-- Manual testing with real Oracle data
-- Production deployment
-- Scheduled sync setup
-
-## Next Steps
-
-1. **Testing Phase**:
- - Write unit tests for sync service
- - Write integration tests for API endpoints
- - Manual testing with real Oracle connection
- - Performance testing with large datasets
-
-2. **Production Readiness**:
- - Update company schema mappings for production
- - Verify Oracle table names
- - Set up cron job for periodic sync
- - Add monitoring and alerting
-
-3. **Enhancements**:
- - Implement scheduled background sync
- - Add sync status dashboard in frontend
- - Implement conflict resolution
- - Add bidirectional sync (SQLite → Oracle)
-
-## Related Documentation
-
-- Complete Guide: `NOMENCLATURE_SYNC.md`
-- Architecture: `docs/data-entry/ARCHITECTURE.md`
-- API Docs: Available at `/docs` when app is running
-
----
-
-**Implementation completed successfully!** All core features are in place and ready for testing.
diff --git a/data-entry-app/backend/NOMENCLATURE_SYNC.md b/data-entry-app/backend/NOMENCLATURE_SYNC.md
deleted file mode 100644
index 32530c1..0000000
--- a/data-entry-app/backend/NOMENCLATURE_SYNC.md
+++ /dev/null
@@ -1,273 +0,0 @@
-# Nomenclature Sync - Implementation Guide
-
-## Overview
-
-This document describes the implementation of FAZA 3: Nomenclature Sync for the Data Entry App.
-
-The nomenclature sync system allows the data-entry-app to maintain a local SQLite cache of nomenclatures (suppliers, cash registers) from Oracle, reducing the need for live Oracle queries and improving performance.
-
-## Architecture
-
-### Database Tables
-
-Three new SQLite tables were added:
-
-1. **synced_suppliers** - Suppliers synced from Oracle NOM_PARTENERI
- - `oracle_id` - Original Oracle ID
- - `company_id` - Company this supplier belongs to
- - `name` - Supplier name
- - `fiscal_code` - CUI/CIF
- - `address` - Supplier address
- - `synced_at` - Last sync timestamp
-
-2. **local_suppliers** - Suppliers created locally from OCR (not in Oracle)
- - `company_id` - Company ID
- - `name` - Supplier name
- - `fiscal_code` - CUI/CIF
- - `address` - Supplier address
- - `created_by` - Username who created it
- - `pending_oracle_sync` - Flag for future Oracle sync
-
-3. **synced_cash_registers** - Cash registers and bank accounts from Oracle
- - `oracle_id` - Original Oracle ID
- - `company_id` - Company ID
- - `name` - Register name
- - `account_code` - Account code (5311, 5121, etc.)
- - `register_type` - 'cash' or 'bank'
- - `synced_at` - Last sync timestamp
-
-### Components
-
-#### 1. Models (`app/db/models/nomenclature.py`)
-SQLModel models for the three tables above.
-
-#### 2. Sync Service (`app/services/sync_service.py`)
-Core business logic for syncing nomenclatures:
-
-- `sync_suppliers()` - Sync suppliers from Oracle to SQLite
-- `sync_cash_registers()` - Sync cash registers from Oracle to SQLite
-- `search_supplier()` - Search in synced + local suppliers
-- `create_local_supplier()` - Create local supplier from OCR data
-- `get_all_suppliers()` - Get all suppliers for dropdown
-- `get_all_cash_registers()` - Get all cash registers for dropdown
-
-#### 3. API Router (`app/routers/nomenclature.py`)
-New API endpoints:
-
-**GET /api/nomenclature/suppliers**
-- Get all suppliers (synced + local) for dropdown/autocomplete
-- Query params: `search`, `company_id`
-- Returns: List of SupplierOption
-
-**GET /api/nomenclature/suppliers/search**
-- Search for supplier by fiscal code or name
-- Query params: `fiscal_code`, `name`, `company_id`
-- Returns: SupplierSearchResult (found, supplier, source)
-
-**POST /api/nomenclature/suppliers/local**
-- Create a local supplier from OCR data
-- Body: LocalSupplierCreate (name, fiscal_code, address)
-- Returns: LocalSupplierResponse
-
-**GET /api/nomenclature/cash-registers**
-- Get all cash registers for a company
-- Query params: `company_id`
-- Returns: List of CashRegisterOption
-
-**POST /api/nomenclature/sync/suppliers**
-- Manually trigger supplier sync from Oracle
-- Returns: SyncResult (synced count, errors)
-
-**POST /api/nomenclature/sync/cash-registers**
-- Manually trigger cash register sync from Oracle
-- Returns: SyncResult (synced count, errors)
-
-**POST /api/nomenclature/sync/all**
-- Sync all nomenclatures (suppliers + cash registers)
-- Returns: Combined sync results
-
-#### 4. Updated Services
-
-**nomenclature_service.py** was updated to use synced data:
-- `get_partners()` - Now accepts optional `session` parameter, returns synced data if available, falls back to mock
-- `get_cash_registers()` - Now accepts optional `session` parameter, returns synced data if available, falls back to mock
-
-**receipts.py router** was updated to pass session to nomenclature service.
-
-## Company Schema Mapping
-
-The sync service needs to know which Oracle schema to query for each company. This is configured in `sync_service.py`:
-
-```python
-COMPANY_SCHEMAS = {
- 1: "CONTAFIN",
- 2: "CONTAFIN2",
-}
-```
-
-**TODO**: Move this to a config table or environment variable for production.
-
-## Oracle Integration
-
-The sync service connects to Oracle using the shared `oracle_pool` from `/shared/database/oracle_pool.py`.
-
-**Prerequisites**:
-- SSH tunnel must be running (development/Linux)
-- Oracle connection pool must be initialized
-- Environment variables must be set (ORACLE_USER, ORACLE_PASSWORD, ORACLE_HOST, ORACLE_PORT, ORACLE_SID)
-
-**Oracle Tables Used**:
-- `{schema}.NOM_PARTENERI` - Suppliers (WHERE ACTIV = 1)
-- `{schema}.NOM_CASE` - Cash registers (WHERE ACTIV = 1)
-
-**Note**: Table and column names may need adjustment based on actual Oracle schema.
-
-## Usage Flow
-
-### Initial Setup (One-time)
-
-1. Ensure Oracle connection is available:
- ```bash
- # Start SSH tunnel (if on Linux/dev)
- ./ssh_tunnel.sh start
- ```
-
-2. Run initial sync:
- ```bash
- # Via API (authenticated request)
- POST /api/nomenclature/sync/all
- ```
-
- Or programmatically:
- ```python
- from app.services.sync_service import SyncService
-
- # Sync for company 1
- synced, errors = await SyncService.sync_suppliers(session, company_id=1)
- synced, errors = await SyncService.sync_cash_registers(session, company_id=1)
- ```
-
-### Periodic Sync
-
-Set up a cron job or scheduled task to sync nomenclatures periodically (e.g., daily):
-
-```python
-# Example: Add to app lifespan or background task
-async def sync_all_companies():
- """Sync nomenclatures for all companies."""
- async with get_db_session() as session:
- for company_id in [1, 2]: # All company IDs
- await SyncService.sync_suppliers(session, company_id)
- await SyncService.sync_cash_registers(session, company_id)
-```
-
-### Using Synced Data
-
-The existing endpoints (`/api/receipts/nomenclature/partners`, `/api/receipts/nomenclature/cash-registers`) now automatically use synced data when available.
-
-**Frontend** - No changes needed! Existing code continues to work:
-```javascript
-// Get suppliers (now from synced data)
-const response = await api.get('/api/receipts/nomenclature/partners?search=OMV');
-```
-
-### Creating Local Suppliers from OCR
-
-When OCR extracts a supplier not in Oracle:
-
-```javascript
-// Create local supplier
-const response = await api.post('/api/nomenclature/suppliers/local', {
- name: "New Supplier SRL",
- fiscal_code: "RO12345678",
- address: "Str. Example 123"
-});
-```
-
-The local supplier will be:
-- Available immediately in dropdowns
-- Flagged for future Oracle sync (`pending_oracle_sync = True`)
-- Created by current user (`created_by = username`)
-
-## Migration
-
-Migration: `20251213_002805_add_nomenclature_tables.py`
-
-Applied with:
-```bash
-alembic upgrade head
-```
-
-To rollback:
-```bash
-alembic downgrade -1
-```
-
-## Testing
-
-### Manual Testing
-
-1. Test sync endpoint:
- ```bash
- curl -X POST http://localhost:8003/api/nomenclature/sync/suppliers \
- -H "Authorization: Bearer YOUR_TOKEN"
- ```
-
-2. Test search:
- ```bash
- curl "http://localhost:8003/api/nomenclature/suppliers/search?name=OMV" \
- -H "Authorization: Bearer YOUR_TOKEN"
- ```
-
-3. Test get all suppliers:
- ```bash
- curl "http://localhost:8003/api/nomenclature/suppliers" \
- -H "Authorization: Bearer YOUR_TOKEN"
- ```
-
-### Unit Tests
-
-TODO: Add unit tests in `tests/test_sync_service.py`:
-- Test supplier sync
-- Test cash register sync
-- Test search functionality
-- Test local supplier creation
-
-## Troubleshooting
-
-### Sync fails with "No schema mapping"
-- Update `COMPANY_SCHEMAS` in `sync_service.py` with correct company-to-schema mappings
-
-### Sync fails with Oracle connection error
-- Verify SSH tunnel is running: `./ssh_tunnel.sh status`
-- Check Oracle credentials in `.env`
-- Test Oracle connection: `curl http://localhost:8003/health`
-
-### Tables not found in Oracle
-- Verify table names in Oracle (may differ from NOM_PARTENERI, NOM_CASE)
-- Update SQL queries in `sync_service.py` to match actual schema
-
-### Duplicate suppliers after sync
-- The sync uses upsert logic (update if exists, insert if new)
-- Check `oracle_id` + `company_id` uniqueness in synced_suppliers table
-
-## Future Enhancements
-
-1. **Scheduled Background Sync** - Add cron job or Celery task for automatic daily sync
-2. **Sync Status Dashboard** - UI to show last sync time, sync statistics
-3. **Conflict Resolution** - Handle cases where local supplier matches synced supplier
-4. **Bidirectional Sync** - Push local suppliers to Oracle when approved
-5. **Incremental Sync** - Only sync changed records (requires last_modified timestamp in Oracle)
-6. **Multi-Company Support** - Auto-detect user's companies and sync all
-7. **Sync Notifications** - Notify users when sync completes or fails
-8. **Audit Log** - Track all sync operations for compliance
-
-## Related Files
-
-- Models: `/app/db/models/nomenclature.py`
-- Service: `/app/services/sync_service.py`
-- Router: `/app/routers/nomenclature.py`
-- Migration: `/migrations/versions/20251213_002805_add_nomenclature_tables.py`
-- Updated: `/app/services/nomenclature_service.py`
-- Updated: `/app/routers/receipts.py`
-- Updated: `/app/main.py`
diff --git a/data-entry-app/backend/app/db/crud/receipt.py b/data-entry-app/backend/app/db/crud/receipt.py
index 3243937..0995153 100644
--- a/data-entry-app/backend/app/db/crud/receipt.py
+++ b/data-entry-app/backend/app/db/crud/receipt.py
@@ -59,7 +59,9 @@ class ReceiptCRUD:
session.add(receipt)
await session.commit()
await session.refresh(receipt)
- return receipt
+
+ # Reload with relationships to avoid lazy loading issues with async
+ return await ReceiptCRUD.get_by_id(session, receipt.id, include_relations=True)
@staticmethod
async def get_by_id(
@@ -175,7 +177,9 @@ class ReceiptCRUD:
session.add(receipt)
await session.commit()
await session.refresh(receipt)
- return receipt
+
+ # Reload with relationships to avoid lazy loading issues with async
+ return await ReceiptCRUD.get_by_id(session, receipt.id, include_relations=True)
@staticmethod
async def update_status(
@@ -206,7 +210,9 @@ class ReceiptCRUD:
session.add(receipt)
await session.commit()
await session.refresh(receipt)
- return receipt
+
+ # Reload with relationships to avoid lazy loading issues with async
+ return await ReceiptCRUD.get_by_id(session, receipt.id, include_relations=True)
@staticmethod
async def delete(session: AsyncSession, receipt: Receipt) -> bool:
diff --git a/data-entry-app/backend/app/main.py b/data-entry-app/backend/app/main.py
index 0c500c1..978e83e 100644
--- a/data-entry-app/backend/app/main.py
+++ b/data-entry-app/backend/app/main.py
@@ -132,6 +132,16 @@ from auth.routes import create_auth_router
auth_router = create_auth_router(prefix="") # No prefix - we set it in include_router
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
+# Shared routes (companies, calendar)
+from routes.companies import create_companies_router
+from routes.calendar import create_calendar_router
+
+companies_router = create_companies_router(oracle_pool) # No cache for data-entry
+calendar_router = create_calendar_router(oracle_pool)
+
+app.include_router(companies_router, prefix="/api/companies", tags=["companies"])
+app.include_router(calendar_router, prefix="/api/calendar", tags=["calendar"])
+
# Root endpoint
@app.get("/")
diff --git a/data-entry-app/backend/app/routers/nomenclature.py b/data-entry-app/backend/app/routers/nomenclature.py
index 70a4cee..1971453 100644
--- a/data-entry-app/backend/app/routers/nomenclature.py
+++ b/data-entry-app/backend/app/routers/nomenclature.py
@@ -1,7 +1,7 @@
"""Nomenclature API endpoints."""
-from typing import Optional, List
-from fastapi import APIRouter, Depends, HTTPException
+from typing import Optional, List, Annotated
+from fastapi import APIRouter, Depends, HTTPException, Header
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
@@ -20,6 +20,38 @@ from auth.models import CurrentUser
router = APIRouter()
+# ============ Selected Company Dependency ============
+
+async def get_selected_company(
+ current_user: CurrentUser = Depends(get_current_user),
+ x_selected_company: Annotated[Optional[str], Header()] = None
+) -> int:
+ """
+ Get selected company from X-Selected-Company header.
+ Validates user access. Falls back to first company if no header.
+ """
+ if x_selected_company:
+ try:
+ company_id = int(x_selected_company)
+ except ValueError:
+ raise HTTPException(400, f"Invalid company ID: {x_selected_company}")
+
+ if str(company_id) in current_user.companies:
+ return company_id
+ raise HTTPException(403, f"Nu aveți acces la firma {company_id}")
+
+ if current_user.companies:
+ try:
+ return int(current_user.companies[0])
+ except (ValueError, IndexError):
+ pass
+
+ raise HTTPException(400, "Nu aveți nicio firmă asignată")
+
+
+SelectedCompany = Annotated[int, Depends(get_selected_company)]
+
+
# Request/Response Models
class SupplierSearchResult(BaseModel):
found: bool
@@ -70,14 +102,13 @@ async def search_supplier(
name: Optional[str] = None,
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
- current_user: CurrentUser = Depends(get_current_user),
+ selected_company: SelectedCompany = None,
):
"""Search for supplier by fiscal code or name."""
if not fiscal_code and not name:
raise HTTPException(status_code=400, detail="Provide fiscal_code or name")
- # Use provided company_id or first from user
- cid = company_id or (current_user.companies[0] if current_user.companies else 1)
+ cid = company_id or selected_company
found, supplier, source = await SyncService.search_supplier(
session, cid, fiscal_code, name
@@ -91,10 +122,10 @@ async def get_suppliers(
search: Optional[str] = None,
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
- current_user: CurrentUser = Depends(get_current_user),
+ selected_company: SelectedCompany = None,
):
"""Get all suppliers (synced + local) for dropdown/autocomplete."""
- cid = company_id or (current_user.companies[0] if current_user.companies else 1)
+ cid = company_id or selected_company
suppliers = await SyncService.get_all_suppliers(session, cid, search)
@@ -115,10 +146,11 @@ async def create_local_supplier(
data: LocalSupplierCreate,
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
+ selected_company: SelectedCompany = None,
current_user: CurrentUser = Depends(get_current_user),
):
"""Create a local supplier from OCR data."""
- cid = company_id or (current_user.companies[0] if current_user.companies else 1)
+ cid = company_id or selected_company
supplier = await SyncService.create_local_supplier(
session, cid, data.name, data.fiscal_code, data.address, current_user.username
@@ -136,10 +168,10 @@ async def create_local_supplier(
async def get_cash_registers(
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
- current_user: CurrentUser = Depends(get_current_user),
+ selected_company: SelectedCompany = None,
):
"""Get all cash registers for a company."""
- cid = company_id or (current_user.companies[0] if current_user.companies else 1)
+ cid = company_id or selected_company
registers = await SyncService.get_all_cash_registers(session, cid)
@@ -159,10 +191,10 @@ async def get_cash_registers(
async def sync_suppliers(
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
- current_user: CurrentUser = Depends(get_current_user),
+ selected_company: SelectedCompany = None,
):
"""Manually trigger supplier sync from Oracle."""
- cid = company_id or (current_user.companies[0] if current_user.companies else 1)
+ cid = company_id or selected_company
synced, errors = await SyncService.sync_suppliers(session, cid)
@@ -177,10 +209,10 @@ async def sync_suppliers(
async def sync_cash_registers(
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
- current_user: CurrentUser = Depends(get_current_user),
+ selected_company: SelectedCompany = None,
):
"""Manually trigger cash register sync from Oracle."""
- cid = company_id or (current_user.companies[0] if current_user.companies else 1)
+ cid = company_id or selected_company
synced, errors = await SyncService.sync_cash_registers(session, cid)
@@ -195,10 +227,10 @@ async def sync_cash_registers(
async def sync_all_nomenclatures(
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
- current_user: CurrentUser = Depends(get_current_user),
+ selected_company: SelectedCompany = None,
):
"""Sync all nomenclatures (suppliers + cash registers) from Oracle."""
- cid = company_id or (current_user.companies[0] if current_user.companies else 1)
+ cid = company_id or selected_company
# Sync suppliers
suppliers_synced, suppliers_errors = await SyncService.sync_suppliers(session, cid)
diff --git a/data-entry-app/backend/app/routers/receipts.py b/data-entry-app/backend/app/routers/receipts.py
index 25c4f12..cf128a3 100644
--- a/data-entry-app/backend/app/routers/receipts.py
+++ b/data-entry-app/backend/app/routers/receipts.py
@@ -1,9 +1,9 @@
"""API endpoints for receipts."""
-from typing import List, Optional
+from typing import List, Optional, Annotated
from pathlib import Path
-from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
+from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Header
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
@@ -39,20 +39,69 @@ from auth.models import CurrentUser
router = APIRouter()
-# ============ Helper for current user's company ============
+# ============ Helper for selected company from header ============
+async def get_selected_company(
+ current_user: CurrentUser = Depends(get_current_user),
+ x_selected_company: Annotated[Optional[str], Header()] = None
+) -> int:
+ """
+ Get selected company from X-Selected-Company header.
+
+ Validates that the user has access to the specified company.
+ Falls back to user's first company if no header is provided.
+
+ Raises:
+ HTTPException 403: If user doesn't have access to specified company
+ HTTPException 400: If user has no companies assigned
+ """
+ if x_selected_company:
+ try:
+ company_id = int(x_selected_company)
+ except ValueError:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Invalid company ID format: {x_selected_company}"
+ )
+
+ # Validate user has access to this company
+ # Auth stores companies as strings
+ if str(company_id) in current_user.companies:
+ return company_id
+
+ raise HTTPException(
+ status_code=403,
+ detail=f"Nu aveți acces la firma {company_id}"
+ )
+
+ # No header - use first company from user's list
+ if current_user.companies:
+ try:
+ return int(current_user.companies[0])
+ except (ValueError, IndexError):
+ pass
+
+ raise HTTPException(
+ status_code=400,
+ detail="Nu aveți nicio firmă asignată"
+ )
+
+
+# Dependency for injection
+SelectedCompany = Annotated[int, Depends(get_selected_company)]
+
+
+# Legacy function for backwards compatibility (deprecated)
def get_current_user_company(current_user: CurrentUser) -> int:
"""
- Get current user's active company.
-
- Returns the first company from the user's companies list.
- In future, this can be enhanced to use a session-based active company.
+ DEPRECATED: Use get_selected_company() dependency instead.
+ This function returns the first company, ignoring X-Selected-Company header.
"""
if current_user.companies:
- # For data-entry-app, we assume company ID is numeric
- # If companies are stored as strings, convert to int
- # For now, return 1 as default (Phase 1)
- return 1
+ try:
+ return int(current_user.companies[0])
+ except (ValueError, IndexError):
+ return 1
return 1
@@ -80,16 +129,14 @@ async def list_receipts(
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
session: AsyncSession = Depends(get_session),
- current_user: CurrentUser = Depends(get_current_user),
+ selected_company: SelectedCompany = None,
):
"""Get paginated list of receipts with filters."""
from datetime import date as date_type
- current_company = get_current_user_company(current_user)
-
filters = ReceiptFilter(
status=status,
- company_id=company_id or current_company,
+ company_id=company_id or selected_company,
created_by=created_by,
date_from=date_type.fromisoformat(date_from) if date_from else None,
date_to=date_type.fromisoformat(date_to) if date_to else None,
@@ -105,12 +152,11 @@ async def list_receipts(
async def list_pending_receipts(
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
- current_user: CurrentUser = Depends(get_current_user),
+ selected_company: SelectedCompany = None,
):
"""Get all receipts pending review (for accountant view)."""
- current_company = get_current_user_company(current_user)
receipts = await ReceiptCRUD.get_pending_review(
- session, company_id or current_company
+ session, company_id or selected_company
)
return [ReceiptResponse.model_validate(r) for r in receipts]
@@ -120,13 +166,13 @@ async def get_receipt_stats(
company_id: Optional[int] = None,
my_receipts: bool = False,
session: AsyncSession = Depends(get_session),
+ selected_company: SelectedCompany = None,
current_user: CurrentUser = Depends(get_current_user),
):
"""Get receipt statistics."""
- current_company = get_current_user_company(current_user)
return await ReceiptCRUD.get_stats(
session,
- company_id or current_company,
+ company_id or selected_company,
created_by=current_user.username if my_receipts else None,
)
@@ -415,12 +461,11 @@ async def get_partners(
search: Optional[str] = None,
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
- current_user: CurrentUser = Depends(get_current_user),
+ selected_company: SelectedCompany = None,
):
"""Get partners (suppliers/customers) for dropdown."""
- current_company = get_current_user_company(current_user)
return await NomenclatureService.get_partners(
- company_id or current_company, search, session
+ company_id or selected_company, search, session
)
@@ -428,12 +473,11 @@ async def get_partners(
async def get_accounts(
prefix: Optional[str] = None,
company_id: Optional[int] = None,
- current_user: CurrentUser = Depends(get_current_user),
+ selected_company: SelectedCompany = None,
):
"""Get chart of accounts for dropdown."""
- current_company = get_current_user_company(current_user)
return await NomenclatureService.get_accounts(
- company_id or current_company, prefix
+ company_id or selected_company, prefix
)
@@ -441,11 +485,10 @@ async def get_accounts(
async def get_cash_registers(
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
- current_user: CurrentUser = Depends(get_current_user),
+ selected_company: SelectedCompany = None,
):
"""Get cash registers and bank accounts for dropdown."""
- current_company = get_current_user_company(current_user)
- return await NomenclatureService.get_cash_registers(company_id or current_company, session)
+ return await NomenclatureService.get_cash_registers(company_id or selected_company, session)
@router.get("/nomenclature/expense-types", response_model=List[ExpenseTypeOption])
diff --git a/data-entry-app/backend/app/services/sync_service.py b/data-entry-app/backend/app/services/sync_service.py
index 7b99fcc..043f041 100644
--- a/data-entry-app/backend/app/services/sync_service.py
+++ b/data-entry-app/backend/app/services/sync_service.py
@@ -18,29 +18,54 @@ from app.db.models.nomenclature import SyncedSupplier, LocalSupplier, SyncedCash
logger = logging.getLogger(__name__)
-# Company ID to Oracle Schema mapping
-# TODO: This should come from a config table or environment variable
-COMPANY_SCHEMAS = {
- 1: "CONTAFIN", # Example mapping - update with real schema names
- 2: "CONTAFIN2",
-}
+# Cache for schema lookups (populated dynamically from Oracle)
+_schema_cache: dict[int, str] = {}
class SyncService:
"""Service for syncing nomenclatures from Oracle."""
@staticmethod
- def get_schema_for_company(company_id: int) -> Optional[str]:
- """Get Oracle schema for company ID."""
- return COMPANY_SCHEMAS.get(company_id)
+ async def get_schema_for_company(company_id: int) -> Optional[str]:
+ """
+ Get Oracle schema for company ID from V_NOM_FIRME view.
+ Results are cached in memory for performance.
+ """
+ # Check cache first
+ if company_id in _schema_cache:
+ return _schema_cache[company_id]
+
+ try:
+ async with oracle_pool.get_connection() as connection:
+ with connection.cursor() as cursor:
+ cursor.execute("""
+ SELECT SCHEMA
+ FROM CONTAFIN_ORACLE.V_NOM_FIRME
+ WHERE ID_FIRMA = :company_id
+ """, {'company_id': company_id})
+ result = cursor.fetchone()
+
+ if result:
+ schema = result[0]
+ _schema_cache[company_id] = schema
+ logger.info(f"Resolved schema for company {company_id}: {schema}")
+ return schema
+ else:
+ logger.warning(f"No schema found for company {company_id}")
+ return None
+
+ except Exception as e:
+ logger.error(f"Error fetching schema for company {company_id}: {e}")
+ return None
@staticmethod
async def sync_suppliers(session: AsyncSession, company_id: int) -> Tuple[int, int]:
"""
- Sync suppliers from Oracle NOM_PARTENERI to SQLite.
+ Sync suppliers (furnizori, id_tip_part=17) from Oracle to SQLite.
+ Uses CORESP_TIP_PART joined with VNOM_PARTENERI view.
Returns (synced_count, error_count).
"""
- schema = SyncService.get_schema_for_company(company_id)
+ schema = await SyncService.get_schema_for_company(company_id)
if not schema:
logger.warning(f"No schema mapping for company {company_id}")
return 0, 0
@@ -51,11 +76,17 @@ class SyncService:
try:
async with oracle_pool.get_connection() as connection:
with connection.cursor() as cursor:
- # Fetch active partners from Oracle
+ # Fetch active suppliers from Oracle
+ # id_tip_part = 17 means "furnizori" (suppliers)
+ # Using CORESP_TIP_PART to filter by partner type
cursor.execute(f"""
- SELECT ID_PART, DEN_PART, COD_FISCAL, ADRESA
- FROM {schema}.NOM_PARTENERI
- WHERE ACTIV = 1
+ SELECT B.ID_PART, B.DENUMIRE, B.COD_FISCAL, B.ADRESA
+ FROM {schema}.CORESP_TIP_PART A
+ INNER JOIN {schema}.VNOM_PARTENERI B ON A.ID_PART = B.ID_PART
+ WHERE A.ID_TIP_PART = 17
+ AND (B.INACTIV = 0 OR B.INACTIV IS NULL)
+ AND B.ID_PART IS NOT NULL
+ ORDER BY B.DENUMIRE
""")
rows = cursor.fetchall()
@@ -110,10 +141,16 @@ class SyncService:
@staticmethod
async def sync_cash_registers(session: AsyncSession, company_id: int) -> Tuple[int, int]:
"""
- Sync cash registers from Oracle to SQLite.
+ Sync cash registers and bank accounts from Oracle to SQLite.
Returns (synced_count, error_count).
+
+ Uses CORESP_TIP_PART with:
+ - id_tip_part = 22: CASA LEI
+ - id_tip_part = 23: CASA VALUTA
+ - id_tip_part = 24: BANCA LEI
+ - id_tip_part = 25: BANCA VALUTA
"""
- schema = SyncService.get_schema_for_company(company_id)
+ schema = await SyncService.get_schema_for_company(company_id)
if not schema:
logger.warning(f"No schema mapping for company {company_id}")
return 0, 0
@@ -121,25 +158,40 @@ class SyncService:
synced = 0
errors = 0
+ # Partner types mapping
+ # 22=CASA LEI, 23=CASA VALUTA -> cash
+ # 24=BANCA LEI, 25=BANCA VALUTA -> bank
+ partner_types = [22, 23, 24, 25]
+
try:
async with oracle_pool.get_connection() as connection:
with connection.cursor() as cursor:
- # Fetch cash registers (both cash and bank)
- # Assuming similar structure to NOM_PARTENERI
- # TODO: Verify actual table name and structure in Oracle
+ # Fetch cash/bank partners from CORESP_TIP_PART
cursor.execute(f"""
- SELECT ID_CASA, DEN_CASA, CONT
- FROM {schema}.NOM_CASE
- WHERE ACTIV = 1
+ SELECT B.ID_PART, B.DENUMIRE, A.ID_TIP_PART
+ FROM {schema}.CORESP_TIP_PART A
+ INNER JOIN {schema}.VNOM_PARTENERI B ON A.ID_PART = B.ID_PART
+ WHERE A.ID_TIP_PART IN (22, 23, 24, 25)
+ AND (B.INACTIV = 0 OR B.INACTIV IS NULL)
+ AND B.ID_PART IS NOT NULL
+ ORDER BY A.ID_TIP_PART, B.DENUMIRE
""")
rows = cursor.fetchall()
+ # Type mapping: 22=CASA LEI, 23=CASA VALUTA -> cash; 24=BANCA LEI, 25=BANCA VALUTA -> bank
+ type_mapping = {
+ 22: ("cash", "CASA_LEI"),
+ 23: ("cash", "CASA_VALUTA"),
+ 24: ("bank", "BANCA_LEI"),
+ 25: ("bank", "BANCA_VALUTA"),
+ }
+
for row in rows:
try:
- oracle_id, name, account_code = row
+ oracle_id, name, tip_part_id = row
- # Determine type based on account code
- register_type = "cash" if account_code.startswith("531") else "bank"
+ # Determine type based on partner type
+ register_type, account_code = type_mapping.get(tip_part_id, ("cash", "UNKNOWN"))
# Check if already exists
stmt = select(SyncedCashRegister).where(
@@ -152,7 +204,7 @@ class SyncService:
if existing:
# Update existing record
existing.name = name or ""
- existing.account_code = account_code or ""
+ existing.account_code = account_code
existing.register_type = register_type
existing.synced_at = datetime.utcnow()
logger.debug(f"Updated cash register {oracle_id}: {name}")
@@ -162,7 +214,7 @@ class SyncService:
oracle_id=oracle_id,
company_id=company_id,
name=name or "",
- account_code=account_code or "",
+ account_code=account_code,
register_type=register_type,
)
session.add(cash_register)
diff --git a/data-entry-app/backend/migrations/env.py b/data-entry-app/backend/migrations/env.py
index 35781d3..8f8d2d0 100644
--- a/data-entry-app/backend/migrations/env.py
+++ b/data-entry-app/backend/migrations/env.py
@@ -1,6 +1,8 @@
"""Alembic environment configuration."""
+import os
from logging.config import fileConfig
+from dotenv import load_dotenv
from sqlalchemy import engine_from_config
from sqlalchemy import pool
@@ -8,14 +10,22 @@ from sqlalchemy import pool
from alembic import context
from sqlmodel import SQLModel
+# Load environment variables from .env file
+load_dotenv()
+
# Import all models to ensure they're registered with SQLModel
from app.db.models.receipt import Receipt, ReceiptAttachment
from app.db.models.accounting_entry import AccountingEntry
+from app.db.models.nomenclature import SyncedSupplier, LocalSupplier, SyncedCashRegister
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
+# Override sqlalchemy.url from environment variable if set
+db_path = os.getenv("SQLITE_DATABASE_PATH", "data/receipts.db")
+config.set_main_option("sqlalchemy.url", f"sqlite:///{db_path}")
+
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
diff --git a/data-entry-app/docs/IMPLEMENTATION_PLAN_AUTH_EXEC.md b/data-entry-app/docs/IMPLEMENTATION_PLAN_AUTH_EXEC.md
deleted file mode 100644
index 2dc53a1..0000000
--- a/data-entry-app/docs/IMPLEMENTATION_PLAN_AUTH_EXEC.md
+++ /dev/null
@@ -1,346 +0,0 @@
-# Plan: Implementare Auth SSO + Nomenclatoare Sync
-
-> **Plan Handover Document** - Salvat pentru continuare în altă sesiune
-> **Data**: 2025-12-13 | **Branch**: `feature/data-entry-receipts`
-
-## Obiectiv
-Integrare autentificare SSO și sincronizare nomenclatoare Oracle în data-entry-app conform `IMPLEMENTATION_PLAN_AUTH_UNITAR.md`.
-
----
-
-## Instrucțiuni Implementare
-
-### Metodologie
-1. **Execută fazele în paralel** unde e posibil (Faza 1+2 pot rula simultan, Faza 3+4 pot rula simultan)
-2. **Folosește agenți Task** pentru viteza - lansează agenți în paralel pentru task-uri independente
-3. **Testează după fiecare fază** - nu trece la următoarea fără validare
-4. **Urmărește progresul** în acest fișier - marchează task-urile completate cu ✅
-
-### Comenzi de Start
-```bash
-# Asigură-te că SSH tunnel rulează (pentru Oracle)
-./ssh_tunnel.sh start
-
-# Backend reports (pentru auth API - port 8001)
-cd reports-app/backend && uvicorn app.main:app --reload --port 8001
-
-# Backend data-entry (port 8003)
-cd data-entry-app/backend && uvicorn app.main:app --reload --port 8003
-
-# Frontend data-entry (port 3010)
-cd data-entry-app/frontend && npm run dev
-```
-
-### Progres Implementare
-- [x] **FAZA 1**: Auth Backend - ✅ 6/6 task-uri COMPLETE
-- [x] **FAZA 2**: Auth Frontend - ✅ 6/6 task-uri COMPLETE
-- [x] **FAZA 3**: Nomenclatoare Sync - ✅ 6/6 task-uri COMPLETE
-- [x] **FAZA 4**: OCR + Supplier Search - ✅ 2/2 task-uri COMPLETE
-
-> **Status**: ✅ **IMPLEMENTARE COMPLETĂ** - 2025-12-13
-
----
-
-## Stare Curentă (IMPLEMENTAT)
-
-### Backend Data-Entry ✅
-- ✅ Models: Receipt, ReceiptAttachment, AccountingEntry - complete
-- ✅ CRUD operations - complete
-- ✅ API Routers: receipts.py, ocr.py, **nomenclature.py**
-- ✅ Services: receipt_service, ocr_service, **sync_service**
-- ✅ Workflow: DRAFT → PENDING → APPROVED/REJECTED
-- ✅ **Auth**: Integrare shared/auth (middleware + CurrentUser)
-- ✅ **Nomenclatoare**: SQLite sync (SyncedSupplier, LocalSupplier, SyncedCashRegister)
-- ✅ `sys.path.insert` pentru shared/ în main.py
-
-### Frontend Data-Entry ✅
-- ✅ Views: List, Create, Detail, Approval, **LoginView**
-- ✅ Components: OCR components + **Create Supplier Dialog**
-- ✅ Store: receiptsStore.js + **auth.js**
-- ✅ Router: routes + **auth guards + /login**
-- ✅ **Auth Store**: `src/stores/auth.js` - creat
-- ✅ **Login View**: `src/views/LoginView.vue` - creat
-- ✅ **Router Guards**: beforeEach cu requiresAuth
-- ✅ **API Service**: `src/services/api.js` - creat cu interceptors
-
-### Shared Auth (disponibil pentru integrare)
-- ✅ `shared/auth/routes.py` - `create_auth_router()` (linia 39-430)
-- ✅ `shared/auth/middleware.py` - `AuthenticationMiddleware`
-- ✅ `shared/auth/dependencies.py` - `get_current_user`
-- ✅ `shared/auth/models.py` - `CurrentUser`, `TokenResponse`
-
-### Referință Reports-App (pentru copiere)
-- `reports-app/frontend/src/stores/auth.js` - 119 linii
-- `reports-app/frontend/src/services/api.js` - 141 linii
-- `reports-app/frontend/src/views/LoginView.vue` - 367 linii
-- `reports-app/frontend/src/router/index.js` - auth guard la liniile 96-114
-
----
-
-## Faze Implementare
-
-### FAZA 1: Auth Backend (6 task-uri)
-
-#### Task 1.1: Adaugă AuthenticationMiddleware în main.py
-**Fișier**: `data-entry-app/backend/app/main.py`
-**Acțiune**: După CORS middleware (linia 75), adaugă:
-```python
-from auth.middleware import AuthenticationMiddleware
-app.add_middleware(
- AuthenticationMiddleware,
- excluded_paths=["/docs", "/redoc", "/openapi.json", "/health", "/", "/api/auth/login", "/api/auth/refresh"]
-)
-```
-
-#### Task 1.2: Adaugă Auth Router în main.py
-**Fișier**: `data-entry-app/backend/app/main.py`
-**Acțiune**: După include_router pentru ocr (linia 98), adaugă:
-```python
-from auth.routes import create_auth_router
-auth_router = create_auth_router()
-app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
-```
-
-#### Task 1.3: Înlocuiește get_current_user în receipts.py
-**Fișier**: `data-entry-app/backend/app/routers/receipts.py`
-**Acțiune**: Șterge liniile 38-59 și înlocuiește cu:
-```python
-from auth.dependencies import get_current_user
-from auth.models import CurrentUser
-```
-Apoi actualizează type hints: `current_user: str` → `current_user: CurrentUser`
-Și accesează `current_user.username` în loc de `current_user`
-
-#### Task 1.4: Înlocuiește get_current_user în ocr.py
-**Fișier**: `data-entry-app/backend/app/routers/ocr.py`
-**Acțiune**: Similar cu receipts.py, adaugă importurile auth și folosește `CurrentUser`
-
-#### Task 1.5: Actualizează type hints în toate endpoint-urile
-Actualizează toate funcțiile care folosesc `current_user: str` să folosească `current_user: CurrentUser`
-
-#### Task 1.6: Testare backend auth
-```bash
-cd data-entry-app/backend
-uvicorn app.main:app --reload --port 8003
-# Test: curl http://localhost:8003/api/receipts/ → 401 Unauthorized
-```
-
----
-
-### FAZA 2: Auth Frontend (6 task-uri)
-
-#### Task 2.1: Crează API service
-**Fișier NOU**: `data-entry-app/frontend/src/services/api.js`
-**Acțiune**: Copiază din `reports-app/frontend/src/services/api.js` cu modificări:
-- Schimbă BASE_URL pentru a funcționa cu proxy-ul
-- Modifică refresh token URL
-
-#### Task 2.2: Crează Auth Store
-**Fișier NOU**: `data-entry-app/frontend/src/stores/auth.js`
-**Acțiune**: Copiază din `reports-app/frontend/src/stores/auth.js`
-- Modifică import apiService din `../services/api`
-
-#### Task 2.3: Crează LoginView
-**Fișier NOU**: `data-entry-app/frontend/src/views/LoginView.vue`
-**Acțiune**: Copiază din `reports-app/frontend/src/views/LoginView.vue`
-- Schimbă titlul: "ROA Reports" → "Data Entry"
-- Schimbă subtitle: "Rapoarte ERP" → "Introducere Bonuri Fiscale"
-- Schimbă redirect după login: "/dashboard" → "/"
-
-#### Task 2.4: Actualizează Router cu auth guards
-**Fișier**: `data-entry-app/frontend/src/router/index.js`
-**Acțiune**: Adaugă auth guard similar cu reports-app (liniile 96-114)
-```javascript
-import { useAuthStore } from '@/stores/auth'
-// Adaugă rută login
-// Adaugă meta: { requiresAuth: true } la rutele protejate
-// Adaugă beforeEach guard
-```
-
-#### Task 2.5: Actualizează vite.config.js pentru auth proxy
-**Fișier**: `data-entry-app/frontend/vite.config.js`
-**Acțiune**: Adaugă proxy pentru auth:
-```javascript
-'/api/auth': {
- target: 'http://localhost:8001',
- changeOrigin: true,
-}
-```
-
-#### Task 2.6: Testare frontend auth
-```bash
-cd data-entry-app/frontend
-npm run dev
-# Test: Accesează http://localhost:3010 → Redirect la /login
-# Login cu credențiale Oracle → Redirect la /
-```
-
----
-
-### FAZA 3: Nomenclatoare Oracle→SQLite (6 task-uri)
-
-#### Task 3.1: Crează modele SQLModel
-**Fișier NOU**: `data-entry-app/backend/app/db/models/nomenclature.py`
-- `SyncedSupplier` - furnizori sincronizați din Oracle
-- `LocalSupplier` - furnizori creați local (din OCR)
-- `SyncedCashRegister` - case/bănci sincronizate
-
-#### Task 3.2: Crează Alembic migration
-```bash
-cd data-entry-app/backend
-alembic revision --autogenerate -m "add nomenclature tables"
-alembic upgrade head
-```
-
-#### Task 3.3: Crează Sync Service
-**Fișier NOU**: `data-entry-app/backend/app/services/sync_service.py`
-- `sync_suppliers(company_id, schema)` - sync furnizori Oracle→SQLite
-- `sync_cash_registers(company_id, schema)` - sync case/bănci
-- `get_schema_for_company(company_id)` - lookup schema
-
-#### Task 3.4: Crează Nomenclature Router
-**Fișier NOU**: `data-entry-app/backend/app/routers/nomenclature.py`
-- `GET /suppliers/search` - căutare furnizor (SQLite + Oracle live)
-- `POST /suppliers/local` - creare furnizor local
-- `POST /sync/suppliers` - trigger manual sync
-
-#### Task 3.5: Înregistrează router în main.py
-```python
-from app.routers import nomenclature
-app.include_router(nomenclature.router, prefix="/api/nomenclature", tags=["nomenclature"])
-```
-
-#### Task 3.6: Actualizare nomenclature_service.py existent
-Înlocuiește mock data cu query-uri din tabelele SQLite sincronizate
-
----
-
-### FAZA 4: Integrare OCR + Supplier Search (2 task-uri)
-
-#### Task 4.1: Actualizare ReceiptCreateView.vue
-**Fișier**: `data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue`
-**Acțiune**: După OCR result, caută automat furnizor după CUI:
-```javascript
-async function handleOCRResult(ocrData) {
- if (ocrData.cui) {
- const result = await receiptsStore.searchSupplier(ocrData.cui);
- if (result.found) {
- form.partner_id = result.supplier.id;
- form.partner_name = result.supplier.name;
- } else {
- showCreateSupplierDialog(ocrData);
- }
- }
-}
-```
-
-#### Task 4.2: Adaugă supplier search în receiptsStore.js
-**Fișier**: `data-entry-app/frontend/src/stores/receiptsStore.js`
-**Acțiune**: Adaugă action `searchSupplier(fiscalCode)` și `createLocalSupplier(data)`
-
----
-
-## Sumar Fișiere
-
-### De Modificat
-| Fișier | Faza |
-|--------|------|
-| `data-entry-app/backend/app/main.py` | 1, 3 |
-| `data-entry-app/backend/app/routers/receipts.py` | 1 |
-| `data-entry-app/backend/app/routers/ocr.py` | 1 |
-| `data-entry-app/frontend/src/router/index.js` | 2 |
-| `data-entry-app/frontend/vite.config.js` | 2 |
-| `data-entry-app/frontend/src/views/receipts/ReceiptCreateView.vue` | 4 |
-| `data-entry-app/frontend/src/stores/receiptsStore.js` | 4 |
-
-### De Creat (NOU)
-| Fișier | Faza |
-|--------|------|
-| `data-entry-app/frontend/src/services/api.js` | 2 |
-| `data-entry-app/frontend/src/stores/auth.js` | 2 |
-| `data-entry-app/frontend/src/views/LoginView.vue` | 2 |
-| `data-entry-app/backend/app/db/models/nomenclature.py` | 3 |
-| `data-entry-app/backend/app/services/sync_service.py` | 3 |
-| `data-entry-app/backend/app/routers/nomenclature.py` | 3 |
-| `migrations/versions/xxx_nomenclature.py` | 3 |
-
----
-
-## Ordine Execuție
-
-**Faza 1 + 2 (Auth)** → **Faza 3 + 4 (Nomenclatoare)**
-
-Fazele 1-2 sunt blocante pentru funcționalitatea completă, dar Faza 3-4 poate fi amânată dacă e nevoie (nomenclatoarele rămân mock data temporar).
-
----
-
-## Strategie Execuție cu Agenți
-
-### Agenți Paraleli Recomandați
-
-**Round 1 - Auth (Backend + Frontend simultan):**
-```
-Agent A: Faza 1 - Task 1.1-1.5 (Backend auth)
-Agent B: Faza 2 - Task 2.1-2.3 (Frontend auth files)
-```
-După Round 1, testare manuală auth flow.
-
-**Round 2 - Finalizare Auth + Start Nomenclatoare:**
-```
-Agent A: Faza 2 - Task 2.4-2.5 (Router guards, vite config)
-Agent B: Faza 3 - Task 3.1-3.2 (Modele SQLModel + migration)
-```
-
-**Round 3 - Nomenclatoare + Integration:**
-```
-Agent A: Faza 3 - Task 3.3-3.6 (Sync service + router)
-Agent B: Faza 4 - Task 4.1-4.2 (Frontend OCR supplier)
-```
-
-### Validare După Fiecare Fază
-
-**După Faza 1:**
-```bash
-curl http://localhost:8003/api/receipts/
-# Expected: 401 Unauthorized
-```
-
-**După Faza 2:**
-```bash
-# Browser: http://localhost:3010
-# Expected: Redirect to /login
-# Login cu credențiale Oracle → Redirect la /
-```
-
-**După Faza 3:**
-```bash
-curl http://localhost:8003/api/nomenclature/suppliers/search?fiscal_code=RO12345678
-# Expected: Search result sau sugestie creare local
-```
-
-**După Faza 4:**
-```
-# Browser: Crează bon nou → Upload poză → OCR
-# Expected: Furnizor găsit automat sau dialog creare
-```
-
----
-
-## Context pentru Sesiune Următoare
-
-### Fișiere Cheie de Citit
-1. Acest plan: `/home/marius/.claude/plans/unified-orbiting-sonnet.md`
-2. CLAUDE.md principal: `/mnt/e/proiecte/roa2web/CLAUDE.md`
-3. CLAUDE.md data-entry: `/mnt/e/proiecte/roa2web/data-entry-app/CLAUDE.md`
-
-### Comenzi Quick Start
-```bash
-cd /mnt/e/proiecte/roa2web
-git status # Verifică branch feature/data-entry-receipts
-./ssh_tunnel.sh start # SSH tunnel pentru Oracle
-```
-
-### Dependențe Servicii
-- **reports-backend:8001** - NECESAR pentru auth API (login, refresh)
-- **data-entry-backend:8003** - Backend principal
-- **Oracle DB** - Via SSH tunnel, necesar pentru auth + nomenclatoare
diff --git a/data-entry-app/docs/IMPLEMENTATION_PLAN_AUTH_UNITAR.md b/data-entry-app/docs/IMPLEMENTATION_PLAN_AUTH_UNITAR.md
deleted file mode 100644
index 25ac284..0000000
--- a/data-entry-app/docs/IMPLEMENTATION_PLAN_AUTH_UNITAR.md
+++ /dev/null
@@ -1,484 +0,0 @@
-# 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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-**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/` |
diff --git a/data-entry-app/frontend/src/App.vue b/data-entry-app/frontend/src/App.vue
index 5854e59..de1f1b2 100644
--- a/data-entry-app/frontend/src/App.vue
+++ b/data-entry-app/frontend/src/App.vue
@@ -1,38 +1,33 @@