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>
2457 lines
100 KiB
Python
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
|