Files
roa2web-service-auto/backend/modules/reports/services/financial_indicators_service.py
Claude Agent dd4b90f922 feat(financial-indicators): Complete Financial Indicators Dashboard Card
Implementare completă a card-ului Indicatori Financiari în Dashboard Solduri:

Backend:
- Model FinancialIndicators cu 22+ indicatori organizați pe categorii
- Service cu calcule din VBAL (Lichiditate, Eficiență, Risc, Cash Flow, Dinamică)
- Altman Z-Score cu toate componentele (X1-X4) și valori absolute
- Profitabilitate cu ROA, ROE, Cifra Afaceri, Cheltuieli separate (operaționale/financiare)
- Caching inteligent pe company_id, luna, an

Frontend:
- FinancialIndicatorsCard.vue cu 4 indicatori principali collapsed
- Expanded view grupat pe categorii (desktop + mobile BottomSheet)
- Subindicatori pentru verificare manuală în balanță
- Traduceri complete în română
- Dark mode support complet
- Sparklines cu tooltips
- Responsive design (desktop grid + mobile carousel)

Documentație:
- PRD complet cu specificații și formule
- Descrieri cu conturi din planul contabil român (OMFP 1802/2014)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 17:32:48 +00:00

2457 lines
100 KiB
Python

"""
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