""" 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 shared.database.oracle_pool import oracle_pool from typing import Dict, Any 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']) async def _get_schema(company_id: int) -> str: """ Obține schema pentru company_id (CACHED 24h) This is cached permanently because company schemas rarely change. """ async with oracle_pool.get_connection() 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']) 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 ) -> 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 Returns: Dictionary cu items, pagination, filters_applied """ # Get schema (cached separately) schema = await TrialBalanceService._get_schema(company_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() 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 }