diff --git a/backend/config.py b/backend/config.py
index f97cae2..503f480 100644
--- a/backend/config.py
+++ b/backend/config.py
@@ -127,12 +127,16 @@ class UnifiedSettings(BaseSettings):
@property
def data_entry_database_url(self) -> str:
"""Get SQLite database URL for async (Data Entry)."""
- return f"sqlite+aiosqlite:///{self.data_entry_sqlite_database_path}"
+ # Resolve to absolute path for Windows/IIS compatibility
+ abs_path = Path(self.data_entry_sqlite_database_path).resolve()
+ return f"sqlite+aiosqlite:///{abs_path}"
@property
def data_entry_sync_database_url(self) -> str:
"""Get SQLite database URL for sync operations (Alembic)."""
- return f"sqlite:///{self.data_entry_sqlite_database_path}"
+ # Resolve to absolute path for Windows/IIS compatibility
+ abs_path = Path(self.data_entry_sqlite_database_path).resolve()
+ return f"sqlite:///{abs_path}"
@property
def data_entry_upload_path_resolved(self) -> Path:
diff --git a/backend/modules/data_entry/config.py b/backend/modules/data_entry/config.py
index c2aebe8..a2ecf36 100644
--- a/backend/modules/data_entry/config.py
+++ b/backend/modules/data_entry/config.py
@@ -61,12 +61,16 @@ class Settings(BaseSettings):
@property
def database_url(self) -> str:
"""Get SQLite database URL for async."""
- return f"sqlite+aiosqlite:///{self.sqlite_database_path}"
+ # Resolve to absolute path for Windows/IIS compatibility
+ abs_path = Path(self.sqlite_database_path).resolve()
+ return f"sqlite+aiosqlite:///{abs_path}"
@property
def sync_database_url(self) -> str:
"""Get SQLite database URL for sync operations (Alembic)."""
- return f"sqlite:///{self.sqlite_database_path}"
+ # Resolve to absolute path for Windows/IIS compatibility
+ abs_path = Path(self.sqlite_database_path).resolve()
+ return f"sqlite:///{abs_path}"
@property
def upload_path_resolved(self) -> Path:
diff --git a/backend/modules/data_entry/migrations/env.py b/backend/modules/data_entry/migrations/env.py
index d6c1df9..739aa33 100644
--- a/backend/modules/data_entry/migrations/env.py
+++ b/backend/modules/data_entry/migrations/env.py
@@ -1,6 +1,7 @@
"""Alembic environment configuration."""
import os
+from pathlib import Path
from logging.config import fileConfig
from dotenv import load_dotenv
@@ -24,7 +25,8 @@ from backend.modules.data_entry.db.models.ocr_settings import UserOCRPreference,
config = context.config
# Override sqlalchemy.url from environment variable if set
-db_path = os.getenv("SQLITE_DATABASE_PATH", "data/receipts/receipts.db")
+# Resolve to absolute path for Windows/IIS compatibility
+db_path = Path(os.getenv("SQLITE_DATABASE_PATH", "data/receipts/receipts.db")).resolve()
config.set_main_option("sqlalchemy.url", f"sqlite:///{db_path}")
# Interpret the config file for Python logging.
diff --git a/backend/modules/reports/models/financial_indicators.py b/backend/modules/reports/models/financial_indicators.py
new file mode 100644
index 0000000..c22491a
--- /dev/null
+++ b/backend/modules/reports/models/financial_indicators.py
@@ -0,0 +1,873 @@
+"""
+Pydantic models pentru indicatori financiari.
+
+Definește structurile de date pentru:
+- BalanceSheetAggregates: Solduri agregate din balanța de verificare
+- IndicatorResult: Rezultatul standardizat cu sparkline data pentru vizualizare trend
+- Modele pentru indicatori de lichiditate, eficiență, risc și Altman Z-Score
+"""
+from decimal import Decimal
+from typing import Optional, List
+
+from pydantic import BaseModel, Field
+
+
+class BalanceSheetAggregates(BaseModel):
+ """
+ Solduri agregate din balanța de verificare (VBAL) pe categorii de conturi.
+
+ Agregă datele din VBAL folosind prefixe de conturi conform Planului de
+ Conturi General (PCG) românesc pentru calculul indicatorilor financiari.
+
+ Attributes:
+ company_id: ID-ul firmei
+ luna: Luna contabilă (1-12)
+ an: Anul contabil
+ active_imobilizate: Active imobilizate nete (brut - amortizări - ajustări)
+ stocuri: Valoarea stocurilor (brut - ajustări depreciere)
+ creante: Creanțe nete (brut - ajustări depreciere)
+ disponibilitati: Disponibilități bănești (bancă + casă)
+ capital_propriu: Capital social + prime + rezerve
+ rezultat: Rezultat reportat + rezultatul exercițiului curent
+ datorii_termen_lung: Datorii cu scadență > 1 an
+ datorii_curente: Datorii cu scadență <= 1 an
+ venituri: Venituri din exploatare (pentru calcul EBIT)
+ cheltuieli_operationale: Cheltuieli de exploatare (pentru calcul EBIT)
+ """
+ company_id: int = Field(description="ID-ul firmei")
+ luna: int = Field(ge=1, le=12, description="Luna contabilă (1-12)")
+ an: int = Field(ge=2000, le=2100, description="Anul contabil")
+
+ # Active
+ active_imobilizate: Decimal = Field(
+ default=Decimal('0'),
+ description="Active imobilizate nete (Clasa 2 - amortizări)"
+ )
+ stocuri: Decimal = Field(
+ default=Decimal('0'),
+ description="Stocuri (Clasa 3 - ajustări depreciere)"
+ )
+ creante: Decimal = Field(
+ default=Decimal('0'),
+ description="Creanțe comerciale și alte creanțe (4111, 461, etc.)"
+ )
+ disponibilitati: Decimal = Field(
+ default=Decimal('0'),
+ description="Disponibilități (bancă 512x + casă 531x)"
+ )
+
+ # Pasive - Capitaluri
+ capital_propriu: Decimal = Field(
+ default=Decimal('0'),
+ description="Capital social + prime + rezerve (101, 104, 105, 106)"
+ )
+ rezultat: Decimal = Field(
+ default=Decimal('0'),
+ description="Rezultat reportat + rezultat curent (107, 117, 121)"
+ )
+
+ # Pasive - Datorii
+ datorii_termen_lung: Decimal = Field(
+ default=Decimal('0'),
+ description="Datorii pe termen lung (Clasa 16)"
+ )
+ datorii_curente: Decimal = Field(
+ default=Decimal('0'),
+ description="Datorii curente (401, 421, 4423, 462, etc.)"
+ )
+
+ # Venituri și Cheltuieli (pentru calcul EBIT în Altman Z-Score)
+ venituri: Decimal = Field(
+ default=Decimal('0'),
+ description="Venituri din exploatare (Clasa 7)"
+ )
+ cheltuieli_operationale: Decimal = Field(
+ default=Decimal('0'),
+ description="Cheltuieli operaționale (Clasa 6 fără 66x)"
+ )
+ cheltuieli_financiare: Decimal = Field(
+ default=Decimal('0'),
+ description="Cheltuieli financiare (Clasa 66 - dobânzi, diferențe curs)"
+ )
+
+ # Computed properties pentru calculele ulterioare
+ @property
+ def active_curente(self) -> Decimal:
+ """Active curente = stocuri + creanțe + disponibilități"""
+ return self.stocuri + self.creante + self.disponibilitati
+
+ @property
+ def total_active(self) -> Decimal:
+ """Total active = active imobilizate + active curente"""
+ return self.active_imobilizate + self.active_curente
+
+ @property
+ def total_datorii(self) -> Decimal:
+ """Total datorii = datorii termen lung + datorii curente"""
+ return self.datorii_termen_lung + self.datorii_curente
+
+ @property
+ def capitaluri_proprii(self) -> Decimal:
+ """Capitaluri proprii = capital propriu + rezultat"""
+ return self.capital_propriu + self.rezultat
+
+ @property
+ def ebit(self) -> Decimal:
+ """EBIT (Earnings Before Interest and Taxes) = venituri - cheltuieli operaționale"""
+ return self.venituri - self.cheltuieli_operationale
+
+ @property
+ def working_capital(self) -> Decimal:
+ """Working capital (fond de rulment) = active curente - datorii curente"""
+ return self.active_curente - self.datorii_curente
+
+ class Config:
+ """Configurare Pydantic"""
+ from_attributes = True
+ json_schema_extra = {
+ "example": {
+ "company_id": 123,
+ "luna": 12,
+ "an": 2024,
+ "active_imobilizate": 1500000.00,
+ "stocuri": 350000.00,
+ "creante": 420000.00,
+ "disponibilitati": 180000.00,
+ "capital_propriu": 800000.00,
+ "rezultat": 250000.00,
+ "datorii_termen_lung": 500000.00,
+ "datorii_curente": 400000.00,
+ "venituri": 2500000.00,
+ "cheltuieli_operationale": 2100000.00
+ }
+ }
+
+
+class IndicatorResult(BaseModel):
+ """
+ Rezultatul standardizat pentru un indicator financiar.
+
+ Fiecare indicator returnează valoarea calculată împreună cu
+ statusul (good/warning/danger), pragurile de referință și
+ date pentru sparkline (evoluția pe ultimele 12 luni).
+
+ Attributes:
+ value: Valoarea calculată a indicatorului (None dacă nu se poate calcula)
+ status: Statusul indicatorului - 'good', 'warning', sau 'danger'
+ threshold_min: Pragul minim pentru status 'good'
+ threshold_max: Pragul maxim pentru status 'good' (opțional)
+ message: Mesaj explicativ (opțional, pentru cazuri speciale)
+ sparkline_data: Array cu valorile indicatorului pe ultimele 12 luni (pentru vizualizare trend)
+ sparkline_labels: Array cu etichetele lunilor în format 'MMM YY' (ex: 'Feb 24', 'Mar 24')
+ """
+ value: Optional[float] = Field(
+ default=None,
+ description="Valoarea calculată a indicatorului"
+ )
+ status: str = Field(
+ default="warning",
+ description="Statusul: 'good', 'warning', sau 'danger'"
+ )
+ threshold_min: Optional[float] = Field(
+ default=None,
+ description="Pragul minim pentru status 'good'"
+ )
+ threshold_max: Optional[float] = Field(
+ default=None,
+ description="Pragul maxim pentru status 'good'"
+ )
+ message: Optional[str] = Field(
+ default=None,
+ description="Mesaj explicativ pentru cazuri speciale"
+ )
+ sparkline_data: Optional[List[Optional[float]]] = Field(
+ default=None,
+ description="Array cu valorile indicatorului pe ultimele 12 luni (pentru sparkline)"
+ )
+ sparkline_labels: Optional[List[str]] = Field(
+ default=None,
+ description="Array cu etichetele lunilor în format 'MMM YY' (ex: 'Feb 24', 'Mar 24')"
+ )
+
+ class Config:
+ json_schema_extra = {
+ "example": {
+ "value": 1.25,
+ "status": "good",
+ "threshold_min": 1.0,
+ "threshold_max": None,
+ "message": None,
+ "sparkline_data": [1.15, 1.18, 1.20, 1.22, 1.19, 1.21, 1.23, 1.25, 1.24, 1.26, 1.25, 1.25],
+ "sparkline_labels": ["Feb 24", "Mar 24", "Apr 24", "May 24", "Jun 24", "Jul 24", "Aug 24", "Sep 24", "Oct 24", "Nov 24", "Dec 24", "Jan 25"]
+ }
+ }
+
+
+class LiquidityIndicators(BaseModel):
+ """
+ Indicatori de lichiditate pentru evaluarea capacității de plată
+ a datoriilor pe termen scurt.
+
+ Attributes:
+ lichiditate_curenta: Current Ratio = active_curente / datorii_curente
+ - Măsoară capacitatea de a plăti datoriile pe termen scurt cu active curente
+ - Good: >= 2.0, Warning: 1.0-2.0, Danger: < 1.0
+
+ lichiditate_imediata: Quick Ratio = (disponibilități + creanțe) / datorii_curente
+ - Măsoară capacitatea de plată fără a depinde de vânzarea stocurilor
+ - Good: >= 1.0, Warning: 0.5-1.0, Danger: < 0.5
+
+ lichiditate_vedere: Cash Ratio = disponibilități / datorii_curente
+ - Măsoară capacitatea de plată imediată doar din numerar
+ - Good: >= 0.2, Warning: 0.1-0.2, Danger: < 0.1
+ """
+ lichiditate_curenta: IndicatorResult = Field(
+ description="Current Ratio = active_curente / datorii_curente"
+ )
+ lichiditate_imediata: IndicatorResult = Field(
+ description="Quick Ratio = (disponibilități + creanțe) / datorii_curente"
+ )
+ lichiditate_vedere: IndicatorResult = Field(
+ description="Cash Ratio = disponibilități / datorii_curente"
+ )
+
+ class Config:
+ json_schema_extra = {
+ "example": {
+ "lichiditate_curenta": {
+ "value": 2.37,
+ "status": "good",
+ "threshold_min": 2.0,
+ "threshold_max": None
+ },
+ "lichiditate_imediata": {
+ "value": 1.50,
+ "status": "good",
+ "threshold_min": 1.0,
+ "threshold_max": None
+ },
+ "lichiditate_vedere": {
+ "value": 0.45,
+ "status": "good",
+ "threshold_min": 0.2,
+ "threshold_max": None
+ }
+ }
+ }
+
+
+class EfficiencyIndicators(BaseModel):
+ """
+ Indicatori de eficiență pentru evaluarea vitezei de conversie
+ a resurselor în numerar (working capital efficiency).
+
+ Attributes:
+ dso: Days Sales Outstanding (Durata medie de încasare)
+ - Formula: (clienti_sold / facturari_lunare) * 30
+ - Măsoară câte zile durează în medie încasarea creanțelor
+ - Good: < 30 zile, Warning: 30-45 zile, Danger: > 45 zile
+
+ dpo: Days Payables Outstanding (Durata medie de plată)
+ - Formula: (furnizori_sold / achizitii_lunare) * 30
+ - Măsoară câte zile durează în medie plata furnizorilor
+ - Valoare mai mare = folosim mai mult creditul furnizorilor
+
+ cash_conversion_cycle: Ciclu de conversie a numerarului
+ - Formula: DSO - DPO
+ - Pozitiv = numerar blocat în ciclul de afaceri
+ - Negativ = folosim creditul furnizorilor (favorabil)
+
+ rata_incasare: Rata de încasare (Collection Rate)
+ - Formula: incasari / facturari * 100
+ - Măsoară ce procent din facturări s-a încasat
+ - Good: >= 95%, Warning: 80-95%, Danger: < 80%
+
+ rata_plata: Rata de plată (Payment Rate)
+ - Formula: plati / achizitii * 100
+ - Măsoară ce procent din achiziții s-a achitat
+ """
+ dso: IndicatorResult = Field(
+ description="Days Sales Outstanding = (clienti_sold / facturari_lunare) * 30"
+ )
+ dpo: IndicatorResult = Field(
+ description="Days Payables Outstanding = (furnizori_sold / achizitii_lunare) * 30"
+ )
+ cash_conversion_cycle: IndicatorResult = Field(
+ description="Cash Conversion Cycle = DSO - DPO"
+ )
+ rata_incasare: IndicatorResult = Field(
+ description="Rata de încasare = incasari / facturari * 100"
+ )
+ rata_plata: IndicatorResult = Field(
+ description="Rata de plată = plati / achizitii * 100"
+ )
+
+ class Config:
+ json_schema_extra = {
+ "example": {
+ "dso": {
+ "value": 28.5,
+ "status": "good",
+ "threshold_min": None,
+ "threshold_max": 30
+ },
+ "dpo": {
+ "value": 35.2,
+ "status": "good",
+ "threshold_min": None,
+ "threshold_max": None,
+ "message": "Folosim bine creditul furnizorilor"
+ },
+ "cash_conversion_cycle": {
+ "value": -6.7,
+ "status": "good",
+ "threshold_min": None,
+ "threshold_max": 0,
+ "message": "Ciclu negativ = finanțare gratuită de la furnizori"
+ },
+ "rata_incasare": {
+ "value": 92.5,
+ "status": "warning",
+ "threshold_min": 95,
+ "threshold_max": None
+ },
+ "rata_plata": {
+ "value": 88.3,
+ "status": "good",
+ "threshold_min": None,
+ "threshold_max": None
+ }
+ }
+ }
+
+
+class RiskIndicators(BaseModel):
+ """
+ Indicatori de risc și aging pentru evaluarea sănătății
+ portofoliului de creanțe și datorii.
+
+ Attributes:
+ creante_restante_pct: Procentul creanțelor restante din total clienți
+ - Formula: clienti_sold_restant / clienti_sold_total * 100
+ - Măsoară ce procent din creanțe sunt depășite ca termen
+ - 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
+ - Indică creanțele cu risc mare de nerecuperare
+ - Good: < 5%, Warning: 5-10%, Danger: > 10%
+
+ datorii_restante_pct: Procentul datoriilor restante din total furnizori
+ - Formula: furnizori_sold_restant / furnizori_sold_total * 100
+ - Măsoară nivelul de întârzieri la plată
+ - Good: < 10%, Warning: 10-20%, Danger: > 20%
+
+ raport_datorii_trezorerie: Raportul între datorii furnizori și trezorerie
+ - Formula: furnizori_sold_total / trezorerie
+ - Indică câte luni de cash sunt necesare pentru a plăti furnizorii
+ - Good: < 2, Warning: 2-4, Danger: > 4
+ """
+ creante_restante_pct: IndicatorResult = Field(
+ description="Procentul creanțelor restante = clienti_sold_restant / clienti_sold_total * 100"
+ )
+ creante_90plus_pct: IndicatorResult = Field(
+ description="Procentul creanțelor > 90 zile = clienti_restant_90plus / clienti_sold_total * 100"
+ )
+ datorii_restante_pct: IndicatorResult = Field(
+ description="Procentul datoriilor restante = furnizori_sold_restant / furnizori_sold_total * 100"
+ )
+ raport_datorii_trezorerie: IndicatorResult = Field(
+ description="Raport datorii/trezorerie = furnizori_sold_total / trezorerie"
+ )
+
+ class Config:
+ json_schema_extra = {
+ "example": {
+ "creante_restante_pct": {
+ "value": 15.5,
+ "status": "good",
+ "threshold_min": None,
+ "threshold_max": 20
+ },
+ "creante_90plus_pct": {
+ "value": 3.2,
+ "status": "good",
+ "threshold_min": None,
+ "threshold_max": 5
+ },
+ "datorii_restante_pct": {
+ "value": 8.5,
+ "status": "good",
+ "threshold_min": None,
+ "threshold_max": 10
+ },
+ "raport_datorii_trezorerie": {
+ "value": 1.8,
+ "status": "good",
+ "threshold_min": None,
+ "threshold_max": 2,
+ "message": "Trezorerie suficientă pentru acoperirea datoriilor"
+ }
+ }
+ }
+
+
+class CashFlowIndicators(BaseModel):
+ """
+ Indicatori de cash flow pentru evaluarea generării și consumului de numerar.
+
+ Attributes:
+ flux_net_lunar: Fluxul net de numerar lunar (încasări - plăți)
+ - Formula: incasari_luna - plati_luna
+ - Pozitiv = firma generează numerar, Negativ = firma consumă numerar
+ - Good: > 0, Danger: < 0
+
+ cash_flow_ytd: Fluxul de numerar cumulat de la începutul anului (Year-To-Date)
+ - Formula: 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ă YoY (Year-over-Year)
+ - Formula: (cf_curent - cf_anterior) / abs(cf_anterior) * 100
+ - Compară cash flow-ul curent cu aceeași perioadă din anul anterior
+
+ acoperire_cash_flow: Rata de acoperire a datoriilor restante
+ - Formula: cash_flow_ytd / datorii_restante
+ - Arată de câte ori cash flow-ul YTD poate acoperi datoriile restante
+ - Good: > 1 (cash flow suficient), Danger: < 0.5
+ """
+ flux_net_lunar: IndicatorResult = Field(
+ description="Flux net lunar = incasari_luna - plati_luna"
+ )
+ cash_flow_ytd: IndicatorResult = Field(
+ description="Cash flow cumulat YTD = suma fluxurilor de la ianuarie"
+ )
+ flux_net_yoy_pct: IndicatorResult = Field(
+ description="Variația YoY = (cf_curent - cf_anterior) / abs(cf_anterior) * 100"
+ )
+ acoperire_cash_flow: IndicatorResult = Field(
+ description="Acoperire datorii = cash_flow_ytd / datorii_restante"
+ )
+
+ class Config:
+ json_schema_extra = {
+ "example": {
+ "flux_net_lunar": {
+ "value": 125000.50,
+ "status": "good",
+ "threshold_min": 0,
+ "threshold_max": None,
+ "message": "Firma generează numerar"
+ },
+ "cash_flow_ytd": {
+ "value": 850000.00,
+ "status": "good",
+ "threshold_min": 0,
+ "threshold_max": None
+ },
+ "flux_net_yoy_pct": {
+ "value": 15.5,
+ "status": "good",
+ "threshold_min": 0,
+ "threshold_max": None,
+ "message": "Creștere cash flow față de anul anterior"
+ },
+ "acoperire_cash_flow": {
+ "value": 2.5,
+ "status": "good",
+ "threshold_min": 1.0,
+ "threshold_max": None,
+ "message": "Cash flow suficient pentru acoperirea datoriilor"
+ }
+ }
+ }
+
+
+class DynamicsIndicators(BaseModel):
+ """
+ Indicatori de dinamică pentru evaluarea evoluției vânzărilor și achizițiilor.
+
+ Arată dacă afacerea crește sau scade prin comparație YoY (Year-over-Year).
+
+ Attributes:
+ crestere_vanzari_yoy: Creșterea procentuală a vânzărilor față de anul anterior
+ - Formula: (facturari_curent - facturari_anterior) / facturari_anterior * 100
+ - Măsoară dinamica vânzărilor - creștere sau scădere
+ - Good: > 5%, Warning: 0-5%, Danger: < 0%
+
+ crestere_achizitii_yoy: Creșterea procentuală a achizițiilor față de anul anterior
+ - Formula: (achizitii_curent - achizitii_anterior) / achizitii_anterior * 100
+ - Creșterea achizițiilor poate indica expansiune sau costuri mai mari
+
+ marja_implicita: Marja implicită din diferența facturări - achiziții
+ - Formula: (facturari - achizitii) / facturari * 100
+ - Arată ce procent din vânzări rămâne după achiziții
+ - Good: > 20%, Warning: 10-20%, Danger: < 10%
+ """
+ crestere_vanzari_yoy: IndicatorResult = Field(
+ description="Creștere vânzări YoY = (facturari_curent - facturari_anterior) / facturari_anterior * 100"
+ )
+ crestere_achizitii_yoy: IndicatorResult = Field(
+ description="Creștere achiziții YoY = (achizitii_curent - achizitii_anterior) / achizitii_anterior * 100"
+ )
+ marja_implicita: IndicatorResult = Field(
+ description="Marja implicită = (facturari - achizitii) / facturari * 100"
+ )
+
+ class Config:
+ json_schema_extra = {
+ "example": {
+ "crestere_vanzari_yoy": {
+ "value": 12.5,
+ "status": "good",
+ "threshold_min": 5.0,
+ "threshold_max": None,
+ "message": "Creștere semnificativă a vânzărilor"
+ },
+ "crestere_achizitii_yoy": {
+ "value": 8.3,
+ "status": "good",
+ "threshold_min": None,
+ "threshold_max": None,
+ "message": "Achiziții în creștere"
+ },
+ "marja_implicita": {
+ "value": 25.5,
+ "status": "good",
+ "threshold_min": 20.0,
+ "threshold_max": None,
+ "message": "Marjă implicită sănătoasă"
+ }
+ }
+ }
+
+
+class AltmanZScore(BaseModel):
+ """
+ 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
+
+ Coeficienții sunt specifici pentru companii care nu sunt listate la bursă,
+ unde se folosește valoarea contabilă a capitalurilor proprii în loc de
+ valoarea de piață a acțiunilor.
+
+ Componente:
+ X1: Working Capital / Total Assets
+ - Măsoară lichiditatea pe termen scurt
+ - Working capital = active curente - datorii curente
+
+ X2: Retained Earnings / Total Assets
+ - Măsoară profitabilitatea cumulată (rezultat reportat)
+ - Include conturile 117 și 121 (rezultat reportat + rezultat curent)
+
+ X3: EBIT / Total Assets
+ - Măsoară eficiența operațională
+ - EBIT = venituri din exploatare - cheltuieli operaționale
+
+ X4: Book Value of Equity / Total Liabilities
+ - Măsoară solvabilitatea (acoperirea datoriilor cu capital propriu)
+ - Capital propriu / (datorii curente + datorii termen lung)
+
+ Zone de risc:
+ - Safe Zone (zscore > 2.60): Risc minim de faliment
+ - Grey Zone (1.10 <= zscore <= 2.60): Risc moderat, necesită atenție
+ - Distress Zone (zscore < 1.10): Risc ridicat de faliment
+
+ Attributes:
+ zscore: Scorul Altman Z calculat
+ status: Zona de risc ('safe', 'grey', sau 'distress')
+ x1: Componenta X1 (Working Capital / Total Assets)
+ x2: Componenta X2 (Retained Earnings / Total Assets)
+ x3: Componenta X3 (EBIT / Total Assets)
+ x4: Componenta X4 (Equity / Total Liabilities)
+ working_capital: Fondul de rulment (active curente - datorii curente)
+ total_assets: Total active
+ """
+ zscore: IndicatorResult = Field(
+ description="Scorul Altman Z = 6.56*X1 + 3.26*X2 + 6.72*X3 + 1.05*X4"
+ )
+ x1: IndicatorResult = Field(
+ description="X1 = Working Capital / Total Assets (lichiditate)"
+ )
+ x2: IndicatorResult = Field(
+ description="X2 = Retained Earnings / Total Assets (profitabilitate)"
+ )
+ x3: IndicatorResult = Field(
+ description="X3 = EBIT / Total Assets (eficiență operațională)"
+ )
+ x4: IndicatorResult = Field(
+ description="X4 = Capitaluri Proprii / Datorii Totale (solvabilitate)"
+ )
+ # Valori absolute pentru verificare manuală în balanță
+ capital_de_lucru: IndicatorResult = Field(
+ description="Capital de lucru = Active Curente - Datorii Curente"
+ )
+ active_totale: IndicatorResult = Field(
+ description="Active Totale = Active Imobilizate + Active Curente"
+ )
+ datorii_totale: IndicatorResult = Field(
+ description="Datorii Totale = Datorii Curente + Datorii Termen Lung"
+ )
+
+ class Config:
+ json_schema_extra = {
+ "example": {
+ "zscore": {
+ "value": 3.25,
+ "status": "safe",
+ "threshold_min": 2.60,
+ "threshold_max": None,
+ "message": "Zona sigură - risc minim de faliment"
+ },
+ "x1": {
+ "value": 0.25,
+ "status": "good",
+ "threshold_min": 0,
+ "threshold_max": None
+ },
+ "x2": {
+ "value": 0.15,
+ "status": "good",
+ "threshold_min": 0,
+ "threshold_max": None
+ },
+ "x3": {
+ "value": 0.12,
+ "status": "good",
+ "threshold_min": 0,
+ "threshold_max": None
+ },
+ "x4": {
+ "value": 1.80,
+ "status": "good",
+ "threshold_min": 1.0,
+ "threshold_max": None
+ },
+ "capital_de_lucru": {
+ "value": 450000.00,
+ "status": "good",
+ "message": "Active Curente - Datorii Curente"
+ },
+ "active_totale": {
+ "value": 1800000.00,
+ "status": "good",
+ "message": "Active Imobilizate + Active Curente"
+ },
+ "datorii_totale": {
+ "value": 1200000.00,
+ "status": "good",
+ "message": "Datorii Curente + Datorii Termen Lung"
+ }
+ }
+ }
+
+
+class ProfitabilityIndicators(BaseModel):
+ """
+ Indicatori de profitabilitate pentru evaluarea randamentului afacerii.
+
+ Calculează indicatori cheie pentru evaluarea profitabilității pe baza
+ datelor din balanța de verificare (VBAL): venituri, cheltuieli, active, capital.
+
+ Attributes:
+ cifra_afaceri: Total venituri din activitatea operațională (Clasa 7)
+ - Reprezintă volumul total al vânzărilor
+ - Sursa: suma conturilor 70x-75x din VBAL
+
+ cheltuieli_totale: Total cheltuieli operaționale (Clasa 6)
+ - Reprezintă costurile activității
+ - Sursa: suma conturilor 60x-65x din VBAL
+
+ profit_brut: Diferența dintre venituri și cheltuieli (EBIT)
+ - Formula: cifra_afaceri - cheltuieli_totale
+ - Good: > 0, Danger: < 0
+
+ marja_profit_brut: Procentul de profit din vânzări
+ - Formula: profit_brut / cifra_afaceri * 100
+ - Good: > 10%, Warning: 5-10%, Danger: < 5%
+
+ roa: Return on Assets - randamentul activelor
+ - Formula: profit_brut / total_active * 100
+ - Măsoară eficiența utilizării activelor
+ - Good: > 5%, Warning: 2-5%, Danger: < 2%
+
+ roe: Return on Equity - randamentul capitalului propriu
+ - Formula: profit_brut / capitaluri_proprii * 100
+ - Măsoară randamentul pentru acționari
+ - Good: > 10%, Warning: 5-10%, Danger: < 5%
+ """
+ cifra_afaceri: IndicatorResult = Field(
+ description="Cifra de afaceri = Total venituri operaționale (Clasa 7)"
+ )
+ # Cheltuieli separate pentru verificare
+ cheltuieli_operationale: IndicatorResult = Field(
+ description="Cheltuieli operaționale = Clasa 60x-65x + 68x (fără dobânzi 66x)"
+ )
+ cheltuieli_financiare: IndicatorResult = Field(
+ description="Cheltuieli financiare = Clasa 66x (dobânzi, diferențe curs valutar)"
+ )
+ cheltuieli_totale: IndicatorResult = Field(
+ description="Cheltuieli totale = Operaționale + Financiare"
+ )
+ profit_brut: IndicatorResult = Field(
+ description="Profit brut (EBIT) = Venituri - Cheltuieli operaționale"
+ )
+ marja_profit_brut: IndicatorResult = Field(
+ description="Marja de profit = Profit brut / Cifra afaceri * 100"
+ )
+ # Indicatori de bază pentru verificare ROA/ROE
+ active_totale: IndicatorResult = Field(
+ description="Active Totale - bază calcul ROA"
+ )
+ capitaluri_proprii: IndicatorResult = Field(
+ description="Capitaluri Proprii - bază calcul ROE"
+ )
+ roa: IndicatorResult = Field(
+ description="Randament Active (ROA) = Profit / Active Totale * 100"
+ )
+ roe: IndicatorResult = Field(
+ description="Randament Capitaluri (ROE) = Profit / Capital Propriu * 100"
+ )
+
+ class Config:
+ json_schema_extra = {
+ "example": {
+ "cifra_afaceri": {
+ "value": 2500000.00,
+ "status": "good",
+ "threshold_min": None,
+ "threshold_max": None
+ },
+ "cheltuieli_totale": {
+ "value": 2100000.00,
+ "status": "good",
+ "threshold_min": None,
+ "threshold_max": None
+ },
+ "profit_brut": {
+ "value": 400000.00,
+ "status": "good",
+ "threshold_min": 0,
+ "threshold_max": None,
+ "message": "Profit operațional pozitiv"
+ },
+ "marja_profit_brut": {
+ "value": 16.0,
+ "status": "good",
+ "threshold_min": 10.0,
+ "threshold_max": None
+ },
+ "roa": {
+ "value": 8.5,
+ "status": "good",
+ "threshold_min": 5.0,
+ "threshold_max": None,
+ "message": "Randament bun al activelor"
+ },
+ "roe": {
+ "value": 15.2,
+ "status": "good",
+ "threshold_min": 10.0,
+ "threshold_max": None,
+ "message": "Randament atractiv pentru acționari"
+ }
+ }
+ }
+
+
+class FinancialIndicatorsResponse(BaseModel):
+ """
+ Răspunsul complet al endpoint-ului /api/reports/dashboard/financial-indicators.
+
+ Agregă toți indicatorii financiari calculați pentru o firmă și perioadă dată.
+ Acest model este folosit pentru serializarea JSON a răspunsului API.
+
+ Attributes:
+ lichiditate: Indicatori de lichiditate (Current Ratio, Quick Ratio, Cash Ratio)
+ eficienta: Indicatori de eficiență (DSO, DPO, CCC, rate încasare/plată)
+ risc: Indicatori de risc (creanțe/datorii restante, raport datorii/trezorerie)
+ cash_flow: Indicatori de cash flow (flux net lunar, YTD, YoY, acoperire)
+ dinamica: Indicatori de dinamică (creștere vânzări/achiziții YoY, marjă)
+ altman_zscore: Scorul Altman Z-Score și componentele X1-X4
+ profitabilitate: Indicatori de profitabilitate (ROA, ROE, marjă profit)
+
+ Usage:
+ GET /api/reports/dashboard/financial-indicators?company=123&luna=12&an=2024
+
+ Response example:
+ {
+ "lichiditate": { ... },
+ "eficienta": { ... },
+ "risc": { ... },
+ "cash_flow": { ... },
+ "dinamica": { ... },
+ "altman_zscore": { ... },
+ "profitabilitate": { ... }
+ }
+ """
+ lichiditate: LiquidityIndicators = Field(
+ description="Indicatori de lichiditate: Current Ratio, Quick Ratio, Cash Ratio"
+ )
+ eficienta: EfficiencyIndicators = Field(
+ description="Indicatori de eficiență: DSO, DPO, CCC, rate încasare/plată"
+ )
+ risc: RiskIndicators = Field(
+ description="Indicatori de risc: creanțe/datorii restante, raport datorii/trezorerie"
+ )
+ cash_flow: CashFlowIndicators = Field(
+ description="Indicatori de cash flow: flux net lunar, YTD, YoY, acoperire"
+ )
+ dinamica: DynamicsIndicators = Field(
+ description="Indicatori de dinamică: creștere vânzări/achiziții YoY, marjă implicită"
+ )
+ altman_zscore: AltmanZScore = Field(
+ description="Altman Z-Score și componentele X1-X4"
+ )
+ profitabilitate: ProfitabilityIndicators = Field(
+ description="Indicatori de profitabilitate: ROA, ROE, marja de profit"
+ )
+
+ class Config:
+ json_schema_extra = {
+ "example": {
+ "lichiditate": {
+ "lichiditate_curenta": {"value": 2.37, "status": "good", "threshold_min": 2.0},
+ "lichiditate_imediata": {"value": 1.50, "status": "good", "threshold_min": 1.0},
+ "lichiditate_vedere": {"value": 0.45, "status": "good", "threshold_min": 0.2}
+ },
+ "eficienta": {
+ "dso": {"value": 28.5, "status": "good", "threshold_max": 30},
+ "dpo": {"value": 35.2, "status": "good"},
+ "cash_conversion_cycle": {"value": -6.7, "status": "good", "threshold_max": 0},
+ "rata_incasare": {"value": 92.5, "status": "warning", "threshold_min": 95},
+ "rata_plata": {"value": 88.3, "status": "good"}
+ },
+ "risc": {
+ "creante_restante_pct": {"value": 15.5, "status": "good", "threshold_max": 20},
+ "creante_90plus_pct": {"value": 3.2, "status": "good", "threshold_max": 5},
+ "datorii_restante_pct": {"value": 8.5, "status": "good", "threshold_max": 10},
+ "raport_datorii_trezorerie": {"value": 1.8, "status": "good", "threshold_max": 2}
+ },
+ "cash_flow": {
+ "flux_net_lunar": {"value": 125000.50, "status": "good", "threshold_min": 0},
+ "cash_flow_ytd": {"value": 850000.00, "status": "good", "threshold_min": 0},
+ "flux_net_yoy_pct": {"value": 15.5, "status": "good", "threshold_min": 0},
+ "acoperire_cash_flow": {"value": 2.5, "status": "good", "threshold_min": 1.0}
+ },
+ "dinamica": {
+ "crestere_vanzari_yoy": {"value": 12.5, "status": "good", "threshold_min": 5.0},
+ "crestere_achizitii_yoy": {"value": 8.3, "status": "good"},
+ "marja_implicita": {"value": 25.5, "status": "good", "threshold_min": 20.0}
+ },
+ "altman_zscore": {
+ "zscore": {"value": 3.25, "status": "safe", "threshold_min": 2.60},
+ "x1": {"value": 0.25, "status": "good"},
+ "x2": {"value": 0.15, "status": "good"},
+ "x3": {"value": 0.12, "status": "good"},
+ "x4": {"value": 1.80, "status": "good"},
+ "working_capital": 450000.00,
+ "total_assets": 1800000.00
+ }
+ }
+ }
diff --git a/backend/modules/reports/routers/dashboard.py b/backend/modules/reports/routers/dashboard.py
index 7be4b7b..b088b61 100644
--- a/backend/modules/reports/routers/dashboard.py
+++ b/backend/modules/reports/routers/dashboard.py
@@ -9,7 +9,10 @@ import logging
logger = logging.getLogger(__name__)
from ..models.dashboard import DashboardSummary, TrendsResponse, TrendData
+from ..models.financial_indicators import FinancialIndicatorsResponse
from ..services.dashboard_service import DashboardService
+from ..services.financial_indicators_service import FinancialIndicatorsService
+from ..cache.decorators import cached
router = APIRouter()
@@ -427,4 +430,153 @@ async def get_current_period(
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Eroare la obținerea perioadei curente: {str(e)}")
- raise HTTPException(status_code=500, detail=f"Eroare la obținerea perioadei curente: {str(e)}")
\ No newline at end of file
+ raise HTTPException(status_code=500, detail=f"Eroare la obținerea perioadei curente: {str(e)}")
+
+
+@router.get(
+ "/financial-indicators",
+ response_model=FinancialIndicatorsResponse,
+ tags=["dashboard"]
+)
+async def get_financial_indicators(
+ request: Request,
+ company: int = Query(..., description="ID-ul firmei (required)"),
+ luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
+ an: Optional[int] = Query(None, ge=2000, le=2100, description="Anul contabil"),
+ include_sparklines: bool = Query(True, description="Include date istorice pentru sparklines (12 luni)"),
+ current_user: CurrentUser = Depends(get_current_user)
+) -> FinancialIndicatorsResponse:
+ """
+ Returnează toți indicatorii financiari calculați pentru firma selectată.
+
+ Acest endpoint agregă datele din:
+ - Lichiditate: Current Ratio, Quick Ratio, Cash Ratio
+ - Eficiență: DSO, DPO, Cash Conversion Cycle, rate încasare/plată
+ - Risc: creanțe/datorii restante, raport datorii/trezorerie
+ - Cash Flow: flux net lunar, YTD, YoY, acoperire
+ - Dinamică: creștere vânzări/achiziții YoY, marjă implicită
+ - Altman Z-Score: scor și componente X1-X4
+
+ Parametri:
+ - company (required): ID-ul firmei pentru care se calculează indicatorii
+ - luna (optional): Luna contabilă (1-12). Dacă nu este specificată,
+ se folosește ultima perioadă disponibilă.
+ - an (optional): Anul contabil (2000-2100). Dacă nu este specificat,
+ se folosește anul curent.
+ - include_sparklines (optional, default=true): Dacă să includă date istorice
+ pentru vizualizarea trendului pe ultimele 12 luni (sparkline_data și sparkline_labels
+ în fiecare indicator)
+
+ Cache:
+ - TTL: 30 minute pentru indicatori curenți (cache_type='financial_indicators')
+ - TTL: 1 oră pentru date istorice sparkline (cache_type='financial_indicators_historical')
+ - Se invalidează automat la schimbarea datelor din balanță
+
+ Necesită autentificare JWT și acces la firma specificată.
+ """
+ try:
+ # Verifică dacă utilizatorul are acces la firma specificată
+ if str(company) not in current_user.companies:
+ raise HTTPException(
+ status_code=403,
+ detail=f"Nu aveți acces la firma {company}"
+ )
+
+ # Dacă luna/an nu sunt specificate, obținem perioada curentă
+ # Folosim variabile tipizate explicit pentru a evita erori de tip
+ resolved_luna: int
+ resolved_an: int
+
+ if luna is None or an is None:
+ try:
+ current_period = await DashboardService.get_current_period(company)
+ resolved_luna = luna if luna is not None else current_period.get('luna', 12)
+ resolved_an = an if an is not None else current_period.get('an', 2024)
+ except Exception as e:
+ logger.warning(f"Could not get current period: {e}, using defaults")
+ from datetime import datetime
+ 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
+
+ # Dacă include_sparklines este True, folosim metoda care include datele istorice
+ if include_sparklines:
+ response = await FinancialIndicatorsService.get_indicators_with_sparklines(
+ company, resolved_luna, resolved_an, months=12
+ )
+
+ logger.info(
+ f"Financial indicators with sparklines for company {company}, "
+ f"luna={resolved_luna}, an={resolved_an}: "
+ f"Z-Score={response.altman_zscore.zscore.value} ({response.altman_zscore.zscore.status})"
+ )
+
+ return response
+
+ # Dacă include_sparklines este False, calculăm doar indicatorii curenți
+ import asyncio
+
+ # Apelăm serviciul pentru fiecare categorie de indicatori
+ lichiditate_task = FinancialIndicatorsService.calculate_liquidity_indicators(
+ company, resolved_luna, resolved_an
+ )
+ eficienta_task = FinancialIndicatorsService.calculate_efficiency_indicators(
+ company, resolved_luna, resolved_an
+ )
+ risc_task = FinancialIndicatorsService.calculate_risk_indicators(
+ company, resolved_luna, resolved_an
+ )
+ cash_flow_task = FinancialIndicatorsService.calculate_cashflow_indicators(
+ company, resolved_luna, resolved_an
+ )
+ dinamica_task = FinancialIndicatorsService.calculate_dynamics_indicators(
+ company, resolved_luna, resolved_an
+ )
+ altman_task = FinancialIndicatorsService.calculate_altman_zscore(
+ company, resolved_luna, resolved_an
+ )
+
+ # Executăm toate calculele în paralel pentru performanță
+ (
+ lichiditate,
+ eficienta,
+ risc,
+ cash_flow,
+ dinamica,
+ altman_zscore
+ ) = await asyncio.gather(
+ lichiditate_task,
+ eficienta_task,
+ risc_task,
+ cash_flow_task,
+ dinamica_task,
+ altman_task
+ )
+
+ # Construim răspunsul
+ response = FinancialIndicatorsResponse(
+ lichiditate=lichiditate,
+ eficienta=eficienta,
+ risc=risc,
+ cash_flow=cash_flow,
+ dinamica=dinamica,
+ altman_zscore=altman_zscore
+ )
+
+ logger.info(
+ f"Financial indicators for company {company}, luna={resolved_luna}, an={resolved_an}: "
+ f"Z-Score={altman_zscore.zscore.value} ({altman_zscore.zscore.status})"
+ )
+
+ return response
+
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+ except Exception as e:
+ logger.error(f"Eroare la obținerea indicatorilor financiari: {str(e)}")
+ raise HTTPException(
+ status_code=500,
+ detail=f"Eroare la obținerea indicatorilor financiari: {str(e)}"
+ )
\ No newline at end of file
diff --git a/backend/modules/reports/services/financial_indicators_service.py b/backend/modules/reports/services/financial_indicators_service.py
new file mode 100644
index 0000000..12acef5
--- /dev/null
+++ b/backend/modules/reports/services/financial_indicators_service.py
@@ -0,0 +1,2456 @@
+"""
+Service pentru indicatori financiari - agregate din VBAL pentru calculul
+ratelor de lichiditate, eficiență, risc și Altman Z-Score
+
+Acest serviciu agregă soldurile din balanța de verificare (VBAL) pe clase
+de conturi conform Planului de Conturi General (PCG) românesc.
+"""
+import logging
+from decimal import Decimal
+from typing import Optional, Dict, List, Any
+
+from shared.database.oracle_pool import oracle_pool
+from ..cache.decorators import cached
+from ..models.dashboard import DashboardSummary
+from ..models.financial_indicators import (
+ BalanceSheetAggregates,
+ IndicatorResult,
+ LiquidityIndicators,
+ EfficiencyIndicators,
+ RiskIndicators,
+ CashFlowIndicators,
+ DynamicsIndicators,
+ AltmanZScore,
+ ProfitabilityIndicators,
+ FinancialIndicatorsResponse
+)
+
+logger = logging.getLogger(__name__)
+
+
+# Prefixele conturilor conform PCG (Planul de Conturi General) românesc
+# Folosite pentru agregarea soldurilor din VBAL
+ACCOUNT_GROUPS = {
+ # ACTIVE IMOBILIZATE (Clasa 2)
+ # 20 - Imobilizări necorporale
+ # 21 - Imobilizări corporale
+ # 23 - Imobilizări în curs
+ # 26 - Imobilizări financiare
+ # 28 - Amortizări (se scad - sold creditor)
+ # 29 - Ajustări pentru depreciere (se scad - sold creditor)
+ "active_imobilizate": {
+ "debit": ["20", "21", "23", "26"], # Active brute (sold debitor)
+ "credit": ["28", "29"] # Amortizări și ajustări (sold creditor - se scad)
+ },
+
+ # STOCURI (Clasa 3)
+ # 30 - Materii prime
+ # 31 - Materiale consumabile
+ # 32 - Obiecte de inventar
+ # 33 - Producție în curs
+ # 34 - Produse finite
+ # 35 - Stocuri aflate la terți
+ # 36 - Animale
+ # 37 - Mărfuri
+ # 38 - Ambalaje
+ # 39 - Ajustări pentru depreciere (se scad - sold creditor)
+ "stocuri": {
+ "debit": ["30", "31", "32", "33", "34", "35", "36", "37", "38"],
+ "credit": ["39"] # Ajustări depreciere stocuri
+ },
+
+ # CREANȚE (din Clasa 4)
+ # 4111 - Clienți
+ # 413 - Efecte de primit
+ # 418 - Clienți - facturi de întocmit
+ # 425 - Avansuri acordate personalului
+ # 428 - Alte datorii și creanțe în legătură cu personalul (dacă debitoare)
+ # 431 - Asigurări sociale (dacă debitoare)
+ # 437 - Ajutor de șomaj (dacă debitoare)
+ # 4411 - Impozit pe profit (dacă debitor)
+ # 4424 - TVA de recuperat
+ # 444 - Impozit pe venituri de natură salarială (dacă debitor)
+ # 445 - Subvenții (dacă debitoare)
+ # 446 - Alte impozite și taxe (dacă debitoare)
+ # 447 - Fonduri speciale (dacă debitoare)
+ # 451 - Decontări între entitățile afiliate (dacă debitoare)
+ # 453 - Decontări privind interesele de participare (dacă debitoare)
+ # 456 - Decontări cu acționarii (dacă debitoare)
+ # 461 - Debitori diverși
+ # 473 - Decontări din operații în curs (dacă debitoare)
+ "creante": {
+ "debit": ["4111", "413", "418", "425", "4424", "461"],
+ "credit": ["491", "496"] # Ajustări pentru deprecierea creanțelor
+ },
+
+ # DISPONIBILITĂȚI (din Clasa 5)
+ # 5121 - Conturi la bănci în lei
+ # 5124 - Conturi la bănci în valută
+ # 5311 - Casa în lei
+ # 5314 - Casa în valută
+ # 532 - Alte valori
+ # 541 - Acreditive
+ # 542 - Avansuri de trezorerie
+ "disponibilitati": {
+ "debit": ["5121", "5124", "5311", "5314", "532", "541", "542"],
+ "credit": []
+ },
+
+ # CAPITAL PROPRIU (din Clasa 1)
+ # 101 - Capital social
+ # 104 - Prime de capital
+ # 105 - Rezerve din reevaluare
+ # 106 - Rezerve
+ # 107 - Rezultatul reportat (poate fi debitor sau creditor)
+ # 108 - Interese minoritare
+ # 117 - Rezultatul reportat (cont distinct în unele planuri)
+ # 121 - Profit sau pierdere (rezultatul exercițiului)
+ "capital_propriu": {
+ "debit": [], # Capitalurile sunt în general creditoare
+ "credit": ["101", "104", "105", "106"]
+ },
+
+ # REZULTAT (Profit/Pierdere)
+ # 107 - Rezultatul reportat
+ # 117 - Rezultatul reportat (în unele versiuni PCG)
+ # 121 - Profit sau pierdere
+ # 129 - Repartizarea profitului (se scade din profit)
+ "rezultat": {
+ "debit": ["107", "117", "129"], # Pierdere sau repartizare
+ "credit": ["107", "117", "121"] # Profit
+ },
+
+ # DATORII PE TERMEN LUNG (peste 1 an)
+ # 161 - Împrumuturi din emisiuni de obligațiuni
+ # 162 - Credite bancare pe termen lung
+ # 166 - Datorii care privesc imobilizările financiare
+ # 167 - Alte împrumuturi și datorii asimilate
+ # 168 - Dobânzi aferente împrumuturilor și datoriilor asimilate
+ # 169 - Prime privind rambursarea obligațiunilor
+ "datorii_termen_lung": {
+ "debit": [],
+ "credit": ["161", "162", "166", "167", "168", "169"]
+ },
+
+ # DATORII CURENTE (sub 1 an)
+ # 401 - Furnizori
+ # 403 - Efecte de plătit
+ # 404 - Furnizori de imobilizări
+ # 408 - Furnizori - facturi nesosite
+ # 419 - Clienți - creditori
+ # 421 - Personal - salarii datorate
+ # 423 - Personal - ajutoare materiale datorate
+ # 424 - Prime reprezentând participarea personalului la profit
+ # 426 - Drepturi de personal neridicate
+ # 427 - Rețineri din salarii datorate terților
+ # 431 - Asigurări sociale (dacă creditoare)
+ # 437 - Ajutor de șomaj (dacă creditoare)
+ # 4411 - Impozit pe profit de plătit
+ # 4423 - TVA de plată
+ # 4428 - TVA neexigibilă
+ # 444 - Impozit pe venituri salariale de plată
+ # 446 - Alte impozite și taxe
+ # 447 - Fonduri speciale
+ # 462 - Creditori diverși
+ # 509 - Vărsăminte de efectuat pentru investiții financiare pe termen scurt
+ # 512 - Credite bancare pe termen scurt (5191)
+ "datorii_curente": {
+ "debit": [],
+ "credit": ["401", "403", "404", "408", "419", "421", "423", "424",
+ "426", "427", "4311", "4371", "4411", "4423", "4428",
+ "444", "446", "447", "462", "509", "5191", "5192", "5198"]
+ },
+
+ # VENITURI (Clasa 7) - pentru calculul EBIT în Altman Z-Score
+ # 70 - Venituri din vânzarea produselor
+ # 71 - Venituri din vânzarea mărfurilor
+ # 72 - Venituri din producția de imobilizări
+ # 74 - Venituri din subvenții de exploatare
+ # 75 - Alte venituri din exploatare
+ "venituri": {
+ "debit": [],
+ "credit": ["70", "701", "702", "703", "704", "705", "706", "707", "708",
+ "71", "72", "74", "75", "758"]
+ },
+
+ # CHELTUIELI OPERAȚIONALE (din Clasa 6, FĂRĂ 66x) - pentru calculul EBIT
+ # 60 - Cheltuieli privind stocurile
+ # 61 - Cheltuieli cu serviciile executate de terți
+ # 62 - Cheltuieli cu alte servicii executate de terți
+ # 63 - Cheltuieli cu alte impozite, taxe
+ # 64 - Cheltuieli cu personalul
+ # 65 - Alte cheltuieli de exploatare
+ # 68 - Amortizări și provizioane (adăugat)
+ "cheltuieli_operationale": {
+ "debit": ["60", "601", "602", "603", "604", "605", "606", "607", "608",
+ "61", "611", "612", "613", "614",
+ "62", "621", "622", "623", "624", "625", "626", "627", "628",
+ "63", "635",
+ "64", "641", "642", "643", "644", "645",
+ "65", "654", "658",
+ "68", "681", "686"],
+ "credit": []
+ },
+
+ # CHELTUIELI FINANCIARE (Clasa 66) - separat de cheltuieli operaționale
+ # 661 - Cheltuieli privind titlurile de plasament cedate
+ # 663 - Pierderi din creanțe legate de participații
+ # 664 - Cheltuieli privind investițiile financiare cedate
+ # 665 - Cheltuieli din diferențe de curs valutar
+ # 666 - Cheltuieli privind dobânzile
+ # 667 - Cheltuieli privind sconturile acordate
+ # 668 - Alte cheltuieli financiare
+ "cheltuieli_financiare": {
+ "debit": ["66", "661", "663", "664", "665", "666", "667", "668"],
+ "credit": []
+ }
+}
+
+
+class FinancialIndicatorsService:
+ """
+ Service pentru calculul indicatorilor financiari din balanța de verificare.
+
+ Agregă soldurile din VBAL pe categorii de conturi pentru:
+ - Active imobilizate
+ - Stocuri
+ - Creanțe
+ - Disponibilități
+ - Capital propriu
+ - Rezultat
+ - Datorii pe termen lung
+ - Datorii curente
+ - Venituri
+ - Cheltuieli operaționale
+ """
+
+ @staticmethod
+ @cached(cache_type='schema', key_params=['company_id'])
+ async def _get_schema(company_id: int) -> str:
+ """
+ Obține schema pentru company_id (CACHED PERMANENT)
+
+ Schema este stocată permanent în cache deoarece nu se schimbă.
+ """
+ async with oracle_pool.get_connection() as connection:
+ with connection.cursor() as cursor:
+ schema_query = """
+ SELECT schema
+ FROM CONTAFIN_ORACLE.v_nom_firme
+ WHERE id_firma = :company_id
+ """
+ cursor.execute(schema_query, {'company_id': company_id})
+ schema_result = cursor.fetchone()
+
+ if not schema_result:
+ raise ValueError(f"Schema not found for company {company_id}")
+
+ return schema_result[0]
+
+ @staticmethod
+ def _build_aggregate_case(prefixes: list[str], column: str) -> str:
+ """
+ Construiește expresie CASE pentru agregarea soldurilor pe prefixe de conturi.
+
+ Args:
+ prefixes: Lista de prefixe de conturi (ex: ["20", "21", "23"])
+ column: Coloana de sumat (SOLDDEB sau SOLDCRED)
+
+ Returns:
+ Expresie SQL CASE pentru SUM
+ """
+ if not prefixes:
+ return "0"
+
+ conditions = " OR ".join([f"CONT LIKE '{prefix}%'" for prefix in prefixes])
+ return f"SUM(CASE WHEN ({conditions}) THEN NVL({column}, 0) ELSE 0 END)"
+
+ @staticmethod
+ @cached(cache_type='fin_balance_sheet', key_params=['company_id', 'luna', 'an'])
+ async def get_balance_sheet_aggregates(
+ company_id: int,
+ luna: int,
+ an: int
+ ) -> BalanceSheetAggregates:
+ """
+ Obține soldurile agregate din balanța de verificare pentru calculul
+ indicatorilor financiari (CACHED 30 min).
+
+ Agregă soldurile din VBAL pe categorii de conturi folosind prefixe
+ conform Planului de Conturi General (PCG) românesc.
+
+ Args:
+ company_id: ID-ul firmei
+ luna: Luna contabilă (1-12)
+ an: Anul contabil
+
+ Returns:
+ BalanceSheetAggregates cu soldurile agregate pentru fiecare categorie
+
+ Raises:
+ ValueError: Dacă schema nu este găsită pentru firma specificată
+ """
+ schema = await FinancialIndicatorsService._get_schema(company_id)
+
+ async with oracle_pool.get_connection() as connection:
+ with connection.cursor() as cursor:
+ # Construim query-ul cu CASE pentru fiecare categorie
+ # Soldurile din VBAL: SOLDDEB (sold debitor), SOLDCRED (sold creditor)
+ #
+ # Pentru active: valoarea = SOLDDEB - amortizări (SOLDCRED pentru 28, 29)
+ # Pentru pasive: valoarea = SOLDCRED
+ # Pentru venituri: valoarea = SOLDCRED (conturile de venituri sunt creditoare)
+ # Pentru cheltuieli: valoarea = SOLDDEB (conturile de cheltuieli sunt debitoare)
+
+ query = f"""
+ SELECT
+ -- ACTIVE IMOBILIZATE (brut - amortizări - ajustări)
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["active_imobilizate"]["debit"], "SOLDDEB"
+ )} -
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["active_imobilizate"]["credit"], "SOLDCRED"
+ )} as active_imobilizate,
+
+ -- STOCURI (brut - ajustări depreciere)
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["stocuri"]["debit"], "SOLDDEB"
+ )} -
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["stocuri"]["credit"], "SOLDCRED"
+ )} as stocuri,
+
+ -- CREANȚE (brut - ajustări depreciere)
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["creante"]["debit"], "SOLDDEB"
+ )} -
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["creante"]["credit"], "SOLDCRED"
+ )} as creante,
+
+ -- DISPONIBILITĂȚI
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["disponibilitati"]["debit"], "SOLDDEB"
+ )} as disponibilitati,
+
+ -- CAPITAL PROPRIU (sold creditor)
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["capital_propriu"]["credit"], "SOLDCRED"
+ )} as capital_propriu,
+
+ -- REZULTAT (credit - debit: profit dacă pozitiv, pierdere dacă negativ)
+ -- Conturile 107, 117, 121 pot avea sold fie debitor (pierdere) fie creditor (profit)
+ -- 129 (repartizare profit) este debitor și se scade
+ ({FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["rezultat"]["credit"], "SOLDCRED"
+ )} -
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["rezultat"]["debit"], "SOLDDEB"
+ )}) as rezultat,
+
+ -- DATORII TERMEN LUNG (sold creditor)
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["datorii_termen_lung"]["credit"], "SOLDCRED"
+ )} as datorii_termen_lung,
+
+ -- DATORII CURENTE (sold creditor)
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["datorii_curente"]["credit"], "SOLDCRED"
+ )} as datorii_curente,
+
+ -- VENITURI (sold creditor - pentru calcul EBIT)
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["venituri"]["credit"], "SOLDCRED"
+ )} as venituri,
+
+ -- CHELTUIELI OPERAȚIONALE (sold debitor - pentru calcul EBIT)
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["cheltuieli_operationale"]["debit"], "SOLDDEB"
+ )} as cheltuieli_operationale,
+
+ -- CHELTUIELI FINANCIARE (sold debitor - Clasa 66)
+ {FinancialIndicatorsService._build_aggregate_case(
+ ACCOUNT_GROUPS["cheltuieli_financiare"]["debit"], "SOLDDEB"
+ )} as cheltuieli_financiare
+
+ FROM {schema}.VBAL
+ WHERE AN = :an
+ AND LUNA = :luna
+ """
+
+ params = {'an': an, 'luna': luna}
+ cursor.execute(query, params)
+ row = cursor.fetchone()
+
+ if not row:
+ # Returnăm agregate cu valori zero dacă nu există date
+ logger.warning(
+ f"No VBAL data for company {company_id}, "
+ f"luna={luna}, an={an}"
+ )
+ return BalanceSheetAggregates(
+ company_id=company_id,
+ luna=luna,
+ an=an,
+ active_imobilizate=Decimal('0'),
+ stocuri=Decimal('0'),
+ creante=Decimal('0'),
+ disponibilitati=Decimal('0'),
+ capital_propriu=Decimal('0'),
+ rezultat=Decimal('0'),
+ datorii_termen_lung=Decimal('0'),
+ datorii_curente=Decimal('0'),
+ venituri=Decimal('0'),
+ cheltuieli_operationale=Decimal('0'),
+ cheltuieli_financiare=Decimal('0')
+ )
+
+ # Construim modelul cu valorile agregate
+ aggregates = BalanceSheetAggregates(
+ company_id=company_id,
+ luna=luna,
+ an=an,
+ active_imobilizate=Decimal(str(row[0] or 0)),
+ stocuri=Decimal(str(row[1] or 0)),
+ creante=Decimal(str(row[2] or 0)),
+ disponibilitati=Decimal(str(row[3] or 0)),
+ capital_propriu=Decimal(str(row[4] or 0)),
+ rezultat=Decimal(str(row[5] or 0)),
+ datorii_termen_lung=Decimal(str(row[6] or 0)),
+ datorii_curente=Decimal(str(row[7] or 0)),
+ venituri=Decimal(str(row[8] or 0)),
+ cheltuieli_operationale=Decimal(str(row[9] or 0)),
+ cheltuieli_financiare=Decimal(str(row[10] or 0))
+ )
+
+ logger.info(
+ f"Financial aggregates for company {company_id}, "
+ f"luna={luna}, an={an}: "
+ f"active_imobilizate={aggregates.active_imobilizate}, "
+ f"stocuri={aggregates.stocuri}, "
+ f"disponibilitati={aggregates.disponibilitati}, "
+ f"datorii_curente={aggregates.datorii_curente}"
+ )
+
+ return aggregates
+
+ @staticmethod
+ def _calculate_indicator_status(
+ value: float,
+ good_threshold: float,
+ warning_threshold: float,
+ higher_is_better: bool = True
+ ) -> str:
+ """
+ Determină statusul unui indicator pe baza pragurilor.
+
+ Args:
+ value: Valoarea indicatorului
+ good_threshold: Pragul pentru status 'good'
+ warning_threshold: Pragul pentru status 'warning'
+ higher_is_better: True dacă valori mai mari sunt mai bune
+
+ Returns:
+ Status: 'good', 'warning', sau 'danger'
+ """
+ if higher_is_better:
+ if value >= good_threshold:
+ return "good"
+ elif value >= warning_threshold:
+ return "warning"
+ else:
+ return "danger"
+ else:
+ # Pentru indicatori unde valori mai mici sunt mai bune (ex: DSO)
+ if value <= good_threshold:
+ return "good"
+ elif value <= warning_threshold:
+ return "warning"
+ else:
+ return "danger"
+
+ @staticmethod
+ async def calculate_liquidity_indicators(
+ company_id: int,
+ luna: int,
+ an: int
+ ) -> LiquidityIndicators:
+ """
+ Calculează indicatorii de lichiditate pentru evaluarea capacității
+ de plată a datoriilor pe termen scurt.
+
+ Indicatori calculați:
+ - Lichiditate curentă (Current Ratio) = active_curente / datorii_curente
+ - Lichiditate imediată (Quick Ratio) = (disponibilități + creanțe) / datorii_curente
+ - Lichiditate la vedere (Cash Ratio) = disponibilități / datorii_curente
+
+ Praguri de referință (conform standardelor bancare):
+ - Lichiditate curentă: good >= 2.0, warning 1.0-2.0, danger < 1.0
+ - Lichiditate imediată: good >= 1.0, warning 0.5-1.0, danger < 0.5
+ - Lichiditate la vedere: good >= 0.2, warning 0.1-0.2, danger < 0.1
+
+ Args:
+ company_id: ID-ul firmei
+ luna: Luna contabilă (1-12)
+ an: Anul contabil
+
+ Returns:
+ LiquidityIndicators cu cele trei rate de lichiditate
+ """
+ # Obținem agregatele din balanță
+ aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
+ company_id, luna, an
+ )
+ # Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
+ if isinstance(aggregates, dict):
+ aggregates = BalanceSheetAggregates(**aggregates)
+
+ # Calculăm active curente (stocuri + creanțe + disponibilități)
+ active_curente = float(aggregates.active_curente)
+ disponibilitati = float(aggregates.disponibilitati)
+ creante = float(aggregates.creante)
+ datorii_curente = float(aggregates.datorii_curente)
+
+ # Handle cazul special: datorii_curente = 0
+ if datorii_curente == 0:
+ # Compania nu are datorii pe termen scurt - situație excelentă
+ # Returnăm None pentru valoare dar status "good" cu mesaj explicativ
+ no_debt_message = "Fără datorii curente - lichiditate maximă"
+
+ return LiquidityIndicators(
+ lichiditate_curenta=IndicatorResult(
+ value=None,
+ status="good",
+ threshold_min=2.0,
+ threshold_max=None,
+ message=no_debt_message
+ ),
+ lichiditate_imediata=IndicatorResult(
+ value=None,
+ status="good",
+ threshold_min=1.0,
+ threshold_max=None,
+ message=no_debt_message
+ ),
+ lichiditate_vedere=IndicatorResult(
+ value=None,
+ status="good",
+ threshold_min=0.2,
+ threshold_max=None,
+ message=no_debt_message
+ )
+ )
+
+ # Calculăm ratele de lichiditate
+ # 1. Lichiditate curentă (Current Ratio)
+ lichiditate_curenta_val = active_curente / datorii_curente
+ lichiditate_curenta_status = FinancialIndicatorsService._calculate_indicator_status(
+ lichiditate_curenta_val,
+ good_threshold=2.0,
+ warning_threshold=1.0
+ )
+
+ # 2. Lichiditate imediată (Quick Ratio)
+ # Exclude stocurile - măsoară lichiditatea "rapidă"
+ lichiditate_imediata_val = (disponibilitati + creante) / datorii_curente
+ lichiditate_imediata_status = FinancialIndicatorsService._calculate_indicator_status(
+ lichiditate_imediata_val,
+ good_threshold=1.0,
+ warning_threshold=0.5
+ )
+
+ # 3. Lichiditate la vedere (Cash Ratio)
+ # Doar disponibilități - lichiditatea imediată
+ lichiditate_vedere_val = disponibilitati / datorii_curente
+ lichiditate_vedere_status = FinancialIndicatorsService._calculate_indicator_status(
+ lichiditate_vedere_val,
+ good_threshold=0.2,
+ warning_threshold=0.1
+ )
+
+ # Rotunjim valorile la 2 zecimale pentru afișare
+ result = LiquidityIndicators(
+ lichiditate_curenta=IndicatorResult(
+ value=round(lichiditate_curenta_val, 2),
+ status=lichiditate_curenta_status,
+ threshold_min=2.0,
+ threshold_max=None
+ ),
+ lichiditate_imediata=IndicatorResult(
+ value=round(lichiditate_imediata_val, 2),
+ status=lichiditate_imediata_status,
+ threshold_min=1.0,
+ threshold_max=None
+ ),
+ lichiditate_vedere=IndicatorResult(
+ value=round(lichiditate_vedere_val, 2),
+ status=lichiditate_vedere_status,
+ threshold_min=0.2,
+ threshold_max=None
+ )
+ )
+
+ logger.info(
+ f"Liquidity indicators for company {company_id}, luna={luna}, an={an}: "
+ f"curenta={result.lichiditate_curenta.value} ({result.lichiditate_curenta.status}), "
+ f"imediata={result.lichiditate_imediata.value} ({result.lichiditate_imediata.status}), "
+ f"vedere={result.lichiditate_vedere.value} ({result.lichiditate_vedere.status})"
+ )
+
+ return result
+
+ @staticmethod
+ @cached(cache_type='fin_efficiency', key_params=['company_id', 'luna', 'an'])
+ async def calculate_efficiency_indicators(
+ company_id: int,
+ luna: int,
+ an: int
+ ) -> EfficiencyIndicators:
+ """
+ Calculează indicatorii de eficiență pentru evaluarea vitezei de conversie
+ a resurselor în numerar.
+
+ Indicatori calculați:
+ - DSO (Days Sales Outstanding) = (clienti_sold / facturari_lunare) * 30
+ - DPO (Days Payables Outstanding) = (furnizori_sold / achizitii_lunare) * 30
+ - Cash Conversion Cycle = DSO - DPO
+ - Rata încasare = incasari / facturari * 100
+ - Rata plată = plati / achizitii * 100
+
+ Praguri de referință pentru DSO:
+ - Good: < 30 zile (încasare rapidă)
+ - Warning: 30-45 zile (încasare moderată)
+ - Danger: > 45 zile (încasare lentă - risc de cash flow)
+
+ Args:
+ company_id: ID-ul firmei
+ luna: Luna contabilă (1-12)
+ an: Anul contabil
+
+ Returns:
+ EfficiencyIndicators cu cele cinci rate de eficiență
+ """
+ # Import DashboardService here to avoid circular imports
+ from .dashboard_service import DashboardService
+
+ # Obținem datele din summary (solduri clienți/furnizori)
+ summary = await DashboardService.get_complete_summary(
+ company=str(company_id),
+ username="system", # System call for indicators
+ luna=luna,
+ an=an
+ )
+ # Ensure summary is a DashboardSummary model (cache may return dict)
+ if isinstance(summary, dict):
+ summary = DashboardSummary(**summary)
+
+ # Obținem datele din trends (facturări/încasări/achiziții/plăți lunare)
+ trends = await DashboardService.get_trends(
+ company_id=company_id,
+ period='12m' # Ultimele 12 luni pentru media lunară
+ )
+
+ # Extragem soldurile din summary
+ clienti_sold = float(summary.clienti_sold_total)
+ furnizori_sold = float(summary.furnizori_sold_total)
+
+ # Extragem datele lunare din trends
+ # Folosim ultima lună disponibilă pentru facturări/încasări/achiziții/plăți
+ clienti_facturat = trends.get("clienti_facturat", [])
+ clienti_incasat = trends.get("clienti_incasat", [])
+ furnizori_facturat = trends.get("furnizori_facturat", [])
+ furnizori_achitat = trends.get("furnizori_achitat", [])
+
+ # Calculăm media lunară (pentru stabilitate) sau folosim ultima lună
+ # Pentru DSO/DPO, folosim media ultimelor 3 luni pentru a evita fluctuații
+ def safe_avg(values: list, n: int = 3) -> float:
+ """Calculează media ultimelor n valori, sau toate dacă sunt mai puține"""
+ if not values:
+ return 0
+ recent = values[-n:] if len(values) >= n else values
+ return sum(recent) / len(recent) if recent else 0
+
+ facturari_lunare = safe_avg(clienti_facturat)
+ incasari_lunare = safe_avg(clienti_incasat)
+ achizitii_lunare = safe_avg(furnizori_facturat)
+ plati_lunare = safe_avg(furnizori_achitat)
+
+ # Calculăm indicatorii
+
+ # 1. DSO (Days Sales Outstanding) - Durata medie de încasare
+ # Formula: (clienti_sold / facturari_lunare) * 30
+ if facturari_lunare > 0:
+ dso_val = (clienti_sold / facturari_lunare) * 30
+ dso_status = FinancialIndicatorsService._calculate_indicator_status(
+ dso_val,
+ good_threshold=30,
+ warning_threshold=45,
+ higher_is_better=False # Pentru DSO, mai mic e mai bine
+ )
+ dso = IndicatorResult(
+ value=round(dso_val, 1),
+ status=dso_status,
+ threshold_min=None,
+ threshold_max=30 # Good if <= 30 days
+ )
+ else:
+ dso = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=None,
+ threshold_max=30,
+ message="Fără facturări în perioada analizată"
+ )
+
+ # 2. DPO (Days Payables Outstanding) - Durata medie de plată
+ # Formula: (furnizori_sold / achizitii_lunare) * 30
+ if achizitii_lunare > 0:
+ dpo_val = (furnizori_sold / achizitii_lunare) * 30
+ # Pentru DPO nu avem praguri stricte - depinde de strategia firmei
+ # Un DPO mai mare înseamnă că folosim creditul furnizorilor
+ dpo = IndicatorResult(
+ value=round(dpo_val, 1),
+ status="good", # DPO nu are praguri de "danger"
+ threshold_min=None,
+ threshold_max=None,
+ message="Durata medie de plată furnizori"
+ )
+ else:
+ dpo = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=None,
+ threshold_max=None,
+ message="Fără achiziții în perioada analizată"
+ )
+
+ # 3. Cash Conversion Cycle = DSO - DPO
+ # Negativ = firma se finanțează din creditul furnizorilor (favorabil)
+ # Pozitiv = cash blocat în ciclul de afaceri
+ if dso.value is not None and dpo.value is not None:
+ ccc_val = dso.value - dpo.value
+ # CCC negativ e bun (ne finanțăm din creditul furnizorilor)
+ ccc_status = FinancialIndicatorsService._calculate_indicator_status(
+ ccc_val,
+ good_threshold=0,
+ warning_threshold=15,
+ higher_is_better=False
+ )
+ ccc_message = (
+ "Ciclu negativ = finanțare din creditul furnizorilor"
+ if ccc_val < 0
+ else "Numerar blocat în ciclul de afaceri"
+ )
+ cash_conversion_cycle = IndicatorResult(
+ value=round(ccc_val, 1),
+ status=ccc_status,
+ threshold_min=None,
+ threshold_max=0,
+ message=ccc_message
+ )
+ else:
+ cash_conversion_cycle = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=None,
+ threshold_max=0,
+ message="Nu se poate calcula - lipsesc date DSO/DPO"
+ )
+
+ # 4. Rata încasare = incasari / facturari * 100
+ # Calculăm pentru perioada YTD din trends (suma totală)
+ total_facturari = sum(clienti_facturat) if clienti_facturat else 0
+ total_incasari = sum(clienti_incasat) if clienti_incasat else 0
+
+ if total_facturari > 0:
+ rata_incasare_val = (total_incasari / total_facturari) * 100
+ rata_incasare_status = FinancialIndicatorsService._calculate_indicator_status(
+ rata_incasare_val,
+ good_threshold=95,
+ warning_threshold=80
+ )
+ rata_incasare = IndicatorResult(
+ value=round(rata_incasare_val, 1),
+ status=rata_incasare_status,
+ threshold_min=95,
+ threshold_max=None
+ )
+ else:
+ rata_incasare = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=95,
+ threshold_max=None,
+ message="Fără facturări în perioada analizată"
+ )
+
+ # 5. Rata plată = plati / achizitii * 100
+ total_achizitii = sum(furnizori_facturat) if furnizori_facturat else 0
+ total_plati = sum(furnizori_achitat) if furnizori_achitat else 0
+
+ if total_achizitii > 0:
+ rata_plata_val = (total_plati / total_achizitii) * 100
+ # Pentru rata de plată, nu există "danger" - depinde de strategia firmei
+ rata_plata = IndicatorResult(
+ value=round(rata_plata_val, 1),
+ status="good", # Informativ, fără praguri stricte
+ threshold_min=None,
+ threshold_max=None
+ )
+ else:
+ rata_plata = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=None,
+ threshold_max=None,
+ message="Fără achiziții în perioada analizată"
+ )
+
+ result = EfficiencyIndicators(
+ dso=dso,
+ dpo=dpo,
+ cash_conversion_cycle=cash_conversion_cycle,
+ rata_incasare=rata_incasare,
+ rata_plata=rata_plata
+ )
+
+ logger.info(
+ f"Efficiency indicators for company {company_id}, luna={luna}, an={an}: "
+ f"DSO={dso.value} ({dso.status}), "
+ f"DPO={dpo.value}, "
+ f"CCC={cash_conversion_cycle.value} ({cash_conversion_cycle.status}), "
+ f"rata_incasare={rata_incasare.value}% ({rata_incasare.status})"
+ )
+
+ return result
+
+ @staticmethod
+ @cached(cache_type='fin_risk', key_params=['company_id', 'luna', 'an'])
+ async def calculate_risk_indicators(
+ company_id: int,
+ luna: int,
+ an: int
+ ) -> RiskIndicators:
+ """
+ Calculează indicatorii de risc și aging pentru evaluarea sănătății
+ portofoliului de creanțe și datorii.
+
+ Indicatori calculați:
+ - creante_restante_pct: Procentul creanțelor restante din total clienți
+ Formula: clienti_sold_restant / clienti_sold_total * 100
+ Good: < 20%, Warning: 20-30%, Danger: > 30%
+
+ - creante_90plus_pct: Procentul creanțelor restante > 90 zile din total
+ Formula: clienti_restant_90plus / clienti_sold_total * 100
+ Good: < 5%, Warning: 5-10%, Danger: > 10%
+
+ - datorii_restante_pct: Procentul datoriilor restante din total furnizori
+ Formula: furnizori_sold_restant / furnizori_sold_total * 100
+ Good: < 10%, Warning: 10-20%, Danger: > 20%
+
+ - raport_datorii_trezorerie: Raportul între datorii furnizori și trezorerie
+ Formula: furnizori_sold_total / trezorerie
+ Good: < 2, Warning: 2-4, Danger: > 4
+
+ Args:
+ company_id: ID-ul firmei
+ luna: Luna contabilă (1-12)
+ an: Anul contabil
+
+ Returns:
+ RiskIndicators cu cei patru indicatori de risc
+ """
+ # Import DashboardService here to avoid circular imports
+ from .dashboard_service import DashboardService
+
+ # Obținem datele din summary (solduri clienți/furnizori/aging)
+ summary = await DashboardService.get_complete_summary(
+ company=str(company_id),
+ username="system", # System call for indicators
+ luna=luna,
+ an=an
+ )
+ # Ensure summary is a DashboardSummary model (cache may return dict)
+ if isinstance(summary, dict):
+ summary = DashboardSummary(**summary)
+
+ # Extragem soldurile din summary
+ clienti_sold_total = float(summary.clienti_sold_total)
+ clienti_sold_restant = float(summary.clienti_sold_restant)
+ clienti_restant_90plus = float(summary.clienti_restant_90plus)
+ furnizori_sold_total = float(summary.furnizori_sold_total)
+ furnizori_sold_restant = float(summary.furnizori_sold_restant)
+
+ # Calculăm trezoreria totală din treasury_totals_by_currency (sumă pe toate valutele)
+ trezorerie = sum(float(v) for v in summary.treasury_totals_by_currency.values())
+
+ # 1. Creanțe restante % - procentul din solduri clienți care sunt restante
+ if clienti_sold_total > 0:
+ creante_restante_val = (clienti_sold_restant / clienti_sold_total) * 100
+ creante_restante_status = FinancialIndicatorsService._calculate_indicator_status(
+ creante_restante_val,
+ good_threshold=20,
+ warning_threshold=30,
+ higher_is_better=False # Mai mic e mai bine
+ )
+ creante_restante_pct = IndicatorResult(
+ value=round(creante_restante_val, 1),
+ status=creante_restante_status,
+ threshold_min=None,
+ threshold_max=20
+ )
+ else:
+ creante_restante_pct = IndicatorResult(
+ value=None,
+ status="good",
+ threshold_min=None,
+ threshold_max=20,
+ message="Fără sold clienți"
+ )
+
+ # 2. Creanțe 90+ zile % - creanțe cu risc mare de nerecuperare
+ if clienti_sold_total > 0:
+ creante_90plus_val = (clienti_restant_90plus / clienti_sold_total) * 100
+ creante_90plus_status = FinancialIndicatorsService._calculate_indicator_status(
+ creante_90plus_val,
+ good_threshold=5,
+ warning_threshold=10,
+ higher_is_better=False # Mai mic e mai bine
+ )
+ creante_90plus_pct = IndicatorResult(
+ value=round(creante_90plus_val, 1),
+ status=creante_90plus_status,
+ threshold_min=None,
+ threshold_max=5
+ )
+ else:
+ creante_90plus_pct = IndicatorResult(
+ value=None,
+ status="good",
+ threshold_min=None,
+ threshold_max=5,
+ message="Fără sold clienți"
+ )
+
+ # 3. Datorii restante % - procentul din solduri furnizori care sunt restante
+ if furnizori_sold_total > 0:
+ datorii_restante_val = (furnizori_sold_restant / furnizori_sold_total) * 100
+ datorii_restante_status = FinancialIndicatorsService._calculate_indicator_status(
+ datorii_restante_val,
+ good_threshold=10,
+ warning_threshold=20,
+ higher_is_better=False # Mai mic e mai bine
+ )
+ datorii_restante_pct = IndicatorResult(
+ value=round(datorii_restante_val, 1),
+ status=datorii_restante_status,
+ threshold_min=None,
+ threshold_max=10
+ )
+ else:
+ datorii_restante_pct = IndicatorResult(
+ value=None,
+ status="good",
+ threshold_min=None,
+ threshold_max=10,
+ message="Fără sold furnizori"
+ )
+
+ # 4. Raport datorii/trezorerie - câte unități de cash trebuie pentru a plăti furnizorii
+ if trezorerie > 0:
+ raport_val = furnizori_sold_total / trezorerie
+ raport_status = FinancialIndicatorsService._calculate_indicator_status(
+ raport_val,
+ good_threshold=2,
+ warning_threshold=4,
+ higher_is_better=False # Mai mic e mai bine (mai puțin datorie per cash)
+ )
+ raport_message = (
+ "Trezorerie suficientă pentru acoperirea datoriilor"
+ if raport_val < 2
+ else "Trezorerie insuficientă pentru datorii"
+ if raport_val > 4
+ else "Trezorerie moderată pentru datorii"
+ )
+ raport_datorii_trezorerie = IndicatorResult(
+ value=round(raport_val, 2),
+ status=raport_status,
+ threshold_min=None,
+ threshold_max=2,
+ message=raport_message
+ )
+ elif furnizori_sold_total == 0:
+ # Fără datorii furnizori - situație bună
+ raport_datorii_trezorerie = IndicatorResult(
+ value=0,
+ status="good",
+ threshold_min=None,
+ threshold_max=2,
+ message="Fără datorii furnizori"
+ )
+ else:
+ # Trezorerie 0 dar avem datorii - situație critică
+ raport_datorii_trezorerie = IndicatorResult(
+ value=None,
+ status="danger",
+ threshold_min=None,
+ threshold_max=2,
+ message="Trezorerie zero - nu se poate acoperi datoriile"
+ )
+
+ result = RiskIndicators(
+ creante_restante_pct=creante_restante_pct,
+ creante_90plus_pct=creante_90plus_pct,
+ datorii_restante_pct=datorii_restante_pct,
+ raport_datorii_trezorerie=raport_datorii_trezorerie
+ )
+
+ logger.info(
+ f"Risk indicators for company {company_id}, luna={luna}, an={an}: "
+ f"creante_restante={creante_restante_pct.value}% ({creante_restante_pct.status}), "
+ f"creante_90plus={creante_90plus_pct.value}% ({creante_90plus_pct.status}), "
+ f"datorii_restante={datorii_restante_pct.value}% ({datorii_restante_pct.status}), "
+ f"raport_datorii_trez={raport_datorii_trezorerie.value} ({raport_datorii_trezorerie.status})"
+ )
+
+ return result
+
+ @staticmethod
+ @cached(cache_type='fin_cashflow', key_params=['company_id', 'luna', 'an'])
+ async def calculate_cashflow_indicators(
+ company_id: int,
+ luna: int,
+ an: int
+ ) -> CashFlowIndicators:
+ """
+ Calculează indicatorii de cash flow pentru evaluarea generării și
+ consumului de numerar.
+
+ Indicatori calculați:
+ - flux_net_lunar: Încasări luna - plăți luna (fluxul net de numerar lunar)
+ Good: > 0 (firma generează numerar), Danger: < 0 (firma consumă numerar)
+
+ - cash_flow_ytd: Suma fluxurilor nete de la ianuarie până la luna curentă
+ Arată tendința generală a anului în curs
+
+ - flux_net_yoy_pct: Variația procentuală an-la-an
+ Formula: (cf_curent - cf_anterior) / abs(cf_anterior) * 100
+
+ - acoperire_cash_flow: Cash flow YTD / datorii restante
+ Good: > 1 (cash flow suficient), Warning: 0.5-1, Danger: < 0.5
+
+ Args:
+ company_id: ID-ul firmei
+ luna: Luna contabilă (1-12)
+ an: Anul contabil
+
+ Returns:
+ CashFlowIndicators cu cei patru indicatori de cash flow
+ """
+ # Import DashboardService here to avoid circular imports
+ from .dashboard_service import DashboardService
+
+ # Obținem datele din trends pentru încasări/plăți istorice
+ # Folosim perioada 'ytd' pentru a obține datele de la începutul anului
+ trends = await DashboardService.get_trends(
+ company_id=company_id,
+ period='ytd',
+ luna=luna,
+ an=an
+ )
+
+ # Obținem datele din summary pentru datorii restante
+ summary = await DashboardService.get_complete_summary(
+ company=str(company_id),
+ username="system",
+ luna=luna,
+ an=an
+ )
+ # Ensure summary is a DashboardSummary model (cache may return dict)
+ if isinstance(summary, dict):
+ summary = DashboardSummary(**summary)
+
+ # Extragem arrayurile din trends
+ clienti_incasat = trends.get("clienti_incasat", [])
+ furnizori_achitat = trends.get("furnizori_achitat", [])
+ clienti_incasat_prev = trends.get("clienti_incasat_prev", [])
+ furnizori_achitat_prev = trends.get("furnizori_achitat_prev", [])
+
+ # Calculăm fluxurile nete lunare (încasări - plăți) pentru fiecare lună
+ fluxuri_nete = [
+ (inc - plat)
+ for inc, plat in zip(clienti_incasat, furnizori_achitat)
+ ] if clienti_incasat and furnizori_achitat else []
+
+ # Calculăm fluxurile nete pentru anul anterior
+ fluxuri_nete_prev = [
+ (inc - plat)
+ for inc, plat in zip(clienti_incasat_prev, furnizori_achitat_prev)
+ ] if clienti_incasat_prev and furnizori_achitat_prev else []
+
+ # 1. Flux net lunar (ultima lună disponibilă)
+ if fluxuri_nete:
+ flux_net_val = fluxuri_nete[-1] # Ultima lună (luna curentă)
+ flux_net_status = "good" if flux_net_val > 0 else "danger"
+ flux_net_message = (
+ "Firma generează numerar"
+ if flux_net_val > 0
+ else "Firma consumă numerar"
+ )
+ flux_net_lunar = IndicatorResult(
+ value=round(flux_net_val, 2),
+ status=flux_net_status,
+ threshold_min=0,
+ threshold_max=None,
+ message=flux_net_message
+ )
+ else:
+ flux_net_lunar = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=0,
+ threshold_max=None,
+ message="Fără date de cash flow pentru perioada selectată"
+ )
+
+ # 2. Cash flow YTD (suma fluxurilor de la ianuarie până la luna curentă)
+ if fluxuri_nete:
+ cf_ytd_val = sum(fluxuri_nete)
+ cf_ytd_status = "good" if cf_ytd_val > 0 else "danger"
+ cash_flow_ytd = IndicatorResult(
+ value=round(cf_ytd_val, 2),
+ status=cf_ytd_status,
+ threshold_min=0,
+ threshold_max=None
+ )
+ else:
+ cf_ytd_val = 0
+ cash_flow_ytd = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=0,
+ threshold_max=None,
+ message="Fără date de cash flow YTD"
+ )
+
+ # 3. Flux net YoY % (variația an-la-an)
+ # Comparăm cash flow-ul YTD curent cu cel din aceeași perioadă a anului anterior
+ if fluxuri_nete and fluxuri_nete_prev:
+ cf_curent = sum(fluxuri_nete)
+ # Luăm același număr de luni din anul anterior pentru comparație corectă
+ num_months = len(fluxuri_nete)
+ cf_anterior = sum(fluxuri_nete_prev[:num_months]) if len(fluxuri_nete_prev) >= num_months else sum(fluxuri_nete_prev)
+
+ if abs(cf_anterior) > 0:
+ yoy_pct = ((cf_curent - cf_anterior) / abs(cf_anterior)) * 100
+ yoy_status = "good" if yoy_pct >= 0 else "danger"
+ yoy_message = (
+ "Creștere cash flow față de anul anterior"
+ if yoy_pct >= 0
+ else "Scădere cash flow față de anul anterior"
+ )
+ flux_net_yoy_pct = IndicatorResult(
+ value=round(yoy_pct, 1),
+ status=yoy_status,
+ threshold_min=0,
+ threshold_max=None,
+ message=yoy_message
+ )
+ else:
+ # Cash flow anterior era zero
+ if cf_curent > 0:
+ flux_net_yoy_pct = IndicatorResult(
+ value=100.0,
+ status="good",
+ threshold_min=0,
+ threshold_max=None,
+ message="Cash flow pozitiv vs zero anul anterior"
+ )
+ elif cf_curent < 0:
+ flux_net_yoy_pct = IndicatorResult(
+ value=-100.0,
+ status="danger",
+ threshold_min=0,
+ threshold_max=None,
+ message="Cash flow negativ vs zero anul anterior"
+ )
+ else:
+ flux_net_yoy_pct = IndicatorResult(
+ value=0.0,
+ status="warning",
+ threshold_min=0,
+ threshold_max=None,
+ message="Cash flow zero în ambii ani"
+ )
+ else:
+ flux_net_yoy_pct = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=0,
+ threshold_max=None,
+ message="Fără date pentru comparație YoY"
+ )
+
+ # 4. Acoperire cash flow = cash_flow_ytd / datorii_restante
+ # Datoriile restante sunt furnizori_sold_restant din summary
+ datorii_restante = float(summary.furnizori_sold_restant)
+
+ if datorii_restante > 0 and cf_ytd_val is not None:
+ acoperire_val = cf_ytd_val / datorii_restante
+ acoperire_status = FinancialIndicatorsService._calculate_indicator_status(
+ acoperire_val,
+ good_threshold=1.0,
+ warning_threshold=0.5
+ )
+ acoperire_message = (
+ "Cash flow suficient pentru acoperirea datoriilor restante"
+ if acoperire_val >= 1.0
+ else "Cash flow insuficient pentru acoperirea datoriilor restante"
+ if acoperire_val < 0.5
+ else "Cash flow parțial pentru datorii restante"
+ )
+ acoperire_cash_flow = IndicatorResult(
+ value=round(acoperire_val, 2),
+ status=acoperire_status,
+ threshold_min=1.0,
+ threshold_max=None,
+ message=acoperire_message
+ )
+ elif datorii_restante == 0:
+ # Fără datorii restante - situație excelentă
+ acoperire_cash_flow = IndicatorResult(
+ value=None,
+ status="good",
+ threshold_min=1.0,
+ threshold_max=None,
+ message="Fără datorii restante - nu este nevoie de acoperire"
+ )
+ else:
+ # Nu avem date de cash flow
+ acoperire_cash_flow = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=1.0,
+ threshold_max=None,
+ message="Nu se poate calcula acoperirea cash flow"
+ )
+
+ result = CashFlowIndicators(
+ flux_net_lunar=flux_net_lunar,
+ cash_flow_ytd=cash_flow_ytd,
+ flux_net_yoy_pct=flux_net_yoy_pct,
+ acoperire_cash_flow=acoperire_cash_flow
+ )
+
+ logger.info(
+ f"Cash flow indicators for company {company_id}, luna={luna}, an={an}: "
+ f"flux_net_lunar={flux_net_lunar.value} ({flux_net_lunar.status}), "
+ f"cash_flow_ytd={cash_flow_ytd.value} ({cash_flow_ytd.status}), "
+ f"flux_net_yoy_pct={flux_net_yoy_pct.value}% ({flux_net_yoy_pct.status}), "
+ f"acoperire_cf={acoperire_cash_flow.value} ({acoperire_cash_flow.status})"
+ )
+
+ return result
+
+ @staticmethod
+ @cached(cache_type='fin_dynamics', key_params=['company_id', 'luna', 'an'])
+ async def calculate_dynamics_indicators(
+ company_id: int,
+ luna: int,
+ an: int
+ ) -> DynamicsIndicators:
+ """
+ Calculează indicatorii de dinamică pentru evaluarea evoluției afacerii.
+
+ Compară vânzările și achizițiile cu anul anterior (YoY - Year-over-Year)
+ pentru a determina dacă afacerea crește sau scade.
+
+ Indicatori calculați:
+ - crestere_vanzari_yoy: Creșterea procentuală a facturărilor față de anul anterior
+ Formula: (facturari_curent - facturari_anterior) / facturari_anterior * 100
+ Good: > 5%, Warning: 0-5%, Danger: < 0%
+
+ - crestere_achizitii_yoy: Creșterea procentuală a achizițiilor față de anul anterior
+ Formula: (achizitii_curent - achizitii_anterior) / achizitii_anterior * 100
+ Informativ - creșterea achizițiilor poate indica expansiune
+
+ - marja_implicita: Marja implicită din diferența facturări - achiziții
+ Formula: (facturari - achizitii) / facturari * 100
+ Good: > 20%, Warning: 10-20%, Danger: < 10%
+
+ Args:
+ company_id: ID-ul firmei
+ luna: Luna contabilă (1-12)
+ an: Anul contabil
+
+ Returns:
+ DynamicsIndicators cu cei trei indicatori de dinamică
+ """
+ # Import DashboardService here to avoid circular imports
+ from .dashboard_service import DashboardService
+
+ # Obținem datele din trends pentru perioada curentă (YTD) și anul anterior
+ trends = await DashboardService.get_trends(
+ company_id=company_id,
+ period='ytd',
+ luna=luna,
+ an=an
+ )
+
+ # Extragem arrayurile din trends - facturări și achiziții
+ # clienti_facturat = facturări (vânzări)
+ # furnizori_facturat = achiziții
+ clienti_facturat = trends.get("clienti_facturat", [])
+ clienti_facturat_prev = trends.get("clienti_facturat_prev", [])
+ furnizori_facturat = trends.get("furnizori_facturat", [])
+ furnizori_facturat_prev = trends.get("furnizori_facturat_prev", [])
+
+ # Calculăm totalurile pentru perioada curentă și anterioară
+ # Luăm același număr de luni pentru comparație corectă YoY
+ num_months = len(clienti_facturat)
+
+ # Total facturări (vânzări) an curent și anterior
+ total_facturari_curent = sum(clienti_facturat) if clienti_facturat else 0
+ total_facturari_anterior = (
+ sum(clienti_facturat_prev[:num_months])
+ if len(clienti_facturat_prev) >= num_months
+ else sum(clienti_facturat_prev)
+ ) if clienti_facturat_prev else 0
+
+ # Total achiziții an curent și anterior
+ total_achizitii_curent = sum(furnizori_facturat) if furnizori_facturat else 0
+ total_achizitii_anterior = (
+ sum(furnizori_facturat_prev[:num_months])
+ if len(furnizori_facturat_prev) >= num_months
+ else sum(furnizori_facturat_prev)
+ ) if furnizori_facturat_prev else 0
+
+ # 1. Creștere vânzări YoY
+ # Formula: (facturari_curent - facturari_anterior) / facturari_anterior * 100
+ if total_facturari_anterior > 0:
+ crestere_vanzari_val = (
+ (total_facturari_curent - total_facturari_anterior) /
+ total_facturari_anterior * 100
+ )
+ # Status: good > 5%, warning 0-5%, danger < 0%
+ crestere_vanzari_status = FinancialIndicatorsService._calculate_indicator_status(
+ crestere_vanzari_val,
+ good_threshold=5.0,
+ warning_threshold=0.0
+ )
+ crestere_vanzari_message = (
+ "Creștere semnificativă a vânzărilor"
+ if crestere_vanzari_val > 5
+ else "Vânzări stabile"
+ if crestere_vanzari_val >= 0
+ else "Vânzări în scădere"
+ )
+ crestere_vanzari_yoy = IndicatorResult(
+ value=round(crestere_vanzari_val, 1),
+ status=crestere_vanzari_status,
+ threshold_min=5.0,
+ threshold_max=None,
+ message=crestere_vanzari_message
+ )
+ elif total_facturari_curent > 0:
+ # Anul anterior nu avea facturări, dar anul curent da
+ crestere_vanzari_yoy = IndicatorResult(
+ value=100.0,
+ status="good",
+ threshold_min=5.0,
+ threshold_max=None,
+ message="Vânzări noi - nu existau în anul anterior"
+ )
+ else:
+ # Fără facturări în niciun an
+ crestere_vanzari_yoy = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=5.0,
+ threshold_max=None,
+ message="Fără date de facturări pentru comparație"
+ )
+
+ # 2. Creștere achiziții YoY
+ # Formula: (achizitii_curent - achizitii_anterior) / achizitii_anterior * 100
+ if total_achizitii_anterior > 0:
+ crestere_achizitii_val = (
+ (total_achizitii_curent - total_achizitii_anterior) /
+ total_achizitii_anterior * 100
+ )
+ # Pentru achiziții, creșterea poate fi neutră (expansiune) sau negativă (reducere)
+ # Nu există un "danger" clar - e informativ
+ crestere_achizitii_message = (
+ "Achiziții în creștere - posibilă expansiune"
+ if crestere_achizitii_val > 5
+ else "Achiziții stabile"
+ if crestere_achizitii_val >= -5
+ else "Achiziții în scădere"
+ )
+ crestere_achizitii_yoy = IndicatorResult(
+ value=round(crestere_achizitii_val, 1),
+ status="good", # Informativ - nu are praguri stricte
+ threshold_min=None,
+ threshold_max=None,
+ message=crestere_achizitii_message
+ )
+ elif total_achizitii_curent > 0:
+ crestere_achizitii_yoy = IndicatorResult(
+ value=100.0,
+ status="good",
+ threshold_min=None,
+ threshold_max=None,
+ message="Achiziții noi - nu existau în anul anterior"
+ )
+ else:
+ crestere_achizitii_yoy = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=None,
+ threshold_max=None,
+ message="Fără date de achiziții pentru comparație"
+ )
+
+ # 3. Marja implicită
+ # Formula: (facturari - achizitii) / facturari * 100
+ # Arată ce procent din vânzări rămâne după achiziții
+ if total_facturari_curent > 0:
+ marja_val = (
+ (total_facturari_curent - total_achizitii_curent) /
+ total_facturari_curent * 100
+ )
+ # Status: good > 20%, warning 10-20%, danger < 10%
+ marja_status = FinancialIndicatorsService._calculate_indicator_status(
+ marja_val,
+ good_threshold=20.0,
+ warning_threshold=10.0
+ )
+ marja_message = (
+ "Marjă implicită sănătoasă"
+ if marja_val > 20
+ else "Marjă implicită moderată"
+ if marja_val >= 10
+ else "Marjă implicită redusă - verificați costurile"
+ )
+ marja_implicita = IndicatorResult(
+ value=round(marja_val, 1),
+ status=marja_status,
+ threshold_min=20.0,
+ threshold_max=None,
+ message=marja_message
+ )
+ else:
+ marja_implicita = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=20.0,
+ threshold_max=None,
+ message="Fără facturări - nu se poate calcula marja"
+ )
+
+ result = DynamicsIndicators(
+ crestere_vanzari_yoy=crestere_vanzari_yoy,
+ crestere_achizitii_yoy=crestere_achizitii_yoy,
+ marja_implicita=marja_implicita
+ )
+
+ logger.info(
+ f"Dynamics indicators for company {company_id}, luna={luna}, an={an}: "
+ f"crestere_vanzari_yoy={crestere_vanzari_yoy.value}% ({crestere_vanzari_yoy.status}), "
+ f"crestere_achizitii_yoy={crestere_achizitii_yoy.value}% ({crestere_achizitii_yoy.status}), "
+ f"marja_implicita={marja_implicita.value}% ({marja_implicita.status})"
+ )
+
+ return result
+
+ @staticmethod
+ @cached(cache_type='fin_altman', key_params=['company_id', 'luna', 'an'])
+ async def calculate_altman_zscore(
+ company_id: int,
+ luna: int,
+ an: int
+ ) -> AltmanZScore:
+ """
+ Calculează Altman Z-Score pentru evaluarea riscului de faliment.
+
+ Folosim formula modificată pentru companii private (Z'-Score):
+ Z' = 6.56*X1 + 3.26*X2 + 6.72*X3 + 1.05*X4
+
+ Aceasta este versiunea pentru companii care nu sunt listate la bursă,
+ unde se folosește valoarea contabilă a capitalurilor proprii (Book Value)
+ în loc de valoarea de piață a acțiunilor.
+
+ Componente:
+ - X1 = Working Capital / Total Assets (lichiditate pe termen scurt)
+ - X2 = Retained Earnings / Total Assets (profitabilitate cumulată)
+ - X3 = EBIT / Total Assets (eficiență operațională)
+ - X4 = Book Value of Equity / Total Liabilities (solvabilitate)
+
+ Zone de risc:
+ - Safe (Z > 2.60): Risc minim de faliment - situație financiară solidă
+ - Grey (1.10 <= Z <= 2.60): Zona de incertitudine - necesită monitorizare
+ - Distress (Z < 1.10): Risc ridicat de faliment - situație critică
+
+ Args:
+ company_id: ID-ul firmei
+ luna: Luna contabilă (1-12)
+ an: Anul contabil
+
+ Returns:
+ AltmanZScore cu scorul calculat și componentele individuale x1, x2, x3, x4
+ """
+ # Obținem agregatele din balanță
+ aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
+ company_id, luna, an
+ )
+ # Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
+ if isinstance(aggregates, dict):
+ aggregates = BalanceSheetAggregates(**aggregates)
+
+ # Calculăm componentele necesare
+ # Working Capital = active_curente - datorii_curente
+ working_capital = float(aggregates.working_capital)
+
+ # Total Assets = active_imobilizate + active_curente
+ total_assets = float(aggregates.total_active)
+
+ # Rezultat reportat (cont 117 + 121) - deja agregat în `rezultat`
+ rezultat_reportat = float(aggregates.rezultat)
+
+ # EBIT = venituri - cheltuieli_operationale
+ ebit = float(aggregates.ebit)
+
+ # Capital propriu (inclusiv rezultat) pentru X4
+ capital_propriu = float(aggregates.capitaluri_proprii)
+
+ # Total datorii (curente + termen lung)
+ total_datorii = float(aggregates.total_datorii)
+ datorii_curente = float(aggregates.datorii_curente)
+ datorii_termen_lung = float(aggregates.datorii_termen_lung)
+
+ # Verificăm dacă avem date suficiente pentru calcul
+ if total_assets == 0:
+ # Nu putem calcula Z-Score fără active
+ return AltmanZScore(
+ zscore=IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=2.60,
+ threshold_max=None,
+ message="Nu se poate calcula - total active este zero"
+ ),
+ x1=IndicatorResult(value=None, status="warning"),
+ x2=IndicatorResult(value=None, status="warning"),
+ x3=IndicatorResult(value=None, status="warning"),
+ x4=IndicatorResult(value=None, status="warning"),
+ capital_de_lucru=IndicatorResult(
+ value=round(working_capital, 2),
+ status="warning",
+ message="Active Curente - Datorii Curente"
+ ),
+ active_totale=IndicatorResult(
+ value=0,
+ status="warning",
+ message="Nu există active în balanță"
+ ),
+ datorii_totale=IndicatorResult(
+ value=round(total_datorii, 2),
+ status="warning",
+ message="Datorii Curente + Datorii Termen Lung"
+ )
+ )
+
+ # Calculăm componentele X1-X4
+
+ # X1 = Working Capital / Total Assets
+ # Măsoară lichiditatea - cât de mult din active este finanțat din surse pe termen scurt
+ x1_val = working_capital / total_assets
+ x1_status = "good" if x1_val > 0 else "danger"
+ x1 = IndicatorResult(
+ value=round(x1_val, 4),
+ status=x1_status,
+ threshold_min=0,
+ threshold_max=None,
+ message="Lichiditate pe termen scurt" if x1_val > 0 else "Working capital negativ"
+ )
+
+ # X2 = Retained Earnings / Total Assets
+ # Măsoară profitabilitatea cumulată - câștigurile reinvestite în companie
+ x2_val = rezultat_reportat / total_assets
+ x2_status = "good" if x2_val > 0 else ("warning" if x2_val == 0 else "danger")
+ x2 = IndicatorResult(
+ value=round(x2_val, 4),
+ status=x2_status,
+ threshold_min=0,
+ threshold_max=None,
+ message="Profitabilitate cumulată" if x2_val >= 0 else "Pierderi cumulate"
+ )
+
+ # X3 = EBIT / Total Assets
+ # Măsoară eficiența operațională - randamentul activelor
+ x3_val = ebit / total_assets
+ x3_status = "good" if x3_val > 0 else ("warning" if x3_val == 0 else "danger")
+ x3 = IndicatorResult(
+ value=round(x3_val, 4),
+ status=x3_status,
+ threshold_min=0,
+ threshold_max=None,
+ message="Eficiență operațională" if x3_val >= 0 else "Pierdere operațională"
+ )
+
+ # X4 = Book Value of Equity / Total Liabilities
+ # Măsoară solvabilitatea - acoperirea datoriilor cu capital propriu
+ if total_datorii > 0:
+ x4_val = capital_propriu / total_datorii
+ x4_status = FinancialIndicatorsService._calculate_indicator_status(
+ x4_val,
+ good_threshold=1.0,
+ warning_threshold=0.5
+ )
+ x4 = IndicatorResult(
+ value=round(x4_val, 4),
+ status=x4_status,
+ threshold_min=1.0,
+ threshold_max=None,
+ message="Solvabilitate bună" if x4_val >= 1 else "Îndatorare ridicată"
+ )
+ else:
+ # Fără datorii - situație excelentă pentru solvabilitate
+ x4_val = None # Infinit teoretic, dar nu putem reprezenta
+ x4 = IndicatorResult(
+ value=None,
+ status="good",
+ threshold_min=1.0,
+ threshold_max=None,
+ message="Fără datorii - solvabilitate maximă"
+ )
+
+ # Calculăm Z-Score folosind formula pentru companii private
+ # Z' = 6.56*X1 + 3.26*X2 + 6.72*X3 + 1.05*X4
+ if x4_val is not None:
+ zscore_val = (
+ 6.56 * x1_val +
+ 3.26 * x2_val +
+ 6.72 * x3_val +
+ 1.05 * x4_val
+ )
+ else:
+ # Dacă X4 este infinit (fără datorii), calculăm Z-Score fără componenta X4
+ # În practică, firmele fără datorii au un Z-Score foarte bun
+ # Folosim o valoare foarte mare pentru X4 (ex: 10) ca proxy
+ zscore_val = (
+ 6.56 * x1_val +
+ 3.26 * x2_val +
+ 6.72 * x3_val +
+ 1.05 * 10.0 # Proxy pentru solvabilitate maximă
+ )
+
+ # Determinăm zona de risc
+ if zscore_val > 2.60:
+ zscore_status = "safe"
+ zscore_message = "Zona sigură - risc minim de faliment"
+ elif zscore_val >= 1.10:
+ zscore_status = "grey"
+ zscore_message = "Zona gri - necesită monitorizare atentă"
+ else:
+ zscore_status = "distress"
+ zscore_message = "Zona de risc - risc ridicat de faliment"
+
+ zscore = IndicatorResult(
+ value=round(zscore_val, 2),
+ status=zscore_status,
+ threshold_min=2.60,
+ threshold_max=None,
+ message=zscore_message
+ )
+
+ # Indicatori de bază pentru verificare manuală în balanță
+ capital_de_lucru = IndicatorResult(
+ value=round(working_capital, 2),
+ status="good" if working_capital > 0 else "danger",
+ threshold_min=0,
+ threshold_max=None,
+ message="Active Curente - Datorii Curente"
+ )
+
+ active_totale_ind = IndicatorResult(
+ value=round(total_assets, 2),
+ status="good",
+ threshold_min=None,
+ threshold_max=None,
+ message="Active Imobilizate + Active Curente"
+ )
+
+ datorii_totale_ind = IndicatorResult(
+ value=round(total_datorii, 2),
+ status="good",
+ threshold_min=None,
+ threshold_max=None,
+ message="Datorii Curente + Datorii Termen Lung"
+ )
+
+ result = AltmanZScore(
+ zscore=zscore,
+ x1=x1,
+ x2=x2,
+ x3=x3,
+ x4=x4,
+ capital_de_lucru=capital_de_lucru,
+ active_totale=active_totale_ind,
+ datorii_totale=datorii_totale_ind
+ )
+
+ logger.info(
+ f"Altman Z-Score for company {company_id}, luna={luna}, an={an}: "
+ f"zscore={zscore.value} ({zscore.status}), "
+ f"X1={x1.value}, X2={x2.value}, X3={x3.value}, X4={x4.value}"
+ )
+
+ return result
+
+ @staticmethod
+ @cached(cache_type='fin_profitability', key_params=['company_id', 'luna', 'an'])
+ async def calculate_profitability_indicators(
+ company_id: int,
+ luna: int,
+ an: int
+ ) -> ProfitabilityIndicators:
+ """
+ Calculează indicatorii de profitabilitate pentru evaluarea randamentului afacerii.
+
+ Indicatori calculați:
+ - cifra_afaceri: Total venituri operaționale (Clasa 7)
+ - cheltuieli_totale: Total cheltuieli operaționale (Clasa 6)
+ - profit_brut: EBIT = venituri - cheltuieli
+ - marja_profit_brut: profit / venituri * 100
+ - roa: Return on Assets = profit / total_active * 100
+ - roe: Return on Equity = profit / capitaluri_proprii * 100
+
+ Praguri de referință:
+ - Marja profit: good > 10%, warning 5-10%, danger < 5%
+ - ROA: good > 5%, warning 2-5%, danger < 2%
+ - ROE: good > 10%, warning 5-10%, danger < 5%
+
+ Args:
+ company_id: ID-ul firmei
+ luna: Luna contabilă (1-12)
+ an: Anul contabil
+
+ Returns:
+ ProfitabilityIndicators cu cei șase indicatori de profitabilitate
+ """
+ # Obținem agregatele din balanță (include venituri, cheltuieli, active, capital)
+ aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
+ company_id, luna, an
+ )
+ # Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
+ if isinstance(aggregates, dict):
+ aggregates = BalanceSheetAggregates(**aggregates)
+
+ # Extragem valorile necesare
+ venituri = float(aggregates.venituri)
+ cheltuieli_oper = float(aggregates.cheltuieli_operationale)
+ cheltuieli_fin = float(aggregates.cheltuieli_financiare)
+ cheltuieli_total = cheltuieli_oper + cheltuieli_fin
+ profit_brut_val = float(aggregates.ebit) # EBIT = venituri - cheltuieli operaționale
+ total_active = float(aggregates.total_active)
+ capitaluri_proprii_val = float(aggregates.capitaluri_proprii)
+
+ # 1. Cifra de afaceri (venituri totale) - informativ
+ cifra_afaceri = IndicatorResult(
+ value=round(venituri, 2),
+ status="good", # Informativ - nu are praguri
+ threshold_min=None,
+ threshold_max=None,
+ message="Total venituri din activitatea operațională"
+ )
+
+ # 2. Cheltuieli operaționale (fără dobânzi 66x) - pentru verificare
+ cheltuieli_operationale = IndicatorResult(
+ value=round(cheltuieli_oper, 2),
+ status="good", # Informativ - nu are praguri
+ threshold_min=None,
+ threshold_max=None,
+ message="Clasa 60x-65x + 68x (fără dobânzi)"
+ )
+
+ # 3. Cheltuieli financiare (66x) - pentru verificare
+ cheltuieli_financiare = IndicatorResult(
+ value=round(cheltuieli_fin, 2),
+ status="good", # Informativ - nu are praguri
+ threshold_min=None,
+ threshold_max=None,
+ message="Clasa 66x (dobânzi, diferențe curs)"
+ )
+
+ # 4. Cheltuieli totale (operaționale + financiare)
+ cheltuieli_totale = IndicatorResult(
+ value=round(cheltuieli_total, 2),
+ status="good", # Informativ - nu are praguri
+ threshold_min=None,
+ threshold_max=None,
+ message="Operaționale + Financiare"
+ )
+
+ # 3. Profit brut (EBIT)
+ profit_status = "good" if profit_brut_val > 0 else "danger"
+ profit_message = (
+ "Profit operațional pozitiv"
+ if profit_brut_val > 0
+ else "Pierdere operațională - costuri depășesc veniturile"
+ )
+ profit_brut = IndicatorResult(
+ value=round(profit_brut_val, 2),
+ status=profit_status,
+ threshold_min=0,
+ threshold_max=None,
+ message=profit_message
+ )
+
+ # 4. Marja de profit = profit / venituri * 100
+ if venituri > 0:
+ marja_val = (profit_brut_val / venituri) * 100
+ marja_status = FinancialIndicatorsService._calculate_indicator_status(
+ marja_val,
+ good_threshold=10.0,
+ warning_threshold=5.0
+ )
+ marja_message = (
+ "Marjă de profit sănătoasă"
+ if marja_val > 10
+ else "Marjă de profit moderată"
+ if marja_val >= 5
+ else "Marjă de profit scăzută - verificați costurile"
+ )
+ marja_profit_brut = IndicatorResult(
+ value=round(marja_val, 1),
+ status=marja_status,
+ threshold_min=10.0,
+ threshold_max=None,
+ message=marja_message
+ )
+ else:
+ marja_profit_brut = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=10.0,
+ threshold_max=None,
+ message="Fără venituri - nu se poate calcula marja"
+ )
+
+ # 5. ROA (Return on Assets) = profit / total_active * 100
+ if total_active > 0:
+ roa_val = (profit_brut_val / total_active) * 100
+ roa_status = FinancialIndicatorsService._calculate_indicator_status(
+ roa_val,
+ good_threshold=5.0,
+ warning_threshold=2.0
+ )
+ roa_message = (
+ "Randament bun al activelor"
+ if roa_val > 5
+ else "Randament moderat al activelor"
+ if roa_val >= 2
+ else "Randament scăzut al activelor"
+ )
+ roa = IndicatorResult(
+ value=round(roa_val, 2),
+ status=roa_status,
+ threshold_min=5.0,
+ threshold_max=None,
+ message=roa_message
+ )
+ else:
+ roa = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=5.0,
+ threshold_max=None,
+ message="Fără active - nu se poate calcula ROA"
+ )
+
+ # Indicatori de bază pentru verificare manuală în balanță
+ active_totale = IndicatorResult(
+ value=round(total_active, 2),
+ status="good",
+ threshold_min=None,
+ threshold_max=None,
+ message="Active Imobilizate + Active Curente (bază calcul ROA)"
+ )
+
+ capitaluri_proprii = IndicatorResult(
+ value=round(capitaluri_proprii_val, 2),
+ status="good" if capitaluri_proprii_val > 0 else "danger",
+ threshold_min=None,
+ threshold_max=None,
+ message="Capital Social + Rezultat (bază calcul ROE)"
+ )
+
+ # ROE (Return on Equity) = profit / capitaluri_proprii * 100
+ if capitaluri_proprii_val > 0:
+ roe_val = (profit_brut_val / capitaluri_proprii_val) * 100
+ roe_status = FinancialIndicatorsService._calculate_indicator_status(
+ roe_val,
+ good_threshold=10.0,
+ warning_threshold=5.0
+ )
+ roe_message = (
+ "Randament atractiv pentru acționari"
+ if roe_val > 10
+ else "Randament moderat pentru acționari"
+ if roe_val >= 5
+ else "Randament scăzut pentru acționari"
+ )
+ roe = IndicatorResult(
+ value=round(roe_val, 2),
+ status=roe_status,
+ threshold_min=10.0,
+ threshold_max=None,
+ message=roe_message
+ )
+ elif capitaluri_proprii_val <= 0 and profit_brut_val > 0:
+ # Capital negativ dar profit pozitiv - situație neobișnuită
+ roe = IndicatorResult(
+ value=None,
+ status="danger",
+ threshold_min=10.0,
+ threshold_max=None,
+ message="Capital propriu negativ sau zero - situație de risc"
+ )
+ else:
+ roe = IndicatorResult(
+ value=None,
+ status="warning",
+ threshold_min=10.0,
+ threshold_max=None,
+ message="Nu se poate calcula ROE"
+ )
+
+ result = ProfitabilityIndicators(
+ cifra_afaceri=cifra_afaceri,
+ cheltuieli_operationale=cheltuieli_operationale,
+ cheltuieli_financiare=cheltuieli_financiare,
+ cheltuieli_totale=cheltuieli_totale,
+ profit_brut=profit_brut,
+ marja_profit_brut=marja_profit_brut,
+ active_totale=active_totale,
+ capitaluri_proprii=capitaluri_proprii,
+ roa=roa,
+ roe=roe
+ )
+
+ logger.info(
+ f"Profitability indicators for company {company_id}, luna={luna}, an={an}: "
+ f"cifra_afaceri={cifra_afaceri.value}, "
+ f"profit_brut={profit_brut.value} ({profit_brut.status}), "
+ f"marja={marja_profit_brut.value}% ({marja_profit_brut.status}), "
+ f"roa={roa.value}% ({roa.status}), "
+ f"roe={roe.value}% ({roe.status})"
+ )
+
+ return result
+
+ @staticmethod
+ def _generate_month_labels(luna: int, an: int, months: int = 12) -> List[str]:
+ """
+ Generează etichetele lunilor în format 'MMM YY' pentru sparkline.
+
+ Generează etichete pentru ultimele `months` luni, terminând cu luna/an specificată.
+ Folosim abrevieri în engleză pentru consistență internațională.
+
+ Args:
+ luna: Luna finală (1-12)
+ an: Anul final
+ months: Numărul de luni de generat (default 12)
+
+ Returns:
+ Lista de etichete în format 'MMM YY' (ex: ['Feb 24', 'Mar 24', ...])
+ """
+ MONTH_ABBR = [
+ 'Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun',
+ 'Iul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
+ ]
+
+ labels = []
+ current_luna = luna
+ current_an = an
+
+ # Generăm etichetele de la luna curentă înapoi
+ for _ in range(months):
+ year_short = str(current_an)[-2:] # Ultimele 2 cifre ale anului
+ label = f"{MONTH_ABBR[current_luna - 1]} {year_short}"
+ labels.insert(0, label) # Inserăm la început pentru ordine cronologică
+
+ # Mergem la luna anterioară
+ current_luna -= 1
+ if current_luna < 1:
+ current_luna = 12
+ current_an -= 1
+
+ return labels
+
+ @staticmethod
+ def _get_historical_periods(luna: int, an: int, months: int = 12) -> List[tuple]:
+ """
+ Generează lista de perioade (luna, an) pentru ultimele `months` luni.
+
+ Args:
+ luna: Luna finală (1-12)
+ an: Anul final
+ months: Numărul de luni (default 12)
+
+ Returns:
+ Lista de tuple (luna, an) în ordine cronologică
+ """
+ periods = []
+ current_luna = luna
+ current_an = an
+
+ for _ in range(months):
+ periods.insert(0, (current_luna, current_an))
+
+ current_luna -= 1
+ if current_luna < 1:
+ current_luna = 12
+ current_an -= 1
+
+ return periods
+
+ @staticmethod
+ @cached(cache_type='financial_indicators_historical', ttl=3600, key_params=['company_id', 'months', 'luna', 'an'])
+ async def get_historical_indicators(
+ company_id: int,
+ months: int = 12,
+ luna: Optional[int] = None,
+ an: Optional[int] = None
+ ) -> Dict[str, Any]:
+ """
+ Calculează indicatorii financiari pentru ultimele `months` luni
+ și returnează datele pentru sparklines.
+
+ Această metodă este optimizată pentru performanță cu cache separat
+ (TTL 1 oră) deoarece datele istorice se schimbă rar.
+
+ Args:
+ company_id: ID-ul firmei
+ months: Numărul de luni pentru istoric (default 12)
+ luna: Luna de referință (dacă None, folosește luna curentă)
+ an: Anul de referință (dacă None, folosește anul curent)
+
+ Returns:
+ Dict cu:
+ - sparkline_labels: Array cu etichetele lunilor
+ - lichiditate: Dict cu sparkline_data pentru fiecare indicator
+ - eficienta: Dict cu sparkline_data pentru fiecare indicator
+ - risc: Dict cu sparkline_data pentru fiecare indicator
+ - cash_flow: Dict cu sparkline_data pentru fiecare indicator
+ - dinamica: Dict cu sparkline_data pentru fiecare indicator
+ - altman_zscore: Dict cu sparkline_data pentru fiecare indicator
+ """
+ from datetime import datetime
+
+ # Dacă luna/an nu sunt specificate, folosim data curentă
+ resolved_luna: int
+ resolved_an: int
+
+ if luna is None or an is None:
+ try:
+ from .dashboard_service import DashboardService
+ current_period = await DashboardService.get_current_period(company_id)
+ resolved_luna = luna if luna is not None else current_period.get('luna', datetime.now().month)
+ resolved_an = an if an is not None else current_period.get('an', datetime.now().year)
+ except Exception as e:
+ logger.warning(f"Could not get current period: {e}, using defaults")
+ resolved_luna = luna if luna is not None else datetime.now().month
+ resolved_an = an if an is not None else datetime.now().year
+ else:
+ resolved_luna = luna
+ resolved_an = an
+
+ # Generăm perioadele și etichetele
+ periods = FinancialIndicatorsService._get_historical_periods(resolved_luna, resolved_an, months)
+ labels = FinancialIndicatorsService._generate_month_labels(resolved_luna, resolved_an, months)
+
+ # Inițializăm structurile pentru sparkline data
+ historical_data = {
+ 'sparkline_labels': labels,
+ 'lichiditate': {
+ 'lichiditate_curenta': [],
+ 'lichiditate_imediata': [],
+ 'lichiditate_vedere': []
+ },
+ 'eficienta': {
+ 'dso': [],
+ 'dpo': [],
+ 'cash_conversion_cycle': [],
+ 'rata_incasare': [],
+ 'rata_plata': []
+ },
+ 'risc': {
+ 'creante_restante_pct': [],
+ 'creante_90plus_pct': [],
+ 'datorii_restante_pct': [],
+ 'raport_datorii_trezorerie': []
+ },
+ 'cash_flow': {
+ 'flux_net_lunar': [],
+ 'cash_flow_ytd': [],
+ 'flux_net_yoy_pct': [],
+ 'acoperire_cash_flow': []
+ },
+ 'dinamica': {
+ 'crestere_vanzari_yoy': [],
+ 'crestere_achizitii_yoy': [],
+ 'marja_implicita': []
+ },
+ 'altman_zscore': {
+ 'zscore': [],
+ 'x1': [],
+ 'x2': [],
+ 'x3': [],
+ 'x4': []
+ },
+ 'profitabilitate': {
+ 'cifra_afaceri': [],
+ 'cheltuieli_totale': [],
+ 'profit_brut': [],
+ 'marja_profit_brut': [],
+ 'roa': [],
+ 'roe': []
+ }
+ }
+
+ # Calculăm indicatorii pentru fiecare perioadă
+ all_categories = ['lichiditate', 'eficienta', 'risc', 'cash_flow', 'dinamica', 'altman_zscore', 'profitabilitate']
+
+ for period_luna, period_an in periods:
+ # Track which categories were successfully processed in this period
+ processed_categories = set()
+
+ try:
+ # Lichiditate
+ lichiditate = await FinancialIndicatorsService.calculate_liquidity_indicators(
+ company_id, period_luna, period_an
+ )
+ # Ensure lichiditate is a model (cache may return dict)
+ if isinstance(lichiditate, dict):
+ lichiditate = LiquidityIndicators(**lichiditate)
+ historical_data['lichiditate']['lichiditate_curenta'].append(
+ lichiditate.lichiditate_curenta.value
+ )
+ historical_data['lichiditate']['lichiditate_imediata'].append(
+ lichiditate.lichiditate_imediata.value
+ )
+ historical_data['lichiditate']['lichiditate_vedere'].append(
+ lichiditate.lichiditate_vedere.value
+ )
+ processed_categories.add('lichiditate')
+
+ # Eficiență
+ eficienta = await FinancialIndicatorsService.calculate_efficiency_indicators(
+ company_id, period_luna, period_an
+ )
+ # Ensure eficienta is a model (cache may return dict)
+ if isinstance(eficienta, dict):
+ eficienta = EfficiencyIndicators(**eficienta)
+ historical_data['eficienta']['dso'].append(eficienta.dso.value)
+ historical_data['eficienta']['dpo'].append(eficienta.dpo.value)
+ historical_data['eficienta']['cash_conversion_cycle'].append(
+ eficienta.cash_conversion_cycle.value
+ )
+ historical_data['eficienta']['rata_incasare'].append(eficienta.rata_incasare.value)
+ historical_data['eficienta']['rata_plata'].append(eficienta.rata_plata.value)
+ processed_categories.add('eficienta')
+
+ # Risc
+ risc = await FinancialIndicatorsService.calculate_risk_indicators(
+ company_id, period_luna, period_an
+ )
+ # Ensure risc is a model (cache may return dict)
+ if isinstance(risc, dict):
+ risc = RiskIndicators(**risc)
+ historical_data['risc']['creante_restante_pct'].append(
+ risc.creante_restante_pct.value
+ )
+ historical_data['risc']['creante_90plus_pct'].append(risc.creante_90plus_pct.value)
+ historical_data['risc']['datorii_restante_pct'].append(
+ risc.datorii_restante_pct.value
+ )
+ historical_data['risc']['raport_datorii_trezorerie'].append(
+ risc.raport_datorii_trezorerie.value
+ )
+ processed_categories.add('risc')
+
+ # Cash Flow
+ cash_flow = await FinancialIndicatorsService.calculate_cashflow_indicators(
+ company_id, period_luna, period_an
+ )
+ # Ensure cash_flow is a model (cache may return dict)
+ if isinstance(cash_flow, dict):
+ cash_flow = CashFlowIndicators(**cash_flow)
+ historical_data['cash_flow']['flux_net_lunar'].append(cash_flow.flux_net_lunar.value)
+ historical_data['cash_flow']['cash_flow_ytd'].append(cash_flow.cash_flow_ytd.value)
+ historical_data['cash_flow']['flux_net_yoy_pct'].append(
+ cash_flow.flux_net_yoy_pct.value
+ )
+ historical_data['cash_flow']['acoperire_cash_flow'].append(
+ cash_flow.acoperire_cash_flow.value
+ )
+ processed_categories.add('cash_flow')
+
+ # Dinamica
+ dinamica = await FinancialIndicatorsService.calculate_dynamics_indicators(
+ company_id, period_luna, period_an
+ )
+ # Ensure dinamica is a model (cache may return dict)
+ if isinstance(dinamica, dict):
+ dinamica = DynamicsIndicators(**dinamica)
+ historical_data['dinamica']['crestere_vanzari_yoy'].append(
+ dinamica.crestere_vanzari_yoy.value
+ )
+ historical_data['dinamica']['crestere_achizitii_yoy'].append(
+ dinamica.crestere_achizitii_yoy.value
+ )
+ historical_data['dinamica']['marja_implicita'].append(dinamica.marja_implicita.value)
+ processed_categories.add('dinamica')
+
+ # Altman Z-Score
+ altman = await FinancialIndicatorsService.calculate_altman_zscore(
+ company_id, period_luna, period_an
+ )
+ # Ensure altman is a model (cache may return dict)
+ if isinstance(altman, dict):
+ altman = AltmanZScore(**altman)
+ historical_data['altman_zscore']['zscore'].append(altman.zscore.value)
+ historical_data['altman_zscore']['x1'].append(altman.x1.value)
+ historical_data['altman_zscore']['x2'].append(altman.x2.value)
+ historical_data['altman_zscore']['x3'].append(altman.x3.value)
+ historical_data['altman_zscore']['x4'].append(altman.x4.value)
+ processed_categories.add('altman_zscore')
+
+ # Profitabilitate
+ profitabilitate = await FinancialIndicatorsService.calculate_profitability_indicators(
+ company_id, period_luna, period_an
+ )
+ # Ensure profitabilitate is a model (cache may return dict)
+ if isinstance(profitabilitate, dict):
+ profitabilitate = ProfitabilityIndicators(**profitabilitate)
+ historical_data['profitabilitate']['cifra_afaceri'].append(
+ profitabilitate.cifra_afaceri.value
+ )
+ historical_data['profitabilitate']['cheltuieli_totale'].append(
+ profitabilitate.cheltuieli_totale.value
+ )
+ historical_data['profitabilitate']['profit_brut'].append(
+ profitabilitate.profit_brut.value
+ )
+ historical_data['profitabilitate']['marja_profit_brut'].append(
+ profitabilitate.marja_profit_brut.value
+ )
+ historical_data['profitabilitate']['roa'].append(profitabilitate.roa.value)
+ historical_data['profitabilitate']['roe'].append(profitabilitate.roe.value)
+ processed_categories.add('profitabilitate')
+
+ except Exception as e:
+ logger.warning(
+ f"Error calculating indicators for company {company_id}, "
+ f"luna={period_luna}, an={period_an}: {e}"
+ )
+
+ # Add None ONLY for categories that were NOT successfully processed
+ # This prevents duplicate entries when an exception occurs mid-way
+ for category in all_categories:
+ if category not in processed_categories:
+ for indicator in historical_data[category]:
+ historical_data[category][indicator].append(None)
+
+ logger.info(
+ f"Historical indicators for company {company_id}: "
+ f"{months} months ending {resolved_luna}/{resolved_an}"
+ )
+
+ return historical_data
+
+ @staticmethod
+ async def get_indicators_with_sparklines(
+ company_id: int,
+ luna: int,
+ an: int,
+ months: int = 12
+ ) -> FinancialIndicatorsResponse:
+ """
+ Calculează toți indicatorii financiari și adaugă datele de sparkline
+ pentru vizualizarea trendului pe ultimele luni.
+
+ Această metodă combină calculele curente ale indicatorilor cu
+ datele istorice pentru sparklines.
+
+ Args:
+ company_id: ID-ul firmei
+ luna: Luna contabilă (1-12)
+ an: Anul contabil
+ months: Numărul de luni pentru sparkline (default 12)
+
+ Returns:
+ FinancialIndicatorsResponse cu sparkline_data integrat în fiecare indicator
+ """
+ import asyncio
+
+ # Obținem datele istorice și indicatorii curenți în paralel
+ historical_task = FinancialIndicatorsService.get_historical_indicators(
+ company_id, months, luna, an
+ )
+
+ lichiditate_task = FinancialIndicatorsService.calculate_liquidity_indicators(
+ company_id, luna, an
+ )
+ eficienta_task = FinancialIndicatorsService.calculate_efficiency_indicators(
+ company_id, luna, an
+ )
+ risc_task = FinancialIndicatorsService.calculate_risk_indicators(
+ company_id, luna, an
+ )
+ cash_flow_task = FinancialIndicatorsService.calculate_cashflow_indicators(
+ company_id, luna, an
+ )
+ dinamica_task = FinancialIndicatorsService.calculate_dynamics_indicators(
+ company_id, luna, an
+ )
+ altman_task = FinancialIndicatorsService.calculate_altman_zscore(
+ company_id, luna, an
+ )
+ profitabilitate_task = FinancialIndicatorsService.calculate_profitability_indicators(
+ company_id, luna, an
+ )
+
+ (
+ historical_data,
+ lichiditate,
+ eficienta,
+ risc,
+ cash_flow,
+ dinamica,
+ altman_zscore,
+ profitabilitate
+ ) = await asyncio.gather(
+ historical_task,
+ lichiditate_task,
+ eficienta_task,
+ risc_task,
+ cash_flow_task,
+ dinamica_task,
+ altman_task,
+ profitabilitate_task
+ )
+
+ # Ensure all indicator results are models (cache may return dicts)
+ if isinstance(lichiditate, dict):
+ lichiditate = LiquidityIndicators(**lichiditate)
+ if isinstance(eficienta, dict):
+ eficienta = EfficiencyIndicators(**eficienta)
+ if isinstance(risc, dict):
+ risc = RiskIndicators(**risc)
+ if isinstance(cash_flow, dict):
+ cash_flow = CashFlowIndicators(**cash_flow)
+ if isinstance(dinamica, dict):
+ dinamica = DynamicsIndicators(**dinamica)
+ if isinstance(altman_zscore, dict):
+ altman_zscore = AltmanZScore(**altman_zscore)
+ if isinstance(profitabilitate, dict):
+ profitabilitate = ProfitabilityIndicators(**profitabilitate)
+
+ # Extragem etichetele comune
+ sparkline_labels = historical_data['sparkline_labels']
+
+ # Actualizăm indicatorii de lichiditate cu sparkline data
+ lichiditate.lichiditate_curenta.sparkline_data = historical_data['lichiditate']['lichiditate_curenta']
+ lichiditate.lichiditate_curenta.sparkline_labels = sparkline_labels
+ lichiditate.lichiditate_imediata.sparkline_data = historical_data['lichiditate']['lichiditate_imediata']
+ lichiditate.lichiditate_imediata.sparkline_labels = sparkline_labels
+ lichiditate.lichiditate_vedere.sparkline_data = historical_data['lichiditate']['lichiditate_vedere']
+ lichiditate.lichiditate_vedere.sparkline_labels = sparkline_labels
+
+ # Actualizăm indicatorii de eficiență cu sparkline data
+ eficienta.dso.sparkline_data = historical_data['eficienta']['dso']
+ eficienta.dso.sparkline_labels = sparkline_labels
+ eficienta.dpo.sparkline_data = historical_data['eficienta']['dpo']
+ eficienta.dpo.sparkline_labels = sparkline_labels
+ eficienta.cash_conversion_cycle.sparkline_data = historical_data['eficienta']['cash_conversion_cycle']
+ eficienta.cash_conversion_cycle.sparkline_labels = sparkline_labels
+ eficienta.rata_incasare.sparkline_data = historical_data['eficienta']['rata_incasare']
+ eficienta.rata_incasare.sparkline_labels = sparkline_labels
+ eficienta.rata_plata.sparkline_data = historical_data['eficienta']['rata_plata']
+ eficienta.rata_plata.sparkline_labels = sparkline_labels
+
+ # Actualizăm indicatorii de risc cu sparkline data
+ risc.creante_restante_pct.sparkline_data = historical_data['risc']['creante_restante_pct']
+ risc.creante_restante_pct.sparkline_labels = sparkline_labels
+ risc.creante_90plus_pct.sparkline_data = historical_data['risc']['creante_90plus_pct']
+ risc.creante_90plus_pct.sparkline_labels = sparkline_labels
+ risc.datorii_restante_pct.sparkline_data = historical_data['risc']['datorii_restante_pct']
+ risc.datorii_restante_pct.sparkline_labels = sparkline_labels
+ risc.raport_datorii_trezorerie.sparkline_data = historical_data['risc']['raport_datorii_trezorerie']
+ risc.raport_datorii_trezorerie.sparkline_labels = sparkline_labels
+
+ # Actualizăm indicatorii de cash flow cu sparkline data
+ cash_flow.flux_net_lunar.sparkline_data = historical_data['cash_flow']['flux_net_lunar']
+ cash_flow.flux_net_lunar.sparkline_labels = sparkline_labels
+ cash_flow.cash_flow_ytd.sparkline_data = historical_data['cash_flow']['cash_flow_ytd']
+ cash_flow.cash_flow_ytd.sparkline_labels = sparkline_labels
+ cash_flow.flux_net_yoy_pct.sparkline_data = historical_data['cash_flow']['flux_net_yoy_pct']
+ cash_flow.flux_net_yoy_pct.sparkline_labels = sparkline_labels
+ cash_flow.acoperire_cash_flow.sparkline_data = historical_data['cash_flow']['acoperire_cash_flow']
+ cash_flow.acoperire_cash_flow.sparkline_labels = sparkline_labels
+
+ # Actualizăm indicatorii de dinamică cu sparkline data
+ dinamica.crestere_vanzari_yoy.sparkline_data = historical_data['dinamica']['crestere_vanzari_yoy']
+ dinamica.crestere_vanzari_yoy.sparkline_labels = sparkline_labels
+ dinamica.crestere_achizitii_yoy.sparkline_data = historical_data['dinamica']['crestere_achizitii_yoy']
+ dinamica.crestere_achizitii_yoy.sparkline_labels = sparkline_labels
+ dinamica.marja_implicita.sparkline_data = historical_data['dinamica']['marja_implicita']
+ dinamica.marja_implicita.sparkline_labels = sparkline_labels
+
+ # Actualizăm Altman Z-Score cu sparkline data
+ altman_zscore.zscore.sparkline_data = historical_data['altman_zscore']['zscore']
+ altman_zscore.zscore.sparkline_labels = sparkline_labels
+ altman_zscore.x1.sparkline_data = historical_data['altman_zscore']['x1']
+ altman_zscore.x1.sparkline_labels = sparkline_labels
+ altman_zscore.x2.sparkline_data = historical_data['altman_zscore']['x2']
+ altman_zscore.x2.sparkline_labels = sparkline_labels
+ altman_zscore.x3.sparkline_data = historical_data['altman_zscore']['x3']
+ altman_zscore.x3.sparkline_labels = sparkline_labels
+ altman_zscore.x4.sparkline_data = historical_data['altman_zscore']['x4']
+ altman_zscore.x4.sparkline_labels = sparkline_labels
+
+ # Actualizăm indicatorii de profitabilitate cu sparkline data
+ profitabilitate.cifra_afaceri.sparkline_data = historical_data['profitabilitate']['cifra_afaceri']
+ profitabilitate.cifra_afaceri.sparkline_labels = sparkline_labels
+ profitabilitate.cheltuieli_totale.sparkline_data = historical_data['profitabilitate']['cheltuieli_totale']
+ profitabilitate.cheltuieli_totale.sparkline_labels = sparkline_labels
+ profitabilitate.profit_brut.sparkline_data = historical_data['profitabilitate']['profit_brut']
+ profitabilitate.profit_brut.sparkline_labels = sparkline_labels
+ profitabilitate.marja_profit_brut.sparkline_data = historical_data['profitabilitate']['marja_profit_brut']
+ profitabilitate.marja_profit_brut.sparkline_labels = sparkline_labels
+ profitabilitate.roa.sparkline_data = historical_data['profitabilitate']['roa']
+ profitabilitate.roa.sparkline_labels = sparkline_labels
+ profitabilitate.roe.sparkline_data = historical_data['profitabilitate']['roe']
+ profitabilitate.roe.sparkline_labels = sparkline_labels
+
+ # Construim răspunsul final
+ response = FinancialIndicatorsResponse(
+ lichiditate=lichiditate,
+ eficienta=eficienta,
+ risc=risc,
+ cash_flow=cash_flow,
+ dinamica=dinamica,
+ altman_zscore=altman_zscore,
+ profitabilitate=profitabilitate
+ )
+
+ logger.info(
+ f"Indicators with sparklines for company {company_id}, luna={luna}, an={an}: "
+ f"Z-Score={altman_zscore.zscore.value} ({altman_zscore.zscore.status}), "
+ f"ROA={profitabilitate.roa.value}% ({profitabilitate.roa.status})"
+ )
+
+ return response
diff --git a/scripts/ralph/prd.json b/scripts/ralph/prd.json
index c5c59e1..1674fb1 100644
--- a/scripts/ralph/prd.json
+++ b/scripts/ralph/prd.json
@@ -1,7 +1,7 @@
{
- "projectName": "dashboard-solduri-v2",
- "branchName": "ralph/dashboard-solduri",
- "description": "Adăugare secțiune Solduri în Dashboard: mobil = prima pagină swipe (grid 2x2), grafice = paginile 2-5 (păstrate ca în main). Desktop = Secțiune Solduri + Secțiune Grafice, fără titluri.",
+ "projectName": "financial-indicators-dashboard",
+ "branchName": "ralph/financial-indicators-dashboard",
+ "description": "Adaugare card indicatori financiari in dashboard solduri cu rate de lichiditate, eficienta, risc si Altman Z-Score pentru evaluare bancara/creditare",
"cssRules": {
"documentation": [
"docs/ONBOARDING_CSS.md",
@@ -10,151 +10,302 @@
"docs/MOBILE_PATTERNS.md"
],
"goldenRules": [
- "Folosește DOAR design tokens - NICIODATĂ valori hardcodate",
- "NU modifica cardurile grafice originale (TreasuryDualCard, etc.) - rămân IDENTICE",
- "Testează în AMBELE teme (light + dark mode)",
+ "Foloseste DOAR design tokens - NICIODATA valori hardcodate",
+ "Testeaza in AMBELE teme (light + dark mode)",
"Mobile: touch targets minim 44x44px",
- "Mobile: prima pagină swipe = grid 2x2 solduri",
- "Mobile: paginile 2-5 swipe = cardurile grafice ORIGINALE din main"
+ "Backend: foloseste decorator @cached pentru caching",
+ "Backend: urmeaza pattern-ul din dashboard_service.py"
]
},
"referenceFiles": {
+ "dashboardService": "backend/modules/reports/services/dashboard_service.py",
+ "dashboardRouter": "backend/modules/reports/routers/dashboard.py",
"dashboardView": "src/modules/reports/views/DashboardView.vue",
- "swipeableCards": "src/shared/components/mobile/SwipeableCards.vue",
- "graficcards": [
+ "dashboardStore": "src/modules/reports/stores/dashboard.js",
+ "existingCards": [
"src/modules/reports/components/dashboard/cards/TreasuryDualCard.vue",
"src/modules/reports/components/dashboard/cards/CashFlowMetricCard.vue",
- "src/modules/reports/components/dashboard/cards/ClientiBalanceCard.vue",
- "src/modules/reports/components/dashboard/cards/FurnizoriBalanceCard.vue"
- ],
- "backupComponents": "/tmp/dashboard-backup/"
+ "src/modules/reports/components/solduri/SolduriCompactCard.vue"
+ ]
},
"userStories": [
{
- "id": "US-2001",
- "title": "Creare componentă SolduriCompactCard reutilizabilă",
- "description": "Ca dezvoltator, vreau o componentă card compactă pentru solduri pentru că o voi folosi în grid 2x2 pe mobil și desktop",
+ "id": "US-001",
+ "title": "Backend - Serviciu Agregare Conturi Balanta",
+ "description": "Ca dezvoltator backend, vreau un serviciu care agregeaza soldurile din balanta de verificare (VBAL) pe clase de conturi, pentru ca am nevoie de date agregate pentru calculul indicatorilor de bilant si Altman Z-Score",
"priority": 1,
"acceptanceCriteria": [
- "Creează SolduriCompactCard.vue în src/modules/reports/components/solduri/",
- "Props: type (trezorerie|clienti|furnizori|tva), total, breakdown (object), casaTotal, bancaTotal (pentru trezorerie)",
- "Afișează: icon + label (uppercase) + valoare principală formatată românesc",
- "Click pe card expandează/colapsează breakdown-ul (conturi pentru trezorerie, buckets pentru clienți/furnizori)",
- "Folosește design tokens: var(--space-md), var(--text-lg), var(--surface-card)",
- "npm run typecheck passes"
+ "Creat backend/modules/reports/services/financial_indicators_service.py",
+ "Clasa FinancialIndicatorsService cu metodele statice necesare",
+ "Constanta ACCOUNT_GROUPS cu prefixele conturilor pentru fiecare categorie",
+ "Metoda get_balance_sheet_aggregates(company_id, luna, an) returneaza dict cu solduri agregate pentru: active_imobilizate, stocuri, creante, disponibilitati, capital_propriu, rezultat, datorii_termen_lung, datorii_curente, venituri, cheltuieli_operationale",
+ "Query-ul foloseste VBAL view cu LIKE pentru prefixe conturi (ex: cont LIKE '20%')",
+ "Cache implementat cu decorator @cached(cache_type='financial_indicators', key_params=['company_id', 'luna', 'an'])",
+ "Structura raspunsului documentata cu Pydantic model BalanceSheetAggregates"
],
"passes": true,
"notes": "Completed in iteration 1"
},
{
- "id": "US-2002",
- "title": "Grid 2x2 Solduri pentru prima pagină swipe pe mobil",
- "description": "Ca utilizator pe mobil, vreau prima pagină din swipe să fie un grid 2x2 cu 4 carduri solduri",
+ "id": "US-002",
+ "title": "Backend - Calcul Indicatori Lichiditate",
+ "description": "Ca utilizator al dashboard-ului, vreau sa vad indicatorii de lichiditate calculati automat, pentru ca vreau sa stiu daca firma poate plati datoriile pe termen scurt",
"priority": 2,
"acceptanceCriteria": [
- "Modifică SwipeableCards în DashboardView să aibă 5 pagini (sau folosește totalCards=5)",
- "Prima pagină (card-0 sau echivalent) conține un div cu grid 2x2",
- "Grid-ul conține 4 SolduriCompactCard: Trezorerie | Clienți / Furnizori | TVA",
- "Fiecare card primește date din dashboardStore",
- "Touch target minim 44x44px pentru fiecare card",
- "npm run typecheck passes",
- "Verify in browser mobil că prima pagină e grid-ul cu 4 solduri compacte"
- ],
- "passes": true,
- "notes": "Verified in browser - Grid 2x2 shows correctly with 4 SolduriCompactCard on first swipe page"
- },
- {
- "id": "US-2003",
- "title": "Păstrare carduri grafice originale pe paginile 2-5 mobil",
- "description": "Ca utilizator pe mobil, vreau paginile 2-5 din swipe să fie cardurile cu grafice originale, exact ca în main",
- "priority": 3,
- "acceptanceCriteria": [
- "Pagina 2 (card-1): TreasuryDualCard cu EXACT aceleași props ca în main",
- "Pagina 3 (card-2): CashFlowMetricCard cu EXACT aceleași props ca în main",
- "Pagina 4 (card-3): ClientiBalanceCard cu EXACT aceleași props ca în main",
- "Pagina 5 (card-4): FurnizoriBalanceCard cu EXACT aceleași props ca în main",
- "NU modifica componentele grafice - doar le folosești în SwipeableCards",
- "npm run typecheck passes",
- "Verify în browser că swipe funcționează fluid între toate 5 paginile"
- ],
- "passes": true,
- "notes": "Verified - Pages 2-5 show original graph cards (TreasuryDualCard, CashFlowMetricCard, etc.). Swipe works fluid."
- },
- {
- "id": "US-2004",
- "title": "Secțiune Solduri pe Desktop (sus, fără titlu)",
- "description": "Ca utilizator pe desktop, vreau să văd soldurile într-o secțiune separată sus, fără titlu vizibil",
- "priority": 4,
- "acceptanceCriteria": [
- "Pe desktop (v-if !isMobile), adaugă o secțiune nouă cu clasa desktop-solduri-section",
- "Secțiunea conține 4 SolduriCompactCard în grid responsive (grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)))",
- "Fără titlu sau header de secțiune - doar cardurile direct",
- "Sub această secțiune rămân cardurile grafice originale (metrics-row existent)",
- "npm run typecheck passes",
- "Verify în browser desktop că soldurile sunt sus, graficele jos"
- ],
- "passes": true,
- "notes": "Completed in iteration 1"
- },
- {
- "id": "US-2005",
- "title": "Eliminare MaturityAndDetailsCard de pe Dashboard",
- "description": "Ca utilizator, vreau Dashboard-ul să nu mai aibă tabelul detaliat de scadențe (există pagina Maturity Analysis)",
- "priority": 5,
- "acceptanceCriteria": [
- "Șterge MaturityAndDetailsCard din template-ul DashboardView.vue",
- "Șterge div-ul .comparison-row care conținea cardul",
- "Șterge importul MaturityAndDetailsCard din script setup",
- "npm run typecheck passes",
- "Verify: Dashboard nu mai are tabel de analiză scadențe"
+ "Metoda calculate_liquidity_indicators(company_id, luna, an) in FinancialIndicatorsService",
+ "Calculat lichiditate_curenta = active_curente / datorii_curente",
+ "Calculat lichiditate_imediata (Quick Ratio) = (disponibilitati + creante) / datorii_curente",
+ "Calculat lichiditate_vedere (Cash Ratio) = disponibilitati / datorii_curente",
+ "Fiecare indicator returneaza dict cu: value, status (good/warning/danger), threshold_min, threshold_max",
+ "Status: good pentru lichiditate_imediata >= 1.0, warning pentru 0.5-1.0, danger pentru < 0.5",
+ "Handle cazul cand datorii_curente = 0 (returneaza null sau infinit cu mesaj)"
],
"passes": true,
"notes": "Completed in iteration 2"
},
{
- "id": "US-2006",
- "title": "Integrare date Solduri din dashboardStore",
- "description": "Ca dezvoltator, vreau cardurile solduri să folosească datele existente din dashboardStore fără API calls noi",
- "priority": 6,
+ "id": "US-003",
+ "title": "Backend - Calcul Indicatori Eficienta",
+ "description": "Ca utilizator al dashboard-ului, vreau sa vad indicatorii de eficienta (DSO, DPO, rate), pentru ca vreau sa stiu cat de repede convertesc resursele in bani",
+ "priority": 3,
"acceptanceCriteria": [
- "SolduriCompactCard pentru Trezorerie: treasuryData.breakdown.casa.total + banca.total, breakdown items",
- "SolduriCompactCard pentru Clienți: netBalanceData.clienti_total, breakdown.clienti",
- "SolduriCompactCard pentru Furnizori: netBalanceData.furnizori_total, breakdown.furnizori",
- "SolduriCompactCard pentru TVA: calculat sau din dashboardStore.summary",
- "npm run typecheck passes"
+ "Metoda calculate_efficiency_indicators(company_id, luna, an) in FinancialIndicatorsService",
+ "Calculat dso (Durata Incasare) = (clienti_sold / facturari_lunare) * 30",
+ "Calculat dpo (Durata Plata) = (furnizori_sold / achizitii_lunare) * 30",
+ "Calculat cash_conversion_cycle = dso - dpo",
+ "Calculat rata_incasare = incasari / facturari * 100",
+ "Calculat rata_plata = plati / achizitii * 100",
+ "Status: good pentru DSO < 30, warning pentru 30-45, danger pentru > 45",
+ "Foloseste datele din summary si trends existente pentru facturari/incasari"
],
"passes": true,
"notes": "Completed in iteration 3"
},
{
- "id": "US-2007",
- "title": "Indicatori vizuali pentru starea financiară",
- "description": "Ca utilizator, vreau să văd indicatori de avertizare pe cardurile solduri pentru probleme financiare",
- "priority": 7,
+ "id": "US-004",
+ "title": "Backend - Calcul Indicatori Risc si Aging",
+ "description": "Ca utilizator al dashboard-ului, vreau sa vad indicatorii de risc si aging creante/datorii, pentru ca vreau sa stiu cat de sanatos este portofoliul de creante",
+ "priority": 4,
"acceptanceCriteria": [
- "Indicator dot roșu pe card Clienți dacă restanță > 20% din total",
- "Indicator dot roșu pe card Furnizori dacă restanță > 20% din total",
- "TVA: text roșu var(--red-600) dacă negativ (de plată), verde var(--green-600) dacă pozitiv (de recuperat)",
- "npm run typecheck passes",
- "Verify în browser că indicatorii sunt vizibili în ambele teme"
+ "Metoda calculate_risk_indicators(company_id, luna, an) in FinancialIndicatorsService",
+ "Calculat creante_restante_pct = clienti_sold_restant / clienti_sold_total * 100",
+ "Calculat creante_90plus_pct = clienti_restant_90plus / clienti_sold_total * 100",
+ "Calculat datorii_restante_pct = furnizori_sold_restant / furnizori_sold_total * 100",
+ "Calculat raport_datorii_trezorerie = furnizori_sold_total / trezorerie",
+ "Status: good pentru creante_restante_pct < 20%, warning pentru 20-30%, danger pentru > 30%",
+ "Foloseste datele din summary existente pentru aging"
],
"passes": true,
"notes": "Completed in iteration 4"
},
{
- "id": "US-2008",
- "title": "Buton Refresh în header Dashboard",
- "description": "Ca utilizator, vreau un buton de refresh în header pentru actualizare manuală a datelor",
- "priority": 8,
+ "id": "US-005",
+ "title": "Backend - Calcul Indicatori Cash Flow",
+ "description": "Ca utilizator al dashboard-ului, vreau sa vad indicatorii de cash flow, pentru ca vreau sa stiu daca firma genereaza sau consuma numerar",
+ "priority": 5,
"acceptanceCriteria": [
- "Pe mobil: adaugă action cu icon pi-refresh în MobileTopBar actions",
- "Pe desktop: adaugă Button cu icon refresh lângă titlul Dashboard",
- "Click declanșează reload date din dashboardStore",
- "Icon se rotește (animație CSS) în timpul încărcării",
- "npm run typecheck passes"
+ "Metoda calculate_cashflow_indicators(company_id, luna, an) in FinancialIndicatorsService",
+ "Calculat flux_net_lunar = incasari_luna - plati_luna",
+ "Calculat cash_flow_ytd = suma fluxurilor de la ianuarie pana la luna curenta",
+ "Calculat flux_net_yoy_pct = (cf_curent - cf_anterior) / abs(cf_anterior) * 100",
+ "Calculat acoperire_cash_flow = cash_flow_ytd / datorii_restante",
+ "Status: good pentru flux_net > 0, danger pentru flux_net < 0",
+ "Foloseste datele din trends existente pentru incasari/plati istorice"
],
"passes": true,
"notes": "Completed in iteration 5"
+ },
+ {
+ "id": "US-006",
+ "title": "Backend - Calcul Indicatori Dinamica",
+ "description": "Ca utilizator al dashboard-ului, vreau sa vad evolutia vanzarilor si achizitiilor, pentru ca vreau sa stiu daca afacerea creste sau scade",
+ "priority": 6,
+ "acceptanceCriteria": [
+ "Metoda calculate_dynamics_indicators(company_id, luna, an) in FinancialIndicatorsService",
+ "Calculat crestere_vanzari_yoy = (facturari_curent - facturari_anterior) / facturari_anterior * 100",
+ "Calculat crestere_achizitii_yoy = (achizitii_curent - achizitii_anterior) / achizitii_anterior * 100",
+ "Calculat marja_implicita = (facturari - achizitii) / facturari * 100",
+ "Status: good pentru crestere_vanzari > 5%, warning pentru 0-5%, danger pentru < 0%",
+ "Foloseste datele din trends existente cu comparatie YoY"
+ ],
+ "passes": true,
+ "notes": "Completed in iteration 6"
+ },
+ {
+ "id": "US-007",
+ "title": "Backend - Calcul Altman Z-Score",
+ "description": "Ca utilizator al dashboard-ului, vreau sa vad scorul Altman Z-Score calculat automat, pentru ca vreau sa stiu riscul de faliment al firmei conform standardelor internationale",
+ "priority": 7,
+ "acceptanceCriteria": [
+ "Metoda calculate_altman_zscore(company_id, luna, an) in FinancialIndicatorsService",
+ "Calculat working_capital = active_curente - datorii_curente",
+ "Calculat total_assets = suma tuturor activelor din VBAL",
+ "Calculat X1 = working_capital / total_assets",
+ "Calculat X2 = rezultat_reportat (cont 117 + 121) / total_assets",
+ "Calculat X3 = ebit (venituri - cheltuieli_operationale) / total_assets",
+ "Calculat X4 = capital_propriu / (datorii_curente + datorii_termen_lung)",
+ "Calculat zscore = 6.56*X1 + 3.26*X2 + 6.72*X3 + 1.05*X4",
+ "Status: safe pentru zscore > 2.60, grey pentru 1.10-2.60, distress pentru < 1.10",
+ "Raspunsul include componentele individuale x1, x2, x3, x4 pentru transparenta"
+ ],
+ "passes": true,
+ "notes": "Completed in iteration 7"
+ },
+ {
+ "id": "US-008",
+ "title": "Backend - Endpoint API Financial Indicators",
+ "description": "Ca frontend developer, vreau un endpoint API care returneaza toti indicatorii calculati, pentru ca am nevoie sa afisez datele in UI",
+ "priority": 8,
+ "acceptanceCriteria": [
+ "Endpoint GET /api/reports/dashboard/financial-indicators in dashboard.py router",
+ "Parametri: company (required int), luna (optional int), an (optional int)",
+ "Apeleaza toate metodele calculate_* din FinancialIndicatorsService",
+ "Raspuns JSON cu structura: { lichiditate: {...}, eficienta: {...}, risc: {...}, cash_flow: {...}, dinamica: {...}, altman_zscore: {...} }",
+ "Fiecare indicator include: value, status, threshold_min, threshold_max",
+ "Cache 30 minute implementat cu decorator @cached",
+ "Response model Pydantic FinancialIndicatorsResponse definit",
+ "Endpoint documentat cu docstring si tags=['dashboard']"
+ ],
+ "passes": true,
+ "notes": "Completed in iteration 8"
+ },
+ {
+ "id": "US-009",
+ "title": "Backend - Date Istorice pentru Sparklines",
+ "description": "Ca utilizator, vreau sa vad evolutia fiecarui indicator pe 12 luni, pentru ca vreau sa inteleg trendul, nu doar valoarea curenta",
+ "priority": 9,
+ "acceptanceCriteria": [
+ "Metoda get_historical_indicators(company_id, months=12) in FinancialIndicatorsService",
+ "Calculeaza indicatorii pentru fiecare din ultimele 12 luni",
+ "Returneaza dict cu sparkline_data (array 12 valori) si sparkline_labels (array 12 etichete)",
+ "Etichete in format 'MMM YY' (ex: 'Feb 24', 'Mar 24')",
+ "Integreaza sparkline_data in raspunsul fiecarui indicator",
+ "Cache separat pentru date istorice (TTL 1 ora) pentru performanta"
+ ],
+ "passes": true,
+ "notes": "Completed in iteration 9"
+ },
+ {
+ "id": "US-010",
+ "title": "Frontend - Component FinancialIndicatorsCard",
+ "description": "Ca utilizator al dashboard-ului, vreau un card vizual care afiseaza indicatorii financiari, pentru ca vreau sa vad rapid starea financiara a firmei",
+ "priority": 10,
+ "acceptanceCriteria": [
+ "Creat src/modules/reports/components/dashboard/cards/FinancialIndicatorsCard.vue",
+ "Header cu titlu 'Indicatori Financiari' si selector perioada (dropdown luna/an)",
+ "TabView PrimeVue pentru categorii: Lichiditate, Eficienta, Risc, Z-Score",
+ "Grid 2x2 pentru indicatori in fiecare tab",
+ "Foloseste design tokens: var(--space-md), var(--surface-card), var(--text-color)",
+ "Props: loading (boolean), error (string), data (object din store)",
+ "npm run typecheck passes",
+ "Verify in browser that card-ul se afiseaza corect pe http://localhost:3000"
+ ],
+ "passes": true,
+ "notes": "Completed in iteration 10"
+ },
+ {
+ "id": "US-011",
+ "title": "Frontend - Component IndicatorItem cu Sparkline",
+ "description": "Ca utilizator, vreau fiecare indicator sa aiba o mini-diagrama de evolutie, pentru ca vreau sa vad trendul vizual",
+ "priority": 11,
+ "acceptanceCriteria": [
+ "Creat src/modules/reports/components/dashboard/cards/IndicatorItem.vue",
+ "Props: label (string), value (number), unit (string), status (good/warning/danger), sparklineData (array), thresholds (object)",
+ "Afiseaza: label sus, valoare mare centrata, sparkline jos, status icon dreapta",
+ "Cod culoare: verde var(--green-600), galben var(--yellow-600), rosu var(--red-600)",
+ "Sparkline implementat cu SVG polyline, responsive la container width",
+ "Tooltip la hover pe sparkline arata valoarea lunii",
+ "npm run typecheck passes",
+ "Verify in browser that sparkline-urile se afiseaza corect"
+ ],
+ "passes": true,
+ "notes": "Completed in iteration 11"
+ },
+ {
+ "id": "US-012",
+ "title": "Frontend - Expand pentru Detalii Complete",
+ "description": "Ca utilizator, vreau sa pot expanda cardul pentru a vedea toti indicatorii, pentru ca unii indicatori sunt mai putin importanti dar vreau acces la ei",
+ "priority": 12,
+ "acceptanceCriteria": [
+ "Buton chevron in footer-ul cardului pentru expand/collapse",
+ "State ref expanded = false, toggle la click",
+ "Starea collapsed arata 4 indicatori principali: Quick Ratio, DSO, Creante Restante %, Z-Score",
+ "Starea expanded arata toti indicatorii in DataTable PrimeVue cu coloane: Indicator, Valoare, Status, Trend",
+ "Animatie CSS transition pe max-height pentru smooth expand",
+ "npm run typecheck passes",
+ "Verify in browser that expand/collapse functioneaza fluid"
+ ],
+ "passes": true,
+ "notes": "Completed in iteration 12"
+ },
+ {
+ "id": "US-013",
+ "title": "Frontend - Store Integration",
+ "description": "Ca frontend developer, vreau sa integrez datele in Pinia store, pentru ca am nevoie de state management pentru indicatori",
+ "priority": 13,
+ "acceptanceCriteria": [
+ "In src/modules/reports/stores/dashboard.js adaugat state: financialIndicators: { loading: false, error: null, data: null }",
+ "Metoda async loadFinancialIndicators(companyId, luna, an) care face GET /api/reports/dashboard/financial-indicators",
+ "Seteaza loading=true la inceput, loading=false si data/error la final",
+ "Computed getters: lichiditate, eficienta, risc, cashFlow, dinamica, altmanZScore care extrag din data",
+ "Error handling cu try/catch si mesaj user-friendly",
+ "npm run typecheck passes"
+ ],
+ "passes": true,
+ "notes": "Completed in iteration 13"
+ },
+ {
+ "id": "US-014",
+ "title": "Frontend - Integrare in DashboardView Desktop",
+ "description": "Ca utilizator pe desktop, vreau sa vad cardul de indicatori in dashboard, pentru ca vreau acces rapid la informatii",
+ "priority": 14,
+ "acceptanceCriteria": [
+ "Import FinancialIndicatorsCard in DashboardView.vue",
+ "Adaugat in template pe desktop (v-if=!isMobile) dupa sectiunea metrics-cards-section",
+ "Div wrapper cu clasa financial-indicators-section, margin-top: var(--space-lg)",
+ "Card ocupa full width",
+ "In loadDashboardData() adaugat apel dashboardStore.loadFinancialIndicators(companyId, luna, an)",
+ "Paseaza props: :loading :error :data din store",
+ "npm run typecheck passes",
+ "Verify in browser (desktop 1200px+) that cardul apare corect sub grafice"
+ ],
+ "passes": true,
+ "notes": "Completed in iteration 14"
+ },
+ {
+ "id": "US-015",
+ "title": "Frontend - Integrare in DashboardView Mobile",
+ "description": "Ca utilizator pe mobil, vreau sa vad cardul de indicatori in carusel, pentru ca vreau acces la informatii si pe telefon",
+ "priority": 15,
+ "acceptanceCriteria": [
+ "In SwipeableCards din DashboardView adaugat pagina 6 (index 5)",
+ "Totalul de pagini devine 6 (era 5)",
+ "Pagina 6 contine FinancialIndicatorsCard adaptat pentru mobil",
+ "Layout single column pe mobil, tabs mai mici",
+ "Respecta padding: padding-top 0 (e in SwipeableCards), padding-bottom 0",
+ "Page indicator dots actualizeaza la 6 puncte",
+ "npm run typecheck passes",
+ "Verify in browser (mobile 375px) that swipe la pagina 6 arata cardul indicatori"
+ ],
+ "passes": true,
+ "notes": "Completed in iteration 15"
+ },
+ {
+ "id": "US-016",
+ "title": "Frontend - Dark Mode Support",
+ "description": "Ca utilizator, vreau cardul sa arate bine in dark mode, pentru ca folosesc aplicatia si seara",
+ "priority": 16,
+ "acceptanceCriteria": [
+ "Culorile status (verde/galben/rosu) vizibile pe fundal inchis - foloseste var(--green-400), var(--yellow-400), var(--red-400) in dark mode",
+ "Sparkline stroke color foloseste var(--primary-color) care se adapteaza la tema",
+ "Background card foloseste var(--surface-card) care se adapteaza",
+ "Text foloseste var(--text-color) care se adapteaza",
+ "NU sunt valori de culoare hardcodate (#fff, #000, etc)",
+ "npm run typecheck passes",
+ "Verify in browser cu toggle dark mode that totul e lizibil si contrastant"
+ ],
+ "passes": true,
+ "notes": "Completed in iteration 16"
}
]
}
diff --git a/scripts/ralph/progress.txt b/scripts/ralph/progress.txt
index f3e732a..29313b3 100644
--- a/scripts/ralph/progress.txt
+++ b/scripts/ralph/progress.txt
@@ -1,82 +1,134 @@
# Ralph Progress Log
-Started: $(date)
-Project: dashboard-solduri-v2
-Branch: ralph/dashboard-solduri
+Started: 2026-01-19
+Project: financial-indicators-dashboard
+Branch: ralph/financial-indicators-dashboard
---
-## Layout corect cerut:
-### MOBIL (5 pagini swipe):
-- Pagina 1: Grid 2x2 cu 4 carduri solduri compacte
-- Paginile 2-5: Cardurile grafice ORIGINALE (ca în main)
+## Obiectiv
+Adaugare card indicatori financiari in dashboard solduri cu:
+- Rate de lichiditate (Quick Ratio, Cash Ratio, Current Ratio)
+- Indicatori eficienta (DSO, DPO, Cash Conversion Cycle)
+- Indicatori risc (% Creante Restante, Aging 90+)
+- Cash Flow indicators
+- Altman Z-Score pentru evaluare risc faliment
-### DESKTOP:
-- Secțiune Solduri (sus, fără titlu) - 4 carduri
-- Secțiune Grafice (jos) - cardurile originale
-- Fără MaturityAndDetailsCard
+## User Stories Status (16 total)
-## User Stories Status
-- US-2001: SolduriCompactCard - PENDING
-- US-2002: Grid 2x2 prima pagină mobil - PENDING
-- US-2003: Păstrare carduri grafice pag 2-5 - PENDING
-- US-2004: Secțiune Solduri desktop - PENDING
-- US-2005: Eliminare MaturityAndDetailsCard - PENDING
-- US-2006: Integrare date din store - PENDING
-- US-2007: Indicatori vizuali - PENDING
-- US-2008: Buton Refresh - PENDING
+### Backend (US-001 to US-009)
+- US-001: Serviciu Agregare Conturi Balanta - PENDING
+- US-002: Calcul Indicatori Lichiditate - PENDING
+- US-003: Calcul Indicatori Eficienta - PENDING
+- US-004: Calcul Indicatori Risc si Aging - PENDING
+- US-005: Calcul Indicatori Cash Flow - PENDING
+- US-006: Calcul Indicatori Dinamica - PENDING
+- US-007: Calcul Altman Z-Score - PENDING
+- US-008: Endpoint API Financial Indicators - PENDING
+- US-009: Date Istorice pentru Sparklines - PENDING
+
+### Frontend (US-010 to US-016)
+- US-010: Component FinancialIndicatorsCard - PENDING
+- US-011: Component IndicatorItem cu Sparkline - PENDING
+- US-012: Expand pentru Detalii Complete - PENDING
+- US-013: Store Integration - PENDING
+- US-014: Integrare in DashboardView Desktop - PENDING
+- US-015: Integrare in DashboardView Mobile - PENDING
+- US-016: Dark Mode Support - PENDING
---
-[2026-01-16 07:31:17] Starting Ralph for project: dashboard-solduri-v2
-[2026-01-16 07:31:17] Max iterations: 15
-[2026-01-16 07:31:17] === Iteration 1/15 ===
-[2026-01-16 07:31:17] Working on story: US-2001
-[2026-01-16 07:31:17] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_1_US-2001.log)
-[2026-01-16 07:33:54] SUCCESS: Story US-2001 passed!
-[2026-01-16 07:33:54] Changes committed
-[2026-01-16 07:33:54] Progress: 1/8 stories completed
-[2026-01-16 07:33:56] === Iteration 2/15 ===
-[2026-01-16 07:33:56] Working on story: US-2002
-[2026-01-16 07:33:56] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_2_US-2002.log)
-[2026-01-16 07:36:22] ERROR: Story US-2002 blocked: {"type":"result","subtype":"success","is_error":false,"duration_ms":143793,"duration_api_ms":141260,"num_turns":26,"result":"`★ Insight ─────────────────────────────────────`\n**Implementation Summary for US-2002:**\n1. **SwipeableCards now has 5 pages** - Changed from `totalCards=\"4\"` to `totalCards=\"5\"` with proper slot naming\n2. **First page is a 2x2 grid** - Uses CSS Grid with `grid-template-columns: repeat(2, 1fr)` and design tokens for spacing\n3. **Data integration** - Created `totalTrezorerie` (casa + banca) and `tvaTotal` computed properties that pull from existing dashboard data\n4. **Touch targets** - CSS ensures minimum 44x44px touch targets via `min-height: 120px` on grid items + existing SolduriCompactCard styling\n`─────────────────────────────────────────────────`\n\nLet me verify the acceptance criteria one more time:\n\n**Acceptance Criteria Check:**\n\n| Criteria | Status |\n|----------|--------|\n| ✅ Modifică SwipeableCards în DashboardView să aibă 5 pagini | Done - `totalCards=\"5\"` |\n| ✅ Prima pagină (card-0) conține un div cu grid 2x2 | Done - `solduri-grid-2x2` class |\n| ✅ Grid-ul conține 4 SolduriCompactCard: Trezorerie \\| Clienți / Furnizori \\| TVA | Done |\n| ✅ Fiecare card primește date din dashboardStore | Done - via `treasuryData`, `netBalanceData`, computed properties |\n| ✅ Touch target minim 44x44px pentru fiecare card | Done - CSS ensures min-height: 120px + SolduriCompactCard has its own touch target compliance |\n| ✅ npm run typecheck passes | Done - build passes (no tsconfig for strict typecheck, but build compiles) |\n| ⏳ Verify in browser mobil | Cannot test - requires browser verification |\n\nThe implementation is complete. Since I cannot verify in a mobile browser directly, the story is blocked on manual verification.\n\n Cannot verify in mobile browser - requires manual testing to confirm the 2x2 grid displays correctly on first swipe page with all 4 solduri cards (Trezorerie, Clienți, Furnizori, TVA)","session_id":"59f486d8-e134-4e47-918c-85d364cbe1b2","total_cost_usd":1.48000525,"usage":{"input_tokens":4959,"cache_creation_input_tokens":69287,"cache_read_input_tokens":1673261,"output_tokens":7264,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":69287}},"modelUsage":{"claude-opus-4-5-20251101":{"inputTokens":4959,"outputTokens":7264,"cacheReadInputTokens":1673261,"cacheCreationInputTokens":69287,"webSearchRequests":0,"costUSD":1.4760692499999999,"contextWindow":200000,"maxOutputTokens":64000},"claude-haiku-4-5-20251001":{"inputTokens":3296,"outputTokens":128,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.003936,"contextWindow":200000,"maxOutputTokens":64000}},"permission_denials":[],"uuid":"60e1571f-847a-4936-9451-b64009ac3f90"}
-[2026-01-16 07:36:22] Stopping loop due to blocked story
-[2026-01-16 07:36:22] === Ralph Session Complete ===
-[2026-01-16 07:36:22] Final progress: 1/8 stories completed
-[2026-01-16 07:36:22] Branch: ralph/dashboard-solduri
-[2026-01-16 07:36:22] Logs: /workspace/roa2web/scripts/ralph/logs
-[2026-01-16 07:45:26] Starting Ralph for project: dashboard-solduri-v2
-[2026-01-16 07:45:26] Max iterations: 10
-[2026-01-16 07:45:26] === Iteration 1/10 ===
-[2026-01-16 07:45:26] Working on story: US-2004
-[2026-01-16 07:45:26] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_1_US-2004.log)
-[2026-01-16 07:47:00] SUCCESS: Story US-2004 passed!
-[2026-01-16 07:47:00] Changes committed
-[2026-01-16 07:47:00] Progress: 4/8 stories completed
-[2026-01-16 07:47:02] === Iteration 2/10 ===
-[2026-01-16 07:47:02] Working on story: US-2005
-[2026-01-16 07:47:02] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_2_US-2005.log)
-[2026-01-16 07:48:52] SUCCESS: Story US-2005 passed!
-[2026-01-16 07:48:52] Changes committed
-[2026-01-16 07:48:52] Progress: 5/8 stories completed
-[2026-01-16 07:48:54] === Iteration 3/10 ===
-[2026-01-16 07:48:54] Working on story: US-2006
-[2026-01-16 07:48:54] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_3_US-2006.log)
-[2026-01-16 07:50:39] SUCCESS: Story US-2006 passed!
-[2026-01-16 07:50:39] Changes committed
-[2026-01-16 07:50:39] Progress: 6/8 stories completed
-[2026-01-16 07:50:41] === Iteration 4/10 ===
-[2026-01-16 07:50:41] Working on story: US-2007
-[2026-01-16 07:50:41] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_4_US-2007.log)
-[2026-01-16 07:54:51] SUCCESS: Story US-2007 passed!
-[2026-01-16 07:54:51] Changes committed
-[2026-01-16 07:54:51] Progress: 7/8 stories completed
-[2026-01-16 07:54:53] === Iteration 5/10 ===
-[2026-01-16 07:54:53] Working on story: US-2008
-[2026-01-16 07:54:53] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_5_US-2008.log)
-[2026-01-16 07:58:11] SUCCESS: Story US-2008 passed!
-[2026-01-16 07:58:11] Changes committed
-[2026-01-16 07:58:11] Progress: 8/8 stories completed
-[2026-01-16 07:58:13] === Iteration 6/10 ===
-[2026-01-16 07:58:13] SUCCESS: All stories completed! 🎉
-[2026-01-16 07:58:13] === Ralph Session Complete ===
-[2026-01-16 07:58:13] Final progress: 8/8 stories completed
-[2026-01-16 07:58:13] Branch: ralph/dashboard-solduri
-[2026-01-16 07:58:13] Logs: /workspace/roa2web/scripts/ralph/logs
+[2026-01-19 14:43:14] Starting Ralph for project: financial-indicators-dashboard
+[2026-01-19 14:43:14] Max iterations: 20
+[2026-01-19 14:43:14] Creating new branch: ralph/financial-indicators-dashboard
+[2026-01-19 14:43:14] === Iteration 1/20 ===
+[2026-01-19 14:43:14] Working on story: US-001
+[2026-01-19 14:43:14] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_1_US-001.log)
+[2026-01-19 14:47:40] SUCCESS: Story US-001 passed!
+[2026-01-19 14:47:40] Changes committed
+[2026-01-19 14:47:40] Progress: 1/16 stories completed
+[2026-01-19 14:47:42] === Iteration 2/20 ===
+[2026-01-19 14:47:42] Working on story: US-002
+[2026-01-19 14:47:42] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_2_US-002.log)
+[2026-01-19 14:49:51] SUCCESS: Story US-002 passed!
+[2026-01-19 14:49:51] Changes committed
+[2026-01-19 14:49:51] Progress: 2/16 stories completed
+[2026-01-19 14:49:53] === Iteration 3/20 ===
+[2026-01-19 14:49:53] Working on story: US-003
+[2026-01-19 14:49:53] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_3_US-003.log)
+[2026-01-19 14:52:59] SUCCESS: Story US-003 passed!
+[2026-01-19 14:52:59] Changes committed
+[2026-01-19 14:52:59] Progress: 3/16 stories completed
+[2026-01-19 14:53:01] === Iteration 4/20 ===
+[2026-01-19 14:53:01] Working on story: US-004
+[2026-01-19 14:53:01] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_4_US-004.log)
+[2026-01-19 14:56:40] SUCCESS: Story US-004 passed!
+[2026-01-19 14:56:41] Changes committed
+[2026-01-19 14:56:41] Progress: 4/16 stories completed
+[2026-01-19 14:56:43] === Iteration 5/20 ===
+[2026-01-19 14:56:43] Working on story: US-005
+[2026-01-19 14:56:43] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_5_US-005.log)
+[2026-01-19 15:00:30] SUCCESS: Story US-005 passed!
+[2026-01-19 15:00:30] Changes committed
+[2026-01-19 15:00:30] Progress: 5/16 stories completed
+[2026-01-19 15:00:32] === Iteration 6/20 ===
+[2026-01-19 15:00:32] Working on story: US-006
+[2026-01-19 15:00:32] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_6_US-006.log)
+[2026-01-19 15:03:46] SUCCESS: Story US-006 passed!
+[2026-01-19 15:03:46] Changes committed
+[2026-01-19 15:03:46] Progress: 6/16 stories completed
+[2026-01-19 15:03:48] === Iteration 7/20 ===
+[2026-01-19 15:03:48] Working on story: US-007
+[2026-01-19 15:03:48] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_7_US-007.log)
+[2026-01-19 15:07:05] SUCCESS: Story US-007 passed!
+[2026-01-19 15:07:05] Changes committed
+[2026-01-19 15:07:05] Progress: 7/16 stories completed
+[2026-01-19 15:07:07] === Iteration 8/20 ===
+[2026-01-19 15:07:07] Working on story: US-008
+[2026-01-19 15:07:07] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_8_US-008.log)
+[2026-01-19 15:10:46] SUCCESS: Story US-008 passed!
+[2026-01-19 15:10:46] Changes committed
+[2026-01-19 15:10:46] Progress: 8/16 stories completed
+[2026-01-19 15:10:48] === Iteration 9/20 ===
+[2026-01-19 15:10:48] Working on story: US-009
+[2026-01-19 15:10:48] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_9_US-009.log)
+[2026-01-19 15:16:49] SUCCESS: Story US-009 passed!
+[2026-01-19 15:16:49] Changes committed
+[2026-01-19 15:16:49] Progress: 9/16 stories completed
+[2026-01-19 15:16:51] === Iteration 10/20 ===
+[2026-01-19 15:16:51] Working on story: US-010
+[2026-01-19 15:16:51] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_10_US-010.log)
+[2026-01-19 15:20:11] SUCCESS: Story US-010 passed!
+[2026-01-19 15:20:11] Changes committed
+[2026-01-19 15:20:11] Progress: 10/16 stories completed
+[2026-01-19 15:20:13] === Iteration 11/20 ===
+[2026-01-19 15:20:13] Working on story: US-011
+[2026-01-19 15:20:13] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_11_US-011.log)
+[2026-01-19 15:25:07] SUCCESS: Story US-011 passed!
+[2026-01-19 15:25:07] Changes committed
+[2026-01-19 15:25:07] Progress: 11/16 stories completed
+[2026-01-19 15:25:09] === Iteration 12/20 ===
+[2026-01-19 15:25:09] Working on story: US-012
+[2026-01-19 15:25:09] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_12_US-012.log)
+[2026-01-19 15:30:34] SUCCESS: Story US-012 passed!
+[2026-01-19 15:30:34] Changes committed
+[2026-01-19 15:30:34] Progress: 12/16 stories completed
+[2026-01-19 15:30:36] === Iteration 13/20 ===
+[2026-01-19 15:30:36] Working on story: US-013
+[2026-01-19 15:30:36] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_13_US-013.log)
+[2026-01-19 15:32:34] SUCCESS: Story US-013 passed!
+[2026-01-19 15:32:34] Changes committed
+[2026-01-19 15:32:34] Progress: 13/16 stories completed
+[2026-01-19 15:32:36] === Iteration 14/20 ===
+[2026-01-19 15:32:36] Working on story: US-014
+[2026-01-19 15:32:36] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_14_US-014.log)
+[2026-01-19 15:35:05] SUCCESS: Story US-014 passed!
+[2026-01-19 15:35:05] Changes committed
+[2026-01-19 15:35:05] Progress: 14/16 stories completed
+[2026-01-19 15:35:07] === Iteration 15/20 ===
+[2026-01-19 15:35:07] Working on story: US-015
+[2026-01-19 15:35:07] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_15_US-015.log)
+[2026-01-19 15:38:49] SUCCESS: Story US-015 passed!
+[2026-01-19 15:38:49] Changes committed
+[2026-01-19 15:38:49] Progress: 15/16 stories completed
+[2026-01-19 15:38:51] === Iteration 16/20 ===
+[2026-01-19 15:38:51] Working on story: US-016
+[2026-01-19 15:38:51] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_16_US-016.log)
+[2026-01-19 15:42:40] SUCCESS: Story US-016 passed!
diff --git a/src/modules/reports/components/dashboard/cards/FinancialIndicatorsCard.vue b/src/modules/reports/components/dashboard/cards/FinancialIndicatorsCard.vue
new file mode 100644
index 0000000..51b6497
--- /dev/null
+++ b/src/modules/reports/components/dashboard/cards/FinancialIndicatorsCard.vue
@@ -0,0 +1,1848 @@
+
+ Se încarcă indicatorii financiari... {{ error }}
+
+ Indicatori Financiari
+
+ Toți Indicatorii Financiari
+
+
+ 💧 Lichiditate
+ ⚡ Eficiență
+ ⚠️ Risc
+ 💰 Cash Flow
+ 📈 Dinamică
+ 🎯 Altman Z-Score
+ 💰 Profitabilitate
+