# import sys # Removed - no longer needed import os from shared.database.oracle_pool import oracle_pool from ..models.dashboard import DashboardSummary, TreasuryAccount, TrendData from ..cache.decorators import cached from decimal import Decimal from typing import Dict, Any, List, Optional from datetime import datetime, timedelta from fastapi import Request import logging logger = logging.getLogger(__name__) class DashboardService: """Service pentru dashboard - date agregate""" @staticmethod def _build_period_cte(schema: str, luna: Optional[int] = None, an: Optional[int] = None) -> tuple[str, dict]: """ Construiește CTE pentru luna curentă. Dacă luna și an sunt specificate, le folosește. Altfel, folosește MAX(anul*12+luna) din calendar. Returns: tuple: (cte_sql, params_dict) """ if luna is not None and an is not None: # Folosește parametrii specificați cte_sql = f""" WITH luna_curenta AS ( SELECT :param_an as anul, :param_luna as luna FROM DUAL )""" params = {'param_an': an, 'param_luna': luna} else: # Folosește MAX din calendar cte_sql = f""" WITH luna_curenta AS ( SELECT anul, luna FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) )""" params = {} return cte_sql, params @staticmethod @cached(cache_type='schema', key_params=['company_id']) async def _get_schema(company_id: int) -> str: """ Obține schema pentru company_id (CACHED PERMANENT) CRITICAL: Acest query este cel mai frecvent - executat la FIECARE request API. Cache permanent reduce queries cu 99.99%. """ 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='dashboard_summary', key_params=['company', 'username', 'luna', 'an']) async def get_complete_summary(company: str, username: str, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> DashboardSummary: """ Obține toate datele pentru dashboard într-un singur apel (CACHED 30 min) Execută 2 query-uri separate: facturi și trezorerie Args: company: ID-ul firmei username: Numele utilizatorului luna: Luna contabilă (1-12), opțional an: Anul contabil, opțional request: Request object pentru cache metadata """ company_id = int(company) schema = await DashboardService._get_schema(company_id) # Construiește CTE pentru perioada curentă period_cte, period_params = DashboardService._build_period_cte(schema, luna, an) async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Query 1: Statistici facturi cu breakdown pe perioade - FIXED ORA-00937 facturi_query = f""" {period_cte}, perioada_stats AS ( SELECT an, luna, -- CLIENȚI SUM(CASE WHEN cont IN ('4111','461') THEN precdeb + debit ELSE 0 END) as clienti_facturat_luna, SUM(CASE WHEN cont IN ('4111','461') THEN preccred + credit ELSE 0 END) as clienti_incasat_luna, -- FURNIZORI SUM(CASE WHEN cont IN ('401','404','462') THEN preccred + credit ELSE 0 END) as furnizori_facturat_luna, SUM(CASE WHEN cont IN ('401','404','462') THEN precdeb + debit ELSE 0 END) as furnizori_achitat_luna FROM {schema}.vireg_parteneri WHERE cont IN ('4111', '461', '401', '404', '462') AND an >= (SELECT anul-1 FROM luna_curenta) GROUP BY an, luna ), facturi_stats AS ( SELECT -- CLIENȚI - Totaluri SUM(CASE WHEN cont IN ('4111','461') THEN precdeb + debit ELSE 0 END) as clienti_total_facturat, SUM(CASE WHEN cont IN ('4111','461') THEN preccred + credit ELSE 0 END) as clienti_total_incasat, SUM(CASE WHEN cont = '419' THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as clienti_avansuri, -- CLIENȚI - Sold Net Total SUM(CASE WHEN cont IN ('4111','461') THEN (precdeb + debit) - (preccred + credit) WHEN cont = '419' THEN -((preccred + credit) - (precdeb + debit)) ELSE 0 END) as clienti_sold_total, -- CLIENȚI - Sold În Termen (datascad >= azi sau NULL) SUM(CASE WHEN cont IN ('4111','461') AND (datascad IS NULL OR datascad >= TRUNC(SYSDATE)) THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_sold_in_termen, -- CLIENȚI - Sold Restant (datascad < azi) SUM(CASE WHEN cont IN ('4111','461') AND datascad < TRUNC(SYSDATE) THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_sold_restant, -- CLIENȚI - Restanțe pe perioade SUM(CASE WHEN cont IN ('4111','461') AND TRUNC(SYSDATE) - datascad BETWEEN 1 AND 7 THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_restant_7, SUM(CASE WHEN cont IN ('4111','461') AND TRUNC(SYSDATE) - datascad BETWEEN 8 AND 14 THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_restant_14, SUM(CASE WHEN cont IN ('4111','461') AND TRUNC(SYSDATE) - datascad BETWEEN 15 AND 30 THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_restant_30, SUM(CASE WHEN cont IN ('4111','461') AND TRUNC(SYSDATE) - datascad BETWEEN 31 AND 60 THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_restant_60, SUM(CASE WHEN cont IN ('4111','461') AND TRUNC(SYSDATE) - datascad BETWEEN 61 AND 90 THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_restant_90, SUM(CASE WHEN cont IN ('4111','461') AND TRUNC(SYSDATE) - datascad > 90 THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_restant_90plus, -- CLIENȚI - Scadențe pe perioade (datascad în viitor) SUM(CASE WHEN cont IN ('4111','461') AND datascad - TRUNC(SYSDATE) BETWEEN 1 AND 7 THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_scadent_7, SUM(CASE WHEN cont IN ('4111','461') AND datascad - TRUNC(SYSDATE) BETWEEN 8 AND 14 THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_scadent_14, SUM(CASE WHEN cont IN ('4111','461') AND datascad - TRUNC(SYSDATE) BETWEEN 15 AND 30 THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_scadent_30, SUM(CASE WHEN cont IN ('4111','461') AND datascad - TRUNC(SYSDATE) BETWEEN 31 AND 60 THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_scadent_60, SUM(CASE WHEN cont IN ('4111','461') AND datascad - TRUNC(SYSDATE) BETWEEN 61 AND 90 THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_scadent_90, SUM(CASE WHEN cont IN ('4111','461') AND datascad - TRUNC(SYSDATE) > 90 THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as clienti_scadent_90plus, -- FURNIZORI - Totaluri SUM(CASE WHEN cont IN ('401','404','462') THEN preccred + credit ELSE 0 END) as furnizori_total_facturat, SUM(CASE WHEN cont IN ('401','404','462') THEN precdeb + debit ELSE 0 END) as furnizori_total_achitat, SUM(CASE WHEN cont LIKE '409%' THEN (precdeb + debit) - (preccred + credit) ELSE 0 END) as furnizori_avansuri, -- FURNIZORI - Sold Net Total SUM(CASE WHEN cont IN ('401','404','462') THEN (preccred + credit) - (precdeb + debit) WHEN cont LIKE '409%' THEN -((precdeb + debit) - (preccred + credit)) ELSE 0 END) as furnizori_sold_total, -- FURNIZORI - Sold În Termen SUM(CASE WHEN cont IN ('401','404','462') AND (datascad IS NULL OR datascad >= TRUNC(SYSDATE)) THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_sold_in_termen, -- FURNIZORI - Sold Restant SUM(CASE WHEN cont IN ('401','404','462') AND datascad < TRUNC(SYSDATE) THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_sold_restant, -- FURNIZORI - Restanțe pe perioade SUM(CASE WHEN cont IN ('401','404','462') AND TRUNC(SYSDATE) - datascad BETWEEN 1 AND 7 THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_restant_7, SUM(CASE WHEN cont IN ('401','404','462') AND TRUNC(SYSDATE) - datascad BETWEEN 8 AND 14 THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_restant_14, SUM(CASE WHEN cont IN ('401','404','462') AND TRUNC(SYSDATE) - datascad BETWEEN 15 AND 30 THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_restant_30, SUM(CASE WHEN cont IN ('401','404','462') AND TRUNC(SYSDATE) - datascad BETWEEN 31 AND 60 THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_restant_60, SUM(CASE WHEN cont IN ('401','404','462') AND TRUNC(SYSDATE) - datascad BETWEEN 61 AND 90 THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_restant_90, SUM(CASE WHEN cont IN ('401','404','462') AND TRUNC(SYSDATE) - datascad > 90 THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_restant_90plus, -- FURNIZORI - Scadențe pe perioade SUM(CASE WHEN cont IN ('401','404','462') AND datascad - TRUNC(SYSDATE) BETWEEN 1 AND 7 THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_scadent_7, SUM(CASE WHEN cont IN ('401','404','462') AND datascad - TRUNC(SYSDATE) BETWEEN 8 AND 14 THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_scadent_14, SUM(CASE WHEN cont IN ('401','404','462') AND datascad - TRUNC(SYSDATE) BETWEEN 15 AND 30 THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_scadent_30, SUM(CASE WHEN cont IN ('401','404','462') AND datascad - TRUNC(SYSDATE) BETWEEN 31 AND 60 THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_scadent_60, SUM(CASE WHEN cont IN ('401','404','462') AND datascad - TRUNC(SYSDATE) BETWEEN 61 AND 90 THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_scadent_90, SUM(CASE WHEN cont IN ('401','404','462') AND datascad - TRUNC(SYSDATE) > 90 THEN (preccred + credit) - (precdeb + debit) ELSE 0 END) as furnizori_scadent_90plus FROM {schema}.vireg_parteneri WHERE an = (SELECT anul FROM luna_curenta) AND luna = (SELECT luna FROM luna_curenta) AND cont IN ('4111', '461', '419', '401', '404', '462', '409','4091','4092','4093','4094') ) SELECT fs.*, -- BREAKDOWN pe perioade - Luna anterioară (SELECT NVL(clienti_facturat_luna, 0) FROM perioada_stats p WHERE p.an*12+p.luna = (SELECT anul*12+luna-1 FROM luna_curenta)) as clienti_facturat_luna_anterioara, (SELECT NVL(furnizori_facturat_luna, 0) FROM perioada_stats p WHERE p.an*12+p.luna = (SELECT anul*12+luna-1 FROM luna_curenta)) as furnizori_facturat_luna_anterioara, -- BREAKDOWN pe perioade - Anul curent vs anterior (SELECT NVL(SUM(clienti_facturat_luna), 0) FROM perioada_stats p WHERE p.an = (SELECT anul FROM luna_curenta)) as clienti_facturat_an_curent, (SELECT NVL(SUM(clienti_facturat_luna), 0) FROM perioada_stats p WHERE p.an = (SELECT anul-1 FROM luna_curenta)) as clienti_facturat_an_anterior, (SELECT NVL(SUM(furnizori_facturat_luna), 0) FROM perioada_stats p WHERE p.an = (SELECT anul FROM luna_curenta)) as furnizori_facturat_an_curent, (SELECT NVL(SUM(furnizori_facturat_luna), 0) FROM perioada_stats p WHERE p.an = (SELECT anul-1 FROM luna_curenta)) as furnizori_facturat_an_anterior FROM facturi_stats fs """ cursor.execute(facturi_query, period_params) facturi_row = cursor.fetchone() # Query 2: Trezorerie (folosește același period_cte) treasury_query = f""" {period_cte} SELECT cont, nume as nume_banca, CASE WHEN cont = '5121' THEN 'Bancă LEI' WHEN cont = '5124' THEN 'Bancă VALUTA' WHEN cont = '5311' THEN 'Casă LEI' WHEN cont = '5314' THEN 'Casă VALUTA' END as nume_cont, SUM(CASE WHEN cont IN ('5121','5311') THEN solddeb - soldcred WHEN cont IN ('5124','5314') THEN soldvaldeb - soldvalcred END) as sold, CASE WHEN cont IN ('5121','5311') THEN 'RON' WHEN cont IN ('5124','5314') THEN NVL(nume_val, 'EUR') END as valuta FROM {schema}.vbalanta_parteneri WHERE an = (SELECT anul FROM luna_curenta) AND luna = (SELECT luna FROM luna_curenta) AND cont IN ('5121', '5124', '5311', '5314') AND ((cont IN ('5121','5311') AND soldcred - solddeb != 0) OR (cont IN ('5124','5314') AND soldvalcred - soldvaldeb != 0)) GROUP BY cont, nume, nume_val ORDER BY cont, nume """ cursor.execute(treasury_query, period_params) treasury_rows = cursor.fetchall() # Query 3: Solduri TVA din tabelul vbal (folosește același period_cte) tva_query = f""" {period_cte} SELECT cont, precdeb, preccred, ruldeb, rulcred FROM {schema}.vbal WHERE an = (SELECT anul FROM luna_curenta) AND luna = (SELECT luna FROM luna_curenta) AND cont IN ('4423', '4424', '4426', '4427') ORDER BY cont """ cursor.execute(tva_query, period_params) tva_rows = cursor.fetchall() # Procesare solduri TVA tva_data = { '4423': {'precdeb': Decimal('0'), 'preccred': Decimal('0'), 'ruldeb': Decimal('0'), 'rulcred': Decimal('0')}, '4424': {'precdeb': Decimal('0'), 'preccred': Decimal('0'), 'ruldeb': Decimal('0'), 'rulcred': Decimal('0')}, '4426': {'precdeb': Decimal('0'), 'preccred': Decimal('0'), 'ruldeb': Decimal('0'), 'rulcred': Decimal('0')}, '4427': {'precdeb': Decimal('0'), 'preccred': Decimal('0'), 'ruldeb': Decimal('0'), 'rulcred': Decimal('0')} } for row in tva_rows: cont = row[0] if cont in tva_data: tva_data[cont]['precdeb'] = Decimal(str(row[1] or 0)) tva_data[cont]['preccred'] = Decimal(str(row[2] or 0)) tva_data[cont]['ruldeb'] = Decimal(str(row[3] or 0)) tva_data[cont]['rulcred'] = Decimal(str(row[4] or 0)) # Calcul TVA Luna Precedentă - FIE de plată (4423) FIE de recuperat (4424) sold_4423 = tva_data['4423']['preccred'] - tva_data['4423']['precdeb'] sold_4424 = tva_data['4424']['precdeb'] - tva_data['4424']['preccred'] if sold_4423 > 0: tva_plata_precedent = sold_4423 tva_recuperat_precedent = Decimal('0') elif sold_4424 > 0: tva_recuperat_precedent = sold_4424 tva_plata_precedent = Decimal('0') else: tva_plata_precedent = Decimal('0') tva_recuperat_precedent = Decimal('0') # Calcul TVA Luna Curentă - FIE de plată FIE de recuperat diferenta_curent = tva_data['4427']['rulcred'] - tva_data['4426']['ruldeb'] if diferenta_curent > 0: tva_plata_curent = diferenta_curent tva_recuperat_curent = Decimal('0') else: tva_recuperat_curent = -diferenta_curent tva_plata_curent = Decimal('0') # Procesare trezorerie treasury_accounts = [] treasury_totals = {} for row in treasury_rows: account = TreasuryAccount( cont=row[0], nume_banca=row[1], nume_cont=row[2], sold=Decimal(str(row[3] or 0)), valuta=row[4] ) treasury_accounts.append(account) # Calculează totaluri pe valută if account.valuta not in treasury_totals: treasury_totals[account.valuta] = Decimal('0') treasury_totals[account.valuta] += account.sold # Returnează rezultatul complet cu toate câmpurile calculate return DashboardSummary( # CLIENȚI - Totaluri principale clienti_total_facturat=Decimal(str(facturi_row[0] or 0)), clienti_total_incasat=Decimal(str(facturi_row[1] or 0)), clienti_avansuri=Decimal(str(facturi_row[2] or 0)), clienti_sold_total=Decimal(str(facturi_row[3] or 0)), clienti_sold_in_termen=Decimal(str(facturi_row[4] or 0)), clienti_sold_restant=Decimal(str(facturi_row[5] or 0)), # CLIENȚI - Restanțe pe perioade clienti_restant_7=Decimal(str(facturi_row[6] or 0)), clienti_restant_14=Decimal(str(facturi_row[7] or 0)), clienti_restant_30=Decimal(str(facturi_row[8] or 0)), clienti_restant_60=Decimal(str(facturi_row[9] or 0)), clienti_restant_90=Decimal(str(facturi_row[10] or 0)), clienti_restant_90plus=Decimal(str(facturi_row[11] or 0)), # CLIENȚI - Scadențe pe perioade clienti_scadent_7=Decimal(str(facturi_row[12] or 0)), clienti_scadent_14=Decimal(str(facturi_row[13] or 0)), clienti_scadent_30=Decimal(str(facturi_row[14] or 0)), clienti_scadent_60=Decimal(str(facturi_row[15] or 0)), clienti_scadent_90=Decimal(str(facturi_row[16] or 0)), clienti_scadent_90plus=Decimal(str(facturi_row[17] or 0)), # FURNIZORI - Totaluri principale furnizori_total_facturat=Decimal(str(facturi_row[18] or 0)), furnizori_total_achitat=Decimal(str(facturi_row[19] or 0)), furnizori_avansuri=Decimal(str(facturi_row[20] or 0)), furnizori_sold_total=Decimal(str(facturi_row[21] or 0)), furnizori_sold_in_termen=Decimal(str(facturi_row[22] or 0)), furnizori_sold_restant=Decimal(str(facturi_row[23] or 0)), # FURNIZORI - Restanțe pe perioade furnizori_restant_7=Decimal(str(facturi_row[24] or 0)), furnizori_restant_14=Decimal(str(facturi_row[25] or 0)), furnizori_restant_30=Decimal(str(facturi_row[26] or 0)), furnizori_restant_60=Decimal(str(facturi_row[27] or 0)), furnizori_restant_90=Decimal(str(facturi_row[28] or 0)), furnizori_restant_90plus=Decimal(str(facturi_row[29] or 0)), # FURNIZORI - Scadențe pe perioade furnizori_scadent_7=Decimal(str(facturi_row[30] or 0)), furnizori_scadent_14=Decimal(str(facturi_row[31] or 0)), furnizori_scadent_30=Decimal(str(facturi_row[32] or 0)), furnizori_scadent_60=Decimal(str(facturi_row[33] or 0)), furnizori_scadent_90=Decimal(str(facturi_row[34] or 0)), furnizori_scadent_90plus=Decimal(str(facturi_row[35] or 0)), # TREZORERIE treasury_accounts=treasury_accounts, treasury_totals_by_currency=treasury_totals, # Date suplimentare pentru trend analysis clienti_facturat_luna_anterioara=Decimal(str(facturi_row[36] or 0)), furnizori_facturat_luna_anterioara=Decimal(str(facturi_row[37] or 0)), clienti_facturat_an_curent=Decimal(str(facturi_row[38] or 0)), clienti_facturat_an_anterior=Decimal(str(facturi_row[39] or 0)), furnizori_facturat_an_curent=Decimal(str(facturi_row[40] or 0)), furnizori_facturat_an_anterior=Decimal(str(facturi_row[41] or 0)), # Solduri TVA tva_plata_precedent=tva_plata_precedent, tva_recuperat_precedent=tva_recuperat_precedent, tva_plata_curent=tva_plata_curent, tva_recuperat_curent=tva_recuperat_curent ) @staticmethod @cached(cache_type='dashboard_trends', key_params=['company_id', 'period', 'luna', 'an']) async def get_trends(company_id: int, period: str = "12m", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]: """Get comprehensive trend analysis data for all dashboard indicators (CACHED 30 min) Args: company_id: ID-ul firmei period: Perioada pentru trends (7d, 30d, ytd, 12m) luna: Luna contabilă (1-12), opțional - dacă nu e specificată, folosește MAX an: Anul contabil, opțional - dacă nu e specificat, folosește MAX request: Request object pentru cache metadata """ try: schema = await DashboardService._get_schema(company_id) async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Determine current period from params or database if luna is not None and an is not None: current_year = an current_month = luna else: # Get current period from database current_period_query = f""" WITH luna_curenta AS ( SELECT anul, luna FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) ) SELECT anul, luna FROM luna_curenta """ cursor.execute(current_period_query) current_period = cursor.fetchone() if not current_period: # Fallback to current system date current_year = 2024 current_month = 12 else: current_year = current_period[0] current_month = current_period[1] # Determine period parameters if period == 'ytd': # Year to date - from January to current month of current year start_year = current_year start_month = 1 end_year = current_year end_month = current_month elif period == '12m': # Last 12 months from datetime import date from dateutil.relativedelta import relativedelta current_date = date(current_year, current_month, 1) start_date = current_date - relativedelta(months=11) # 12 months including current start_year = start_date.year start_month = start_date.month end_year = current_year end_month = current_month else: # Default to 12 months from datetime import date from dateutil.relativedelta import relativedelta current_date = date(current_year, current_month, 1) start_date = current_date - relativedelta(months=11) start_year = start_date.year start_month = start_date.month end_year = current_year end_month = current_month # Calculate previous period (12 months before current period) from datetime import date from dateutil.relativedelta import relativedelta prev_start_date = date(start_year, start_month, 1) - relativedelta(months=12) prev_end_date = date(end_year, end_month, 1) - relativedelta(months=12) prev_start_year = prev_start_date.year prev_start_month = prev_start_date.month prev_end_year = prev_end_date.year prev_end_month = prev_end_date.month # Comprehensive trends query for all indicators (current + previous period) trends_query = f""" WITH trend_periods AS ( SELECT DISTINCT an as anul, luna FROM {schema}.vbalanta_parteneri WHERE (an*100 + luna >= {start_year * 100 + start_month}) AND (an*100 + luna <= {end_year * 100 + end_month}) ), prev_trend_periods AS ( SELECT DISTINCT an as anul, luna FROM {schema}.vbalanta_parteneri WHERE (an*100 + luna >= {prev_start_year * 100 + prev_start_month}) AND (an*100 + luna <= {prev_end_year * 100 + prev_end_month}) ), comprehensive_data AS ( SELECT tp.anul, tp.luna, tp.anul||'-'||LPAD(tp.luna,2,'0') as perioada, -- CLIENTI - facturat (cifra de afaceri - vanzari) COALESCE(SUM(CASE WHEN vb.cont in ('4111', '461') THEN vb.debit ELSE 0 END), 0) as clienti_facturat, -- CLIENTI - incasat (incasari de la clienti) COALESCE(SUM(CASE WHEN vb.cont IN ('4111', '461') THEN vb.credit WHEN vb.cont in ('419') THEN vb.debit ELSE 0 END), 0) as clienti_incasat, -- FURNIZORI - facturat (achizitii) COALESCE(SUM(CASE WHEN vb.cont in ('401', '404', '462') THEN vb.credit ELSE 0 END), 0) as furnizori_facturat, -- FURNIZORI - achitat (plati catre furnizori) COALESCE(SUM(CASE WHEN vb.cont in ('401', '404', '462') THEN vb.debit WHEN vb.cont in ('409') THEN vb.credit ELSE 0 END), 0) as furnizori_achitat, -- CLIENTI SOLD (balanta clienti) COALESCE(SUM(CASE WHEN vb.cont IN ('4111', '461') THEN vb.solddeb - vb.soldcred WHEN vb.cont IN ('419') THEN vb.soldcred - vb.solddeb ELSE 0 END), 0) as clienti_sold, -- FURNIZORI SOLD (balanta furnizori) COALESCE(SUM(CASE WHEN vb.cont in ('401', '404', '462') THEN vb.soldcred - vb.solddeb WHEN vb.cont IN ('409') THEN vb.solddeb - vb.soldcred ELSE 0 END), 0) as furnizori_sold, -- TREZORERIE SOLD COALESCE(SUM(CASE WHEN vb.cont IN ('5121','5311','5124','5314') THEN vb.solddeb - vb.soldcred ELSE 0 END), 0) as trezorerie_sold FROM trend_periods tp LEFT JOIN {schema}.vbalanta_parteneri vb ON vb.an = tp.anul AND vb.luna = tp.luna AND vb.cont in ('4111', '461', '419', '401', '404', '462', '409','5121','5311','5124','5314') GROUP BY tp.anul, tp.luna ), prev_comprehensive_data AS ( SELECT tp.anul, tp.luna, tp.anul||'-'||LPAD(tp.luna,2,'0') as perioada, -- CLIENTI - facturat (cifra de afaceri - vanzari) COALESCE(SUM(CASE WHEN vb.cont in ('4111', '461') THEN vb.debit ELSE 0 END), 0) as clienti_facturat, -- CLIENTI - incasat (incasari de la clienti) COALESCE(SUM(CASE WHEN vb.cont IN ('4111', '461') THEN vb.credit WHEN vb.cont in ('419') THEN vb.debit ELSE 0 END), 0) as clienti_incasat, -- FURNIZORI - facturat (achizitii) COALESCE(SUM(CASE WHEN vb.cont in ('401', '404', '462') THEN vb.credit ELSE 0 END), 0) as furnizori_facturat, -- FURNIZORI - achitat (plati catre furnizori) COALESCE(SUM(CASE WHEN vb.cont in ('401', '404', '462') THEN vb.debit WHEN vb.cont in ('409') THEN vb.credit ELSE 0 END), 0) as furnizori_achitat, -- CLIENTI SOLD (balanta clienti) COALESCE(SUM(CASE WHEN vb.cont IN ('4111', '461') THEN vb.solddeb - vb.soldcred WHEN vb.cont IN ('419') THEN vb.soldcred - vb.solddeb ELSE 0 END), 0) as clienti_sold, -- FURNIZORI SOLD (balanta furnizori) COALESCE(SUM(CASE WHEN vb.cont in ('401', '404', '462') THEN vb.soldcred - vb.solddeb WHEN vb.cont IN ('409') THEN vb.solddeb - vb.soldcred ELSE 0 END), 0) as furnizori_sold, -- TREZORERIE SOLD COALESCE(SUM(CASE WHEN vb.cont IN ('5121','5311','5124','5314') THEN vb.solddeb - vb.soldcred ELSE 0 END), 0) as trezorerie_sold FROM prev_trend_periods tp LEFT JOIN {schema}.vbalanta_parteneri vb ON vb.an = tp.anul AND vb.luna = tp.luna AND vb.cont in ('4111', '461', '419', '401', '404', '462', '409','5121','5311','5124','5314') GROUP BY tp.anul, tp.luna ) SELECT 'current' as data_type, cd.anul, cd.luna, cd.perioada, cd.clienti_facturat, cd.clienti_incasat, cd.furnizori_facturat, cd.furnizori_achitat, cd.clienti_sold, cd.furnizori_sold, cd.trezorerie_sold FROM comprehensive_data cd UNION ALL SELECT 'previous' as data_type, pcd.anul, pcd.luna, pcd.perioada, pcd.clienti_facturat, pcd.clienti_incasat, pcd.furnizori_facturat, pcd.furnizori_achitat, pcd.clienti_sold, pcd.furnizori_sold, pcd.trezorerie_sold FROM prev_comprehensive_data pcd ORDER BY data_type DESC, anul ASC, luna ASC """ cursor.execute(trends_query) all_results = cursor.fetchall() # Separate current and previous results result = [row[1:] for row in all_results if row[0] == 'current'] prev_result = [row[1:] for row in all_results if row[0] == 'previous'] if not result: # Return empty arrays in the expected format return { "periods": [], "clienti_facturat": [], "clienti_incasat": [], "furnizori_facturat": [], "furnizori_achitat": [], "clienti_sold": [], "furnizori_sold": [], "trezorerie_sold": [], "rata_incasare_clienti": [], "rata_achitare_furnizori": [], "previous_periods": [], "clienti_facturat_prev": [], "clienti_incasat_prev": [], "furnizori_facturat_prev": [], "furnizori_achitat_prev": [], "clienti_sold_prev": [], "furnizori_sold_prev": [], "trezorerie_sold_prev": [], "metadata": { "period": period, "company_id": company_id, "data_points": 0, "grouping": "monthly" } } # Process results into the expected format periods = [] clienti_facturat = [] clienti_incasat = [] furnizori_facturat = [] furnizori_achitat = [] clienti_sold = [] furnizori_sold = [] trezorerie_sold = [] rata_incasare_clienti = [] rata_achitare_furnizori = [] # Process previous period results previous_periods = [] clienti_facturat_prev = [] clienti_incasat_prev = [] furnizori_facturat_prev = [] furnizori_achitat_prev = [] clienti_sold_prev = [] furnizori_sold_prev = [] trezorerie_sold_prev = [] for row in result: # After row[1:], indices are: 0=anul, 1=luna, 2=perioada, 3=clienti_facturat, etc. periods.append(row[2]) # perioada clienti_facturat.append(float(row[3] or 0)) clienti_incasat.append(float(row[4] or 0)) furnizori_facturat.append(float(row[5] or 0)) furnizori_achitat.append(float(row[6] or 0)) clienti_sold.append(float(row[7] or 0)) furnizori_sold.append(float(row[8] or 0)) trezorerie_sold.append(float(row[9] or 0)) # Calculate collection and payment rates cf = float(row[3] or 0) # clienti_facturat ci = float(row[4] or 0) # clienti_incasat ff = float(row[5] or 0) # furnizori_facturat fa = float(row[6] or 0) # furnizori_achitat # Collection rate (rata incasare clienti) rata_incasare = (ci / cf * 100) if cf > 0 else 0 rata_incasare_clienti.append(round(rata_incasare, 2)) # Payment rate (rata achitare furnizori) rata_achitare = (fa / ff * 100) if ff > 0 else 0 rata_achitare_furnizori.append(round(rata_achitare, 2)) # Process previous period data for row in prev_result: previous_periods.append(row[2]) # perioada clienti_facturat_prev.append(float(row[3] or 0)) clienti_incasat_prev.append(float(row[4] or 0)) furnizori_facturat_prev.append(float(row[5] or 0)) furnizori_achitat_prev.append(float(row[6] or 0)) clienti_sold_prev.append(float(row[7] or 0)) furnizori_sold_prev.append(float(row[8] or 0)) trezorerie_sold_prev.append(float(row[9] or 0)) # Calculate growth rates growth_rates = {} if len(periods) >= 2: datasets = { 'clienti_facturat': clienti_facturat, 'clienti_incasat': clienti_incasat, 'furnizori_facturat': furnizori_facturat, 'furnizori_achitat': furnizori_achitat, 'trezorerie_sold': trezorerie_sold } for key, values in datasets.items(): if len(values) >= 2: previous_value = values[0] current_value = values[-1] if previous_value != 0: growth_rate = round((current_value - previous_value) / abs(previous_value) * 100, 2) else: growth_rate = 100.0 if current_value > 0 else -100.0 if current_value < 0 else 0.0 growth_rates[key] = growth_rate return { "periods": periods, "clienti_facturat": clienti_facturat, "clienti_incasat": clienti_incasat, "furnizori_facturat": furnizori_facturat, "furnizori_achitat": furnizori_achitat, "clienti_sold": clienti_sold, "furnizori_sold": furnizori_sold, "trezorerie_sold": trezorerie_sold, "rata_incasare_clienti": rata_incasare_clienti, "rata_achitare_furnizori": rata_achitare_furnizori, "previous_periods": previous_periods, "clienti_facturat_prev": clienti_facturat_prev, "clienti_incasat_prev": clienti_incasat_prev, "furnizori_facturat_prev": furnizori_facturat_prev, "furnizori_achitat_prev": furnizori_achitat_prev, "clienti_sold_prev": clienti_sold_prev, "furnizori_sold_prev": furnizori_sold_prev, "trezorerie_sold_prev": trezorerie_sold_prev, "metadata": { "period": period, "company_id": company_id, "data_points": len(periods), "previous_data_points": len(previous_periods), "grouping": "monthly" }, "growth_rates": growth_rates } except Exception as e: logger.error(f"Error getting comprehensive trends: {str(e)}") raise @staticmethod async def get_detailed_data(company: str, data_type: str, luna: Optional[int] = None, an: Optional[int] = None, page: int = 1, page_size: int = 25, search: str = ""): """ Obține date detaliate pentru tabelele din dashboard Fixed to use existing vireg_parteneri view instead of missing tables Args: company: ID-ul firmei data_type: Tipul datelor (clients, suppliers, treasury) luna: Luna contabilă (1-12), opțional an: Anul contabil, opțional page: Pagina curentă page_size: Mărimea paginii search: Termen de căutare """ logger.info(f"get_detailed_data called: company={company}, data_type={data_type}, luna={luna}, an={an}, page={page}") async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: try: # Get schema for company schema_query = """ SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id """ cursor.execute(schema_query, {'company_id': int(company)}) schema_result = cursor.fetchone() if not schema_result: logger.error(f"Schema not found for company {company}") return {"error": "Schema not found for company", "data": [], "total": 0} schema = schema_result[0] logger.info(f"Found schema: {schema}") # Construiește CTE pentru perioada curentă period_cte, period_params = DashboardService._build_period_cte(schema, luna, an) # Calculate offset for pagination offset = (page - 1) * page_size logger.info(f"Pagination params: page={page}, page_size={page_size}, offset={offset}") # Handle treasury early return if data_type == "treasury": return { "data": [], "total": 0, "page": page, "page_size": page_size, "total_pages": 0, "message": "Date detaliate pentru trezorerie nu sunt disponibile" } # Build query based on data type if data_type == "clients": # Query cu paginare pe CLIENȚI (nu pe facturi individuale) base_query = f""" {period_cte}, clienti_cu_sold AS ( -- Pasul 1: Identifică TOȚI clienții cu sold != 0 SELECT DISTINCT vp.nume as client_name FROM {schema}.vireg_parteneri vp, luna_curenta lc WHERE vp.an = lc.anul AND vp.luna = lc.luna AND vp.cont IN ('4111','461') AND vp.nume IS NOT NULL AND ((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) <> 0 AND (UPPER(vp.nume) LIKE UPPER('%{search}%') OR '{search}' = '') ORDER BY vp.nume ASC ), clienti_pagina AS ( -- Pasul 2: Paginează pe CLIENȚI (25 clienți/pagină) SELECT * FROM ( SELECT t.*, ROWNUM as rn FROM clienti_cu_sold t WHERE ROWNUM <= {offset + page_size} ) WHERE rn > {offset} ) -- Pasul 3: Ia TOATE facturile pentru clienții din pagina curentă SELECT vp.nume as client, vp.nract as numar_document, vp.dataact as data_document, (vp.precdeb + vp.debit) as facturat, (vp.preccred + vp.credit) as incasat, (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) as sold, NVL(vp.datascad, vp.dataact + 30) as data_scadenta, CASE WHEN NVL(vp.datascad, vp.dataact + 30) < TRUNC(SYSDATE) THEN 'Restant' ELSE 'In termen' END as status FROM {schema}.vireg_parteneri vp, luna_curenta lc, clienti_pagina cp WHERE vp.an = lc.anul AND vp.luna = lc.luna AND vp.nume = cp.client_name AND vp.cont IN ('4111','461') AND ((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) <> 0 ORDER BY vp.nume ASC, vp.dataact DESC """ elif data_type == "suppliers": # Query cu paginare pe FURNIZORI (nu pe facturi individuale) base_query = f""" {period_cte}, furnizori_cu_sold AS ( -- Pasul 1: Identifică TOȚI furnizorii cu sold != 0 SELECT DISTINCT vp.nume as furnizor_name FROM {schema}.vireg_parteneri vp, luna_curenta lc WHERE vp.an = lc.anul AND vp.luna = lc.luna AND vp.cont IN ('401','404','462') AND vp.nume IS NOT NULL AND ((vp.preccred + vp.credit) - (vp.precdeb + vp.debit)) <> 0 AND (UPPER(vp.nume) LIKE UPPER('%{search}%') OR '{search}' = '') ORDER BY vp.nume ASC ), furnizori_pagina AS ( -- Pasul 2: Paginează pe FURNIZORI (25 furnizori/pagină) SELECT * FROM ( SELECT t.*, ROWNUM as rn FROM furnizori_cu_sold t WHERE ROWNUM <= {offset + page_size} ) WHERE rn > {offset} ) -- Pasul 3: Ia TOATE facturile pentru furnizorii din pagina curentă SELECT vp.nume as furnizor, vp.nract as numar_document, vp.dataact as data_document, (vp.preccred + vp.credit) as facturat, (vp.precdeb + vp.debit) as achitat, (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) as sold, NVL(vp.datascad, vp.dataact + 30) as data_scadenta, CASE WHEN NVL(vp.datascad, vp.dataact + 30) < TRUNC(SYSDATE) THEN 'Restant' ELSE 'In termen' END as status FROM {schema}.vireg_parteneri vp, luna_curenta lc, furnizori_pagina fp WHERE vp.an = lc.anul AND vp.luna = lc.luna AND vp.nume = fp.furnizor_name AND vp.cont IN ('401','404','462') AND ((vp.preccred + vp.credit) - (vp.precdeb + vp.debit)) <> 0 ORDER BY vp.nume ASC, vp.dataact DESC """ else: return {"error": "Invalid data type", "data": [], "total": 0} # Get total count of CLIENȚI/FURNIZORI (not individual invoices) if data_type == "clients": count_query = f""" {period_cte} SELECT COUNT(DISTINCT vp.nume) FROM {schema}.vireg_parteneri vp, luna_curenta lc WHERE vp.an = lc.anul AND vp.luna = lc.luna AND vp.cont IN ('4111','461') AND vp.nume IS NOT NULL AND ((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) <> 0 AND (UPPER(vp.nume) LIKE UPPER('%{search}%') OR '{search}' = '') """ elif data_type == "suppliers": count_query = f""" {period_cte} SELECT COUNT(DISTINCT vp.nume) FROM {schema}.vireg_parteneri vp, luna_curenta lc WHERE vp.an = lc.anul AND vp.luna = lc.luna AND vp.cont IN ('401','404','462') AND vp.nume IS NOT NULL AND ((vp.preccred + vp.credit) - (vp.precdeb + vp.debit)) <> 0 AND (UPPER(vp.nume) LIKE UPPER('%{search}%') OR '{search}' = '') """ cursor.execute(count_query, period_params) total = cursor.fetchone()[0] logger.info(f"Total {data_type}: {total}") # Execute base query directly (pagination already included in CTE) logger.info(f"Executing query with offset={offset}, page_size={page_size}") cursor.execute(base_query, period_params) columns = [desc[0].lower() for desc in cursor.description] data = [] for row in cursor.fetchall(): # Map row data to column names data.append(dict(zip(columns, row))) # Count unique clients/suppliers in returned data if data_type == "clients": unique_names = len(set(row.get('client') for row in data)) logger.info(f"Returned {len(data)} invoices from {unique_names} unique clients (expected max {page_size} clients)") elif data_type == "suppliers": unique_names = len(set(row.get('furnizor') for row in data)) logger.info(f"Returned {len(data)} invoices from {unique_names} unique suppliers (expected max {page_size} suppliers)") logger.info(f"Detailed data query returned {len(data)} rows out of {total} total") return { "data": data, "total": total, "page": page, "page_size": page_size, "total_pages": (total + page_size - 1) // page_size } except Exception as e: logger.error(f"Error in get_detailed_data: {str(e)}") return {"error": f"Database error: {str(e)}", "data": [], "total": 0} @staticmethod async def get_performance_data(company_id: int, period: str = "7d") -> Dict[str, Any]: """ Calculează performanța încasări/plăți pentru perioada selectată Args: company_id: ID-ul companiei period: Perioada ("7d", "1m", "3m", "6m", "ytd", "12m") Returns: { labels: List[str] - etichete pentru perioadele de timp incasari: List[float] - valorile încasărilor pe perioadă plati: List[float] - valorile plăților pe perioadă indicators: { rataIncasare: float - rata de încasare (%) cashConversion: int - zilele de conversie cash workingCapital: float - capitalul de lucru trend: str - tendința ("up", "down", "stable") } } """ try: async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Get schema 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 nu a fost găsită pentru id_firma {company_id}") schema = schema_result[0] # Pentru acum returnăm date mock cu structura corectă # TODO: Implementați query-uri Oracle pentru perioada selectată # Mock data based on period if period == "7d": labels = ["Lun", "Mar", "Mie", "Joi", "Vin", "Sâm", "Dum"] incasari = [45000, 52000, 38000, 61000, 49000, 33000, 28000] plati = [31000, 44000, 29000, 48000, 37000, 25000, 19000] elif period == "1m": labels = ["Săpt 1", "Săpt 2", "Săpt 3", "Săpt 4"] incasari = [185000, 201000, 168000, 195000] plati = [142000, 156000, 133000, 151000] elif period == "3m": labels = ["Luna 1", "Luna 2", "Luna 3"] incasari = [749000, 698000, 823000] plati = [582000, 519000, 634000] elif period == "6m": labels = ["Ian", "Feb", "Mar", "Apr", "Mai", "Iun"] incasari = [749000, 698000, 823000, 756000, 681000, 792000] plati = [582000, 519000, 634000, 588000, 523000, 612000] elif period == "ytd": labels = ["Ian", "Feb", "Mar", "Apr", "Mai", "Iun", "Iul", "Aug", "Sep", "Oct", "Nov", "Dec"] incasari = [749000, 698000, 823000, 756000, 681000, 792000, 834000, 712000, 768000, 695000, 743000, 821000] plati = [582000, 519000, 634000, 588000, 523000, 612000, 647000, 553000, 596000, 539000, 577000, 638000] else: # 12m labels = ["Ian", "Feb", "Mar", "Apr", "Mai", "Iun", "Iul", "Aug", "Sep", "Oct", "Nov", "Dec"] incasari = [749000, 698000, 823000, 756000, 681000, 792000, 834000, 712000, 768000, 695000, 743000, 821000] plati = [582000, 519000, 634000, 588000, 523000, 612000, 647000, 553000, 596000, 539000, 577000, 638000] # Calculate indicators total_incasari = sum(incasari) total_plati = sum(plati) rata_incasare = round((total_incasari / (total_incasari * 1.15)) * 100, 1) if total_incasari > 0 else 0 # Assuming 15% more was invoiced cash_conversion = 45 # Days - mock value working_capital = total_incasari - total_plati # Determine trend if len(incasari) >= 2: trend = "up" if incasari[-1] > incasari[-2] else "down" if incasari[-1] < incasari[-2] else "stable" else: trend = "stable" return { "labels": labels, "incasari": incasari, "plati": plati, "indicators": { "rataIncasare": rata_incasare, "cashConversion": cash_conversion, "workingCapital": working_capital, "trend": trend } } except Exception as e: logger.error(f"Error getting performance data: {str(e)}") raise @staticmethod async def get_cashflow_forecast(company_id: int, period: str = "7d") -> Dict[str, Any]: """ Calculează previziunea cash flow bazată pe scadențe Args: company_id: ID-ul companiei period: Perioada ("7d", "1m", "3m", "6m") Returns: { periods: List[str] - perioadele de timp inflows: List[float] - intrările de cash estimate outflows: List[float] - ieșirile de cash estimate netFlow: List[float] - fluxul net pe perioadă cumulative: List[float] - fluxul cumulativ criticalDays: List[str] - zilele cu deficit critic netTotal: float - totalul net al perioadei } """ try: async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Get schema 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 nu a fost găsită pentru id_firma {company_id}") schema = schema_result[0] # Pentru acum returnăm date mock cu structura corectă # TODO: Implementați query-uri pentru scadențe viitoare # Mock data based on period if period == "7d": periods = ["Lun", "Mar", "Mie", "Joi", "Vin", "Sâm", "Dum"] inflows = [25000, 18000, 32000, 45000, 28000, 15000, 12000] outflows = [31000, 22000, 19000, 38000, 25000, 18000, 8000] elif period == "1m": periods = ["Săpt 1", "Săpt 2", "Săpt 3", "Săpt 4"] inflows = [95000, 112000, 88000, 105000] outflows = [108000, 92000, 75000, 89000] elif period == "3m": periods = ["Luna 1", "Luna 2", "Luna 3"] inflows = [400000, 365000, 420000] outflows = [364000, 298000, 385000] else: # 6m periods = ["Ian", "Feb", "Mar", "Apr", "Mai", "Iun"] inflows = [400000, 365000, 420000, 385000, 342000, 396000] outflows = [364000, 298000, 385000, 356000, 289000, 367000] # Calculate net flow and cumulative net_flow = [inf - out for inf, out in zip(inflows, outflows)] cumulative = [] running_total = 150000 # Starting cash position (mock) for net in net_flow: running_total += net cumulative.append(running_total) # Identify critical days (negative cumulative) critical_days = [] for i, (period_name, cum) in enumerate(zip(periods, cumulative)): if cum < 0: critical_days.append(period_name) net_total = sum(net_flow) return { "periods": periods, "inflows": inflows, "outflows": outflows, "netFlow": net_flow, "cumulative": cumulative, "criticalDays": critical_days, "netTotal": net_total } except Exception as e: logger.error(f"Error getting cashflow forecast: {str(e)}") raise @staticmethod @cached(cache_type='maturity_analysis', key_params=['company_id', 'period', 'luna', 'an']) async def get_maturity_analysis(company_id: int, period: str = "7d", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]: """ Analizează scadențele clienți vs furnizori cu date reale din Oracle (CACHED 30 min) Args: company_id: ID-ul companiei period: Perioada ("7d", "1m", "3m", "6m", "12m", "over12m") luna: Luna contabilă (1-12), opțional an: Anul contabil, opțional Returns: { clients: List[Dict] - [{name, amount, dueDate, daysOverdue}, ...] suppliers: List[Dict] - [{name, amount, dueDate, daysOverdue}, ...] balance: float - balanța între clienți și furnizori recommendations: List[str] - recomandări pentru îmbunătățire } """ try: async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Get schema 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 nu a fost găsită pentru id_firma {company_id}") schema = schema_result[0] # Construiește CTE pentru perioada curentă period_cte, period_params = DashboardService._build_period_cte(schema, luna, an) # Determină filtrele pentru perioada selectată (orizont de planificare) # Logică: Include TOATE restanțele + scadențele viitoare din perioada selectată if period == "7d": days_filter = "datascad <= TRUNC(SYSDATE) + 7" elif period == "1m": days_filter = "datascad <= TRUNC(SYSDATE) + 30" elif period == "3m": days_filter = "datascad <= TRUNC(SYSDATE) + 90" elif period == "6m": days_filter = "datascad <= TRUNC(SYSDATE) + 180" elif period == "12m": days_filter = "datascad <= TRUNC(SYSDATE) + 365" else: # "all" - toate soldurile days_filter = "1=1" # Query pentru clienți (facturi de încasat) clients_query = f""" {period_cte} SELECT client_name, SUM(amount) as amount, MAX(due_date) as due_date, MAX(days_overdue) as days_overdue FROM (SELECT vp.nume as client_name, ((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) as amount, NVL(vp.datascad, vp.DATAACT + 30) as due_date, TRUNC(SYSDATE) - NVL(vp.datascad, vp.DATAACT + 30) as days_overdue FROM {schema}.vireg_parteneri vp, luna_curenta lc WHERE vp.an = lc.anul AND vp.luna = lc.luna AND vp.cont IN ('4111', '461') AND vp.nume IS NOT NULL AND {days_filter} AND ((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) <> 0) GROUP BY client_name HAVING SUM (amount) <> 0 ORDER BY days_overdue desc """ cursor.execute(clients_query, period_params) clients_rows = cursor.fetchall() clients = [] for row in clients_rows: client_name = row[0] amount = float(row[1] or 0) due_date = row[2].strftime('%Y-%m-%d') if row[2] else None days_overdue = int(row[3] or 0) clients.append({ "name": client_name, "amount": amount, "dueDate": due_date, "daysOverdue": days_overdue }) # Sortare îmbunătățită: Restanțe primele (descrescător), apoi scadențe viitoare (crescător) clients.sort(key=lambda x: (-1 if x["daysOverdue"] > 0 else 1, -x["daysOverdue"] if x["daysOverdue"] > 0 else x["daysOverdue"])) # Query pentru furnizori (facturi de plătit) suppliers_query = f""" {period_cte} SELECT client_name, SUM(amount) as amount, MIN(due_date) as due_date, MAX(days_overdue) as days_overdue FROM (SELECT vp.nume as client_name, ((vp.preccred + vp.credit)-(vp.precdeb + vp.debit)) as amount, NVL(vp.datascad, vp.DATAACT + 30) as due_date, TRUNC(SYSDATE) - NVL(vp.datascad, vp.DATAACT + 30) as days_overdue FROM {schema}.vireg_parteneri vp, luna_curenta lc WHERE vp.an = lc.anul AND vp.luna = lc.luna AND vp.cont IN ('401', '404', '462') AND vp.nume IS NOT NULL AND {days_filter} AND ((vp.preccred + vp.credit)-(vp.precdeb + vp.debit)) <> 0) GROUP BY client_name HAVING SUM (amount) <> 0 ORDER BY days_overdue desc """ cursor.execute(suppliers_query, period_params) suppliers_rows = cursor.fetchall() suppliers = [] for row in suppliers_rows: supplier_name = row[0] amount = float(row[1] or 0) due_date = row[2].strftime('%Y-%m-%d') if row[2] else None days_overdue = int(row[3] or 0) suppliers.append({ "name": supplier_name, "amount": amount, "dueDate": due_date, "daysOverdue": days_overdue }) # Sortare îmbunătățită: Restanțe primele (descrescător), apoi scadențe viitoare (crescător) suppliers.sort(key=lambda x: (-1 if x["daysOverdue"] > 0 else 1, -x["daysOverdue"] if x["daysOverdue"] > 0 else x["daysOverdue"])) # Calculează balanța total_clients = sum(c["amount"] for c in clients) total_suppliers = sum(s["amount"] for s in suppliers) balance = total_clients - total_suppliers # Generează recomandări bazate pe date reale recommendations = [] if balance < -50000: recommendations.extend([ "Deficit de cash flow identificat - prioritizați încasările", "Negociați termeni de plată mai buni cu furnizorii", "Considerați finanțare pe termen scurt" ]) elif balance > 100000: recommendations.extend([ "Surplus de cash disponibil pentru investiții", "Considerați plăți anticipate pentru reduceri", "Evaluați oportunități de investire a excesului" ]) else: recommendations.append("Balanța cash flow este echilibrată") overdue_clients = [c for c in clients if c["daysOverdue"] > 0] if overdue_clients: total_overdue = sum(c["amount"] for c in overdue_clients) recommendations.append(f"Atenție: {len(overdue_clients)} clienți în restanță (total: {total_overdue:,.0f} RON)") overdue_suppliers = [s for s in suppliers if s["daysOverdue"] > 0] if overdue_suppliers: total_overdue = sum(s["amount"] for s in overdue_suppliers) recommendations.append(f"Urgent: {len(overdue_suppliers)} furnizori în restanță (total: {total_overdue:,.0f} RON)") # Adaugă recomandări specifice pentru clienți cu restanțe mari critical_clients = [c for c in overdue_clients if c["daysOverdue"] > 30] if critical_clients: recommendations.append(f"Critică: {len(critical_clients)} clienți cu restanțe peste 30 de zile") # Adaugă metadate pentru context complet metadata = { "period": period, "total_clients": len(clients), "total_suppliers": len(suppliers), "overdue_clients": len(overdue_clients), "overdue_suppliers": len(overdue_suppliers), "critical_clients": len(critical_clients) if critical_clients else 0, "total_overdue_amount_clients": sum(c["amount"] for c in overdue_clients) if overdue_clients else 0, "total_overdue_amount_suppliers": sum(s["amount"] for s in overdue_suppliers) if overdue_suppliers else 0 } return { "clients": clients, "suppliers": suppliers, "balance": balance, "recommendations": recommendations, "metadata": metadata } except Exception as e: logger.error(f"Error getting maturity analysis: {str(e)}") raise @staticmethod async def get_monthly_flows(company: int, luna: Optional[int] = None, an: Optional[int] = None) -> Dict[str, Any]: """ Obține fluxurile lunare de intrare și ieșire pentru luna curentă Args: company: ID-ul firmei luna: Luna contabilă (1-12), opțional an: Anul contabil, opțional """ try: async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Obține schema company_id = company 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 nu a fost găsită pentru id_firma {company_id}") schema = schema_result[0] # Construiește CTE pentru perioada curentă (cu period column) if luna is not None and an is not None: period_cte = f""" WITH luna_curenta AS ( SELECT :param_an as anul, :param_luna as luna, :param_an || '-' || LPAD(:param_luna, 2, '0') as period FROM DUAL )""" period_params = {'param_an': an, 'param_luna': luna} else: period_cte = f""" WITH luna_curenta AS ( SELECT anul, luna, anul || '-' || LPAD(luna, 2, '0') as period FROM {schema}.calendar WHERE anul * 12 + luna = (SELECT MAX(c2.anul * 12 + c2.luna) FROM {schema}.calendar c2) )""" period_params = {} # Query pentru fluxuri lunare flows_query = f""" {period_cte} SELECT SUM(CASE WHEN vp.cont IN ('4111', '461') THEN vp.credit WHEN vp.cont IN ('419') THEN vp.debit ELSE 0 END) as monthly_inflows, SUM(CASE WHEN vp.cont IN ('401', '404', '462') THEN vp.debit WHEN vp.cont IN ('409') THEN vp.credit ELSE 0 END) as monthly_outflows, MAX(lc.period) as period FROM {schema}.vireg_parteneri vp, luna_curenta lc WHERE vp.an = lc.anul AND vp.luna = lc.luna AND vp.cont IN ('4111', '461', '419', '401', '404', '462', '409') """ cursor.execute(flows_query, period_params) flow_row = cursor.fetchone() if not flow_row: # Default period from params or current date default_period = f"{an}-{str(luna).zfill(2)}" if luna and an else "2025-12" return { "inflows": 0, "outflows": 0, "period": default_period, "currency": "RON" } return { "inflows": float(flow_row[0] or 0), "outflows": float(flow_row[1] or 0), "period": flow_row[2] or "2025-08", "currency": "RON" } except Exception as e: logger.error(f"Error in get_monthly_flows: {str(e)}") raise @staticmethod @cached(cache_type='treasury_breakdown', key_params=['company', 'luna', 'an']) async def get_treasury_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]: """ Obține breakdown-ul trezoreriei pe casă și bancă (CACHED 30 min) Args: company: ID-ul firmei luna: Luna contabilă (1-12), opțional an: Anul contabil, opțional """ try: async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Obține schema company_id = company 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 nu a fost găsită pentru id_firma {company_id}") schema = schema_result[0] # Construiește CTE pentru perioada curentă period_cte, period_params = DashboardService._build_period_cte(schema, luna, an) # Query pentru breakdown trezorerie - cu nume reale și sub-breakdown treasury_query = f""" {period_cte} SELECT vb.cont, vb.nume as nume_real, (vb.solddeb - vb.soldcred) as sold, CASE WHEN vb.cont IN ('5311', '5314', '5328') THEN 'casa' WHEN vb.cont IN ('5121', '5124') THEN 'banca' END as tip FROM {schema}.vbalanta_parteneri vb, luna_curenta lc WHERE vb.an = lc.anul AND vb.luna = lc.luna AND vb.cont IN ('5311', '5314', '5328', '5121', '5124') AND (vb.solddeb - vb.soldcred) <> 0 ORDER BY tip, vb.cont """ cursor.execute(treasury_query, period_params) treasury_rows = cursor.fetchall() if not treasury_rows: return { "total": 0, "breakdown": { "casa": {"total": 0, "items": []}, "banca": {"total": 0, "items": []} }, "currency": "RON" } # Procesare rezultate cu grupare pe tip casa_items = [] banca_items = [] casa_total = 0 banca_total = 0 for row in treasury_rows: cont = row[0] nume_real = row[1] # Nume din vbalanta_parteneri.nume sold = float(row[2] or 0) tip = row[3] item = { "nume": nume_real or f"Cont {cont}", # Fallback la nume generic "cont": cont, "sold": sold } if tip == 'casa': casa_items.append(item) casa_total += sold else: # banca banca_items.append(item) banca_total += sold total = casa_total + banca_total return { "total": total, "breakdown": { "casa": { "total": casa_total, "items": casa_items }, "banca": { "total": banca_total, "items": banca_items } }, "currency": "RON" } except Exception as e: logger.error(f"Error in get_treasury_breakdown: {str(e)}") raise @staticmethod @cached(cache_type='net_balance_breakdown', key_params=['company', 'luna', 'an']) async def get_net_balance_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]: """ Obține breakdown-ul balanței nete pe clienți și furnizori cu detaliere pe perioade (CACHED 30 min) Args: company: ID-ul firmei luna: Luna contabilă (1-12), opțional an: Anul contabil, opțional """ try: async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Obține schema company_id = company 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 nu a fost găsită pentru id_firma {company_id}") schema = schema_result[0] # Construiește CTE pentru perioada curentă period_cte, period_params = DashboardService._build_period_cte(schema, luna, an) # Query extins pentru breakdown detaliat pe perioade balance_query = f""" {period_cte} SELECT -- CLIENȚI - Sold total SUM(CASE WHEN vp.cont IN ('4111','461') THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) WHEN vp.cont = '419' THEN -((vp.preccred + vp.credit) - (vp.precdeb + vp.debit)) ELSE 0 END) as clienti_total, -- CLIENȚI - În termen (total) SUM(CASE WHEN vp.cont IN ('4111','461') AND NVL(vp.datascad, vp.dataact + 30) >= TRUNC(SYSDATE) THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) WHEN vp.cont = '419' AND NVL(vp.datascad, vp.dataact + 30) >= TRUNC(SYSDATE) THEN -((vp.preccred + vp.credit) - (vp.precdeb + vp.debit)) ELSE 0 END) as clienti_in_termen, -- CLIENȚI - Restanți (total) SUM(CASE WHEN vp.cont IN ('4111','461') AND NVL(vp.datascad, vp.dataact + 30) < TRUNC(SYSDATE) THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) WHEN vp.cont = '419' AND NVL(vp.datascad, vp.dataact + 30) < TRUNC(SYSDATE) THEN -((vp.preccred + vp.credit) - (vp.precdeb + vp.debit)) ELSE 0 END) as clienti_restanti, -- CLIENȚI - Restanți pe perioade SUM(CASE WHEN vp.cont IN ('4111','461') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 1 AND 7 THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) ELSE 0 END) as clienti_restant_7, SUM(CASE WHEN vp.cont IN ('4111','461') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 8 AND 14 THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) ELSE 0 END) as clienti_restant_14, SUM(CASE WHEN vp.cont IN ('4111','461') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 15 AND 30 THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) ELSE 0 END) as clienti_restant_30, SUM(CASE WHEN vp.cont IN ('4111','461') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 31 AND 60 THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) ELSE 0 END) as clienti_restant_60, SUM(CASE WHEN vp.cont IN ('4111','461') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 61 AND 90 THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) ELSE 0 END) as clienti_restant_90, SUM(CASE WHEN vp.cont IN ('4111','461') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) > 90 THEN (vp.precdeb + vp.debit) - (vp.preccred + vp.credit) ELSE 0 END) as clienti_restant_90plus, -- FURNIZORI - Sold total SUM(CASE WHEN vp.cont IN ('401','404','462') THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) WHEN vp.cont IN ('409') THEN -((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) ELSE 0 END) as furnizori_total, -- FURNIZORI - În termen (total) SUM(CASE WHEN vp.cont IN ('401','404','462') AND NVL(vp.datascad, vp.dataact + 30) >= TRUNC(SYSDATE) THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) WHEN vp.cont IN ('409') AND NVL(vp.datascad, vp.dataact + 30) >= TRUNC(SYSDATE) THEN -((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) ELSE 0 END) as furnizori_in_termen, -- FURNIZORI - Restanți (total) SUM(CASE WHEN vp.cont IN ('401','404','462') AND NVL(vp.datascad, vp.dataact + 30) < TRUNC(SYSDATE) THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) WHEN vp.cont IN ('409') AND NVL(vp.datascad, vp.dataact + 30) < TRUNC(SYSDATE) THEN -((vp.precdeb + vp.debit) - (vp.preccred + vp.credit)) ELSE 0 END) as furnizori_restanti, -- FURNIZORI - Restanți pe perioade SUM(CASE WHEN vp.cont IN ('401','404','462') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 1 AND 7 THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) ELSE 0 END) as furnizori_restant_7, SUM(CASE WHEN vp.cont IN ('401','404','462') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 8 AND 14 THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) ELSE 0 END) as furnizori_restant_14, SUM(CASE WHEN vp.cont IN ('401','404','462') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 15 AND 30 THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) ELSE 0 END) as furnizori_restant_30, SUM(CASE WHEN vp.cont IN ('401','404','462') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 31 AND 60 THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) ELSE 0 END) as furnizori_restant_60, SUM(CASE WHEN vp.cont IN ('401','404','462') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) BETWEEN 61 AND 90 THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) ELSE 0 END) as furnizori_restant_90, SUM(CASE WHEN vp.cont IN ('401','404','462') AND TRUNC(SYSDATE) - NVL(vp.datascad, vp.dataact + 30) > 90 THEN (vp.preccred + vp.credit) - (vp.precdeb + vp.debit) ELSE 0 END) as furnizori_restant_90plus FROM {schema}.vireg_parteneri vp, luna_curenta lc WHERE vp.an = lc.anul AND vp.luna = lc.luna AND vp.cont IN ('4111', '461', '419', '401', '404', '462','409') """ cursor.execute(balance_query, period_params) row = cursor.fetchone() if not row: return { "clienti_total": 0, "furnizori_total": 0, "breakdown": { "clienti": { "total": 0, "in_termen": {"total": 0}, "restant": {"total": 0, "perioade": {}} }, "furnizori": { "total": 0, "in_termen": {"total": 0}, "restant": {"total": 0, "perioade": {}} } }, "currency": "RON" } # Procesare rezultate - INDEXARE CORECTATĂ # Coloane: 0-8 = clienti (9 coloane), 9-17 = furnizori (9 coloane) return { "clienti_total": float(row[0] or 0), "furnizori_total": float(row[9] or 0), # CORECTAT: row[9] nu row[10] "breakdown": { "clienti": { "total": float(row[0] or 0), "in_termen": { "total": float(row[1] or 0) }, "restant": { "total": float(row[2] or 0), "perioade": { "7_zile": float(row[3] or 0), "14_zile": float(row[4] or 0), "30_zile": float(row[5] or 0), "60_zile": float(row[6] or 0), "90_zile": float(row[7] or 0), "peste_90_zile": float(row[8] or 0) } } }, "furnizori": { "total": float(row[9] or 0), # CORECTAT: row[9] nu row[10] "in_termen": { "total": float(row[10] or 0) # CORECTAT: row[10] nu row[11] }, "restant": { "total": float(row[11] or 0), # CORECTAT: row[11] nu row[12] "perioade": { "7_zile": float(row[12] or 0), # CORECTAT "14_zile": float(row[13] or 0), # CORECTAT "30_zile": float(row[14] or 0), # CORECTAT "60_zile": float(row[15] or 0), # CORECTAT "90_zile": float(row[16] or 0), # CORECTAT "peste_90_zile": float(row[17] or 0) # CORECTAT } } } }, "currency": "RON" } except Exception as e: logger.error(f"Error in get_net_balance_breakdown: {str(e)}") raise @staticmethod async def get_current_period(company: int) -> Dict[str, Any]: """ Obține perioada curentă (an și lună) din calendarul Oracle Args: company: ID-ul companiei Returns: { year: int - anul curent month: int - luna curentă (1-12) period: str - perioada în format YYYY-MM } """ try: async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Obține schema company_id = company 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 nu a fost găsită pentru id_firma {company_id}") schema = schema_result[0] # Query pentru perioada curentă din calendar Oracle current_period_query = f""" SELECT anul, luna, anul || '-' || LPAD(luna, 2, '0') as period FROM {schema}.calendar WHERE anul*12+luna = (SELECT MAX(anul*12+luna) FROM {schema}.calendar) """ cursor.execute(current_period_query) period_row = cursor.fetchone() if not period_row: # Fallback la data curentă sistem from datetime import datetime now = datetime.now() return { "year": now.year, "month": now.month, "period": f"{now.year}-{now.month:02d}" } return { "year": int(period_row[0]), "month": int(period_row[1]), "period": period_row[2] } except Exception as e: logger.error(f"Error in get_current_period: {str(e)}") raise