""" Service pentru indicatori financiari - agregate din VBAL pentru calculul ratelor de lichiditate, eficiență, risc și Altman Z-Score Acest serviciu agregă soldurile din balanța de verificare (VBAL) pe clase de conturi conform Planului de Conturi General (PCG) românesc. """ import logging from decimal import Decimal from typing import Optional, Dict, List, Any from shared.database.oracle_pool import oracle_pool from ..cache.decorators import cached from ..models.dashboard import DashboardSummary from ..models.financial_indicators import ( BalanceSheetAggregates, IndicatorResult, LiquidityIndicators, EfficiencyIndicators, RiskIndicators, CashFlowIndicators, DynamicsIndicators, AltmanZScore, ProfitabilityIndicators, FinancialIndicatorsResponse ) logger = logging.getLogger(__name__) # Prefixele conturilor conform PCG (Planul de Conturi General) românesc # Folosite pentru agregarea soldurilor din VBAL ACCOUNT_GROUPS = { # ACTIVE IMOBILIZATE (Clasa 2) # 20 - Imobilizări necorporale # 21 - Imobilizări corporale # 23 - Imobilizări în curs # 26 - Imobilizări financiare # 28 - Amortizări (se scad - sold creditor) # 29 - Ajustări pentru depreciere (se scad - sold creditor) "active_imobilizate": { "debit": ["20", "21", "23", "26"], # Active brute (sold debitor) "credit": ["28", "29"] # Amortizări și ajustări (sold creditor - se scad) }, # STOCURI (Clasa 3) # 30 - Materii prime # 31 - Materiale consumabile # 32 - Obiecte de inventar # 33 - Producție în curs # 34 - Produse finite # 35 - Stocuri aflate la terți # 36 - Animale # 37 - Mărfuri # 38 - Ambalaje # 39 - Ajustări pentru depreciere (se scad - sold creditor) "stocuri": { "debit": ["30", "31", "32", "33", "34", "35", "36", "37", "38"], "credit": ["39"] # Ajustări depreciere stocuri }, # CREANȚE (din Clasa 4) # 4111 - Clienți # 413 - Efecte de primit # 418 - Clienți - facturi de întocmit # 425 - Avansuri acordate personalului # 428 - Alte datorii și creanțe în legătură cu personalul (dacă debitoare) # 431 - Asigurări sociale (dacă debitoare) # 437 - Ajutor de șomaj (dacă debitoare) # 4411 - Impozit pe profit (dacă debitor) # 4424 - TVA de recuperat # 444 - Impozit pe venituri de natură salarială (dacă debitor) # 445 - Subvenții (dacă debitoare) # 446 - Alte impozite și taxe (dacă debitoare) # 447 - Fonduri speciale (dacă debitoare) # 451 - Decontări între entitățile afiliate (dacă debitoare) # 453 - Decontări privind interesele de participare (dacă debitoare) # 456 - Decontări cu acționarii (dacă debitoare) # 461 - Debitori diverși # 473 - Decontări din operații în curs (dacă debitoare) "creante": { "debit": ["4111", "413", "418", "425", "4424", "461"], "credit": ["491", "496"] # Ajustări pentru deprecierea creanțelor }, # DISPONIBILITĂȚI (din Clasa 5) # 5121 - Conturi la bănci în lei # 5124 - Conturi la bănci în valută # 5311 - Casa în lei # 5314 - Casa în valută # 532 - Alte valori # 541 - Acreditive # 542 - Avansuri de trezorerie "disponibilitati": { "debit": ["5121", "5124", "5311", "5314", "532", "541", "542"], "credit": [] }, # CAPITAL PROPRIU (din Clasa 1) # 101 - Capital social # 104 - Prime de capital # 105 - Rezerve din reevaluare # 106 - Rezerve # 107 - Rezultatul reportat (poate fi debitor sau creditor) # 108 - Interese minoritare # 117 - Rezultatul reportat (cont distinct în unele planuri) # 121 - Profit sau pierdere (rezultatul exercițiului) "capital_propriu": { "debit": [], # Capitalurile sunt în general creditoare "credit": ["101", "104", "105", "106"] }, # REZULTAT (Profit/Pierdere) # 107 - Rezultatul reportat # 117 - Rezultatul reportat (în unele versiuni PCG) # 121 - Profit sau pierdere # 129 - Repartizarea profitului (se scade din profit) "rezultat": { "debit": ["107", "117", "129"], # Pierdere sau repartizare "credit": ["107", "117", "121"] # Profit }, # DATORII PE TERMEN LUNG (peste 1 an) # 161 - Împrumuturi din emisiuni de obligațiuni # 162 - Credite bancare pe termen lung # 166 - Datorii care privesc imobilizările financiare # 167 - Alte împrumuturi și datorii asimilate # 168 - Dobânzi aferente împrumuturilor și datoriilor asimilate # 169 - Prime privind rambursarea obligațiunilor "datorii_termen_lung": { "debit": [], "credit": ["161", "162", "166", "167", "168", "169"] }, # DATORII CURENTE (sub 1 an) # 401 - Furnizori # 403 - Efecte de plătit # 404 - Furnizori de imobilizări # 408 - Furnizori - facturi nesosite # 419 - Clienți - creditori # 421 - Personal - salarii datorate # 423 - Personal - ajutoare materiale datorate # 424 - Prime reprezentând participarea personalului la profit # 426 - Drepturi de personal neridicate # 427 - Rețineri din salarii datorate terților # 431 - Asigurări sociale (dacă creditoare) # 437 - Ajutor de șomaj (dacă creditoare) # 4411 - Impozit pe profit de plătit # 4423 - TVA de plată # 4428 - TVA neexigibilă # 444 - Impozit pe venituri salariale de plată # 446 - Alte impozite și taxe # 447 - Fonduri speciale # 462 - Creditori diverși # 509 - Vărsăminte de efectuat pentru investiții financiare pe termen scurt # 512 - Credite bancare pe termen scurt (5191) "datorii_curente": { "debit": [], "credit": ["401", "403", "404", "408", "419", "421", "423", "424", "426", "427", "4311", "4371", "4411", "4423", "4428", "444", "446", "447", "462", "509", "5191", "5192", "5198"] }, # VENITURI (Clasa 7) - pentru calculul EBIT în Altman Z-Score # 70 - Venituri din vânzarea produselor # 71 - Venituri din vânzarea mărfurilor # 72 - Venituri din producția de imobilizări # 74 - Venituri din subvenții de exploatare # 75 - Alte venituri din exploatare "venituri": { "debit": [], "credit": ["70", "701", "702", "703", "704", "705", "706", "707", "708", "71", "72", "74", "75", "758"] }, # CHELTUIELI OPERAȚIONALE (din Clasa 6, FĂRĂ 66x) - pentru calculul EBIT # 60 - Cheltuieli privind stocurile # 61 - Cheltuieli cu serviciile executate de terți # 62 - Cheltuieli cu alte servicii executate de terți # 63 - Cheltuieli cu alte impozite, taxe # 64 - Cheltuieli cu personalul # 65 - Alte cheltuieli de exploatare # 68 - Amortizări și provizioane (adăugat) "cheltuieli_operationale": { "debit": ["60", "601", "602", "603", "604", "605", "606", "607", "608", "61", "611", "612", "613", "614", "62", "621", "622", "623", "624", "625", "626", "627", "628", "63", "635", "64", "641", "642", "643", "644", "645", "65", "654", "658", "68", "681", "686"], "credit": [] }, # CHELTUIELI FINANCIARE (Clasa 66) - separat de cheltuieli operaționale # 661 - Cheltuieli privind titlurile de plasament cedate # 663 - Pierderi din creanțe legate de participații # 664 - Cheltuieli privind investițiile financiare cedate # 665 - Cheltuieli din diferențe de curs valutar # 666 - Cheltuieli privind dobânzile # 667 - Cheltuieli privind sconturile acordate # 668 - Alte cheltuieli financiare "cheltuieli_financiare": { "debit": ["66", "661", "663", "664", "665", "666", "667", "668"], "credit": [] } } class FinancialIndicatorsService: """ Service pentru calculul indicatorilor financiari din balanța de verificare. Agregă soldurile din VBAL pe categorii de conturi pentru: - Active imobilizate - Stocuri - Creanțe - Disponibilități - Capital propriu - Rezultat - Datorii pe termen lung - Datorii curente - Venituri - Cheltuieli operaționale """ @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) Schema este stocată permanent în cache deoarece nu se schimbă. """ 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 def _build_aggregate_case(prefixes: list[str], column: str) -> str: """ Construiește expresie CASE pentru agregarea soldurilor pe prefixe de conturi. Args: prefixes: Lista de prefixe de conturi (ex: ["20", "21", "23"]) column: Coloana de sumat (SOLDDEB sau SOLDCRED) Returns: Expresie SQL CASE pentru SUM """ if not prefixes: return "0" conditions = " OR ".join([f"CONT LIKE '{prefix}%'" for prefix in prefixes]) return f"SUM(CASE WHEN ({conditions}) THEN NVL({column}, 0) ELSE 0 END)" @staticmethod @cached(cache_type='fin_balance_sheet', key_params=['company_id', 'luna', 'an']) async def get_balance_sheet_aggregates( company_id: int, luna: int, an: int ) -> BalanceSheetAggregates: """ Obține soldurile agregate din balanța de verificare pentru calculul indicatorilor financiari (CACHED 30 min). Agregă soldurile din VBAL pe categorii de conturi folosind prefixe conform Planului de Conturi General (PCG) românesc. Args: company_id: ID-ul firmei luna: Luna contabilă (1-12) an: Anul contabil Returns: BalanceSheetAggregates cu soldurile agregate pentru fiecare categorie Raises: ValueError: Dacă schema nu este găsită pentru firma specificată """ schema = await FinancialIndicatorsService._get_schema(company_id) async with oracle_pool.get_connection() as connection: with connection.cursor() as cursor: # Construim query-ul cu CASE pentru fiecare categorie # Soldurile din VBAL: SOLDDEB (sold debitor), SOLDCRED (sold creditor) # # Pentru active: valoarea = SOLDDEB - amortizări (SOLDCRED pentru 28, 29) # Pentru pasive: valoarea = SOLDCRED # Pentru venituri: valoarea = SOLDCRED (conturile de venituri sunt creditoare) # Pentru cheltuieli: valoarea = SOLDDEB (conturile de cheltuieli sunt debitoare) query = f""" SELECT -- ACTIVE IMOBILIZATE (brut - amortizări - ajustări) {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["active_imobilizate"]["debit"], "SOLDDEB" )} - {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["active_imobilizate"]["credit"], "SOLDCRED" )} as active_imobilizate, -- STOCURI (brut - ajustări depreciere) {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["stocuri"]["debit"], "SOLDDEB" )} - {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["stocuri"]["credit"], "SOLDCRED" )} as stocuri, -- CREANȚE (brut - ajustări depreciere) {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["creante"]["debit"], "SOLDDEB" )} - {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["creante"]["credit"], "SOLDCRED" )} as creante, -- DISPONIBILITĂȚI {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["disponibilitati"]["debit"], "SOLDDEB" )} as disponibilitati, -- CAPITAL PROPRIU (sold creditor) {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["capital_propriu"]["credit"], "SOLDCRED" )} as capital_propriu, -- REZULTAT (credit - debit: profit dacă pozitiv, pierdere dacă negativ) -- Conturile 107, 117, 121 pot avea sold fie debitor (pierdere) fie creditor (profit) -- 129 (repartizare profit) este debitor și se scade ({FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["rezultat"]["credit"], "SOLDCRED" )} - {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["rezultat"]["debit"], "SOLDDEB" )}) as rezultat, -- DATORII TERMEN LUNG (sold creditor) {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["datorii_termen_lung"]["credit"], "SOLDCRED" )} as datorii_termen_lung, -- DATORII CURENTE (sold creditor) {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["datorii_curente"]["credit"], "SOLDCRED" )} as datorii_curente, -- VENITURI (sold creditor - pentru calcul EBIT) {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["venituri"]["credit"], "SOLDCRED" )} as venituri, -- CHELTUIELI OPERAȚIONALE (sold debitor - pentru calcul EBIT) {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["cheltuieli_operationale"]["debit"], "SOLDDEB" )} as cheltuieli_operationale, -- CHELTUIELI FINANCIARE (sold debitor - Clasa 66) {FinancialIndicatorsService._build_aggregate_case( ACCOUNT_GROUPS["cheltuieli_financiare"]["debit"], "SOLDDEB" )} as cheltuieli_financiare FROM {schema}.VBAL WHERE AN = :an AND LUNA = :luna """ params = {'an': an, 'luna': luna} cursor.execute(query, params) row = cursor.fetchone() if not row: # Returnăm agregate cu valori zero dacă nu există date logger.warning( f"No VBAL data for company {company_id}, " f"luna={luna}, an={an}" ) return BalanceSheetAggregates( company_id=company_id, luna=luna, an=an, active_imobilizate=Decimal('0'), stocuri=Decimal('0'), creante=Decimal('0'), disponibilitati=Decimal('0'), capital_propriu=Decimal('0'), rezultat=Decimal('0'), datorii_termen_lung=Decimal('0'), datorii_curente=Decimal('0'), venituri=Decimal('0'), cheltuieli_operationale=Decimal('0'), cheltuieli_financiare=Decimal('0') ) # Construim modelul cu valorile agregate aggregates = BalanceSheetAggregates( company_id=company_id, luna=luna, an=an, active_imobilizate=Decimal(str(row[0] or 0)), stocuri=Decimal(str(row[1] or 0)), creante=Decimal(str(row[2] or 0)), disponibilitati=Decimal(str(row[3] or 0)), capital_propriu=Decimal(str(row[4] or 0)), rezultat=Decimal(str(row[5] or 0)), datorii_termen_lung=Decimal(str(row[6] or 0)), datorii_curente=Decimal(str(row[7] or 0)), venituri=Decimal(str(row[8] or 0)), cheltuieli_operationale=Decimal(str(row[9] or 0)), cheltuieli_financiare=Decimal(str(row[10] or 0)) ) logger.info( f"Financial aggregates for company {company_id}, " f"luna={luna}, an={an}: " f"active_imobilizate={aggregates.active_imobilizate}, " f"stocuri={aggregates.stocuri}, " f"disponibilitati={aggregates.disponibilitati}, " f"datorii_curente={aggregates.datorii_curente}" ) return aggregates @staticmethod def _calculate_indicator_status( value: float, good_threshold: float, warning_threshold: float, higher_is_better: bool = True ) -> str: """ Determină statusul unui indicator pe baza pragurilor. Args: value: Valoarea indicatorului good_threshold: Pragul pentru status 'good' warning_threshold: Pragul pentru status 'warning' higher_is_better: True dacă valori mai mari sunt mai bune Returns: Status: 'good', 'warning', sau 'danger' """ if higher_is_better: if value >= good_threshold: return "good" elif value >= warning_threshold: return "warning" else: return "danger" else: # Pentru indicatori unde valori mai mici sunt mai bune (ex: DSO) if value <= good_threshold: return "good" elif value <= warning_threshold: return "warning" else: return "danger" @staticmethod async def calculate_liquidity_indicators( company_id: int, luna: int, an: int ) -> LiquidityIndicators: """ Calculează indicatorii de lichiditate pentru evaluarea capacității de plată a datoriilor pe termen scurt. Indicatori calculați: - Lichiditate curentă (Current Ratio) = active_curente / datorii_curente - Lichiditate imediată (Quick Ratio) = (disponibilități + creanțe) / datorii_curente - Lichiditate la vedere (Cash Ratio) = disponibilități / datorii_curente Praguri de referință (conform standardelor bancare): - Lichiditate curentă: good >= 2.0, warning 1.0-2.0, danger < 1.0 - Lichiditate imediată: good >= 1.0, warning 0.5-1.0, danger < 0.5 - Lichiditate la vedere: good >= 0.2, warning 0.1-0.2, danger < 0.1 Args: company_id: ID-ul firmei luna: Luna contabilă (1-12) an: Anul contabil Returns: LiquidityIndicators cu cele trei rate de lichiditate """ # Obținem agregatele din balanță aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates( company_id, luna, an ) # Ensure aggregates is a BalanceSheetAggregates model (cache may return dict) if isinstance(aggregates, dict): aggregates = BalanceSheetAggregates(**aggregates) # Calculăm active curente (stocuri + creanțe + disponibilități) active_curente = float(aggregates.active_curente) disponibilitati = float(aggregates.disponibilitati) creante = float(aggregates.creante) datorii_curente = float(aggregates.datorii_curente) # Handle cazul special: datorii_curente = 0 if datorii_curente == 0: # Compania nu are datorii pe termen scurt - situație excelentă # Returnăm None pentru valoare dar status "good" cu mesaj explicativ no_debt_message = "Fără datorii curente - lichiditate maximă" return LiquidityIndicators( lichiditate_curenta=IndicatorResult( value=None, status="good", threshold_min=2.0, threshold_max=None, message=no_debt_message ), lichiditate_imediata=IndicatorResult( value=None, status="good", threshold_min=1.0, threshold_max=None, message=no_debt_message ), lichiditate_vedere=IndicatorResult( value=None, status="good", threshold_min=0.2, threshold_max=None, message=no_debt_message ) ) # Calculăm ratele de lichiditate # 1. Lichiditate curentă (Current Ratio) lichiditate_curenta_val = active_curente / datorii_curente lichiditate_curenta_status = FinancialIndicatorsService._calculate_indicator_status( lichiditate_curenta_val, good_threshold=2.0, warning_threshold=1.0 ) # 2. Lichiditate imediată (Quick Ratio) # Exclude stocurile - măsoară lichiditatea "rapidă" lichiditate_imediata_val = (disponibilitati + creante) / datorii_curente lichiditate_imediata_status = FinancialIndicatorsService._calculate_indicator_status( lichiditate_imediata_val, good_threshold=1.0, warning_threshold=0.5 ) # 3. Lichiditate la vedere (Cash Ratio) # Doar disponibilități - lichiditatea imediată lichiditate_vedere_val = disponibilitati / datorii_curente lichiditate_vedere_status = FinancialIndicatorsService._calculate_indicator_status( lichiditate_vedere_val, good_threshold=0.2, warning_threshold=0.1 ) # Rotunjim valorile la 2 zecimale pentru afișare result = LiquidityIndicators( lichiditate_curenta=IndicatorResult( value=round(lichiditate_curenta_val, 2), status=lichiditate_curenta_status, threshold_min=2.0, threshold_max=None ), lichiditate_imediata=IndicatorResult( value=round(lichiditate_imediata_val, 2), status=lichiditate_imediata_status, threshold_min=1.0, threshold_max=None ), lichiditate_vedere=IndicatorResult( value=round(lichiditate_vedere_val, 2), status=lichiditate_vedere_status, threshold_min=0.2, threshold_max=None ) ) logger.info( f"Liquidity indicators for company {company_id}, luna={luna}, an={an}: " f"curenta={result.lichiditate_curenta.value} ({result.lichiditate_curenta.status}), " f"imediata={result.lichiditate_imediata.value} ({result.lichiditate_imediata.status}), " f"vedere={result.lichiditate_vedere.value} ({result.lichiditate_vedere.status})" ) return result @staticmethod @cached(cache_type='fin_efficiency', key_params=['company_id', 'luna', 'an']) async def calculate_efficiency_indicators( company_id: int, luna: int, an: int ) -> EfficiencyIndicators: """ Calculează indicatorii de eficiență pentru evaluarea vitezei de conversie a resurselor în numerar. Indicatori calculați: - DSO (Days Sales Outstanding) = (clienti_sold / facturari_lunare) * 30 - DPO (Days Payables Outstanding) = (furnizori_sold / achizitii_lunare) * 30 - Cash Conversion Cycle = DSO - DPO - Rata încasare = incasari / facturari * 100 - Rata plată = plati / achizitii * 100 Praguri de referință pentru DSO: - Good: < 30 zile (încasare rapidă) - Warning: 30-45 zile (încasare moderată) - Danger: > 45 zile (încasare lentă - risc de cash flow) Args: company_id: ID-ul firmei luna: Luna contabilă (1-12) an: Anul contabil Returns: EfficiencyIndicators cu cele cinci rate de eficiență """ # Import DashboardService here to avoid circular imports from .dashboard_service import DashboardService # Obținem datele din summary (solduri clienți/furnizori) summary = await DashboardService.get_complete_summary( company=str(company_id), username="system", # System call for indicators luna=luna, an=an ) # Ensure summary is a DashboardSummary model (cache may return dict) if isinstance(summary, dict): summary = DashboardSummary(**summary) # Obținem datele din trends (facturări/încasări/achiziții/plăți lunare) trends = await DashboardService.get_trends( company_id=company_id, period='12m' # Ultimele 12 luni pentru media lunară ) # Extragem soldurile din summary clienti_sold = float(summary.clienti_sold_total) furnizori_sold = float(summary.furnizori_sold_total) # Extragem datele lunare din trends # Folosim ultima lună disponibilă pentru facturări/încasări/achiziții/plăți clienti_facturat = trends.get("clienti_facturat", []) clienti_incasat = trends.get("clienti_incasat", []) furnizori_facturat = trends.get("furnizori_facturat", []) furnizori_achitat = trends.get("furnizori_achitat", []) # Calculăm media lunară (pentru stabilitate) sau folosim ultima lună # Pentru DSO/DPO, folosim media ultimelor 3 luni pentru a evita fluctuații def safe_avg(values: list, n: int = 3) -> float: """Calculează media ultimelor n valori, sau toate dacă sunt mai puține""" if not values: return 0 recent = values[-n:] if len(values) >= n else values return sum(recent) / len(recent) if recent else 0 facturari_lunare = safe_avg(clienti_facturat) incasari_lunare = safe_avg(clienti_incasat) achizitii_lunare = safe_avg(furnizori_facturat) plati_lunare = safe_avg(furnizori_achitat) # Calculăm indicatorii # 1. DSO (Days Sales Outstanding) - Durata medie de încasare # Formula: (clienti_sold / facturari_lunare) * 30 if facturari_lunare > 0: dso_val = (clienti_sold / facturari_lunare) * 30 dso_status = FinancialIndicatorsService._calculate_indicator_status( dso_val, good_threshold=30, warning_threshold=45, higher_is_better=False # Pentru DSO, mai mic e mai bine ) dso = IndicatorResult( value=round(dso_val, 1), status=dso_status, threshold_min=None, threshold_max=30 # Good if <= 30 days ) else: dso = IndicatorResult( value=None, status="warning", threshold_min=None, threshold_max=30, message="Fără facturări în perioada analizată" ) # 2. DPO (Days Payables Outstanding) - Durata medie de plată # Formula: (furnizori_sold / achizitii_lunare) * 30 if achizitii_lunare > 0: dpo_val = (furnizori_sold / achizitii_lunare) * 30 # Pentru DPO nu avem praguri stricte - depinde de strategia firmei # Un DPO mai mare înseamnă că folosim creditul furnizorilor dpo = IndicatorResult( value=round(dpo_val, 1), status="good", # DPO nu are praguri de "danger" threshold_min=None, threshold_max=None, message="Durata medie de plată furnizori" ) else: dpo = IndicatorResult( value=None, status="warning", threshold_min=None, threshold_max=None, message="Fără achiziții în perioada analizată" ) # 3. Cash Conversion Cycle = DSO - DPO # Negativ = firma se finanțează din creditul furnizorilor (favorabil) # Pozitiv = cash blocat în ciclul de afaceri if dso.value is not None and dpo.value is not None: ccc_val = dso.value - dpo.value # CCC negativ e bun (ne finanțăm din creditul furnizorilor) ccc_status = FinancialIndicatorsService._calculate_indicator_status( ccc_val, good_threshold=0, warning_threshold=15, higher_is_better=False ) ccc_message = ( "Ciclu negativ = finanțare din creditul furnizorilor" if ccc_val < 0 else "Numerar blocat în ciclul de afaceri" ) cash_conversion_cycle = IndicatorResult( value=round(ccc_val, 1), status=ccc_status, threshold_min=None, threshold_max=0, message=ccc_message ) else: cash_conversion_cycle = IndicatorResult( value=None, status="warning", threshold_min=None, threshold_max=0, message="Nu se poate calcula - lipsesc date DSO/DPO" ) # 4. Rata încasare = incasari / facturari * 100 # Calculăm pentru perioada YTD din trends (suma totală) total_facturari = sum(clienti_facturat) if clienti_facturat else 0 total_incasari = sum(clienti_incasat) if clienti_incasat else 0 if total_facturari > 0: rata_incasare_val = (total_incasari / total_facturari) * 100 rata_incasare_status = FinancialIndicatorsService._calculate_indicator_status( rata_incasare_val, good_threshold=95, warning_threshold=80 ) rata_incasare = IndicatorResult( value=round(rata_incasare_val, 1), status=rata_incasare_status, threshold_min=95, threshold_max=None ) else: rata_incasare = IndicatorResult( value=None, status="warning", threshold_min=95, threshold_max=None, message="Fără facturări în perioada analizată" ) # 5. Rata plată = plati / achizitii * 100 total_achizitii = sum(furnizori_facturat) if furnizori_facturat else 0 total_plati = sum(furnizori_achitat) if furnizori_achitat else 0 if total_achizitii > 0: rata_plata_val = (total_plati / total_achizitii) * 100 # Pentru rata de plată, nu există "danger" - depinde de strategia firmei rata_plata = IndicatorResult( value=round(rata_plata_val, 1), status="good", # Informativ, fără praguri stricte threshold_min=None, threshold_max=None ) else: rata_plata = IndicatorResult( value=None, status="warning", threshold_min=None, threshold_max=None, message="Fără achiziții în perioada analizată" ) result = EfficiencyIndicators( dso=dso, dpo=dpo, cash_conversion_cycle=cash_conversion_cycle, rata_incasare=rata_incasare, rata_plata=rata_plata ) logger.info( f"Efficiency indicators for company {company_id}, luna={luna}, an={an}: " f"DSO={dso.value} ({dso.status}), " f"DPO={dpo.value}, " f"CCC={cash_conversion_cycle.value} ({cash_conversion_cycle.status}), " f"rata_incasare={rata_incasare.value}% ({rata_incasare.status})" ) return result @staticmethod @cached(cache_type='fin_risk', key_params=['company_id', 'luna', 'an']) async def calculate_risk_indicators( company_id: int, luna: int, an: int ) -> RiskIndicators: """ Calculează indicatorii de risc și aging pentru evaluarea sănătății portofoliului de creanțe și datorii. Indicatori calculați: - creante_restante_pct: Procentul creanțelor restante din total clienți Formula: clienti_sold_restant / clienti_sold_total * 100 Good: < 20%, Warning: 20-30%, Danger: > 30% - creante_90plus_pct: Procentul creanțelor restante > 90 zile din total Formula: clienti_restant_90plus / clienti_sold_total * 100 Good: < 5%, Warning: 5-10%, Danger: > 10% - datorii_restante_pct: Procentul datoriilor restante din total furnizori Formula: furnizori_sold_restant / furnizori_sold_total * 100 Good: < 10%, Warning: 10-20%, Danger: > 20% - raport_datorii_trezorerie: Raportul între datorii furnizori și trezorerie Formula: furnizori_sold_total / trezorerie Good: < 2, Warning: 2-4, Danger: > 4 Args: company_id: ID-ul firmei luna: Luna contabilă (1-12) an: Anul contabil Returns: RiskIndicators cu cei patru indicatori de risc """ # Import DashboardService here to avoid circular imports from .dashboard_service import DashboardService # Obținem datele din summary (solduri clienți/furnizori/aging) summary = await DashboardService.get_complete_summary( company=str(company_id), username="system", # System call for indicators luna=luna, an=an ) # Ensure summary is a DashboardSummary model (cache may return dict) if isinstance(summary, dict): summary = DashboardSummary(**summary) # Extragem soldurile din summary clienti_sold_total = float(summary.clienti_sold_total) clienti_sold_restant = float(summary.clienti_sold_restant) clienti_restant_90plus = float(summary.clienti_restant_90plus) furnizori_sold_total = float(summary.furnizori_sold_total) furnizori_sold_restant = float(summary.furnizori_sold_restant) # Calculăm trezoreria totală din treasury_totals_by_currency (sumă pe toate valutele) trezorerie = sum(float(v) for v in summary.treasury_totals_by_currency.values()) # 1. Creanțe restante % - procentul din solduri clienți care sunt restante if clienti_sold_total > 0: creante_restante_val = (clienti_sold_restant / clienti_sold_total) * 100 creante_restante_status = FinancialIndicatorsService._calculate_indicator_status( creante_restante_val, good_threshold=20, warning_threshold=30, higher_is_better=False # Mai mic e mai bine ) creante_restante_pct = IndicatorResult( value=round(creante_restante_val, 1), status=creante_restante_status, threshold_min=None, threshold_max=20 ) else: creante_restante_pct = IndicatorResult( value=None, status="good", threshold_min=None, threshold_max=20, message="Fără sold clienți" ) # 2. Creanțe 90+ zile % - creanțe cu risc mare de nerecuperare if clienti_sold_total > 0: creante_90plus_val = (clienti_restant_90plus / clienti_sold_total) * 100 creante_90plus_status = FinancialIndicatorsService._calculate_indicator_status( creante_90plus_val, good_threshold=5, warning_threshold=10, higher_is_better=False # Mai mic e mai bine ) creante_90plus_pct = IndicatorResult( value=round(creante_90plus_val, 1), status=creante_90plus_status, threshold_min=None, threshold_max=5 ) else: creante_90plus_pct = IndicatorResult( value=None, status="good", threshold_min=None, threshold_max=5, message="Fără sold clienți" ) # 3. Datorii restante % - procentul din solduri furnizori care sunt restante if furnizori_sold_total > 0: datorii_restante_val = (furnizori_sold_restant / furnizori_sold_total) * 100 datorii_restante_status = FinancialIndicatorsService._calculate_indicator_status( datorii_restante_val, good_threshold=10, warning_threshold=20, higher_is_better=False # Mai mic e mai bine ) datorii_restante_pct = IndicatorResult( value=round(datorii_restante_val, 1), status=datorii_restante_status, threshold_min=None, threshold_max=10 ) else: datorii_restante_pct = IndicatorResult( value=None, status="good", threshold_min=None, threshold_max=10, message="Fără sold furnizori" ) # 4. Raport datorii/trezorerie - câte unități de cash trebuie pentru a plăti furnizorii if trezorerie > 0: raport_val = furnizori_sold_total / trezorerie raport_status = FinancialIndicatorsService._calculate_indicator_status( raport_val, good_threshold=2, warning_threshold=4, higher_is_better=False # Mai mic e mai bine (mai puțin datorie per cash) ) raport_message = ( "Trezorerie suficientă pentru acoperirea datoriilor" if raport_val < 2 else "Trezorerie insuficientă pentru datorii" if raport_val > 4 else "Trezorerie moderată pentru datorii" ) raport_datorii_trezorerie = IndicatorResult( value=round(raport_val, 2), status=raport_status, threshold_min=None, threshold_max=2, message=raport_message ) elif furnizori_sold_total == 0: # Fără datorii furnizori - situație bună raport_datorii_trezorerie = IndicatorResult( value=0, status="good", threshold_min=None, threshold_max=2, message="Fără datorii furnizori" ) else: # Trezorerie 0 dar avem datorii - situație critică raport_datorii_trezorerie = IndicatorResult( value=None, status="danger", threshold_min=None, threshold_max=2, message="Trezorerie zero - nu se poate acoperi datoriile" ) result = RiskIndicators( creante_restante_pct=creante_restante_pct, creante_90plus_pct=creante_90plus_pct, datorii_restante_pct=datorii_restante_pct, raport_datorii_trezorerie=raport_datorii_trezorerie ) logger.info( f"Risk indicators for company {company_id}, luna={luna}, an={an}: " f"creante_restante={creante_restante_pct.value}% ({creante_restante_pct.status}), " f"creante_90plus={creante_90plus_pct.value}% ({creante_90plus_pct.status}), " f"datorii_restante={datorii_restante_pct.value}% ({datorii_restante_pct.status}), " f"raport_datorii_trez={raport_datorii_trezorerie.value} ({raport_datorii_trezorerie.status})" ) return result @staticmethod @cached(cache_type='fin_cashflow', key_params=['company_id', 'luna', 'an']) async def calculate_cashflow_indicators( company_id: int, luna: int, an: int ) -> CashFlowIndicators: """ Calculează indicatorii de cash flow pentru evaluarea generării și consumului de numerar. Indicatori calculați: - flux_net_lunar: Încasări luna - plăți luna (fluxul net de numerar lunar) Good: > 0 (firma generează numerar), Danger: < 0 (firma consumă numerar) - cash_flow_ytd: Suma fluxurilor nete de la ianuarie până la luna curentă Arată tendința generală a anului în curs - flux_net_yoy_pct: Variația procentuală an-la-an Formula: (cf_curent - cf_anterior) / abs(cf_anterior) * 100 - acoperire_cash_flow: Cash flow YTD / datorii restante Good: > 1 (cash flow suficient), Warning: 0.5-1, Danger: < 0.5 Args: company_id: ID-ul firmei luna: Luna contabilă (1-12) an: Anul contabil Returns: CashFlowIndicators cu cei patru indicatori de cash flow """ # Import DashboardService here to avoid circular imports from .dashboard_service import DashboardService # Obținem datele din trends pentru încasări/plăți istorice # Folosim perioada 'ytd' pentru a obține datele de la începutul anului trends = await DashboardService.get_trends( company_id=company_id, period='ytd', luna=luna, an=an ) # Obținem datele din summary pentru datorii restante summary = await DashboardService.get_complete_summary( company=str(company_id), username="system", luna=luna, an=an ) # Ensure summary is a DashboardSummary model (cache may return dict) if isinstance(summary, dict): summary = DashboardSummary(**summary) # Extragem arrayurile din trends clienti_incasat = trends.get("clienti_incasat", []) furnizori_achitat = trends.get("furnizori_achitat", []) clienti_incasat_prev = trends.get("clienti_incasat_prev", []) furnizori_achitat_prev = trends.get("furnizori_achitat_prev", []) # Calculăm fluxurile nete lunare (încasări - plăți) pentru fiecare lună fluxuri_nete = [ (inc - plat) for inc, plat in zip(clienti_incasat, furnizori_achitat) ] if clienti_incasat and furnizori_achitat else [] # Calculăm fluxurile nete pentru anul anterior fluxuri_nete_prev = [ (inc - plat) for inc, plat in zip(clienti_incasat_prev, furnizori_achitat_prev) ] if clienti_incasat_prev and furnizori_achitat_prev else [] # 1. Flux net lunar (ultima lună disponibilă) if fluxuri_nete: flux_net_val = fluxuri_nete[-1] # Ultima lună (luna curentă) flux_net_status = "good" if flux_net_val > 0 else "danger" flux_net_message = ( "Firma generează numerar" if flux_net_val > 0 else "Firma consumă numerar" ) flux_net_lunar = IndicatorResult( value=round(flux_net_val, 2), status=flux_net_status, threshold_min=0, threshold_max=None, message=flux_net_message ) else: flux_net_lunar = IndicatorResult( value=None, status="warning", threshold_min=0, threshold_max=None, message="Fără date de cash flow pentru perioada selectată" ) # 2. Cash flow YTD (suma fluxurilor de la ianuarie până la luna curentă) if fluxuri_nete: cf_ytd_val = sum(fluxuri_nete) cf_ytd_status = "good" if cf_ytd_val > 0 else "danger" cash_flow_ytd = IndicatorResult( value=round(cf_ytd_val, 2), status=cf_ytd_status, threshold_min=0, threshold_max=None ) else: cf_ytd_val = 0 cash_flow_ytd = IndicatorResult( value=None, status="warning", threshold_min=0, threshold_max=None, message="Fără date de cash flow YTD" ) # 3. Flux net YoY % (variația an-la-an) # Comparăm cash flow-ul YTD curent cu cel din aceeași perioadă a anului anterior if fluxuri_nete and fluxuri_nete_prev: cf_curent = sum(fluxuri_nete) # Luăm același număr de luni din anul anterior pentru comparație corectă num_months = len(fluxuri_nete) cf_anterior = sum(fluxuri_nete_prev[:num_months]) if len(fluxuri_nete_prev) >= num_months else sum(fluxuri_nete_prev) if abs(cf_anterior) > 0: yoy_pct = ((cf_curent - cf_anterior) / abs(cf_anterior)) * 100 yoy_status = "good" if yoy_pct >= 0 else "danger" yoy_message = ( "Creștere cash flow față de anul anterior" if yoy_pct >= 0 else "Scădere cash flow față de anul anterior" ) flux_net_yoy_pct = IndicatorResult( value=round(yoy_pct, 1), status=yoy_status, threshold_min=0, threshold_max=None, message=yoy_message ) else: # Cash flow anterior era zero if cf_curent > 0: flux_net_yoy_pct = IndicatorResult( value=100.0, status="good", threshold_min=0, threshold_max=None, message="Cash flow pozitiv vs zero anul anterior" ) elif cf_curent < 0: flux_net_yoy_pct = IndicatorResult( value=-100.0, status="danger", threshold_min=0, threshold_max=None, message="Cash flow negativ vs zero anul anterior" ) else: flux_net_yoy_pct = IndicatorResult( value=0.0, status="warning", threshold_min=0, threshold_max=None, message="Cash flow zero în ambii ani" ) else: flux_net_yoy_pct = IndicatorResult( value=None, status="warning", threshold_min=0, threshold_max=None, message="Fără date pentru comparație YoY" ) # 4. Acoperire cash flow = cash_flow_ytd / datorii_restante # Datoriile restante sunt furnizori_sold_restant din summary datorii_restante = float(summary.furnizori_sold_restant) if datorii_restante > 0 and cf_ytd_val is not None: acoperire_val = cf_ytd_val / datorii_restante acoperire_status = FinancialIndicatorsService._calculate_indicator_status( acoperire_val, good_threshold=1.0, warning_threshold=0.5 ) acoperire_message = ( "Cash flow suficient pentru acoperirea datoriilor restante" if acoperire_val >= 1.0 else "Cash flow insuficient pentru acoperirea datoriilor restante" if acoperire_val < 0.5 else "Cash flow parțial pentru datorii restante" ) acoperire_cash_flow = IndicatorResult( value=round(acoperire_val, 2), status=acoperire_status, threshold_min=1.0, threshold_max=None, message=acoperire_message ) elif datorii_restante == 0: # Fără datorii restante - situație excelentă acoperire_cash_flow = IndicatorResult( value=None, status="good", threshold_min=1.0, threshold_max=None, message="Fără datorii restante - nu este nevoie de acoperire" ) else: # Nu avem date de cash flow acoperire_cash_flow = IndicatorResult( value=None, status="warning", threshold_min=1.0, threshold_max=None, message="Nu se poate calcula acoperirea cash flow" ) result = CashFlowIndicators( flux_net_lunar=flux_net_lunar, cash_flow_ytd=cash_flow_ytd, flux_net_yoy_pct=flux_net_yoy_pct, acoperire_cash_flow=acoperire_cash_flow ) logger.info( f"Cash flow indicators for company {company_id}, luna={luna}, an={an}: " f"flux_net_lunar={flux_net_lunar.value} ({flux_net_lunar.status}), " f"cash_flow_ytd={cash_flow_ytd.value} ({cash_flow_ytd.status}), " f"flux_net_yoy_pct={flux_net_yoy_pct.value}% ({flux_net_yoy_pct.status}), " f"acoperire_cf={acoperire_cash_flow.value} ({acoperire_cash_flow.status})" ) return result @staticmethod @cached(cache_type='fin_dynamics', key_params=['company_id', 'luna', 'an']) async def calculate_dynamics_indicators( company_id: int, luna: int, an: int ) -> DynamicsIndicators: """ Calculează indicatorii de dinamică pentru evaluarea evoluției afacerii. Compară vânzările și achizițiile cu anul anterior (YoY - Year-over-Year) pentru a determina dacă afacerea crește sau scade. Indicatori calculați: - crestere_vanzari_yoy: Creșterea procentuală a facturărilor față de anul anterior Formula: (facturari_curent - facturari_anterior) / facturari_anterior * 100 Good: > 5%, Warning: 0-5%, Danger: < 0% - crestere_achizitii_yoy: Creșterea procentuală a achizițiilor față de anul anterior Formula: (achizitii_curent - achizitii_anterior) / achizitii_anterior * 100 Informativ - creșterea achizițiilor poate indica expansiune - marja_implicita: Marja implicită din diferența facturări - achiziții Formula: (facturari - achizitii) / facturari * 100 Good: > 20%, Warning: 10-20%, Danger: < 10% Args: company_id: ID-ul firmei luna: Luna contabilă (1-12) an: Anul contabil Returns: DynamicsIndicators cu cei trei indicatori de dinamică """ # Import DashboardService here to avoid circular imports from .dashboard_service import DashboardService # Obținem datele din trends pentru perioada curentă (YTD) și anul anterior trends = await DashboardService.get_trends( company_id=company_id, period='ytd', luna=luna, an=an ) # Extragem arrayurile din trends - facturări și achiziții # clienti_facturat = facturări (vânzări) # furnizori_facturat = achiziții clienti_facturat = trends.get("clienti_facturat", []) clienti_facturat_prev = trends.get("clienti_facturat_prev", []) furnizori_facturat = trends.get("furnizori_facturat", []) furnizori_facturat_prev = trends.get("furnizori_facturat_prev", []) # Calculăm totalurile pentru perioada curentă și anterioară # Luăm același număr de luni pentru comparație corectă YoY num_months = len(clienti_facturat) # Total facturări (vânzări) an curent și anterior total_facturari_curent = sum(clienti_facturat) if clienti_facturat else 0 total_facturari_anterior = ( sum(clienti_facturat_prev[:num_months]) if len(clienti_facturat_prev) >= num_months else sum(clienti_facturat_prev) ) if clienti_facturat_prev else 0 # Total achiziții an curent și anterior total_achizitii_curent = sum(furnizori_facturat) if furnizori_facturat else 0 total_achizitii_anterior = ( sum(furnizori_facturat_prev[:num_months]) if len(furnizori_facturat_prev) >= num_months else sum(furnizori_facturat_prev) ) if furnizori_facturat_prev else 0 # 1. Creștere vânzări YoY # Formula: (facturari_curent - facturari_anterior) / facturari_anterior * 100 if total_facturari_anterior > 0: crestere_vanzari_val = ( (total_facturari_curent - total_facturari_anterior) / total_facturari_anterior * 100 ) # Status: good > 5%, warning 0-5%, danger < 0% crestere_vanzari_status = FinancialIndicatorsService._calculate_indicator_status( crestere_vanzari_val, good_threshold=5.0, warning_threshold=0.0 ) crestere_vanzari_message = ( "Creștere semnificativă a vânzărilor" if crestere_vanzari_val > 5 else "Vânzări stabile" if crestere_vanzari_val >= 0 else "Vânzări în scădere" ) crestere_vanzari_yoy = IndicatorResult( value=round(crestere_vanzari_val, 1), status=crestere_vanzari_status, threshold_min=5.0, threshold_max=None, message=crestere_vanzari_message ) elif total_facturari_curent > 0: # Anul anterior nu avea facturări, dar anul curent da crestere_vanzari_yoy = IndicatorResult( value=100.0, status="good", threshold_min=5.0, threshold_max=None, message="Vânzări noi - nu existau în anul anterior" ) else: # Fără facturări în niciun an crestere_vanzari_yoy = IndicatorResult( value=None, status="warning", threshold_min=5.0, threshold_max=None, message="Fără date de facturări pentru comparație" ) # 2. Creștere achiziții YoY # Formula: (achizitii_curent - achizitii_anterior) / achizitii_anterior * 100 if total_achizitii_anterior > 0: crestere_achizitii_val = ( (total_achizitii_curent - total_achizitii_anterior) / total_achizitii_anterior * 100 ) # Pentru achiziții, creșterea poate fi neutră (expansiune) sau negativă (reducere) # Nu există un "danger" clar - e informativ crestere_achizitii_message = ( "Achiziții în creștere - posibilă expansiune" if crestere_achizitii_val > 5 else "Achiziții stabile" if crestere_achizitii_val >= -5 else "Achiziții în scădere" ) crestere_achizitii_yoy = IndicatorResult( value=round(crestere_achizitii_val, 1), status="good", # Informativ - nu are praguri stricte threshold_min=None, threshold_max=None, message=crestere_achizitii_message ) elif total_achizitii_curent > 0: crestere_achizitii_yoy = IndicatorResult( value=100.0, status="good", threshold_min=None, threshold_max=None, message="Achiziții noi - nu existau în anul anterior" ) else: crestere_achizitii_yoy = IndicatorResult( value=None, status="warning", threshold_min=None, threshold_max=None, message="Fără date de achiziții pentru comparație" ) # 3. Marja implicită # Formula: (facturari - achizitii) / facturari * 100 # Arată ce procent din vânzări rămâne după achiziții if total_facturari_curent > 0: marja_val = ( (total_facturari_curent - total_achizitii_curent) / total_facturari_curent * 100 ) # Status: good > 20%, warning 10-20%, danger < 10% marja_status = FinancialIndicatorsService._calculate_indicator_status( marja_val, good_threshold=20.0, warning_threshold=10.0 ) marja_message = ( "Marjă implicită sănătoasă" if marja_val > 20 else "Marjă implicită moderată" if marja_val >= 10 else "Marjă implicită redusă - verificați costurile" ) marja_implicita = IndicatorResult( value=round(marja_val, 1), status=marja_status, threshold_min=20.0, threshold_max=None, message=marja_message ) else: marja_implicita = IndicatorResult( value=None, status="warning", threshold_min=20.0, threshold_max=None, message="Fără facturări - nu se poate calcula marja" ) result = DynamicsIndicators( crestere_vanzari_yoy=crestere_vanzari_yoy, crestere_achizitii_yoy=crestere_achizitii_yoy, marja_implicita=marja_implicita ) logger.info( f"Dynamics indicators for company {company_id}, luna={luna}, an={an}: " f"crestere_vanzari_yoy={crestere_vanzari_yoy.value}% ({crestere_vanzari_yoy.status}), " f"crestere_achizitii_yoy={crestere_achizitii_yoy.value}% ({crestere_achizitii_yoy.status}), " f"marja_implicita={marja_implicita.value}% ({marja_implicita.status})" ) return result @staticmethod @cached(cache_type='fin_altman', key_params=['company_id', 'luna', 'an']) async def calculate_altman_zscore( company_id: int, luna: int, an: int ) -> AltmanZScore: """ Calculează Altman Z-Score pentru evaluarea riscului de faliment. Folosim formula modificată pentru companii private (Z'-Score): Z' = 6.56*X1 + 3.26*X2 + 6.72*X3 + 1.05*X4 Aceasta este versiunea pentru companii care nu sunt listate la bursă, unde se folosește valoarea contabilă a capitalurilor proprii (Book Value) în loc de valoarea de piață a acțiunilor. Componente: - X1 = Working Capital / Total Assets (lichiditate pe termen scurt) - X2 = Retained Earnings / Total Assets (profitabilitate cumulată) - X3 = EBIT / Total Assets (eficiență operațională) - X4 = Book Value of Equity / Total Liabilities (solvabilitate) Zone de risc: - Safe (Z > 2.60): Risc minim de faliment - situație financiară solidă - Grey (1.10 <= Z <= 2.60): Zona de incertitudine - necesită monitorizare - Distress (Z < 1.10): Risc ridicat de faliment - situație critică Args: company_id: ID-ul firmei luna: Luna contabilă (1-12) an: Anul contabil Returns: AltmanZScore cu scorul calculat și componentele individuale x1, x2, x3, x4 """ # Obținem agregatele din balanță aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates( company_id, luna, an ) # Ensure aggregates is a BalanceSheetAggregates model (cache may return dict) if isinstance(aggregates, dict): aggregates = BalanceSheetAggregates(**aggregates) # Calculăm componentele necesare # Working Capital = active_curente - datorii_curente working_capital = float(aggregates.working_capital) # Total Assets = active_imobilizate + active_curente total_assets = float(aggregates.total_active) # Rezultat reportat (cont 117 + 121) - deja agregat în `rezultat` rezultat_reportat = float(aggregates.rezultat) # EBIT = venituri - cheltuieli_operationale ebit = float(aggregates.ebit) # Capital propriu (inclusiv rezultat) pentru X4 capital_propriu = float(aggregates.capitaluri_proprii) # Total datorii (curente + termen lung) total_datorii = float(aggregates.total_datorii) datorii_curente = float(aggregates.datorii_curente) datorii_termen_lung = float(aggregates.datorii_termen_lung) # Verificăm dacă avem date suficiente pentru calcul if total_assets == 0: # Nu putem calcula Z-Score fără active return AltmanZScore( zscore=IndicatorResult( value=None, status="warning", threshold_min=2.60, threshold_max=None, message="Nu se poate calcula - total active este zero" ), x1=IndicatorResult(value=None, status="warning"), x2=IndicatorResult(value=None, status="warning"), x3=IndicatorResult(value=None, status="warning"), x4=IndicatorResult(value=None, status="warning"), capital_de_lucru=IndicatorResult( value=round(working_capital, 2), status="warning", message="Active Curente - Datorii Curente" ), active_totale=IndicatorResult( value=0, status="warning", message="Nu există active în balanță" ), datorii_totale=IndicatorResult( value=round(total_datorii, 2), status="warning", message="Datorii Curente + Datorii Termen Lung" ) ) # Calculăm componentele X1-X4 # X1 = Working Capital / Total Assets # Măsoară lichiditatea - cât de mult din active este finanțat din surse pe termen scurt x1_val = working_capital / total_assets x1_status = "good" if x1_val > 0 else "danger" x1 = IndicatorResult( value=round(x1_val, 4), status=x1_status, threshold_min=0, threshold_max=None, message="Lichiditate pe termen scurt" if x1_val > 0 else "Working capital negativ" ) # X2 = Retained Earnings / Total Assets # Măsoară profitabilitatea cumulată - câștigurile reinvestite în companie x2_val = rezultat_reportat / total_assets x2_status = "good" if x2_val > 0 else ("warning" if x2_val == 0 else "danger") x2 = IndicatorResult( value=round(x2_val, 4), status=x2_status, threshold_min=0, threshold_max=None, message="Profitabilitate cumulată" if x2_val >= 0 else "Pierderi cumulate" ) # X3 = EBIT / Total Assets # Măsoară eficiența operațională - randamentul activelor x3_val = ebit / total_assets x3_status = "good" if x3_val > 0 else ("warning" if x3_val == 0 else "danger") x3 = IndicatorResult( value=round(x3_val, 4), status=x3_status, threshold_min=0, threshold_max=None, message="Eficiență operațională" if x3_val >= 0 else "Pierdere operațională" ) # X4 = Book Value of Equity / Total Liabilities # Măsoară solvabilitatea - acoperirea datoriilor cu capital propriu if total_datorii > 0: x4_val = capital_propriu / total_datorii x4_status = FinancialIndicatorsService._calculate_indicator_status( x4_val, good_threshold=1.0, warning_threshold=0.5 ) x4 = IndicatorResult( value=round(x4_val, 4), status=x4_status, threshold_min=1.0, threshold_max=None, message="Solvabilitate bună" if x4_val >= 1 else "Îndatorare ridicată" ) else: # Fără datorii - situație excelentă pentru solvabilitate x4_val = None # Infinit teoretic, dar nu putem reprezenta x4 = IndicatorResult( value=None, status="good", threshold_min=1.0, threshold_max=None, message="Fără datorii - solvabilitate maximă" ) # Calculăm Z-Score folosind formula pentru companii private # Z' = 6.56*X1 + 3.26*X2 + 6.72*X3 + 1.05*X4 if x4_val is not None: zscore_val = ( 6.56 * x1_val + 3.26 * x2_val + 6.72 * x3_val + 1.05 * x4_val ) else: # Dacă X4 este infinit (fără datorii), calculăm Z-Score fără componenta X4 # În practică, firmele fără datorii au un Z-Score foarte bun # Folosim o valoare foarte mare pentru X4 (ex: 10) ca proxy zscore_val = ( 6.56 * x1_val + 3.26 * x2_val + 6.72 * x3_val + 1.05 * 10.0 # Proxy pentru solvabilitate maximă ) # Determinăm zona de risc if zscore_val > 2.60: zscore_status = "safe" zscore_message = "Zona sigură - risc minim de faliment" elif zscore_val >= 1.10: zscore_status = "grey" zscore_message = "Zona gri - necesită monitorizare atentă" else: zscore_status = "distress" zscore_message = "Zona de risc - risc ridicat de faliment" zscore = IndicatorResult( value=round(zscore_val, 2), status=zscore_status, threshold_min=2.60, threshold_max=None, message=zscore_message ) # Indicatori de bază pentru verificare manuală în balanță capital_de_lucru = IndicatorResult( value=round(working_capital, 2), status="good" if working_capital > 0 else "danger", threshold_min=0, threshold_max=None, message="Active Curente - Datorii Curente" ) active_totale_ind = IndicatorResult( value=round(total_assets, 2), status="good", threshold_min=None, threshold_max=None, message="Active Imobilizate + Active Curente" ) datorii_totale_ind = IndicatorResult( value=round(total_datorii, 2), status="good", threshold_min=None, threshold_max=None, message="Datorii Curente + Datorii Termen Lung" ) result = AltmanZScore( zscore=zscore, x1=x1, x2=x2, x3=x3, x4=x4, capital_de_lucru=capital_de_lucru, active_totale=active_totale_ind, datorii_totale=datorii_totale_ind ) logger.info( f"Altman Z-Score for company {company_id}, luna={luna}, an={an}: " f"zscore={zscore.value} ({zscore.status}), " f"X1={x1.value}, X2={x2.value}, X3={x3.value}, X4={x4.value}" ) return result @staticmethod @cached(cache_type='fin_profitability', key_params=['company_id', 'luna', 'an']) async def calculate_profitability_indicators( company_id: int, luna: int, an: int ) -> ProfitabilityIndicators: """ Calculează indicatorii de profitabilitate pentru evaluarea randamentului afacerii. Indicatori calculați: - cifra_afaceri: Total venituri operaționale (Clasa 7) - cheltuieli_totale: Total cheltuieli operaționale (Clasa 6) - profit_brut: EBIT = venituri - cheltuieli - marja_profit_brut: profit / venituri * 100 - roa: Return on Assets = profit / total_active * 100 - roe: Return on Equity = profit / capitaluri_proprii * 100 Praguri de referință: - Marja profit: good > 10%, warning 5-10%, danger < 5% - ROA: good > 5%, warning 2-5%, danger < 2% - ROE: good > 10%, warning 5-10%, danger < 5% Args: company_id: ID-ul firmei luna: Luna contabilă (1-12) an: Anul contabil Returns: ProfitabilityIndicators cu cei șase indicatori de profitabilitate """ # Obținem agregatele din balanță (include venituri, cheltuieli, active, capital) aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates( company_id, luna, an ) # Ensure aggregates is a BalanceSheetAggregates model (cache may return dict) if isinstance(aggregates, dict): aggregates = BalanceSheetAggregates(**aggregates) # Extragem valorile necesare venituri = float(aggregates.venituri) cheltuieli_oper = float(aggregates.cheltuieli_operationale) cheltuieli_fin = float(aggregates.cheltuieli_financiare) cheltuieli_total = cheltuieli_oper + cheltuieli_fin profit_brut_val = float(aggregates.ebit) # EBIT = venituri - cheltuieli operaționale total_active = float(aggregates.total_active) capitaluri_proprii_val = float(aggregates.capitaluri_proprii) # 1. Cifra de afaceri (venituri totale) - informativ cifra_afaceri = IndicatorResult( value=round(venituri, 2), status="good", # Informativ - nu are praguri threshold_min=None, threshold_max=None, message="Total venituri din activitatea operațională" ) # 2. Cheltuieli operaționale (fără dobânzi 66x) - pentru verificare cheltuieli_operationale = IndicatorResult( value=round(cheltuieli_oper, 2), status="good", # Informativ - nu are praguri threshold_min=None, threshold_max=None, message="Clasa 60x-65x + 68x (fără dobânzi)" ) # 3. Cheltuieli financiare (66x) - pentru verificare cheltuieli_financiare = IndicatorResult( value=round(cheltuieli_fin, 2), status="good", # Informativ - nu are praguri threshold_min=None, threshold_max=None, message="Clasa 66x (dobânzi, diferențe curs)" ) # 4. Cheltuieli totale (operaționale + financiare) cheltuieli_totale = IndicatorResult( value=round(cheltuieli_total, 2), status="good", # Informativ - nu are praguri threshold_min=None, threshold_max=None, message="Operaționale + Financiare" ) # 3. Profit brut (EBIT) profit_status = "good" if profit_brut_val > 0 else "danger" profit_message = ( "Profit operațional pozitiv" if profit_brut_val > 0 else "Pierdere operațională - costuri depășesc veniturile" ) profit_brut = IndicatorResult( value=round(profit_brut_val, 2), status=profit_status, threshold_min=0, threshold_max=None, message=profit_message ) # 4. Marja de profit = profit / venituri * 100 if venituri > 0: marja_val = (profit_brut_val / venituri) * 100 marja_status = FinancialIndicatorsService._calculate_indicator_status( marja_val, good_threshold=10.0, warning_threshold=5.0 ) marja_message = ( "Marjă de profit sănătoasă" if marja_val > 10 else "Marjă de profit moderată" if marja_val >= 5 else "Marjă de profit scăzută - verificați costurile" ) marja_profit_brut = IndicatorResult( value=round(marja_val, 1), status=marja_status, threshold_min=10.0, threshold_max=None, message=marja_message ) else: marja_profit_brut = IndicatorResult( value=None, status="warning", threshold_min=10.0, threshold_max=None, message="Fără venituri - nu se poate calcula marja" ) # 5. ROA (Return on Assets) = profit / total_active * 100 if total_active > 0: roa_val = (profit_brut_val / total_active) * 100 roa_status = FinancialIndicatorsService._calculate_indicator_status( roa_val, good_threshold=5.0, warning_threshold=2.0 ) roa_message = ( "Randament bun al activelor" if roa_val > 5 else "Randament moderat al activelor" if roa_val >= 2 else "Randament scăzut al activelor" ) roa = IndicatorResult( value=round(roa_val, 2), status=roa_status, threshold_min=5.0, threshold_max=None, message=roa_message ) else: roa = IndicatorResult( value=None, status="warning", threshold_min=5.0, threshold_max=None, message="Fără active - nu se poate calcula ROA" ) # Indicatori de bază pentru verificare manuală în balanță active_totale = IndicatorResult( value=round(total_active, 2), status="good", threshold_min=None, threshold_max=None, message="Active Imobilizate + Active Curente (bază calcul ROA)" ) capitaluri_proprii = IndicatorResult( value=round(capitaluri_proprii_val, 2), status="good" if capitaluri_proprii_val > 0 else "danger", threshold_min=None, threshold_max=None, message="Capital Social + Rezultat (bază calcul ROE)" ) # ROE (Return on Equity) = profit / capitaluri_proprii * 100 if capitaluri_proprii_val > 0: roe_val = (profit_brut_val / capitaluri_proprii_val) * 100 roe_status = FinancialIndicatorsService._calculate_indicator_status( roe_val, good_threshold=10.0, warning_threshold=5.0 ) roe_message = ( "Randament atractiv pentru acționari" if roe_val > 10 else "Randament moderat pentru acționari" if roe_val >= 5 else "Randament scăzut pentru acționari" ) roe = IndicatorResult( value=round(roe_val, 2), status=roe_status, threshold_min=10.0, threshold_max=None, message=roe_message ) elif capitaluri_proprii_val <= 0 and profit_brut_val > 0: # Capital negativ dar profit pozitiv - situație neobișnuită roe = IndicatorResult( value=None, status="danger", threshold_min=10.0, threshold_max=None, message="Capital propriu negativ sau zero - situație de risc" ) else: roe = IndicatorResult( value=None, status="warning", threshold_min=10.0, threshold_max=None, message="Nu se poate calcula ROE" ) result = ProfitabilityIndicators( cifra_afaceri=cifra_afaceri, cheltuieli_operationale=cheltuieli_operationale, cheltuieli_financiare=cheltuieli_financiare, cheltuieli_totale=cheltuieli_totale, profit_brut=profit_brut, marja_profit_brut=marja_profit_brut, active_totale=active_totale, capitaluri_proprii=capitaluri_proprii, roa=roa, roe=roe ) logger.info( f"Profitability indicators for company {company_id}, luna={luna}, an={an}: " f"cifra_afaceri={cifra_afaceri.value}, " f"profit_brut={profit_brut.value} ({profit_brut.status}), " f"marja={marja_profit_brut.value}% ({marja_profit_brut.status}), " f"roa={roa.value}% ({roa.status}), " f"roe={roe.value}% ({roe.status})" ) return result @staticmethod def _generate_month_labels(luna: int, an: int, months: int = 12) -> List[str]: """ Generează etichetele lunilor în format 'MMM YY' pentru sparkline. Generează etichete pentru ultimele `months` luni, terminând cu luna/an specificată. Folosim abrevieri în engleză pentru consistență internațională. Args: luna: Luna finală (1-12) an: Anul final months: Numărul de luni de generat (default 12) Returns: Lista de etichete în format 'MMM YY' (ex: ['Feb 24', 'Mar 24', ...]) """ MONTH_ABBR = [ 'Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Iul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] labels = [] current_luna = luna current_an = an # Generăm etichetele de la luna curentă înapoi for _ in range(months): year_short = str(current_an)[-2:] # Ultimele 2 cifre ale anului label = f"{MONTH_ABBR[current_luna - 1]} {year_short}" labels.insert(0, label) # Inserăm la început pentru ordine cronologică # Mergem la luna anterioară current_luna -= 1 if current_luna < 1: current_luna = 12 current_an -= 1 return labels @staticmethod def _get_historical_periods(luna: int, an: int, months: int = 12) -> List[tuple]: """ Generează lista de perioade (luna, an) pentru ultimele `months` luni. Args: luna: Luna finală (1-12) an: Anul final months: Numărul de luni (default 12) Returns: Lista de tuple (luna, an) în ordine cronologică """ periods = [] current_luna = luna current_an = an for _ in range(months): periods.insert(0, (current_luna, current_an)) current_luna -= 1 if current_luna < 1: current_luna = 12 current_an -= 1 return periods @staticmethod @cached(cache_type='financial_indicators_historical', ttl=3600, key_params=['company_id', 'months', 'luna', 'an']) async def get_historical_indicators( company_id: int, months: int = 12, luna: Optional[int] = None, an: Optional[int] = None ) -> Dict[str, Any]: """ Calculează indicatorii financiari pentru ultimele `months` luni și returnează datele pentru sparklines. Această metodă este optimizată pentru performanță cu cache separat (TTL 1 oră) deoarece datele istorice se schimbă rar. Args: company_id: ID-ul firmei months: Numărul de luni pentru istoric (default 12) luna: Luna de referință (dacă None, folosește luna curentă) an: Anul de referință (dacă None, folosește anul curent) Returns: Dict cu: - sparkline_labels: Array cu etichetele lunilor - lichiditate: Dict cu sparkline_data pentru fiecare indicator - eficienta: Dict cu sparkline_data pentru fiecare indicator - risc: Dict cu sparkline_data pentru fiecare indicator - cash_flow: Dict cu sparkline_data pentru fiecare indicator - dinamica: Dict cu sparkline_data pentru fiecare indicator - altman_zscore: Dict cu sparkline_data pentru fiecare indicator """ from datetime import datetime # Dacă luna/an nu sunt specificate, folosim data curentă resolved_luna: int resolved_an: int if luna is None or an is None: try: from .dashboard_service import DashboardService current_period = await DashboardService.get_current_period(company_id) resolved_luna = luna if luna is not None else current_period.get('luna', datetime.now().month) resolved_an = an if an is not None else current_period.get('an', datetime.now().year) except Exception as e: logger.warning(f"Could not get current period: {e}, using defaults") resolved_luna = luna if luna is not None else datetime.now().month resolved_an = an if an is not None else datetime.now().year else: resolved_luna = luna resolved_an = an # Generăm perioadele și etichetele periods = FinancialIndicatorsService._get_historical_periods(resolved_luna, resolved_an, months) labels = FinancialIndicatorsService._generate_month_labels(resolved_luna, resolved_an, months) # Inițializăm structurile pentru sparkline data historical_data = { 'sparkline_labels': labels, 'lichiditate': { 'lichiditate_curenta': [], 'lichiditate_imediata': [], 'lichiditate_vedere': [] }, 'eficienta': { 'dso': [], 'dpo': [], 'cash_conversion_cycle': [], 'rata_incasare': [], 'rata_plata': [] }, 'risc': { 'creante_restante_pct': [], 'creante_90plus_pct': [], 'datorii_restante_pct': [], 'raport_datorii_trezorerie': [] }, 'cash_flow': { 'flux_net_lunar': [], 'cash_flow_ytd': [], 'flux_net_yoy_pct': [], 'acoperire_cash_flow': [] }, 'dinamica': { 'crestere_vanzari_yoy': [], 'crestere_achizitii_yoy': [], 'marja_implicita': [] }, 'altman_zscore': { 'zscore': [], 'x1': [], 'x2': [], 'x3': [], 'x4': [] }, 'profitabilitate': { 'cifra_afaceri': [], 'cheltuieli_totale': [], 'profit_brut': [], 'marja_profit_brut': [], 'roa': [], 'roe': [] } } # Calculăm indicatorii pentru fiecare perioadă all_categories = ['lichiditate', 'eficienta', 'risc', 'cash_flow', 'dinamica', 'altman_zscore', 'profitabilitate'] for period_luna, period_an in periods: # Track which categories were successfully processed in this period processed_categories = set() try: # Lichiditate lichiditate = await FinancialIndicatorsService.calculate_liquidity_indicators( company_id, period_luna, period_an ) # Ensure lichiditate is a model (cache may return dict) if isinstance(lichiditate, dict): lichiditate = LiquidityIndicators(**lichiditate) historical_data['lichiditate']['lichiditate_curenta'].append( lichiditate.lichiditate_curenta.value ) historical_data['lichiditate']['lichiditate_imediata'].append( lichiditate.lichiditate_imediata.value ) historical_data['lichiditate']['lichiditate_vedere'].append( lichiditate.lichiditate_vedere.value ) processed_categories.add('lichiditate') # Eficiență eficienta = await FinancialIndicatorsService.calculate_efficiency_indicators( company_id, period_luna, period_an ) # Ensure eficienta is a model (cache may return dict) if isinstance(eficienta, dict): eficienta = EfficiencyIndicators(**eficienta) historical_data['eficienta']['dso'].append(eficienta.dso.value) historical_data['eficienta']['dpo'].append(eficienta.dpo.value) historical_data['eficienta']['cash_conversion_cycle'].append( eficienta.cash_conversion_cycle.value ) historical_data['eficienta']['rata_incasare'].append(eficienta.rata_incasare.value) historical_data['eficienta']['rata_plata'].append(eficienta.rata_plata.value) processed_categories.add('eficienta') # Risc risc = await FinancialIndicatorsService.calculate_risk_indicators( company_id, period_luna, period_an ) # Ensure risc is a model (cache may return dict) if isinstance(risc, dict): risc = RiskIndicators(**risc) historical_data['risc']['creante_restante_pct'].append( risc.creante_restante_pct.value ) historical_data['risc']['creante_90plus_pct'].append(risc.creante_90plus_pct.value) historical_data['risc']['datorii_restante_pct'].append( risc.datorii_restante_pct.value ) historical_data['risc']['raport_datorii_trezorerie'].append( risc.raport_datorii_trezorerie.value ) processed_categories.add('risc') # Cash Flow cash_flow = await FinancialIndicatorsService.calculate_cashflow_indicators( company_id, period_luna, period_an ) # Ensure cash_flow is a model (cache may return dict) if isinstance(cash_flow, dict): cash_flow = CashFlowIndicators(**cash_flow) historical_data['cash_flow']['flux_net_lunar'].append(cash_flow.flux_net_lunar.value) historical_data['cash_flow']['cash_flow_ytd'].append(cash_flow.cash_flow_ytd.value) historical_data['cash_flow']['flux_net_yoy_pct'].append( cash_flow.flux_net_yoy_pct.value ) historical_data['cash_flow']['acoperire_cash_flow'].append( cash_flow.acoperire_cash_flow.value ) processed_categories.add('cash_flow') # Dinamica dinamica = await FinancialIndicatorsService.calculate_dynamics_indicators( company_id, period_luna, period_an ) # Ensure dinamica is a model (cache may return dict) if isinstance(dinamica, dict): dinamica = DynamicsIndicators(**dinamica) historical_data['dinamica']['crestere_vanzari_yoy'].append( dinamica.crestere_vanzari_yoy.value ) historical_data['dinamica']['crestere_achizitii_yoy'].append( dinamica.crestere_achizitii_yoy.value ) historical_data['dinamica']['marja_implicita'].append(dinamica.marja_implicita.value) processed_categories.add('dinamica') # Altman Z-Score altman = await FinancialIndicatorsService.calculate_altman_zscore( company_id, period_luna, period_an ) # Ensure altman is a model (cache may return dict) if isinstance(altman, dict): altman = AltmanZScore(**altman) historical_data['altman_zscore']['zscore'].append(altman.zscore.value) historical_data['altman_zscore']['x1'].append(altman.x1.value) historical_data['altman_zscore']['x2'].append(altman.x2.value) historical_data['altman_zscore']['x3'].append(altman.x3.value) historical_data['altman_zscore']['x4'].append(altman.x4.value) processed_categories.add('altman_zscore') # Profitabilitate profitabilitate = await FinancialIndicatorsService.calculate_profitability_indicators( company_id, period_luna, period_an ) # Ensure profitabilitate is a model (cache may return dict) if isinstance(profitabilitate, dict): profitabilitate = ProfitabilityIndicators(**profitabilitate) historical_data['profitabilitate']['cifra_afaceri'].append( profitabilitate.cifra_afaceri.value ) historical_data['profitabilitate']['cheltuieli_totale'].append( profitabilitate.cheltuieli_totale.value ) historical_data['profitabilitate']['profit_brut'].append( profitabilitate.profit_brut.value ) historical_data['profitabilitate']['marja_profit_brut'].append( profitabilitate.marja_profit_brut.value ) historical_data['profitabilitate']['roa'].append(profitabilitate.roa.value) historical_data['profitabilitate']['roe'].append(profitabilitate.roe.value) processed_categories.add('profitabilitate') except Exception as e: logger.warning( f"Error calculating indicators for company {company_id}, " f"luna={period_luna}, an={period_an}: {e}" ) # Add None ONLY for categories that were NOT successfully processed # This prevents duplicate entries when an exception occurs mid-way for category in all_categories: if category not in processed_categories: for indicator in historical_data[category]: historical_data[category][indicator].append(None) logger.info( f"Historical indicators for company {company_id}: " f"{months} months ending {resolved_luna}/{resolved_an}" ) return historical_data @staticmethod async def get_indicators_with_sparklines( company_id: int, luna: int, an: int, months: int = 12 ) -> FinancialIndicatorsResponse: """ Calculează toți indicatorii financiari și adaugă datele de sparkline pentru vizualizarea trendului pe ultimele luni. Această metodă combină calculele curente ale indicatorilor cu datele istorice pentru sparklines. Args: company_id: ID-ul firmei luna: Luna contabilă (1-12) an: Anul contabil months: Numărul de luni pentru sparkline (default 12) Returns: FinancialIndicatorsResponse cu sparkline_data integrat în fiecare indicator """ import asyncio # Obținem datele istorice și indicatorii curenți în paralel historical_task = FinancialIndicatorsService.get_historical_indicators( company_id, months, luna, an ) lichiditate_task = FinancialIndicatorsService.calculate_liquidity_indicators( company_id, luna, an ) eficienta_task = FinancialIndicatorsService.calculate_efficiency_indicators( company_id, luna, an ) risc_task = FinancialIndicatorsService.calculate_risk_indicators( company_id, luna, an ) cash_flow_task = FinancialIndicatorsService.calculate_cashflow_indicators( company_id, luna, an ) dinamica_task = FinancialIndicatorsService.calculate_dynamics_indicators( company_id, luna, an ) altman_task = FinancialIndicatorsService.calculate_altman_zscore( company_id, luna, an ) profitabilitate_task = FinancialIndicatorsService.calculate_profitability_indicators( company_id, luna, an ) ( historical_data, lichiditate, eficienta, risc, cash_flow, dinamica, altman_zscore, profitabilitate ) = await asyncio.gather( historical_task, lichiditate_task, eficienta_task, risc_task, cash_flow_task, dinamica_task, altman_task, profitabilitate_task ) # Ensure all indicator results are models (cache may return dicts) if isinstance(lichiditate, dict): lichiditate = LiquidityIndicators(**lichiditate) if isinstance(eficienta, dict): eficienta = EfficiencyIndicators(**eficienta) if isinstance(risc, dict): risc = RiskIndicators(**risc) if isinstance(cash_flow, dict): cash_flow = CashFlowIndicators(**cash_flow) if isinstance(dinamica, dict): dinamica = DynamicsIndicators(**dinamica) if isinstance(altman_zscore, dict): altman_zscore = AltmanZScore(**altman_zscore) if isinstance(profitabilitate, dict): profitabilitate = ProfitabilityIndicators(**profitabilitate) # Extragem etichetele comune sparkline_labels = historical_data['sparkline_labels'] # Actualizăm indicatorii de lichiditate cu sparkline data lichiditate.lichiditate_curenta.sparkline_data = historical_data['lichiditate']['lichiditate_curenta'] lichiditate.lichiditate_curenta.sparkline_labels = sparkline_labels lichiditate.lichiditate_imediata.sparkline_data = historical_data['lichiditate']['lichiditate_imediata'] lichiditate.lichiditate_imediata.sparkline_labels = sparkline_labels lichiditate.lichiditate_vedere.sparkline_data = historical_data['lichiditate']['lichiditate_vedere'] lichiditate.lichiditate_vedere.sparkline_labels = sparkline_labels # Actualizăm indicatorii de eficiență cu sparkline data eficienta.dso.sparkline_data = historical_data['eficienta']['dso'] eficienta.dso.sparkline_labels = sparkline_labels eficienta.dpo.sparkline_data = historical_data['eficienta']['dpo'] eficienta.dpo.sparkline_labels = sparkline_labels eficienta.cash_conversion_cycle.sparkline_data = historical_data['eficienta']['cash_conversion_cycle'] eficienta.cash_conversion_cycle.sparkline_labels = sparkline_labels eficienta.rata_incasare.sparkline_data = historical_data['eficienta']['rata_incasare'] eficienta.rata_incasare.sparkline_labels = sparkline_labels eficienta.rata_plata.sparkline_data = historical_data['eficienta']['rata_plata'] eficienta.rata_plata.sparkline_labels = sparkline_labels # Actualizăm indicatorii de risc cu sparkline data risc.creante_restante_pct.sparkline_data = historical_data['risc']['creante_restante_pct'] risc.creante_restante_pct.sparkline_labels = sparkline_labels risc.creante_90plus_pct.sparkline_data = historical_data['risc']['creante_90plus_pct'] risc.creante_90plus_pct.sparkline_labels = sparkline_labels risc.datorii_restante_pct.sparkline_data = historical_data['risc']['datorii_restante_pct'] risc.datorii_restante_pct.sparkline_labels = sparkline_labels risc.raport_datorii_trezorerie.sparkline_data = historical_data['risc']['raport_datorii_trezorerie'] risc.raport_datorii_trezorerie.sparkline_labels = sparkline_labels # Actualizăm indicatorii de cash flow cu sparkline data cash_flow.flux_net_lunar.sparkline_data = historical_data['cash_flow']['flux_net_lunar'] cash_flow.flux_net_lunar.sparkline_labels = sparkline_labels cash_flow.cash_flow_ytd.sparkline_data = historical_data['cash_flow']['cash_flow_ytd'] cash_flow.cash_flow_ytd.sparkline_labels = sparkline_labels cash_flow.flux_net_yoy_pct.sparkline_data = historical_data['cash_flow']['flux_net_yoy_pct'] cash_flow.flux_net_yoy_pct.sparkline_labels = sparkline_labels cash_flow.acoperire_cash_flow.sparkline_data = historical_data['cash_flow']['acoperire_cash_flow'] cash_flow.acoperire_cash_flow.sparkline_labels = sparkline_labels # Actualizăm indicatorii de dinamică cu sparkline data dinamica.crestere_vanzari_yoy.sparkline_data = historical_data['dinamica']['crestere_vanzari_yoy'] dinamica.crestere_vanzari_yoy.sparkline_labels = sparkline_labels dinamica.crestere_achizitii_yoy.sparkline_data = historical_data['dinamica']['crestere_achizitii_yoy'] dinamica.crestere_achizitii_yoy.sparkline_labels = sparkline_labels dinamica.marja_implicita.sparkline_data = historical_data['dinamica']['marja_implicita'] dinamica.marja_implicita.sparkline_labels = sparkline_labels # Actualizăm Altman Z-Score cu sparkline data altman_zscore.zscore.sparkline_data = historical_data['altman_zscore']['zscore'] altman_zscore.zscore.sparkline_labels = sparkline_labels altman_zscore.x1.sparkline_data = historical_data['altman_zscore']['x1'] altman_zscore.x1.sparkline_labels = sparkline_labels altman_zscore.x2.sparkline_data = historical_data['altman_zscore']['x2'] altman_zscore.x2.sparkline_labels = sparkline_labels altman_zscore.x3.sparkline_data = historical_data['altman_zscore']['x3'] altman_zscore.x3.sparkline_labels = sparkline_labels altman_zscore.x4.sparkline_data = historical_data['altman_zscore']['x4'] altman_zscore.x4.sparkline_labels = sparkline_labels # Actualizăm indicatorii de profitabilitate cu sparkline data profitabilitate.cifra_afaceri.sparkline_data = historical_data['profitabilitate']['cifra_afaceri'] profitabilitate.cifra_afaceri.sparkline_labels = sparkline_labels profitabilitate.cheltuieli_totale.sparkline_data = historical_data['profitabilitate']['cheltuieli_totale'] profitabilitate.cheltuieli_totale.sparkline_labels = sparkline_labels profitabilitate.profit_brut.sparkline_data = historical_data['profitabilitate']['profit_brut'] profitabilitate.profit_brut.sparkline_labels = sparkline_labels profitabilitate.marja_profit_brut.sparkline_data = historical_data['profitabilitate']['marja_profit_brut'] profitabilitate.marja_profit_brut.sparkline_labels = sparkline_labels profitabilitate.roa.sparkline_data = historical_data['profitabilitate']['roa'] profitabilitate.roa.sparkline_labels = sparkline_labels profitabilitate.roe.sparkline_data = historical_data['profitabilitate']['roe'] profitabilitate.roe.sparkline_labels = sparkline_labels # Construim răspunsul final response = FinancialIndicatorsResponse( lichiditate=lichiditate, eficienta=eficienta, risc=risc, cash_flow=cash_flow, dinamica=dinamica, altman_zscore=altman_zscore, profitabilitate=profitabilitate ) logger.info( f"Indicators with sparklines for company {company_id}, luna={luna}, an={an}: " f"Z-Score={altman_zscore.zscore.value} ({altman_zscore.zscore.status}), " f"ROA={profitabilitate.roa.value}% ({profitabilitate.roa.status})" ) return response