User Stories Completed: - US-001: Eliminare SolduriCompactCard de pe Desktop - US-002: Eliminare Icoane din Header-ul CollapsibleCard - US-003: Reorganizare TreasuryDualCard - Text Înainte de Grafice - US-004: Reorganizare ClientiBalanceCard - Text Înainte de Grafice - US-005: Reorganizare FurnizoriBalanceCard - Text Înainte de Grafice - US-006: Grafice Colapsabile în TreasuryDualCard - US-007: Grafice Colapsabile în ClientiBalanceCard - US-008: Grafice Colapsabile în FurnizoriBalanceCard - US-009: Grafice Colapsabile în CashFlowMetricCard Additional Improvements: - Add cache metadata display (CacheFooter component) for all dashboard cards - Add @cached decorators to get_monthly_flows and get_indicators_with_sparklines - Fix financial indicators calculations and sparkline sync - Add state reset on company change to prevent stale data - New shared components: CacheFooter.vue, authRedirect.js - Enhanced FinancialIndicatorsCard with sparklines and period selection Squashed from branch: ralph/dashboard-desktop-cleanup (11 commits) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3056 lines
127 KiB
Python
3056 lines
127 KiB
Python
"""
|
||
Service pentru indicatori financiari - agregate din VBAL pentru calculul
|
||
ratelor de lichiditate, eficiență, risc și Altman Z-Score
|
||
|
||
Acest serviciu agregă soldurile din balanța de verificare (VBAL) pe clase
|
||
de conturi conform Planului de Conturi General (PCG) românesc.
|
||
"""
|
||
import logging
|
||
from decimal import Decimal
|
||
from typing import Optional, Dict, List, Any
|
||
|
||
from 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'])
|
||
async def _get_schema(company_id: int) -> str:
|
||
"""
|
||
Obține schema pentru company_id (CACHED PERMANENT)
|
||
|
||
Schema este stocată permanent în cache deoarece nu se schimbă.
|
||
"""
|
||
async with oracle_pool.get_connection() as connection:
|
||
with connection.cursor() as cursor:
|
||
schema_query = """
|
||
SELECT schema
|
||
FROM CONTAFIN_ORACLE.v_nom_firme
|
||
WHERE id_firma = :company_id
|
||
"""
|
||
cursor.execute(schema_query, {'company_id': company_id})
|
||
schema_result = cursor.fetchone()
|
||
|
||
if not schema_result:
|
||
raise ValueError(f"Schema not found for company {company_id}")
|
||
|
||
return schema_result[0]
|
||
|
||
@staticmethod
|
||
def _build_aggregate_case(prefixes: list[str], column: str) -> str:
|
||
"""
|
||
Construiește expresie CASE pentru agregarea soldurilor pe prefixe de conturi.
|
||
|
||
Args:
|
||
prefixes: Lista de prefixe de conturi (ex: ["20", "21", "23"])
|
||
column: Coloana de sumat (SOLDDEB sau SOLDCRED)
|
||
|
||
Returns:
|
||
Expresie SQL CASE pentru SUM
|
||
"""
|
||
if not prefixes:
|
||
return "0"
|
||
|
||
conditions = " OR ".join([f"CONT LIKE '{prefix}%'" for prefix in prefixes])
|
||
return f"SUM(CASE WHEN ({conditions}) THEN NVL({column}, 0) ELSE 0 END)"
|
||
|
||
@staticmethod
|
||
@cached(cache_type='fin_balance_sheet', key_params=['company_id', 'luna', 'an'])
|
||
async def get_balance_sheet_aggregates(
|
||
company_id: int,
|
||
luna: int,
|
||
an: int
|
||
) -> BalanceSheetAggregates:
|
||
"""
|
||
Obține soldurile agregate din balanța de verificare pentru calculul
|
||
indicatorilor financiari (CACHED 30 min).
|
||
|
||
Agregă soldurile din VBAL pe categorii de conturi folosind prefixe
|
||
conform Planului de Conturi General (PCG) românesc.
|
||
|
||
Args:
|
||
company_id: ID-ul firmei
|
||
luna: Luna contabilă (1-12)
|
||
an: Anul contabil
|
||
|
||
Returns:
|
||
BalanceSheetAggregates cu soldurile agregate pentru fiecare categorie
|
||
|
||
Raises:
|
||
ValueError: Dacă schema nu este găsită pentru firma specificată
|
||
"""
|
||
schema = await FinancialIndicatorsService._get_schema(company_id)
|
||
|
||
async with oracle_pool.get_connection() as connection:
|
||
with connection.cursor() as cursor:
|
||
# Construim query-ul cu CASE pentru fiecare categorie
|
||
# Soldurile din VBAL: SOLDDEB (sold debitor), SOLDCRED (sold creditor)
|
||
#
|
||
# Pentru active: valoarea = SOLDDEB - amortizări (SOLDCRED pentru 28, 29)
|
||
# Pentru pasive: valoarea = SOLDCRED
|
||
# Pentru venituri: valoarea = SOLDCRED (conturile de venituri sunt creditoare)
|
||
# Pentru cheltuieli: valoarea = SOLDDEB (conturile de cheltuieli sunt debitoare)
|
||
|
||
query = f"""
|
||
SELECT
|
||
-- ACTIVE IMOBILIZATE (brut - amortizări - ajustări)
|
||
{FinancialIndicatorsService._build_aggregate_case(
|
||
ACCOUNT_GROUPS["active_imobilizate"]["debit"], "SOLDDEB"
|
||
)} -
|
||
{FinancialIndicatorsService._build_aggregate_case(
|
||
ACCOUNT_GROUPS["active_imobilizate"]["credit"], "SOLDCRED"
|
||
)} as active_imobilizate,
|
||
|
||
-- STOCURI (brut - ajustări depreciere)
|
||
{FinancialIndicatorsService._build_aggregate_case(
|
||
ACCOUNT_GROUPS["stocuri"]["debit"], "SOLDDEB"
|
||
)} -
|
||
{FinancialIndicatorsService._build_aggregate_case(
|
||
ACCOUNT_GROUPS["stocuri"]["credit"], "SOLDCRED"
|
||
)} as stocuri,
|
||
|
||
-- CREANȚE (brut - ajustări depreciere)
|
||
{FinancialIndicatorsService._build_aggregate_case(
|
||
ACCOUNT_GROUPS["creante"]["debit"], "SOLDDEB"
|
||
)} -
|
||
{FinancialIndicatorsService._build_aggregate_case(
|
||
ACCOUNT_GROUPS["creante"]["credit"], "SOLDCRED"
|
||
)} as creante,
|
||
|
||
-- DISPONIBILITĂȚI
|
||
{FinancialIndicatorsService._build_aggregate_case(
|
||
ACCOUNT_GROUPS["disponibilitati"]["debit"], "SOLDDEB"
|
||
)} as disponibilitati,
|
||
|
||
-- CAPITAL PROPRIU (sold creditor)
|
||
{FinancialIndicatorsService._build_aggregate_case(
|
||
ACCOUNT_GROUPS["capital_propriu"]["credit"], "SOLDCRED"
|
||
)} as capital_propriu,
|
||
|
||
-- REZULTAT (credit - debit: profit dacă pozitiv, pierdere dacă negativ)
|
||
-- Conturile 107, 117, 121 pot avea sold fie debitor (pierdere) fie creditor (profit)
|
||
-- 129 (repartizare profit) este debitor și se scade
|
||
({FinancialIndicatorsService._build_aggregate_case(
|
||
ACCOUNT_GROUPS["rezultat"]["credit"], "SOLDCRED"
|
||
)} -
|
||
{FinancialIndicatorsService._build_aggregate_case(
|
||
ACCOUNT_GROUPS["rezultat"]["debit"], "SOLDDEB"
|
||
)}) as rezultat,
|
||
|
||
-- DATORII TERMEN LUNG (sold creditor)
|
||
{FinancialIndicatorsService._build_aggregate_case(
|
||
ACCOUNT_GROUPS["datorii_termen_lung"]["credit"], "SOLDCRED"
|
||
)} as datorii_termen_lung,
|
||
|
||
-- DATORII CURENTE (sold creditor)
|
||
{FinancialIndicatorsService._build_aggregate_case(
|
||
ACCOUNT_GROUPS["datorii_curente"]["credit"], "SOLDCRED"
|
||
)} as datorii_curente,
|
||
|
||
-- VENITURI 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'])
|
||
async def get_achizitii_ytd(
|
||
company_id: int,
|
||
luna: int,
|
||
an: int
|
||
) -> 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)
|
||
|
||
async with oracle_pool.get_connection() 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'])
|
||
async def get_cashflow_from_vbal(
|
||
company_id: int,
|
||
luna: int,
|
||
an: int
|
||
) -> 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)
|
||
|
||
async with oracle_pool.get_connection() 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
|
||
) -> LiquidityIndicators:
|
||
"""
|
||
Calculează indicatorii de lichiditate pentru evaluarea capacității
|
||
de plată a datoriilor pe termen scurt.
|
||
|
||
Indicatori calculați:
|
||
- Lichiditate curentă (Current Ratio) = active_curente / datorii_curente
|
||
- Lichiditate imediată (Quick Ratio) = (disponibilități + creanțe) / datorii_curente
|
||
- Lichiditate la vedere (Cash Ratio) = disponibilități / datorii_curente
|
||
|
||
Praguri de referință (conform standardelor bancare):
|
||
- Lichiditate curentă: good >= 2.0, warning 1.0-2.0, danger < 1.0
|
||
- Lichiditate imediată: good >= 1.0, warning 0.5-1.0, danger < 0.5
|
||
- Lichiditate la vedere: good >= 0.2, warning 0.1-0.2, danger < 0.1
|
||
|
||
Args:
|
||
company_id: ID-ul firmei
|
||
luna: Luna contabilă (1-12)
|
||
an: Anul contabil
|
||
|
||
Returns:
|
||
LiquidityIndicators cu cele trei rate de lichiditate
|
||
"""
|
||
# Obținem agregatele din balanță
|
||
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
|
||
company_id, luna, an
|
||
)
|
||
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
|
||
if isinstance(aggregates, dict):
|
||
aggregates = BalanceSheetAggregates(**aggregates)
|
||
|
||
# Calculăm active curente (stocuri + creanțe + disponibilități)
|
||
active_curente = float(aggregates.active_curente)
|
||
disponibilitati = float(aggregates.disponibilitati)
|
||
creante = float(aggregates.creante)
|
||
datorii_curente = float(aggregates.datorii_curente)
|
||
|
||
# Handle cazul special: datorii_curente = 0
|
||
if datorii_curente == 0:
|
||
# Compania nu are datorii pe termen scurt - situație excelentă
|
||
# Returnăm None pentru valoare dar status "good" cu mesaj explicativ
|
||
no_debt_message = "Fără datorii curente - lichiditate maximă"
|
||
|
||
return LiquidityIndicators(
|
||
lichiditate_curenta=IndicatorResult(
|
||
value=None,
|
||
status="good",
|
||
threshold_min=2.0,
|
||
threshold_max=None,
|
||
message=no_debt_message
|
||
),
|
||
lichiditate_imediata=IndicatorResult(
|
||
value=None,
|
||
status="good",
|
||
threshold_min=1.0,
|
||
threshold_max=None,
|
||
message=no_debt_message
|
||
),
|
||
lichiditate_vedere=IndicatorResult(
|
||
value=None,
|
||
status="good",
|
||
threshold_min=0.2,
|
||
threshold_max=None,
|
||
message=no_debt_message
|
||
),
|
||
# 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'])
|
||
async def calculate_efficiency_indicators(
|
||
company_id: int,
|
||
luna: int,
|
||
an: int
|
||
) -> EfficiencyIndicators:
|
||
"""
|
||
Calculează indicatorii de eficiență pentru evaluarea vitezei de conversie
|
||
a resurselor în numerar.
|
||
|
||
Indicatori calculați:
|
||
- DSO (Days Sales Outstanding) = (clienti_sold / facturari_lunare) * 30
|
||
- DPO (Days Payables Outstanding) = (furnizori_sold / achizitii_lunare) * 30
|
||
- Cash Conversion Cycle = DSO - DPO
|
||
- Rata încasare = incasari / facturari * 100
|
||
- Rata plată = plati / achizitii * 100
|
||
|
||
Praguri de referință pentru DSO:
|
||
- Good: < 30 zile (încasare rapidă)
|
||
- Warning: 30-45 zile (încasare moderată)
|
||
- Danger: > 45 zile (încasare lentă - risc de cash flow)
|
||
|
||
Args:
|
||
company_id: ID-ul firmei
|
||
luna: Luna contabilă (1-12)
|
||
an: Anul contabil
|
||
|
||
Returns:
|
||
EfficiencyIndicators cu cele cinci rate de eficiență
|
||
"""
|
||
# Import DashboardService here to avoid circular imports
|
||
from .dashboard_service import DashboardService
|
||
|
||
# Obținem datele din summary (solduri clienți/furnizori)
|
||
summary = await DashboardService.get_complete_summary(
|
||
company=str(company_id),
|
||
username="system", # System call for indicators
|
||
luna=luna,
|
||
an=an
|
||
)
|
||
# Ensure summary is a DashboardSummary model (cache may return dict)
|
||
if isinstance(summary, dict):
|
||
summary = DashboardSummary(**summary)
|
||
|
||
# Obținem datele din trends (facturări/încasări/achiziții/plăți lunare)
|
||
trends = await DashboardService.get_trends(
|
||
company_id=company_id,
|
||
period='12m' # Ultimele 12 luni pentru media lunară
|
||
)
|
||
|
||
# Extragem soldurile din summary
|
||
clienti_sold = float(summary.clienti_sold_total)
|
||
furnizori_sold = float(summary.furnizori_sold_total)
|
||
|
||
# Extragem datele lunare din trends
|
||
# Folosim ultima lună disponibilă pentru facturări/încasări/achiziții/plăți
|
||
clienti_facturat = trends.get("clienti_facturat", [])
|
||
clienti_incasat = trends.get("clienti_incasat", [])
|
||
furnizori_facturat = trends.get("furnizori_facturat", [])
|
||
furnizori_achitat = trends.get("furnizori_achitat", [])
|
||
|
||
# Calculăm media lunară (pentru stabilitate) sau folosim ultima lună
|
||
# Pentru DSO/DPO, folosim media ultimelor 3 luni pentru a evita fluctuații
|
||
def safe_avg(values: list, n: int = 3) -> float:
|
||
"""Calculează media ultimelor n valori, sau toate dacă sunt mai puține"""
|
||
if not values:
|
||
return 0
|
||
recent = values[-n:] if len(values) >= n else values
|
||
return sum(recent) / len(recent) if recent else 0
|
||
|
||
facturari_lunare = safe_avg(clienti_facturat)
|
||
incasari_lunare = safe_avg(clienti_incasat)
|
||
achizitii_lunare = safe_avg(furnizori_facturat)
|
||
plati_lunare = safe_avg(furnizori_achitat)
|
||
|
||
# Calculăm indicatorii
|
||
|
||
# 1. DSO (Days Sales Outstanding) - Durata medie de încasare
|
||
# Formula: (clienti_sold / facturari_lunare) * 30
|
||
if facturari_lunare > 0:
|
||
dso_val = (clienti_sold / facturari_lunare) * 30
|
||
dso_status = FinancialIndicatorsService._calculate_indicator_status(
|
||
dso_val,
|
||
good_threshold=30,
|
||
warning_threshold=45,
|
||
higher_is_better=False # Pentru DSO, mai mic e mai bine
|
||
)
|
||
dso = IndicatorResult(
|
||
value=round(dso_val, 1),
|
||
status=dso_status,
|
||
threshold_min=None,
|
||
threshold_max=30 # Good if <= 30 days
|
||
)
|
||
else:
|
||
dso = IndicatorResult(
|
||
value=None,
|
||
status="warning",
|
||
threshold_min=None,
|
||
threshold_max=30,
|
||
message="Fără facturări în perioada analizată"
|
||
)
|
||
|
||
# 2. DPO (Days Payables Outstanding) - Durata medie de plată
|
||
# Formula: (furnizori_sold / achizitii_lunare) * 30
|
||
if achizitii_lunare > 0:
|
||
dpo_val = (furnizori_sold / achizitii_lunare) * 30
|
||
# Pentru DPO nu avem praguri stricte - depinde de strategia firmei
|
||
# Un DPO mai mare înseamnă că folosim creditul furnizorilor
|
||
dpo = IndicatorResult(
|
||
value=round(dpo_val, 1),
|
||
status="good", # DPO nu are praguri de "danger"
|
||
threshold_min=None,
|
||
threshold_max=None,
|
||
message="Durata medie de plată furnizori"
|
||
)
|
||
else:
|
||
dpo = IndicatorResult(
|
||
value=None,
|
||
status="warning",
|
||
threshold_min=None,
|
||
threshold_max=None,
|
||
message="Fără achiziții în perioada analizată"
|
||
)
|
||
|
||
# 3. Cash Conversion Cycle = DSO - DPO
|
||
# Negativ = firma se finanțează din creditul furnizorilor (favorabil)
|
||
# Pozitiv = cash blocat în ciclul de afaceri
|
||
if dso.value is not None and dpo.value is not None:
|
||
ccc_val = dso.value - dpo.value
|
||
# CCC negativ e bun (ne finanțăm din creditul furnizorilor)
|
||
ccc_status = FinancialIndicatorsService._calculate_indicator_status(
|
||
ccc_val,
|
||
good_threshold=0,
|
||
warning_threshold=15,
|
||
higher_is_better=False
|
||
)
|
||
ccc_message = (
|
||
"Ciclu negativ = finanțare din creditul furnizorilor"
|
||
if ccc_val < 0
|
||
else "Numerar blocat în ciclul de afaceri"
|
||
)
|
||
cash_conversion_cycle = IndicatorResult(
|
||
value=round(ccc_val, 1),
|
||
status=ccc_status,
|
||
threshold_min=None,
|
||
threshold_max=0,
|
||
message=ccc_message
|
||
)
|
||
else:
|
||
cash_conversion_cycle = IndicatorResult(
|
||
value=None,
|
||
status="warning",
|
||
threshold_min=None,
|
||
threshold_max=0,
|
||
message="Nu se poate calcula - lipsesc date DSO/DPO"
|
||
)
|
||
|
||
# 4. Rata încasare = incasari / facturari * 100
|
||
# Calculăm pentru perioada YTD din trends (suma totală)
|
||
total_facturari = sum(clienti_facturat) if clienti_facturat else 0
|
||
total_incasari = sum(clienti_incasat) if clienti_incasat else 0
|
||
|
||
if total_facturari > 0:
|
||
rata_incasare_val = (total_incasari / total_facturari) * 100
|
||
rata_incasare_status = FinancialIndicatorsService._calculate_indicator_status(
|
||
rata_incasare_val,
|
||
good_threshold=95,
|
||
warning_threshold=80
|
||
)
|
||
rata_incasare = IndicatorResult(
|
||
value=round(rata_incasare_val, 1),
|
||
status=rata_incasare_status,
|
||
threshold_min=95,
|
||
threshold_max=None
|
||
)
|
||
else:
|
||
rata_incasare = IndicatorResult(
|
||
value=None,
|
||
status="warning",
|
||
threshold_min=95,
|
||
threshold_max=None,
|
||
message="Fără facturări în perioada analizată"
|
||
)
|
||
|
||
# 5. Rata plată = plati / achizitii * 100
|
||
total_achizitii = sum(furnizori_facturat) if furnizori_facturat else 0
|
||
total_plati = sum(furnizori_achitat) if furnizori_achitat else 0
|
||
|
||
if total_achizitii > 0:
|
||
rata_plata_val = (total_plati / total_achizitii) * 100
|
||
# Pentru rata de plată, nu există "danger" - depinde de strategia firmei
|
||
rata_plata = IndicatorResult(
|
||
value=round(rata_plata_val, 1),
|
||
status="good", # Informativ, fără praguri stricte
|
||
threshold_min=None,
|
||
threshold_max=None
|
||
)
|
||
else:
|
||
rata_plata = IndicatorResult(
|
||
value=None,
|
||
status="warning",
|
||
threshold_min=None,
|
||
threshold_max=None,
|
||
message="Fără achiziții în perioada analizată"
|
||
)
|
||
|
||
result = EfficiencyIndicators(
|
||
dso=dso,
|
||
dpo=dpo,
|
||
cash_conversion_cycle=cash_conversion_cycle,
|
||
rata_incasare=rata_incasare,
|
||
rata_plata=rata_plata,
|
||
# 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'])
|
||
async def calculate_risk_indicators(
|
||
company_id: int,
|
||
luna: int,
|
||
an: int
|
||
) -> RiskIndicators:
|
||
"""
|
||
Calculează indicatorii de risc și aging pentru evaluarea sănătății
|
||
portofoliului de creanțe și datorii.
|
||
|
||
Indicatori calculați:
|
||
- creante_restante_pct: Procentul creanțelor restante din total clienți
|
||
Formula: clienti_sold_restant / clienti_sold_total * 100
|
||
Good: < 20%, Warning: 20-30%, Danger: > 30%
|
||
|
||
- creante_90plus_pct: Procentul creanțelor restante > 90 zile din total
|
||
Formula: clienti_restant_90plus / clienti_sold_total * 100
|
||
Good: < 5%, Warning: 5-10%, Danger: > 10%
|
||
|
||
- datorii_restante_pct: Procentul datoriilor restante din total furnizori
|
||
Formula: furnizori_sold_restant / furnizori_sold_total * 100
|
||
Good: < 10%, Warning: 10-20%, Danger: > 20%
|
||
|
||
- raport_datorii_trezorerie: Raportul între datorii furnizori și trezorerie
|
||
Formula: furnizori_sold_total / trezorerie
|
||
Good: < 2, Warning: 2-4, Danger: > 4
|
||
|
||
Args:
|
||
company_id: ID-ul firmei
|
||
luna: Luna contabilă (1-12)
|
||
an: Anul contabil
|
||
|
||
Returns:
|
||
RiskIndicators cu cei patru indicatori de risc
|
||
"""
|
||
# Import DashboardService here to avoid circular imports
|
||
from .dashboard_service import DashboardService
|
||
|
||
# Obținem datele din summary (solduri clienți/furnizori/aging)
|
||
summary = await DashboardService.get_complete_summary(
|
||
company=str(company_id),
|
||
username="system", # System call for indicators
|
||
luna=luna,
|
||
an=an
|
||
)
|
||
# Ensure summary is a DashboardSummary model (cache may return dict)
|
||
if isinstance(summary, dict):
|
||
summary = DashboardSummary(**summary)
|
||
|
||
# Extragem soldurile din summary
|
||
clienti_sold_total = float(summary.clienti_sold_total)
|
||
clienti_sold_restant = float(summary.clienti_sold_restant)
|
||
clienti_restant_90plus = float(summary.clienti_restant_90plus)
|
||
furnizori_sold_total = float(summary.furnizori_sold_total)
|
||
furnizori_sold_restant = float(summary.furnizori_sold_restant)
|
||
|
||
# Calculăm trezoreria totală din treasury_totals_by_currency (sumă pe toate valutele)
|
||
trezorerie = sum(float(v) for v in summary.treasury_totals_by_currency.values())
|
||
|
||
# 1. Creanțe restante % - procentul din solduri clienți care sunt restante
|
||
if clienti_sold_total > 0:
|
||
creante_restante_val = (clienti_sold_restant / clienti_sold_total) * 100
|
||
creante_restante_status = FinancialIndicatorsService._calculate_indicator_status(
|
||
creante_restante_val,
|
||
good_threshold=20,
|
||
warning_threshold=30,
|
||
higher_is_better=False # Mai mic e mai bine
|
||
)
|
||
creante_restante_pct = IndicatorResult(
|
||
value=round(creante_restante_val, 1),
|
||
status=creante_restante_status,
|
||
threshold_min=None,
|
||
threshold_max=20
|
||
)
|
||
else:
|
||
creante_restante_pct = IndicatorResult(
|
||
value=None,
|
||
status="good",
|
||
threshold_min=None,
|
||
threshold_max=20,
|
||
message="Fără sold clienți"
|
||
)
|
||
|
||
# 2. Creanțe 90+ zile % - creanțe cu risc mare de nerecuperare
|
||
if clienti_sold_total > 0:
|
||
creante_90plus_val = (clienti_restant_90plus / clienti_sold_total) * 100
|
||
creante_90plus_status = FinancialIndicatorsService._calculate_indicator_status(
|
||
creante_90plus_val,
|
||
good_threshold=5,
|
||
warning_threshold=10,
|
||
higher_is_better=False # Mai mic e mai bine
|
||
)
|
||
creante_90plus_pct = IndicatorResult(
|
||
value=round(creante_90plus_val, 1),
|
||
status=creante_90plus_status,
|
||
threshold_min=None,
|
||
threshold_max=5
|
||
)
|
||
else:
|
||
creante_90plus_pct = IndicatorResult(
|
||
value=None,
|
||
status="good",
|
||
threshold_min=None,
|
||
threshold_max=5,
|
||
message="Fără sold clienți"
|
||
)
|
||
|
||
# 3. Datorii restante % - procentul din solduri furnizori care sunt restante
|
||
if furnizori_sold_total > 0:
|
||
datorii_restante_val = (furnizori_sold_restant / furnizori_sold_total) * 100
|
||
datorii_restante_status = FinancialIndicatorsService._calculate_indicator_status(
|
||
datorii_restante_val,
|
||
good_threshold=10,
|
||
warning_threshold=20,
|
||
higher_is_better=False # Mai mic e mai bine
|
||
)
|
||
datorii_restante_pct = IndicatorResult(
|
||
value=round(datorii_restante_val, 1),
|
||
status=datorii_restante_status,
|
||
threshold_min=None,
|
||
threshold_max=10
|
||
)
|
||
else:
|
||
datorii_restante_pct = IndicatorResult(
|
||
value=None,
|
||
status="good",
|
||
threshold_min=None,
|
||
threshold_max=10,
|
||
message="Fără sold furnizori"
|
||
)
|
||
|
||
# 4. Raport datorii/trezorerie - câte unități de cash trebuie pentru a plăti furnizorii
|
||
if trezorerie > 0:
|
||
raport_val = furnizori_sold_total / trezorerie
|
||
raport_status = FinancialIndicatorsService._calculate_indicator_status(
|
||
raport_val,
|
||
good_threshold=2,
|
||
warning_threshold=4,
|
||
higher_is_better=False # Mai mic e mai bine (mai puțin datorie per cash)
|
||
)
|
||
raport_message = (
|
||
"Trezorerie suficientă pentru acoperirea datoriilor"
|
||
if raport_val < 2
|
||
else "Trezorerie insuficientă pentru datorii"
|
||
if raport_val > 4
|
||
else "Trezorerie moderată pentru datorii"
|
||
)
|
||
raport_datorii_trezorerie = IndicatorResult(
|
||
value=round(raport_val, 2),
|
||
status=raport_status,
|
||
threshold_min=None,
|
||
threshold_max=2,
|
||
message=raport_message
|
||
)
|
||
elif furnizori_sold_total == 0:
|
||
# Fără datorii furnizori - situație bună
|
||
raport_datorii_trezorerie = IndicatorResult(
|
||
value=0,
|
||
status="good",
|
||
threshold_min=None,
|
||
threshold_max=2,
|
||
message="Fără datorii furnizori"
|
||
)
|
||
else:
|
||
# Trezorerie 0 dar avem datorii - situație critică
|
||
raport_datorii_trezorerie = IndicatorResult(
|
||
value=None,
|
||
status="danger",
|
||
threshold_min=None,
|
||
threshold_max=2,
|
||
message="Trezorerie zero - nu se poate acoperi datoriile"
|
||
)
|
||
|
||
result = RiskIndicators(
|
||
creante_restante_pct=creante_restante_pct,
|
||
creante_90plus_pct=creante_90plus_pct,
|
||
datorii_restante_pct=datorii_restante_pct,
|
||
raport_datorii_trezorerie=raport_datorii_trezorerie,
|
||
# 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'])
|
||
async def calculate_cashflow_indicators(
|
||
company_id: int,
|
||
luna: int,
|
||
an: int
|
||
) -> CashFlowIndicators:
|
||
"""
|
||
Calculează indicatorii de cash flow pentru evaluarea generării și
|
||
consumului de numerar.
|
||
|
||
Indicatori calculați:
|
||
- flux_net_lunar: Încasări luna - plăți luna (fluxul net de numerar lunar)
|
||
Good: > 0 (firma generează numerar), Danger: < 0 (firma consumă numerar)
|
||
|
||
- cash_flow_ytd: Suma fluxurilor nete de la ianuarie până la luna curentă
|
||
Arată tendința generală a anului în curs
|
||
|
||
- flux_net_yoy_pct: Variația procentuală an-la-an
|
||
Formula: (cf_curent - cf_anterior) / abs(cf_anterior) * 100
|
||
|
||
- acoperire_cash_flow: Cash flow YTD / datorii restante
|
||
Good: > 1 (cash flow suficient), Warning: 0.5-1, Danger: < 0.5
|
||
|
||
Args:
|
||
company_id: ID-ul firmei
|
||
luna: Luna contabilă (1-12)
|
||
an: Anul contabil
|
||
|
||
Returns:
|
||
CashFlowIndicators cu cei patru indicatori de cash flow
|
||
"""
|
||
# Import DashboardService here to avoid circular imports (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
|
||
)
|
||
cf_data_anterior = await FinancialIndicatorsService.get_cashflow_from_vbal(
|
||
company_id, luna, an - 1
|
||
)
|
||
|
||
# Obținem datele din summary pentru datorii restante
|
||
summary = await DashboardService.get_complete_summary(
|
||
company=str(company_id),
|
||
username="system",
|
||
luna=luna,
|
||
an=an
|
||
)
|
||
# Ensure summary is a DashboardSummary model (cache may return dict)
|
||
if isinstance(summary, dict):
|
||
summary = DashboardSummary(**summary)
|
||
|
||
# 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'])
|
||
async def calculate_dynamics_indicators(
|
||
company_id: int,
|
||
luna: int,
|
||
an: int
|
||
) -> DynamicsIndicators:
|
||
"""
|
||
Calculează indicatorii de dinamică pentru evaluarea evoluției afacerii.
|
||
|
||
Compară vânzările și achizițiile cu anul anterior (YoY - Year-over-Year)
|
||
pentru a determina dacă afacerea crește sau scade.
|
||
|
||
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
|
||
)
|
||
aggregates_anterior = await FinancialIndicatorsService.get_balance_sheet_aggregates(
|
||
company_id, luna, an - 1
|
||
)
|
||
|
||
# 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
|
||
)
|
||
achizitii_anterior = await FinancialIndicatorsService.get_achizitii_ytd(
|
||
company_id, luna, an - 1
|
||
)
|
||
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'])
|
||
async def calculate_altman_zscore(
|
||
company_id: int,
|
||
luna: int,
|
||
an: int
|
||
) -> AltmanZScore:
|
||
"""
|
||
Calculează Altman Z-Score pentru evaluarea riscului de faliment.
|
||
|
||
Folosim formula modificată pentru companii private (Z'-Score):
|
||
Z' = 6.56*X1 + 3.26*X2 + 6.72*X3 + 1.05*X4
|
||
|
||
Aceasta este versiunea pentru companii care nu sunt listate la bursă,
|
||
unde se folosește valoarea contabilă a capitalurilor proprii (Book Value)
|
||
în loc de valoarea de piață a acțiunilor.
|
||
|
||
Componente:
|
||
- X1 = Working Capital / Total Assets (lichiditate pe termen scurt)
|
||
- X2 = Retained Earnings / Total Assets (profitabilitate cumulată)
|
||
- X3 = EBIT / Total Assets (eficiență operațională)
|
||
- X4 = Book Value of Equity / Total Liabilities (solvabilitate)
|
||
|
||
Zone de risc:
|
||
- Safe (Z > 2.60): Risc minim de faliment - situație financiară solidă
|
||
- Grey (1.10 <= Z <= 2.60): Zona de incertitudine - necesită monitorizare
|
||
- Distress (Z < 1.10): Risc ridicat de faliment - situație critică
|
||
|
||
Args:
|
||
company_id: ID-ul firmei
|
||
luna: Luna contabilă (1-12)
|
||
an: Anul contabil
|
||
|
||
Returns:
|
||
AltmanZScore cu scorul calculat și componentele individuale x1, x2, x3, x4
|
||
"""
|
||
# Obținem agregatele din balanță
|
||
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
|
||
company_id, luna, an
|
||
)
|
||
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
|
||
if isinstance(aggregates, dict):
|
||
aggregates = BalanceSheetAggregates(**aggregates)
|
||
|
||
# Calculăm componentele necesare
|
||
# Working Capital = active_curente - datorii_curente
|
||
working_capital = float(aggregates.working_capital)
|
||
|
||
# Total Assets = active_imobilizate + active_curente
|
||
total_assets = float(aggregates.total_active)
|
||
|
||
# Rezultat reportat (cont 117 + 121) - deja agregat în `rezultat`
|
||
rezultat_reportat = float(aggregates.rezultat)
|
||
|
||
# EBIT = venituri - cheltuieli_operationale
|
||
ebit = float(aggregates.ebit)
|
||
|
||
# Capital propriu (inclusiv rezultat) pentru X4
|
||
capital_propriu = float(aggregates.capitaluri_proprii)
|
||
|
||
# Total datorii (curente + termen lung)
|
||
total_datorii = float(aggregates.total_datorii)
|
||
datorii_curente = float(aggregates.datorii_curente)
|
||
datorii_termen_lung = float(aggregates.datorii_termen_lung)
|
||
|
||
# Verificăm dacă avem date suficiente pentru calcul
|
||
if total_assets == 0:
|
||
# Nu putem calcula Z-Score fără active
|
||
return AltmanZScore(
|
||
zscore=IndicatorResult(
|
||
value=None,
|
||
status="warning",
|
||
threshold_min=2.60,
|
||
threshold_max=None,
|
||
message="Nu se poate calcula - total active este zero"
|
||
),
|
||
x1=IndicatorResult(value=None, status="warning"),
|
||
x2=IndicatorResult(value=None, status="warning"),
|
||
x3=IndicatorResult(value=None, status="warning"),
|
||
x4=IndicatorResult(value=None, status="warning"),
|
||
capital_de_lucru=IndicatorResult(
|
||
value=round(working_capital, 2),
|
||
status="warning",
|
||
message="Active Curente - Datorii Curente"
|
||
),
|
||
active_totale=IndicatorResult(
|
||
value=0,
|
||
status="warning",
|
||
message="Nu există active în balanță"
|
||
),
|
||
datorii_totale=IndicatorResult(
|
||
value=round(total_datorii, 2),
|
||
status="warning",
|
||
message="Datorii Curente + Datorii Termen Lung"
|
||
)
|
||
)
|
||
|
||
# Calculăm componentele X1-X4
|
||
|
||
# X1 = Working Capital / Total Assets
|
||
# Măsoară lichiditatea - cât de mult din active este finanțat din surse pe termen scurt
|
||
x1_val = working_capital / total_assets
|
||
x1_status = "good" if x1_val > 0 else "danger"
|
||
x1 = IndicatorResult(
|
||
value=round(x1_val, 4),
|
||
status=x1_status,
|
||
threshold_min=0,
|
||
threshold_max=None,
|
||
message="Lichiditate pe termen scurt" if x1_val > 0 else "Working capital negativ"
|
||
)
|
||
|
||
# X2 = Retained Earnings / Total Assets
|
||
# Măsoară profitabilitatea cumulată - câștigurile reinvestite în companie
|
||
x2_val = rezultat_reportat / total_assets
|
||
x2_status = "good" if x2_val > 0 else ("warning" if x2_val == 0 else "danger")
|
||
x2 = IndicatorResult(
|
||
value=round(x2_val, 4),
|
||
status=x2_status,
|
||
threshold_min=0,
|
||
threshold_max=None,
|
||
message="Profitabilitate cumulată" if x2_val >= 0 else "Pierderi cumulate"
|
||
)
|
||
|
||
# X3 = EBIT / Total Assets
|
||
# Măsoară eficiența operațională - randamentul activelor
|
||
x3_val = ebit / total_assets
|
||
x3_status = "good" if x3_val > 0 else ("warning" if x3_val == 0 else "danger")
|
||
x3 = IndicatorResult(
|
||
value=round(x3_val, 4),
|
||
status=x3_status,
|
||
threshold_min=0,
|
||
threshold_max=None,
|
||
message="Eficiență operațională" if x3_val >= 0 else "Pierdere operațională"
|
||
)
|
||
|
||
# X4 = Book Value of Equity / Total Liabilities
|
||
# Măsoară solvabilitatea - acoperirea datoriilor cu capital propriu
|
||
if total_datorii > 0:
|
||
x4_val = capital_propriu / total_datorii
|
||
x4_status = FinancialIndicatorsService._calculate_indicator_status(
|
||
x4_val,
|
||
good_threshold=1.0,
|
||
warning_threshold=0.5
|
||
)
|
||
x4 = IndicatorResult(
|
||
value=round(x4_val, 4),
|
||
status=x4_status,
|
||
threshold_min=1.0,
|
||
threshold_max=None,
|
||
message="Solvabilitate bună" if x4_val >= 1 else "Îndatorare ridicată"
|
||
)
|
||
else:
|
||
# Fără datorii - situație excelentă pentru solvabilitate
|
||
x4_val = None # Infinit teoretic, dar nu putem reprezenta
|
||
x4 = IndicatorResult(
|
||
value=None,
|
||
status="good",
|
||
threshold_min=1.0,
|
||
threshold_max=None,
|
||
message="Fără datorii - solvabilitate maximă"
|
||
)
|
||
|
||
# Calculăm Z-Score folosind formula pentru companii private
|
||
# Z' = 6.56*X1 + 3.26*X2 + 6.72*X3 + 1.05*X4
|
||
if x4_val is not None:
|
||
zscore_val = (
|
||
6.56 * x1_val +
|
||
3.26 * x2_val +
|
||
6.72 * x3_val +
|
||
1.05 * x4_val
|
||
)
|
||
else:
|
||
# Dacă X4 este infinit (fără datorii), calculăm Z-Score fără componenta X4
|
||
# În practică, firmele fără datorii au un Z-Score foarte bun
|
||
# Folosim o valoare foarte mare pentru X4 (ex: 10) ca proxy
|
||
zscore_val = (
|
||
6.56 * x1_val +
|
||
3.26 * x2_val +
|
||
6.72 * x3_val +
|
||
1.05 * 10.0 # Proxy pentru solvabilitate maximă
|
||
)
|
||
|
||
# Determinăm zona de risc
|
||
if zscore_val > 2.60:
|
||
zscore_status = "safe"
|
||
zscore_message = "Zona sigură - risc minim de faliment"
|
||
elif zscore_val >= 1.10:
|
||
zscore_status = "grey"
|
||
zscore_message = "Zona gri - necesită monitorizare atentă"
|
||
else:
|
||
zscore_status = "distress"
|
||
zscore_message = "Zona de risc - risc ridicat de faliment"
|
||
|
||
zscore = IndicatorResult(
|
||
value=round(zscore_val, 2),
|
||
status=zscore_status,
|
||
threshold_min=2.60,
|
||
threshold_max=None,
|
||
message=zscore_message
|
||
)
|
||
|
||
# Indicatori de bază pentru verificare manuală în balanță
|
||
capital_de_lucru = IndicatorResult(
|
||
value=round(working_capital, 2),
|
||
status="good" if working_capital > 0 else "danger",
|
||
threshold_min=0,
|
||
threshold_max=None,
|
||
message="Active Curente - Datorii Curente"
|
||
)
|
||
|
||
active_totale_ind = IndicatorResult(
|
||
value=round(total_assets, 2),
|
||
status="good",
|
||
threshold_min=None,
|
||
threshold_max=None,
|
||
message="Active Imobilizate + Active Curente"
|
||
)
|
||
|
||
datorii_totale_ind = IndicatorResult(
|
||
value=round(total_datorii, 2),
|
||
status="good",
|
||
threshold_min=None,
|
||
threshold_max=None,
|
||
message="Datorii Curente + Datorii Termen Lung"
|
||
)
|
||
|
||
result = AltmanZScore(
|
||
zscore=zscore,
|
||
x1=x1,
|
||
x2=x2,
|
||
x3=x3,
|
||
x4=x4,
|
||
capital_de_lucru=capital_de_lucru,
|
||
active_totale=active_totale_ind,
|
||
datorii_totale=datorii_totale_ind
|
||
)
|
||
|
||
logger.info(
|
||
f"Altman Z-Score for company {company_id}, luna={luna}, an={an}: "
|
||
f"zscore={zscore.value} ({zscore.status}), "
|
||
f"X1={x1.value}, X2={x2.value}, X3={x3.value}, X4={x4.value}"
|
||
)
|
||
|
||
return result
|
||
|
||
@staticmethod
|
||
@cached(cache_type='fin_profitability', key_params=['company_id', 'luna', 'an'])
|
||
async def calculate_profitability_indicators(
|
||
company_id: int,
|
||
luna: int,
|
||
an: int
|
||
) -> ProfitabilityIndicators:
|
||
"""
|
||
Calculează indicatorii de profitabilitate pentru evaluarea randamentului afacerii.
|
||
|
||
Indicatori calculați:
|
||
- cifra_afaceri: Total venituri operaționale (Clasa 7)
|
||
- cheltuieli_totale: Total cheltuieli operaționale (Clasa 6)
|
||
- profit_brut: EBIT = venituri - cheltuieli
|
||
- marja_profit_brut: profit / venituri * 100
|
||
- roa: Return on Assets = profit / total_active * 100
|
||
- roe: Return on Equity = profit / capitaluri_proprii * 100
|
||
|
||
Praguri de referință:
|
||
- Marja profit: good > 10%, warning 5-10%, danger < 5%
|
||
- ROA: good > 5%, warning 2-5%, danger < 2%
|
||
- ROE: good > 10%, warning 5-10%, danger < 5%
|
||
|
||
Args:
|
||
company_id: ID-ul firmei
|
||
luna: Luna contabilă (1-12)
|
||
an: Anul contabil
|
||
|
||
Returns:
|
||
ProfitabilityIndicators cu cei șase indicatori de profitabilitate
|
||
"""
|
||
# Obținem agregatele din balanță (include venituri, cheltuieli, active, capital)
|
||
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
|
||
company_id, luna, an
|
||
)
|
||
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
|
||
if isinstance(aggregates, dict):
|
||
aggregates = BalanceSheetAggregates(**aggregates)
|
||
|
||
# Extragem valorile necesare
|
||
venituri = float(aggregates.venituri)
|
||
cheltuieli_oper = float(aggregates.cheltuieli_operationale)
|
||
cheltuieli_fin = float(aggregates.cheltuieli_financiare)
|
||
cheltuieli_total = cheltuieli_oper + cheltuieli_fin
|
||
profit_brut_val = float(aggregates.ebit) # EBIT = venituri - cheltuieli operaționale
|
||
total_active = float(aggregates.total_active)
|
||
capitaluri_proprii_val = float(aggregates.capitaluri_proprii)
|
||
|
||
# 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
|
||
) -> 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
|
||
)
|
||
# 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'])
|
||
async def get_historical_indicators(
|
||
company_id: int,
|
||
months: int = 12,
|
||
luna: Optional[int] = None,
|
||
an: Optional[int] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
Calculează indicatorii financiari pentru ultimele `months` luni
|
||
și returnează datele pentru sparklines.
|
||
|
||
Această metodă este optimizată pentru performanță cu cache separat
|
||
(TTL 1 oră) deoarece datele istorice se schimbă rar.
|
||
|
||
Args:
|
||
company_id: ID-ul firmei
|
||
months: Numărul de luni pentru istoric (default 12)
|
||
luna: Luna de referință (dacă None, folosește luna curentă)
|
||
an: Anul de referință (dacă None, folosește anul curent)
|
||
|
||
Returns:
|
||
Dict cu:
|
||
- sparkline_labels: Array cu etichetele lunilor
|
||
- lichiditate: Dict cu sparkline_data pentru fiecare indicator
|
||
- eficienta: Dict cu sparkline_data pentru fiecare indicator
|
||
- risc: Dict cu sparkline_data pentru fiecare indicator
|
||
- cash_flow: Dict cu sparkline_data pentru fiecare indicator
|
||
- dinamica: Dict cu sparkline_data pentru fiecare indicator
|
||
- altman_zscore: Dict cu sparkline_data pentru fiecare indicator
|
||
"""
|
||
from datetime import datetime
|
||
|
||
# Dacă luna/an nu sunt specificate, folosim data curentă
|
||
resolved_luna: int
|
||
resolved_an: int
|
||
|
||
if luna is None or an is None:
|
||
try:
|
||
from .dashboard_service import DashboardService
|
||
current_period = await DashboardService.get_current_period(company_id)
|
||
resolved_luna = luna if luna is not None else current_period.get('luna', datetime.now().month)
|
||
resolved_an = an if an is not None else current_period.get('an', datetime.now().year)
|
||
except Exception as e:
|
||
logger.warning(f"Could not get current period: {e}, using defaults")
|
||
resolved_luna = luna if luna is not None else datetime.now().month
|
||
resolved_an = an if an is not None else datetime.now().year
|
||
else:
|
||
resolved_luna = luna
|
||
resolved_an = an
|
||
|
||
# Generăm perioadele și etichetele
|
||
periods = FinancialIndicatorsService._get_historical_periods(resolved_luna, resolved_an, months)
|
||
labels = FinancialIndicatorsService._generate_month_labels(resolved_luna, resolved_an, months)
|
||
|
||
# Inițializăm structurile pentru sparkline data
|
||
historical_data = {
|
||
'sparkline_labels': labels,
|
||
'lichiditate': {
|
||
'lichiditate_curenta': [],
|
||
'lichiditate_imediata': [],
|
||
'lichiditate_vedere': []
|
||
},
|
||
'eficienta': {
|
||
'dso': [],
|
||
'dpo': [],
|
||
'cash_conversion_cycle': [],
|
||
'rata_incasare': [],
|
||
'rata_plata': []
|
||
},
|
||
'risc': {
|
||
'creante_restante_pct': [],
|
||
'creante_90plus_pct': [],
|
||
'datorii_restante_pct': [],
|
||
'raport_datorii_trezorerie': []
|
||
},
|
||
'cash_flow': {
|
||
'flux_net_lunar': [],
|
||
'cash_flow_ytd': [],
|
||
'flux_net_yoy_pct': [],
|
||
'acoperire_cash_flow': []
|
||
},
|
||
'dinamica': {
|
||
'crestere_vanzari_yoy': [],
|
||
'crestere_achizitii_yoy': [],
|
||
'marja_implicita': []
|
||
},
|
||
'altman_zscore': {
|
||
'zscore': [],
|
||
'x1': [],
|
||
'x2': [],
|
||
'x3': [],
|
||
'x4': []
|
||
},
|
||
'profitabilitate': {
|
||
'cifra_afaceri': [],
|
||
'cheltuieli_totale': [],
|
||
'profit_brut': [],
|
||
'marja_profit_brut': [],
|
||
'roa': [],
|
||
'roe': []
|
||
},
|
||
'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
|
||
)
|
||
# Ensure lichiditate is a model (cache may return dict)
|
||
if isinstance(lichiditate, dict):
|
||
lichiditate = LiquidityIndicators(**lichiditate)
|
||
historical_data['lichiditate']['lichiditate_curenta'].append(
|
||
lichiditate.lichiditate_curenta.value
|
||
)
|
||
historical_data['lichiditate']['lichiditate_imediata'].append(
|
||
lichiditate.lichiditate_imediata.value
|
||
)
|
||
historical_data['lichiditate']['lichiditate_vedere'].append(
|
||
lichiditate.lichiditate_vedere.value
|
||
)
|
||
processed_categories.add('lichiditate')
|
||
|
||
# Eficiență
|
||
eficienta = await FinancialIndicatorsService.calculate_efficiency_indicators(
|
||
company_id, period_luna, period_an
|
||
)
|
||
# Ensure eficienta is a model (cache may return dict)
|
||
if isinstance(eficienta, dict):
|
||
eficienta = EfficiencyIndicators(**eficienta)
|
||
historical_data['eficienta']['dso'].append(eficienta.dso.value)
|
||
historical_data['eficienta']['dpo'].append(eficienta.dpo.value)
|
||
historical_data['eficienta']['cash_conversion_cycle'].append(
|
||
eficienta.cash_conversion_cycle.value
|
||
)
|
||
historical_data['eficienta']['rata_incasare'].append(eficienta.rata_incasare.value)
|
||
historical_data['eficienta']['rata_plata'].append(eficienta.rata_plata.value)
|
||
processed_categories.add('eficienta')
|
||
|
||
# Risc
|
||
risc = await FinancialIndicatorsService.calculate_risk_indicators(
|
||
company_id, period_luna, period_an
|
||
)
|
||
# Ensure risc is a model (cache may return dict)
|
||
if isinstance(risc, dict):
|
||
risc = RiskIndicators(**risc)
|
||
historical_data['risc']['creante_restante_pct'].append(
|
||
risc.creante_restante_pct.value
|
||
)
|
||
historical_data['risc']['creante_90plus_pct'].append(risc.creante_90plus_pct.value)
|
||
historical_data['risc']['datorii_restante_pct'].append(
|
||
risc.datorii_restante_pct.value
|
||
)
|
||
historical_data['risc']['raport_datorii_trezorerie'].append(
|
||
risc.raport_datorii_trezorerie.value
|
||
)
|
||
processed_categories.add('risc')
|
||
|
||
# Cash Flow
|
||
cash_flow = await FinancialIndicatorsService.calculate_cashflow_indicators(
|
||
company_id, period_luna, period_an
|
||
)
|
||
# Ensure cash_flow is a model (cache may return dict)
|
||
if isinstance(cash_flow, dict):
|
||
cash_flow = CashFlowIndicators(**cash_flow)
|
||
historical_data['cash_flow']['flux_net_lunar'].append(cash_flow.flux_net_lunar.value)
|
||
historical_data['cash_flow']['cash_flow_ytd'].append(cash_flow.cash_flow_ytd.value)
|
||
historical_data['cash_flow']['flux_net_yoy_pct'].append(
|
||
cash_flow.flux_net_yoy_pct.value
|
||
)
|
||
historical_data['cash_flow']['acoperire_cash_flow'].append(
|
||
cash_flow.acoperire_cash_flow.value
|
||
)
|
||
processed_categories.add('cash_flow')
|
||
|
||
# Dinamica
|
||
dinamica = await FinancialIndicatorsService.calculate_dynamics_indicators(
|
||
company_id, period_luna, period_an
|
||
)
|
||
# Ensure dinamica is a model (cache may return dict)
|
||
if isinstance(dinamica, dict):
|
||
dinamica = DynamicsIndicators(**dinamica)
|
||
historical_data['dinamica']['crestere_vanzari_yoy'].append(
|
||
dinamica.crestere_vanzari_yoy.value
|
||
)
|
||
historical_data['dinamica']['crestere_achizitii_yoy'].append(
|
||
dinamica.crestere_achizitii_yoy.value
|
||
)
|
||
historical_data['dinamica']['marja_implicita'].append(dinamica.marja_implicita.value)
|
||
processed_categories.add('dinamica')
|
||
|
||
# Altman Z-Score
|
||
altman = await FinancialIndicatorsService.calculate_altman_zscore(
|
||
company_id, period_luna, period_an
|
||
)
|
||
# Ensure altman is a model (cache may return dict)
|
||
if isinstance(altman, dict):
|
||
altman = AltmanZScore(**altman)
|
||
historical_data['altman_zscore']['zscore'].append(altman.zscore.value)
|
||
historical_data['altman_zscore']['x1'].append(altman.x1.value)
|
||
historical_data['altman_zscore']['x2'].append(altman.x2.value)
|
||
historical_data['altman_zscore']['x3'].append(altman.x3.value)
|
||
historical_data['altman_zscore']['x4'].append(altman.x4.value)
|
||
processed_categories.add('altman_zscore')
|
||
|
||
# Profitabilitate
|
||
profitabilitate = await FinancialIndicatorsService.calculate_profitability_indicators(
|
||
company_id, period_luna, period_an
|
||
)
|
||
# Ensure profitabilitate is a model (cache may return dict)
|
||
if isinstance(profitabilitate, dict):
|
||
profitabilitate = ProfitabilityIndicators(**profitabilitate)
|
||
historical_data['profitabilitate']['cifra_afaceri'].append(
|
||
profitabilitate.cifra_afaceri.value
|
||
)
|
||
historical_data['profitabilitate']['cheltuieli_totale'].append(
|
||
profitabilitate.cheltuieli_totale.value
|
||
)
|
||
historical_data['profitabilitate']['profit_brut'].append(
|
||
profitabilitate.profit_brut.value
|
||
)
|
||
historical_data['profitabilitate']['marja_profit_brut'].append(
|
||
profitabilitate.marja_profit_brut.value
|
||
)
|
||
historical_data['profitabilitate']['roa'].append(profitabilitate.roa.value)
|
||
historical_data['profitabilitate']['roe'].append(profitabilitate.roe.value)
|
||
processed_categories.add('profitabilitate')
|
||
|
||
# Solvabilitate
|
||
solvabilitate = await FinancialIndicatorsService.calculate_solvability_indicators(
|
||
company_id, period_luna, period_an
|
||
)
|
||
# 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'])
|
||
async def get_indicators_with_sparklines(
|
||
company_id: int,
|
||
luna: int,
|
||
an: int,
|
||
months: int = 12,
|
||
request: Optional[Request] = 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
|
||
)
|
||
|
||
lichiditate_task = FinancialIndicatorsService.calculate_liquidity_indicators(
|
||
company_id, luna, an
|
||
)
|
||
eficienta_task = FinancialIndicatorsService.calculate_efficiency_indicators(
|
||
company_id, luna, an
|
||
)
|
||
risc_task = FinancialIndicatorsService.calculate_risk_indicators(
|
||
company_id, luna, an
|
||
)
|
||
cash_flow_task = FinancialIndicatorsService.calculate_cashflow_indicators(
|
||
company_id, luna, an
|
||
)
|
||
dinamica_task = FinancialIndicatorsService.calculate_dynamics_indicators(
|
||
company_id, luna, an
|
||
)
|
||
altman_task = FinancialIndicatorsService.calculate_altman_zscore(
|
||
company_id, luna, an
|
||
)
|
||
profitabilitate_task = FinancialIndicatorsService.calculate_profitability_indicators(
|
||
company_id, luna, an
|
||
)
|
||
solvabilitate_task = FinancialIndicatorsService.calculate_solvability_indicators(
|
||
company_id, luna, an
|
||
)
|
||
|
||
(
|
||
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
|