Files
roa2web-service-auto/backend/modules/reports/services/financial_indicators_service.py
Claude Agent b137e80b71 feat: multi-Oracle server support with runtime switching
Complete implementation of multi-server Oracle database support:

Backend:
- Multi-pool Oracle with lazy loading per server
- Email-to-server cache for automatic server discovery
- JWT tokens include server_id claim
- /auth/check-identity and /auth/check-email endpoints
- /auth/my-servers endpoint for listing user's accessible servers
- Server switch with password re-authentication

Frontend:
- New ServerSelector component for header dropdown
- Multi-step login flow (identity → server → password)
- Server switching from header with password modal
- Mobile drawer menu with server selection
- Dark mode support for all new components
- URL bookmark support with ?server= query param

Scripts:
- Unified start.sh replacing start-prod.sh/start-test.sh
- Unified ssh-tunnel.sh with multi-server support
- Updated status.sh for new architecture

Tests:
- E2E tests for multi-server and single-server login flows
- Backend unit tests for all new endpoints
- Oracle multi-pool integration tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 22:39:06 +00:00

3073 lines
128 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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 fastapi import Request
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,
SolvabilityIndicators,
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"]
},
# CAPITAL SOCIAL STRICT (doar contul 101)
# Folosit pentru calculul Rata ANC / Capital Social conform definiției legale stricte
# 101 - Capital social subscris și vărsat
"capital_social_strict": {
"debit": [],
"credit": ["101"] # Doar Capital Social subscris și vărsat
},
# 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ă (NU ESTE DATORIE)
# 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",
"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
# 709 - Reduceri comerciale acordate (se scade din venituri)
# IMPORTANT: Folosim TOTCRED/TOTDEB (YTD - de la începutul anului) pentru că
# conturile din clasa 7 se închid lunar în 121 (SOLD=0 după închidere)
# RULCRED/RULDEB ar da doar valoarea lunară, nu YTD!
"venituri": {
"tot_credit": ["70", "701", "702", "703", "704", "705", "706", "707", "708",
"71", "72", "74", "75", "758"],
"tot_debit": ["709"] # Reduceri acordate clienților - funcționează INVERS
},
# 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)
# 609 - Reduceri comerciale primite (se scade din cheltuieli)
# IMPORTANT: Folosim TOTDEB/TOTCRED (YTD - de la începutul anului) pentru că
# conturile din clasa 6 se închid lunar în 121 (SOLD=0 după închidere)
# RULDEB/RULCRED ar da doar valoarea lunară, nu YTD!
"cheltuieli_operationale": {
"tot_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"],
"tot_credit": ["609"] # Reduceri primite de la furnizori - funcționează INVERS
},
# 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
# IMPORTANT: Folosim TOTDEB (YTD - de la începutul anului) pentru că
# conturile din clasa 6 se închid lunar în 121 (SOLD=0 după închidere)
# RULDEB ar da doar valoarea lunară, nu YTD!
"cheltuieli_financiare": {
"tot_debit": ["66", "661", "663", "664", "665", "666", "667", "668"]
},
# CIFRA DE AFACERI (doar conturile 70x - venituri din vânzări)
# Conform definiției oficiale, CA include DOAR venituri din vânzarea de bunuri și servicii
# NU include:
# - 71x (Variația stocurilor)
# - 72x (Venituri din producția de imobilizări)
# - 74x (Venituri din subvenții de exploatare)
# - 75x (Alte venituri din exploatare)
# - 76x (Venituri financiare)
# Formula: TOTCRED(701-708) - TOTDEB(709 reduceri comerciale acordate)
# IMPORTANT: Acesta este FĂRĂ TVA - TVA-ul merge în 4427, nu în 7xx
"cifra_afaceri": {
"tot_credit": ["701", "702", "703", "704", "705", "706", "707", "708"],
"tot_debit": ["709"] # Reduceri comerciale acordate (se scad)
},
# ACHIZIȚII STOCURI (Clasa 3 - intrări în stocuri FĂRĂ TVA)
# Când cumpărăm: Debit 301/371 = Credit 401 → 301/371 DEBIT este FĂRĂ TVA
# TVA-ul merge în 4426/4428, NU în contul de stoc
# Aceasta evită complexitatea TVA la încasare, facturi nesosită, etc.
# Include: materii prime (30x), materiale (31x, 32x), producție în curs (33x),
# produse finite (34x), stocuri la terți (35x), animale (36x), mărfuri (37x), ambalaje (38x)
"achizitii_stocuri": {
"tot_debit": ["30", "301", "302", "303", "308",
"31", "311", "312",
"32", "321", "322", "323",
"33", "331", "332",
"34", "341", "345", "346", "348",
"35", "351", "354", "356", "357", "358",
"36", "361", "368",
"37", "371", "378",
"38", "381", "388"]
}
}
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', 'server_id'])
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> 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(server_id) 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', 'server_id'])
async def get_balance_sheet_aggregates(
company_id: int,
luna: int,
an: int,
server_id: Optional[str] = None
) -> 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, server_id)
async with oracle_pool.get_connection(server_id) 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 YTD (TOTCRED - TOTDEB pentru 709)
-- Folosim TOTCRED/TOTDEB pentru valori cumulate de la începutul anului
({FinancialIndicatorsService._build_aggregate_case(
ACCOUNT_GROUPS["venituri"]["tot_credit"], "TOTCRED"
)} -
{FinancialIndicatorsService._build_aggregate_case(
ACCOUNT_GROUPS["venituri"]["tot_debit"], "TOTDEB"
)}) as venituri,
-- CHELTUIELI OPERAȚIONALE YTD (TOTDEB - TOTCRED pentru 609)
-- Folosim TOTDEB/TOTCRED pentru valori cumulate de la începutul anului
({FinancialIndicatorsService._build_aggregate_case(
ACCOUNT_GROUPS["cheltuieli_operationale"]["tot_debit"], "TOTDEB"
)} -
{FinancialIndicatorsService._build_aggregate_case(
ACCOUNT_GROUPS["cheltuieli_operationale"]["tot_credit"], "TOTCRED"
)}) as cheltuieli_operationale,
-- CHELTUIELI FINANCIARE YTD (TOTDEB - Clasa 66)
-- Folosim TOTDEB pentru valori cumulate de la începutul anului
{FinancialIndicatorsService._build_aggregate_case(
ACCOUNT_GROUPS["cheltuieli_financiare"]["tot_debit"], "TOTDEB"
)} as cheltuieli_financiare,
-- CAPITAL SOCIAL STRICT (sold creditor - doar contul 101)
-- Pentru calculul Rata ANC / Capital Social conform definiției legale stricte
{FinancialIndicatorsService._build_aggregate_case(
ACCOUNT_GROUPS["capital_social_strict"]["credit"], "SOLDCRED"
)} as capital_social_strict,
-- CIFRA DE AFACERI YTD (doar 70x - venituri din vânzări, FĂRĂ TVA)
-- NU include: 71x variația stocurilor, 72x producție imobilizări,
-- 74x subvenții, 75x alte venituri
-- Formula: TOTCRED(701-708) - TOTDEB(709 reduceri comerciale acordate)
({FinancialIndicatorsService._build_aggregate_case(
ACCOUNT_GROUPS["cifra_afaceri"]["tot_credit"], "TOTCRED"
)} -
{FinancialIndicatorsService._build_aggregate_case(
ACCOUNT_GROUPS["cifra_afaceri"]["tot_debit"], "TOTDEB"
)}) as cifra_afaceri,
-- ACHIZIȚII STOCURI YTD (Clasa 3 TOTDEB - intrări în stocuri, FĂRĂ TVA)
-- TVA-ul merge în 4426/4428, nu în conturile de stoc
{FinancialIndicatorsService._build_aggregate_case(
ACCOUNT_GROUPS["achizitii_stocuri"]["tot_debit"], "TOTDEB"
)} as achizitii_stocuri
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'),
capital_social_strict=Decimal('0'),
cifra_afaceri=Decimal('0'),
achizitii_stocuri=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)),
capital_social_strict=Decimal(str(row[11] or 0)),
cifra_afaceri=Decimal(str(row[12] or 0)),
achizitii_stocuri=Decimal(str(row[13] 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}, "
f"venituri={aggregates.venituri}, "
f"cheltuieli_op={aggregates.cheltuieli_operationale}, "
f"cifra_afaceri={aggregates.cifra_afaceri}, "
f"achizitii_stocuri={aggregates.achizitii_stocuri}"
)
# Warning: venituri/cheltuieli = 0 but balance sheet has data
# This typically means VBAL doesn't have Class 6/7 data for this period
# (accounting period may not be closed yet)
has_balance_data = (
aggregates.active_imobilizate > 0 or
aggregates.disponibilitati > 0 or
aggregates.datorii_curente > 0
)
if has_balance_data and aggregates.venituri == 0 and aggregates.cheltuieli_operationale == 0:
logger.warning(
f"Company {company_id}, luna={luna}, an={an}: "
f"Balance sheet data exists but venituri/cheltuieli = 0. "
f"VBAL may not have Class 6/7 data for this period "
f"(accounting period not closed?)."
)
return aggregates
@staticmethod
@cached(cache_type='fin_achizitii', key_params=['company_id', 'luna', 'an', 'server_id'])
async def get_achizitii_ytd(
company_id: int,
luna: int,
an: int,
server_id: Optional[str] = None
) -> Decimal:
"""
Calculează totalul achizițiilor YTD din Registrul Jurnal (ACT).
Această metodă folosește tabelul ACT (înregistrări contabile) în loc de VBAL
deoarece captează TOATE achizițiile:
- Achiziții stocuri: 3x = 4x/46x (materii prime, mărfuri, materiale)
- Achiziții directe: 6x = 4x/46x (servicii, consumabile pe cheltuieli)
IMPORTANT: SUMA în tabelul ACT este ÎNTOTDEAUNA fără TVA!
TVA-ul merge în conturi separate (4426, 4428).
Formula:
- (+) Intrări în stocuri/cheltuieli: SCD în 3x sau 6x, SCC în 4x sau 46x
- (-) Discount/rabat primit: SCD în 40x, SCC în 667 sau 609
Args:
company_id: ID-ul firmei
luna: Luna contabilă (1-12) - calcul YTD până la această lună
an: Anul contabil
Returns:
Total achiziții YTD fără TVA (Decimal)
"""
schema = await FinancialIndicatorsService._get_schema(company_id, server_id)
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
query = f"""
SELECT
NVL(SUM(CASE
WHEN (SCD LIKE '3%' OR SCD LIKE '6%')
AND (SCC LIKE '4%' OR SCC LIKE '46%')
THEN SUMA
ELSE 0
END), 0)
-
NVL(SUM(CASE
WHEN SCD LIKE '40%' AND (SCC = '667' OR SCC = '609')
THEN SUMA
ELSE 0
END), 0) as achizitii_ytd
FROM {schema}.ACT
WHERE AN = :an
AND LUNA <= :luna
AND NVL(STERS, 0) = 0
"""
cursor.execute(query, {'an': an, 'luna': luna})
row = cursor.fetchone()
achizitii_total = Decimal(str(row[0] or 0))
logger.info(
f"Achiziții YTD from ACT for company {company_id}, "
f"luna<={luna}, an={an}: {achizitii_total}"
)
return achizitii_total
@staticmethod
@cached(cache_type='fin_cashflow_vbal', key_params=['company_id', 'luna', 'an', 'server_id'])
async def get_cashflow_from_vbal(
company_id: int,
luna: int,
an: int,
server_id: Optional[str] = None
) -> dict:
"""
Calculează datele de Cash Flow direct din VBAL (balanța de verificare).
Aceasta este sursa preferată pentru Cash Flow, în loc de vbalanta_parteneri,
pentru consistență cu celelalte calcule de indicatori.
IMPORTANT: Cash Flow INCLUDE TVA deoarece măsoară fluxuri reale de numerar.
Când clientul plătește 1.190 RON, primim 1.190 RON (nu 1.000).
Coloane VBAL utilizate:
- RULCRED/RULDEB: Rulaj lunar (mișcări în luna curentă)
- TOTCRED/TOTDEB: Total YTD (de la 1 ianuarie până la luna curentă)
Args:
company_id: ID-ul firmei
luna: Luna contabilă (1-12)
an: Anul contabil
Returns:
Dict cu:
- incasari_luna: Încasări lunare (4111+461 RULCRED)
- plati_luna: Plăți lunare (401+404+462 RULDEB)
- incasari_ytd: Încasări YTD (4111+461 TOTCRED)
- plati_ytd: Plăți YTD (401+404+462 TOTDEB)
"""
schema = await FinancialIndicatorsService._get_schema(company_id, server_id)
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
query = f"""
SELECT
-- Încasări luna curentă (4111+461 RULCRED)
-- Credit pe 4111 = client plătește
NVL(SUM(CASE
WHEN CONT IN ('4111', '461') THEN RULCRED
ELSE 0
END), 0) as incasari_luna,
-- Plăți luna curentă (401+404+462 RULDEB)
-- Debit pe 401 = plătim furnizorul
NVL(SUM(CASE
WHEN CONT IN ('401', '404', '462') THEN RULDEB
ELSE 0
END), 0) as plati_luna,
-- Încasări YTD (4111+461 TOTCRED)
NVL(SUM(CASE
WHEN CONT IN ('4111', '461') THEN TOTCRED
ELSE 0
END), 0) as incasari_ytd,
-- Plăți YTD (401+404+462 TOTDEB)
NVL(SUM(CASE
WHEN CONT IN ('401', '404', '462') THEN TOTDEB
ELSE 0
END), 0) as plati_ytd
FROM {schema}.VBAL
WHERE AN = :an AND LUNA = :luna
"""
cursor.execute(query, {'an': an, 'luna': luna})
row = cursor.fetchone()
result = {
'incasari_luna': Decimal(str(row[0] or 0)),
'plati_luna': Decimal(str(row[1] or 0)),
'incasari_ytd': Decimal(str(row[2] or 0)),
'plati_ytd': Decimal(str(row[3] or 0))
}
logger.info(
f"Cash flow from VBAL for company {company_id}, "
f"luna={luna}, an={an}: "
f"incasari_luna={result['incasari_luna']}, "
f"plati_luna={result['plati_luna']}, "
f"incasari_ytd={result['incasari_ytd']}, "
f"plati_ytd={result['plati_ytd']}"
)
return result
@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,
server_id: Optional[str] = None
) -> 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, server_id
)
# 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
),
# Sub-indicatori pentru verificare manuală în balanță
active_curente=IndicatorResult(
value=round(active_curente, 2),
status="good",
message="Stocuri + Creanțe + Disponibilități"
),
disponibilitati=IndicatorResult(
value=round(disponibilitati, 2),
status="good",
message="Bancă (512x) + Casă (531x)"
),
creante=IndicatorResult(
value=round(creante, 2),
status="good",
message="Clienți (411x) + Debitori (461x)"
),
datorii_curente=IndicatorResult(
value=0,
status="good",
message="Fără datorii pe termen scurt"
)
)
# 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
),
# Sub-indicatori pentru verificare manuală în balanță
active_curente=IndicatorResult(
value=round(active_curente, 2),
status="good",
message="Stocuri + Creanțe + Disponibilități"
),
disponibilitati=IndicatorResult(
value=round(disponibilitati, 2),
status="good",
message="Bancă (512x) + Casă (531x)"
),
creante=IndicatorResult(
value=round(creante, 2),
status="good",
message="Clienți (411x) + Debitori (461x)"
),
datorii_curente=IndicatorResult(
value=round(datorii_curente, 2),
status="good",
message="Furnizori + TVA + Salarii etc."
)
)
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', 'server_id'])
async def calculate_efficiency_indicators(
company_id: int,
luna: int,
an: int,
server_id: Optional[str] = None
) -> 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,
server_id=server_id
)
# 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ă
server_id=server_id
)
# 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,
# Sub-indicatori pentru verificare manuală
sold_clienti=IndicatorResult(
value=round(clienti_sold, 2),
status="good",
message="Sold Clienți la final de lună"
),
facturari_lunare=IndicatorResult(
value=round(facturari_lunare, 2),
status="good",
message="Media facturărilor lunare (3 luni)"
),
sold_furnizori=IndicatorResult(
value=round(furnizori_sold, 2),
status="good",
message="Sold Furnizori la final de lună"
),
achizitii_lunare=IndicatorResult(
value=round(achizitii_lunare, 2),
status="good",
message="Media achizițiilor lunare (3 luni)"
),
incasari_luna=IndicatorResult(
value=round(incasari_lunare, 2),
status="good",
message="Media încasărilor lunare (3 luni)"
),
plati_luna=IndicatorResult(
value=round(plati_lunare, 2),
status="good",
message="Media plăților lunare (3 luni)"
)
)
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', 'server_id'])
async def calculate_risk_indicators(
company_id: int,
luna: int,
an: int,
server_id: Optional[str] = None
) -> 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,
server_id=server_id
)
# 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,
# Sub-indicatori pentru verificare manuală
total_clienti=IndicatorResult(
value=round(clienti_sold_total, 2),
status="good",
message="Sold total clienți (411x)"
),
clienti_restanti=IndicatorResult(
value=round(clienti_sold_restant, 2),
status="good",
message="Clienți cu facturi restante"
),
clienti_90plus=IndicatorResult(
value=round(clienti_restant_90plus, 2),
status="good",
message="Clienți restant >90 zile"
),
total_furnizori=IndicatorResult(
value=round(furnizori_sold_total, 2),
status="good",
message="Sold total furnizori (401x)"
),
furnizori_restanti=IndicatorResult(
value=round(furnizori_sold_restant, 2),
status="good",
message="Furnizori cu facturi restante"
),
trezorerie=IndicatorResult(
value=round(trezorerie, 2),
status="good",
message="Disponibilități (512x + 531x)"
)
)
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', 'server_id'])
async def calculate_cashflow_indicators(
company_id: int,
luna: int,
an: int,
server_id: Optional[str] = None
) -> 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 (for datorii restante)
from .dashboard_service import DashboardService
# Obținem datele de cash flow din VBAL (sursa preferată)
# VBAL oferă date directe: RULCRED/RULDEB pentru lunar, TOTCRED/TOTDEB pentru YTD
cf_data_curent = await FinancialIndicatorsService.get_cashflow_from_vbal(
company_id, luna, an, server_id
)
cf_data_anterior = await FinancialIndicatorsService.get_cashflow_from_vbal(
company_id, luna, an - 1, server_id
)
# Obținem datele din summary pentru datorii restante
summary = await DashboardService.get_complete_summary(
company=str(company_id),
username="system",
luna=luna,
an=an,
server_id=server_id
)
# Ensure summary is a DashboardSummary model (cache may return dict)
if isinstance(summary, dict):
summary = DashboardSummary(**summary)
# Calculăm valorile de cash flow din VBAL
# Flux net lunar = încasări luna - plăți luna (RULCRED - RULDEB)
incasari_luna = float(cf_data_curent['incasari_luna'])
plati_luna = float(cf_data_curent['plati_luna'])
flux_net_val = incasari_luna - plati_luna
# Cash flow YTD = încasări YTD - plăți YTD (TOTCRED - TOTDEB)
incasari_ytd = float(cf_data_curent['incasari_ytd'])
plati_ytd = float(cf_data_curent['plati_ytd'])
cf_ytd_val = incasari_ytd - plati_ytd
# Cash flow YTD an anterior (pentru comparație YoY)
incasari_ytd_prev = float(cf_data_anterior['incasari_ytd'])
plati_ytd_prev = float(cf_data_anterior['plati_ytd'])
cf_ytd_anterior = incasari_ytd_prev - plati_ytd_prev
# 1. Flux net lunar
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
)
# 2. Cash flow YTD (încasări YTD - plăți YTD din VBAL TOTCRED/TOTDEB)
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
)
# 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 abs(cf_ytd_anterior) > 0:
yoy_pct = ((cf_ytd_val - cf_ytd_anterior) / abs(cf_ytd_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_ytd_val > 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_ytd_val < 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"
)
# 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"
)
# Sub-indicatori pentru verificare manuală
# NOTA: incasari_luna și plati_luna sunt calculate mai sus din VBAL (liniile 1442-1443)
# cf_ytd_anterior este calculat mai sus din VBAL (linia 1454)
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,
# Sub-indicatori pentru verificare manuală (din VBAL)
incasari_luna=IndicatorResult(
value=round(incasari_luna, 2),
status="good",
message="Încasări luna curentă (4111+461 RULCRED)"
),
plati_luna=IndicatorResult(
value=round(plati_luna, 2),
status="good",
message="Plăți luna curentă (401+404+462 RULDEB)"
),
cf_an_precedent=IndicatorResult(
value=round(cf_ytd_anterior, 2),
status="good",
message="Cash Flow YTD an precedent (din VBAL)"
),
datorii_restante=IndicatorResult(
value=round(datorii_restante, 2),
status="good",
message="Datorii cu scadență depășită"
)
)
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', 'server_id'])
async def calculate_dynamics_indicators(
company_id: int,
luna: int,
an: int,
server_id: Optional[str] = None
) -> 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.
SURSE DE DATE (toate FĂRĂ TVA):
- Vânzări: Cifra de Afaceri din VBAL (Clasa 7 - conturile 70x)
- Achiziții: Registru Jurnal ACT (stocuri 3x=4x + cheltuieli directe 6x=4x)
Anterior, indicatorii foloseau vbalanta_parteneri care include TVA în 4111
(Clienți DEBIT), ceea ce ducea la cifre ~20% mai mari decât realitatea.
Indicatori calculați:
- crestere_vanzari_yoy: Creșterea procentuală a Cifrei de Afaceri față de anul anterior
Formula: (CA_curent - CA_anterior) / CA_anterior * 100
Good: > 5%, Warning: 0-5%, Danger: < 0%
- crestere_achizitii_yoy: Creșterea procentuală a achizițiilor totale față de anul anterior
Formula: (achizitii_curent - achizitii_anterior) / achizitii_anterior * 100
Sursa: ACT (include stocuri + cheltuieli directe, fără TVA)
Informativ - creșterea achizițiilor poate indica expansiune
- marja_implicita: Marja implicită din diferența CA - achiziții totale
Formula: (CA - achizitii) / CA * 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ă
"""
# Obținem agregatele pentru anul curent și anul anterior
# Cifra de Afaceri (70x) din VBAL - FĂRĂ TVA
aggregates_curent = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an, server_id
)
aggregates_anterior = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an - 1, server_id
)
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
if isinstance(aggregates_curent, dict):
aggregates_curent = BalanceSheetAggregates(**aggregates_curent)
if isinstance(aggregates_anterior, dict):
aggregates_anterior = BalanceSheetAggregates(**aggregates_anterior)
# Cifra de Afaceri YTD (Clasa 7 - 70x, FĂRĂ TVA)
# Aceasta este sursa corectă pentru vânzări
total_vanzari_curent = float(aggregates_curent.cifra_afaceri)
total_vanzari_anterior = float(aggregates_anterior.cifra_afaceri)
# Achiziții TOTALE YTD din ACT (stocuri + cheltuieli directe, FĂRĂ TVA)
# Include: 3x=4x (stocuri) + 6x=4x (servicii, consumabile)
# Exclude: discount/rabat (40x=667/609)
achizitii_curent = await FinancialIndicatorsService.get_achizitii_ytd(
company_id, luna, an, server_id
)
achizitii_anterior = await FinancialIndicatorsService.get_achizitii_ytd(
company_id, luna, an - 1, server_id
)
total_achizitii_curent = float(achizitii_curent)
total_achizitii_anterior = float(achizitii_anterior)
# 1. Creștere vânzări YoY (Cifra de Afaceri)
# Formula: (CA_curent - CA_anterior) / CA_anterior * 100
if total_vanzari_anterior > 0:
crestere_vanzari_val = (
(total_vanzari_curent - total_vanzari_anterior) /
total_vanzari_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_vanzari_curent > 0:
# Anul anterior nu avea vânză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ă vânzări în niciun an
crestere_vanzari_yoy = IndicatorResult(
value=None,
status="warning",
threshold_min=5.0,
threshold_max=None,
message="Fără date de vânză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: (CA - achizitii_stocuri) / CA * 100
# Arată ce procent din Cifra de Afaceri rămâne după achiziții de stocuri
if total_vanzari_curent > 0:
marja_val = (
(total_vanzari_curent - total_achizitii_curent) /
total_vanzari_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ă vânzări - nu se poate calcula marja"
)
result = DynamicsIndicators(
crestere_vanzari_yoy=crestere_vanzari_yoy,
crestere_achizitii_yoy=crestere_achizitii_yoy,
marja_implicita=marja_implicita,
# Sub-indicatori pentru verificare manuală
# IMPORTANT: Acestea sunt acum FĂRĂ TVA (Cifra de Afaceri din Clasa 7)
vanzari_an_curent=IndicatorResult(
value=round(total_vanzari_curent, 2),
status="good",
message="Cifra de Afaceri YTD an curent (fără TVA)"
),
vanzari_an_precedent=IndicatorResult(
value=round(total_vanzari_anterior, 2),
status="good",
message="Cifra de Afaceri YTD an precedent (fără TVA)"
),
achizitii_an_curent=IndicatorResult(
value=round(total_achizitii_curent, 2),
status="good",
message="Achiziții totale YTD an curent (stocuri + cheltuieli directe, fără TVA)"
),
achizitii_an_precedent=IndicatorResult(
value=round(total_achizitii_anterior, 2),
status="good",
message="Achiziții totale YTD an precedent (stocuri + cheltuieli directe, fără TVA)"
)
)
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', 'server_id'])
async def calculate_altman_zscore(
company_id: int,
luna: int,
an: int,
server_id: Optional[str] = None
) -> 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, server_id
)
# 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', 'server_id'])
async def calculate_profitability_indicators(
company_id: int,
luna: int,
an: int,
server_id: Optional[str] = None
) -> 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, server_id
)
# 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)
# Detect if balance sheet has data but income/expense accounts are 0
# This typically means the accounting period is not closed in Oracle
has_balance_data = total_active > 0 or float(aggregates.datorii_curente) > 0
no_income_data = venituri == 0 and cheltuieli_oper == 0
period_not_closed = has_balance_data and no_income_data
# 1. Cifra de afaceri (venituri totale) - informativ
if period_not_closed:
cifra_afaceri_status = "warning"
cifra_afaceri_message = "Date indisponibile - perioada contabilă neînchisă?"
else:
cifra_afaceri_status = "good"
cifra_afaceri_message = "Total venituri din activitatea operațională"
cifra_afaceri = IndicatorResult(
value=round(venituri, 2),
status=cifra_afaceri_status,
threshold_min=None,
threshold_max=None,
message=cifra_afaceri_message
)
# 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)
if period_not_closed:
profit_status = "warning"
profit_message = "Date indisponibile - perioada contabilă neînchisă?"
elif profit_brut_val > 0:
profit_status = "good"
profit_message = "Profit operațional pozitiv"
else:
profit_status = "danger"
profit_message = "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,
# Sub-indicator pentru verificare calcul EBIT
venituri=IndicatorResult(
value=round(venituri, 2),
status="good",
message="Total venituri (Clasa 7) - verificare EBIT"
),
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
async def calculate_solvability_indicators(
company_id: int,
luna: int,
an: int,
server_id: Optional[str] = None
) -> SolvabilityIndicators:
"""
Calculează indicatorii de solvabilitate pentru evaluarea capacității
firmei de a-și acoperi datoriile pe termen lung.
Indicatori calculați:
- Activ Net Contabil (ANC) = Total Active - Total Datorii
- Rata ANC/Capital Social = (ANC / Capital Social) × 100
Praguri de referință:
- ANC: Good: > 0, Danger: <= 0
- Rata ANC/Capital: Good: >= 100%, Warning: 50-100%, Danger: < 50%
Implicații legale (din 1 ianuarie 2026):
Sub 50% din capital social → restricții dividende, restituire împrumuturi,
acordare împrumuturi noi.
Args:
company_id: ID-ul firmei
luna: Luna contabilă (1-12)
an: Anul contabil
Returns:
SolvabilityIndicators cu ANC și rata ANC/Capital Social
"""
# Obținem agregatele din balanță
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an, server_id
)
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
if isinstance(aggregates, dict):
aggregates = BalanceSheetAggregates(**aggregates)
# Extragem valorile necesare
total_active = float(aggregates.total_active)
total_datorii = float(aggregates.total_datorii)
# Folosim capital_social_strict (doar contul 101) conform definiției legale stricte
capital_social = float(aggregates.capital_social_strict) # Capital Social = doar contul 101
# 1. Calculăm Activul Net Contabil (ANC)
anc_val = total_active - total_datorii
# Status pentru ANC: pozitiv = good, negativ sau zero = danger
if anc_val > 0:
anc_status = "good"
anc_message = "Activ net pozitiv - firma solvabilă"
else:
anc_status = "danger"
anc_message = "Activ net negativ sau zero - risc de insolvență"
activ_net_contabil = IndicatorResult(
value=round(anc_val, 2),
status=anc_status,
threshold_min=0,
threshold_max=None,
message=anc_message
)
# 2. Calculăm Rata ANC / Capital Social
if capital_social > 0:
rata_val = (anc_val / capital_social) * 100
# Praguri conform legislației din 2026
if rata_val >= 100:
rata_status = "good"
rata_message = "ANC peste capitalul social - situație sănătoasă"
elif rata_val >= 50:
rata_status = "warning"
rata_message = "ANC între 50-100% din capital - atenție la evoluție"
else:
rata_status = "danger"
rata_message = "ANC sub 50% din capital - restricții legale aplicabile"
rata_anc_capital = IndicatorResult(
value=round(rata_val, 2),
status=rata_status,
threshold_min=100.0,
threshold_max=None,
message=rata_message
)
else:
# Capital social zero sau negativ - situație critică
rata_anc_capital = IndicatorResult(
value=None,
status="danger",
threshold_min=100.0,
threshold_max=None,
message="Capital social zero sau negativ - situație critică"
)
# Indicatori de bază pentru verificare
total_active_indicator = IndicatorResult(
value=round(total_active, 2),
status="good",
threshold_min=None,
threshold_max=None,
message="Active Imobilizate + Active Curente"
)
total_datorii_indicator = IndicatorResult(
value=round(total_datorii, 2),
status="good" if total_datorii < total_active else "warning",
threshold_min=None,
threshold_max=None,
message="Datorii Curente + Datorii Termen Lung"
)
capital_social_indicator = IndicatorResult(
value=round(capital_social, 2),
status="good" if capital_social > 0 else "danger",
threshold_min=None,
threshold_max=None,
message="Capital social subscris și vărsat"
)
result = SolvabilityIndicators(
activ_net_contabil=activ_net_contabil,
rata_anc_capital=rata_anc_capital,
total_active=total_active_indicator,
total_datorii=total_datorii_indicator,
capital_social=capital_social_indicator
)
logger.info(
f"Solvability indicators for company {company_id}, luna={luna}, an={an}: "
f"ANC={activ_net_contabil.value} ({activ_net_contabil.status}), "
f"Rata ANC/CS={rata_anc_capital.value}% ({rata_anc_capital.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', 'server_id'])
async def get_historical_indicators(
company_id: int,
months: int = 12,
luna: Optional[int] = None,
an: Optional[int] = None,
server_id: Optional[str] = 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': []
},
'solvabilitate': {
'activ_net_contabil': [],
'rata_anc_capital': []
}
}
# Calculăm indicatorii pentru fiecare perioadă
all_categories = ['lichiditate', 'eficienta', 'risc', 'cash_flow', 'dinamica', 'altman_zscore', 'profitabilitate', 'solvabilitate']
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, server_id
)
# 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, server_id
)
# 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, server_id
)
# 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, server_id
)
# 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, server_id
)
# 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, server_id
)
# 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, server_id
)
# 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')
# Solvabilitate
solvabilitate = await FinancialIndicatorsService.calculate_solvability_indicators(
company_id, period_luna, period_an, server_id
)
# Ensure solvabilitate is a model (cache may return dict)
if isinstance(solvabilitate, dict):
solvabilitate = SolvabilityIndicators(**solvabilitate)
historical_data['solvabilitate']['activ_net_contabil'].append(
solvabilitate.activ_net_contabil.value
)
historical_data['solvabilitate']['rata_anc_capital'].append(
solvabilitate.rata_anc_capital.value
)
processed_categories.add('solvabilitate')
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
@cached(cache_type='financial_indicators_sparklines', key_params=['company_id', 'luna', 'an', 'months', 'server_id'])
async def get_indicators_with_sparklines(
company_id: int,
luna: int,
an: int,
months: int = 12,
request: Optional[Request] = None,
server_id: Optional[str] = None
) -> FinancialIndicatorsResponse:
"""
Calculează toți indicatorii financiari și adaugă datele de sparkline
pentru vizualizarea trendului pe ultimele luni (CACHED 30 min).
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)
request: Request object pentru cache metadata
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, server_id
)
lichiditate_task = FinancialIndicatorsService.calculate_liquidity_indicators(
company_id, luna, an, server_id
)
eficienta_task = FinancialIndicatorsService.calculate_efficiency_indicators(
company_id, luna, an, server_id
)
risc_task = FinancialIndicatorsService.calculate_risk_indicators(
company_id, luna, an, server_id
)
cash_flow_task = FinancialIndicatorsService.calculate_cashflow_indicators(
company_id, luna, an, server_id
)
dinamica_task = FinancialIndicatorsService.calculate_dynamics_indicators(
company_id, luna, an, server_id
)
altman_task = FinancialIndicatorsService.calculate_altman_zscore(
company_id, luna, an, server_id
)
profitabilitate_task = FinancialIndicatorsService.calculate_profitability_indicators(
company_id, luna, an, server_id
)
solvabilitate_task = FinancialIndicatorsService.calculate_solvability_indicators(
company_id, luna, an, server_id
)
(
historical_data,
lichiditate,
eficienta,
risc,
cash_flow,
dinamica,
altman_zscore,
profitabilitate,
solvabilitate
) = await asyncio.gather(
historical_task,
lichiditate_task,
eficienta_task,
risc_task,
cash_flow_task,
dinamica_task,
altman_task,
profitabilitate_task,
solvabilitate_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)
if isinstance(solvabilitate, dict):
solvabilitate = SolvabilityIndicators(**solvabilitate)
# 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
# Actualizăm indicatorii de solvabilitate cu sparkline data
solvabilitate.activ_net_contabil.sparkline_data = historical_data['solvabilitate']['activ_net_contabil']
solvabilitate.activ_net_contabil.sparkline_labels = sparkline_labels
solvabilitate.rata_anc_capital.sparkline_data = historical_data['solvabilitate']['rata_anc_capital']
solvabilitate.rata_anc_capital.sparkline_labels = sparkline_labels
# FIX: Sincronizare ultima valoare sparkline cu valoarea curentă calculată
# Aceasta rezolvă inconsistențele când cache-ul pentru indicatori curenți
# se reîmprospătează mai des decât cache-ul pentru istoric (sparklines)
def sync_last_sparkline_value(indicator_obj, attr_name):
"""Înlocuiește ultima valoare din sparkline cu valoarea curentă calculată"""
indicator = getattr(indicator_obj, attr_name, None)
if indicator and hasattr(indicator, 'sparkline_data') and indicator.sparkline_data:
if hasattr(indicator, 'value') and indicator.value is not None:
indicator.sparkline_data[-1] = indicator.value
# Sincronizăm toți indicatorii
for ind_obj, attrs in [
(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']),
(solvabilitate, ['activ_net_contabil', 'rata_anc_capital']),
]:
for attr in attrs:
sync_last_sparkline_value(ind_obj, attr)
# Construim răspunsul final
response = FinancialIndicatorsResponse(
lichiditate=lichiditate,
eficienta=eficienta,
risc=risc,
cash_flow=cash_flow,
dinamica=dinamica,
altman_zscore=altman_zscore,
profitabilitate=profitabilitate,
solvabilitate=solvabilitate
)
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}), "
f"ANC={solvabilitate.activ_net_contabil.value} ({solvabilitate.activ_net_contabil.status})"
)
return response