Files
roa2web-service-auto/backend/modules/reports/services/trial_balance_service.py
Claude Agent b137e80b71 feat: multi-Oracle server support with runtime switching
Complete implementation of multi-server Oracle database support:

Backend:
- Multi-pool Oracle with lazy loading per server
- Email-to-server cache for automatic server discovery
- JWT tokens include server_id claim
- /auth/check-identity and /auth/check-email endpoints
- /auth/my-servers endpoint for listing user's accessible servers
- Server switch with password re-authentication

Frontend:
- New ServerSelector component for header dropdown
- Multi-step login flow (identity → server → password)
- Server switching from header with password modal
- Mobile drawer menu with server selection
- Dark mode support for all new components
- URL bookmark support with ?server= query param

Scripts:
- Unified start.sh replacing start-prod.sh/start-test.sh
- Unified ssh-tunnel.sh with multi-server support
- Updated status.sh for new architecture

Tests:
- E2E tests for multi-server and single-server login flows
- Backend unit tests for all new endpoints
- Oracle multi-pool integration tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 22:39:06 +00:00

220 lines
8.7 KiB
Python

"""
Service pentru Trial Balance (Balanță de Verificare) - Query VBAL VIEW
Refactored to use caching system for optimal performance
"""
# import sys # Removed - no longer needed
import os
from typing import Dict, Any, Optional
from shared.database.oracle_pool import oracle_pool
from ..models.trial_balance import (
TrialBalanceItem,
TrialBalanceFilters,
TrialBalancePagination,
TrialBalanceResponse
)
from ..cache.decorators import cached
from decimal import Decimal
import math
import logging
logger = logging.getLogger(__name__)
class TrialBalanceService:
"""Service pentru gestionarea balanței de verificare cu cache"""
@staticmethod
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
"""
Obține schema pentru company_id (CACHED 24h)
This is cached permanently because company schemas rarely change.
"""
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
schema_query = """
SELECT schema
FROM CONTAFIN_ORACLE.v_nom_firme
WHERE id_firma = :company_id
"""
cursor.execute(schema_query, {'company_id': company_id})
schema_result = cursor.fetchone()
if not schema_result:
raise ValueError(f"Schema not found for company {company_id}")
return schema_result[0]
@staticmethod
@cached(cache_type='trial_balance', key_params=['company_id', 'luna', 'an', 'cont_filter',
'denumire_filter', 'sort_by', 'sort_order',
'page', 'page_size', 'username', 'server_id'])
async def get_trial_balance(
company_id: int,
luna: int,
an: int,
cont_filter: str | None,
denumire_filter: str | None,
sort_by: str,
sort_order: str,
page: int,
page_size: int,
username: str,
server_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Obține balanța de verificare sintetică (CACHED 10 min)
Cache key includes all filter parameters to ensure unique cache entries
for different query variations.
Args:
company_id: ID firmei
luna: Luna (1-12)
an: Anul
cont_filter: Filtru număr cont (optional)
denumire_filter: Filtru denumire cont (optional)
sort_by: Coloană pentru sortare
sort_order: Ordinea sortării (asc/desc)
page: Pagina
page_size: Mărimea paginii
username: Username pentru cache tracking
server_id: Optional Oracle server identifier for multi-server support
Returns:
Dictionary cu items, pagination, filters_applied
"""
# Get schema (cached separately)
schema = await TrialBalanceService._get_schema(company_id, server_id)
# Validate sort_order
if sort_order.lower() not in ['asc', 'desc']:
sort_order = 'asc'
# Validate sort_by (prevent SQL injection)
valid_sort_columns = ['CONT', 'DENUMIRE', 'PRECDEB', 'PRECCRED',
'RULDEB', 'RULCRED', 'SOLDDEB', 'SOLDCRED']
if sort_by.upper() not in valid_sort_columns:
sort_by = 'CONT'
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Build base query for VBAL VIEW
base_query = f"""
SELECT
CONT,
NVL(DENUMIRE, '') as DENUMIRE,
NVL(PRECDEB, 0) as PRECDEB,
NVL(PRECCRED, 0) as PRECCRED,
NVL(RULDEB, 0) as RULDEB,
NVL(RULCRED, 0) as RULCRED,
NVL(SOLDDEB, 0) as SOLDDEB,
NVL(SOLDCRED, 0) as SOLDCRED
FROM {schema}.VBAL
WHERE AN = :an
AND LUNA = :luna
"""
params = {
'an': an,
'luna': luna
}
# Add dynamic filters
if cont_filter:
base_query += " AND CONT LIKE :cont_filter"
params['cont_filter'] = f"{cont_filter}%"
if denumire_filter:
base_query += " AND UPPER(DENUMIRE) LIKE UPPER(:denumire_filter)"
params['denumire_filter'] = f"%{denumire_filter}%"
# Count total for pagination
count_query = f"SELECT COUNT(*) FROM ({base_query})"
cursor.execute(count_query, params)
total_count = cursor.fetchone()[0]
# Query pentru TOTALURI din TOATE înregistrările filtrate (nu doar pagina curentă)
totals_query = f"""
SELECT
NVL(SUM(PRECDEB), 0) as total_prec_deb,
NVL(SUM(PRECCRED), 0) as total_prec_cred,
NVL(SUM(RULDEB), 0) as total_rul_deb,
NVL(SUM(RULCRED), 0) as total_rul_cred,
NVL(SUM(SOLDDEB), 0) as total_sold_deb,
NVL(SUM(SOLDCRED), 0) as total_sold_cred
FROM ({base_query})
"""
cursor.execute(totals_query, params)
totals_row = cursor.fetchone()
totals = {
"total_sold_precedent_debit": Decimal(str(totals_row[0])) if totals_row else Decimal('0.00'),
"total_sold_precedent_credit": Decimal(str(totals_row[1])) if totals_row else Decimal('0.00'),
"total_rulaj_lunar_debit": Decimal(str(totals_row[2])) if totals_row else Decimal('0.00'),
"total_rulaj_lunar_credit": Decimal(str(totals_row[3])) if totals_row else Decimal('0.00'),
"total_sold_final_debit": Decimal(str(totals_row[4])) if totals_row else Decimal('0.00'),
"total_sold_final_credit": Decimal(str(totals_row[5])) if totals_row else Decimal('0.00')
}
# Add sorting
base_query += f" ORDER BY {sort_by.upper()} {sort_order.upper()}"
# Pagination (Oracle ROWNUM with ORDER BY)
offset = (page - 1) * page_size
limit = offset + page_size
paginated_query = f"""
SELECT * FROM (
SELECT a.*, ROWNUM rnum FROM (
{base_query}
) a WHERE ROWNUM <= :limit
) WHERE rnum > :offset
"""
params['offset'] = offset
params['limit'] = limit
cursor.execute(paginated_query, params)
rows = cursor.fetchall()
# Process results
# Index: CONT(0), DENUMIRE(1), PRECDEB(2), PRECCRED(3),
# RULDEB(4), RULCRED(5), SOLDDEB(6), SOLDCRED(7), rnum(8)
items = []
for row in rows:
item = TrialBalanceItem(
cont=row[0] or '',
denumire=row[1] or '',
sold_precedent_debit=Decimal(str(row[2] or 0)),
sold_precedent_credit=Decimal(str(row[3] or 0)),
rulaj_lunar_debit=Decimal(str(row[4] or 0)),
rulaj_lunar_credit=Decimal(str(row[5] or 0)),
sold_final_debit=Decimal(str(row[6] or 0)),
sold_final_credit=Decimal(str(row[7] or 0))
)
items.append(item.dict())
# Calculate pagination
total_pages = math.ceil(total_count / page_size) if page_size > 0 else 0
# Build response
return {
"items": items,
"pagination": {
"total_items": total_count,
"total_pages": total_pages,
"current_page": page,
"page_size": page_size
},
"filters_applied": {
"luna": luna,
"an": an,
"cont_filter": cont_filter,
"denumire_filter": denumire_filter
},
# Totaluri din TOATE înregistrările filtrate (nu doar pagina curentă)
"totals": totals
}