Initial commit: Data Intelligence Report Generator
- Oracle ERP ROA integration with sales analytics and margin analysis - Excel multi-sheet reports with conditional formatting - PDF executive summaries with charts via ReportLab - Optimized SQL queries (no cartesian products) - Docker support for cross-platform deployment - Configurable alert thresholds for business intelligence 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
581
recommendations.py
Normal file
581
recommendations.py
Normal file
@@ -0,0 +1,581 @@
|
||||
"""
|
||||
Recommendations Engine Module
|
||||
Generates automatic business recommendations based on data analysis
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Optional
|
||||
import pandas as pd
|
||||
|
||||
|
||||
@dataclass
|
||||
class Recommendation:
|
||||
"""Single business recommendation with context"""
|
||||
categorie: str # Marja | Clienti | Stoc | Financiar
|
||||
indicator: str # Indicator name
|
||||
valoare: str # Current value
|
||||
status: str # OK | ATENTIE | ALERTA
|
||||
status_icon: str # OK | ATENTIE | ALERTA (for Excel without emojis)
|
||||
explicatie: str # Explanation for entrepreneur
|
||||
recomandare: str # Action suggestion
|
||||
vezi_detalii: str # Reference to detail sheet
|
||||
|
||||
|
||||
class RecommendationsEngine:
|
||||
"""Engine that analyzes data and generates recommendations"""
|
||||
|
||||
def __init__(self, thresholds: dict):
|
||||
"""Initialize with threshold configuration"""
|
||||
self.thresholds = thresholds
|
||||
self.recommendations: List[Recommendation] = []
|
||||
|
||||
def analyze_all(self, results: Dict[str, pd.DataFrame]) -> pd.DataFrame:
|
||||
"""Run all analyses and return recommendations as DataFrame"""
|
||||
self.recommendations = []
|
||||
|
||||
# Run all analysis modules
|
||||
self._analyze_margin(results)
|
||||
self._analyze_clients(results)
|
||||
self._analyze_stock(results)
|
||||
self._analyze_financial(results)
|
||||
self._analyze_general(results)
|
||||
self._analyze_liquidity(results)
|
||||
|
||||
# Convert to DataFrame
|
||||
if not self.recommendations:
|
||||
return pd.DataFrame(columns=[
|
||||
'CATEGORIE', 'INDICATOR', 'VALOARE', 'STATUS',
|
||||
'EXPLICATIE', 'RECOMANDARE', 'VEZI_DETALII'
|
||||
])
|
||||
|
||||
return pd.DataFrame([
|
||||
{
|
||||
'CATEGORIE': r.categorie,
|
||||
'INDICATOR': r.indicator,
|
||||
'VALOARE': r.valoare,
|
||||
'STATUS': r.status_icon,
|
||||
'EXPLICATIE': r.explicatie,
|
||||
'RECOMANDARE': r.recomandare,
|
||||
'VEZI_DETALII': r.vezi_detalii
|
||||
}
|
||||
for r in self.recommendations
|
||||
])
|
||||
|
||||
def _add_recommendation(self, categorie: str, indicator: str, valoare: str,
|
||||
status: str, explicatie: str, recomandare: str,
|
||||
vezi_detalii: str):
|
||||
"""Add a recommendation with proper status icon"""
|
||||
status_icons = {
|
||||
'OK': 'OK',
|
||||
'ATENTIE': 'ATENTIE',
|
||||
'ALERTA': 'ALERTA'
|
||||
}
|
||||
|
||||
self.recommendations.append(Recommendation(
|
||||
categorie=categorie,
|
||||
indicator=indicator,
|
||||
valoare=valoare,
|
||||
status=status,
|
||||
status_icon=status_icons.get(status, status),
|
||||
explicatie=explicatie,
|
||||
recomandare=recomandare,
|
||||
vezi_detalii=vezi_detalii
|
||||
))
|
||||
|
||||
def _analyze_margin(self, results: Dict[str, pd.DataFrame]):
|
||||
"""Analyze margin indicators"""
|
||||
|
||||
# 1. Check average margin from sumar_executiv
|
||||
sumar = results.get('sumar_executiv')
|
||||
if sumar is not None and not sumar.empty:
|
||||
marja_row = sumar[sumar['INDICATOR'].str.contains('marj', case=False, na=False)]
|
||||
if not marja_row.empty:
|
||||
try:
|
||||
marja_val = float(str(marja_row['VALOARE'].iloc[0]).strip().replace(',', '.'))
|
||||
marja_tinta = self.thresholds.get('marja_tinta', 20)
|
||||
marja_minima = self.thresholds.get('marja_minima', 15)
|
||||
|
||||
if marja_val < marja_minima:
|
||||
self._add_recommendation(
|
||||
categorie='Marja',
|
||||
indicator='Marja medie globala',
|
||||
valoare=f'{marja_val:.1f}%',
|
||||
status='ALERTA',
|
||||
explicatie=f'Marja medie de {marja_val:.1f}% este sub pragul minim de {marja_minima}%',
|
||||
recomandare='Revizuieste urgent preturile sau renegociaza cu furnizorii',
|
||||
vezi_detalii='Sheet: Marja Per Client'
|
||||
)
|
||||
elif marja_val < marja_tinta:
|
||||
self._add_recommendation(
|
||||
categorie='Marja',
|
||||
indicator='Marja medie globala',
|
||||
valoare=f'{marja_val:.1f}%',
|
||||
status='ATENTIE',
|
||||
explicatie=f'Marja medie de {marja_val:.1f}% este sub tinta de {marja_tinta}%',
|
||||
recomandare='Analizeaza clientii cu marja mica si negociaza preturi',
|
||||
vezi_detalii='Sheet: Clienti Marja Mica'
|
||||
)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
# 2. Check sales below cost
|
||||
vanzari_sub_cost = results.get('vanzari_sub_cost')
|
||||
if vanzari_sub_cost is not None and not vanzari_sub_cost.empty:
|
||||
count = len(vanzari_sub_cost)
|
||||
total_pierdere = abs(vanzari_sub_cost['PIERDERE'].sum()) if 'PIERDERE' in vanzari_sub_cost.columns else 0
|
||||
|
||||
self._add_recommendation(
|
||||
categorie='Marja',
|
||||
indicator='Vanzari sub cost',
|
||||
valoare=f'{count} tranzactii',
|
||||
status='ALERTA',
|
||||
explicatie=f'{count} produse vandute sub cost cu pierdere totala de {total_pierdere:,.0f} RON',
|
||||
recomandare='Verifica preturile si opreste vanzarile in pierdere',
|
||||
vezi_detalii='Sheet: Vanzari Sub Cost'
|
||||
)
|
||||
|
||||
# 3. Check clients with low margin
|
||||
clienti_marja_mica = results.get('clienti_marja_mica')
|
||||
if clienti_marja_mica is not None and not clienti_marja_mica.empty:
|
||||
count = len(clienti_marja_mica)
|
||||
total_vanzari = clienti_marja_mica['VANZARI_FARA_TVA'].sum() if 'VANZARI_FARA_TVA' in clienti_marja_mica.columns else 0
|
||||
|
||||
self._add_recommendation(
|
||||
categorie='Marja',
|
||||
indicator='Clienti cu marja mica',
|
||||
valoare=f'{count} clienti',
|
||||
status='ATENTIE',
|
||||
explicatie=f'{count} clienti cu marja sub 15% totalizeaza {total_vanzari:,.0f} RON vanzari',
|
||||
recomandare='Renegociaza preturile sau ajusteaza conditiile comerciale',
|
||||
vezi_detalii='Sheet: Clienti Marja Mica'
|
||||
)
|
||||
|
||||
def _analyze_clients(self, results: Dict[str, pd.DataFrame]):
|
||||
"""Analyze client-related indicators"""
|
||||
|
||||
# 1. Check client concentration risk
|
||||
concentrare_risc = results.get('concentrare_risc')
|
||||
if concentrare_risc is not None and not concentrare_risc.empty:
|
||||
# Top 1 client concentration
|
||||
top1_max = self.thresholds.get('concentrare_top1_max', 25)
|
||||
top5_max = self.thresholds.get('concentrare_top5_max', 60)
|
||||
|
||||
top1_row = concentrare_risc[concentrare_risc['INDICATOR'].str.contains('Top 1', case=False, na=False)]
|
||||
if not top1_row.empty:
|
||||
try:
|
||||
top1_val = float(top1_row['PROCENT'].iloc[0])
|
||||
if top1_val > top1_max:
|
||||
self._add_recommendation(
|
||||
categorie='Clienti',
|
||||
indicator='Concentrare Top 1 client',
|
||||
valoare=f'{top1_val:.1f}%',
|
||||
status='ALERTA',
|
||||
explicatie=f'Un singur client reprezinta {top1_val:.1f}% din vanzari (risc major)',
|
||||
recomandare='Diversifica portofoliul urgent, atrage clienti noi',
|
||||
vezi_detalii='Sheet: Concentrare Risc'
|
||||
)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
top5_row = concentrare_risc[concentrare_risc['INDICATOR'].str.contains('Top 5', case=False, na=False)]
|
||||
if not top5_row.empty:
|
||||
try:
|
||||
top5_val = float(top5_row['PROCENT'].iloc[0])
|
||||
if top5_val > top5_max:
|
||||
self._add_recommendation(
|
||||
categorie='Clienti',
|
||||
indicator='Concentrare Top 5 clienti',
|
||||
valoare=f'{top5_val:.1f}%',
|
||||
status='ATENTIE',
|
||||
explicatie=f'Doar 5 clienti genereaza {top5_val:.1f}% din vanzari',
|
||||
recomandare='Reduce dependenta de clientii mari prin diversificare',
|
||||
vezi_detalii='Sheet: Concentrare Risc'
|
||||
)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
# 2. Check from concentrare_clienti if concentrare_risc not available
|
||||
concentrare_clienti = results.get('concentrare_clienti')
|
||||
if concentrare_clienti is not None and not concentrare_clienti.empty:
|
||||
top5_max = self.thresholds.get('concentrare_top5_max', 60)
|
||||
|
||||
if len(concentrare_clienti) >= 5 and 'PROCENT_CUMULAT' in concentrare_clienti.columns:
|
||||
top5_pct = concentrare_clienti['PROCENT_CUMULAT'].iloc[4] if len(concentrare_clienti) > 4 else 0
|
||||
|
||||
if top5_pct > top5_max and not any(r.indicator == 'Concentrare Top 5 clienti' for r in self.recommendations):
|
||||
self._add_recommendation(
|
||||
categorie='Clienti',
|
||||
indicator='Concentrare Top 5 clienti',
|
||||
valoare=f'{top5_pct:.1f}%',
|
||||
status='ATENTIE',
|
||||
explicatie=f'{top5_pct:.1f}% din vanzari vin de la doar 5 clienti',
|
||||
recomandare='Diversifica portofoliul, atrage clienti noi',
|
||||
vezi_detalii='Sheet: Concentrare Clienti'
|
||||
)
|
||||
|
||||
# 3. Check portfolio health
|
||||
portofoliu = results.get('portofoliu_clienti')
|
||||
if portofoliu is not None and not portofoliu.empty:
|
||||
clienti_pierduti = portofoliu[portofoliu['INDICATOR'].str.contains('pierdut', case=False, na=False)]
|
||||
if not clienti_pierduti.empty:
|
||||
try:
|
||||
pierduti_val = int(clienti_pierduti['VALOARE'].iloc[0])
|
||||
if pierduti_val > 5:
|
||||
self._add_recommendation(
|
||||
categorie='Clienti',
|
||||
indicator='Clienti pierduti',
|
||||
valoare=f'{pierduti_val} clienti',
|
||||
status='ATENTIE',
|
||||
explicatie=f'{pierduti_val} clienti nu au mai cumparat de peste 6 luni',
|
||||
recomandare='Contacteaza clientii inactivi pentru reactivare',
|
||||
vezi_detalii='Sheet: Portofoliu Clienti'
|
||||
)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
# 4. Check trending clients for negative trends
|
||||
trending = results.get('trending_clienti')
|
||||
if trending is not None and not trending.empty and 'TREND' in trending.columns:
|
||||
pierduti = trending[trending['TREND'] == 'PIERDUT']
|
||||
scadere = trending[trending['TREND'] == 'SCADERE']
|
||||
|
||||
if len(pierduti) > 3:
|
||||
self._add_recommendation(
|
||||
categorie='Clienti',
|
||||
indicator='Clienti pierduti YoY',
|
||||
valoare=f'{len(pierduti)} clienti',
|
||||
status='ALERTA',
|
||||
explicatie=f'{len(pierduti)} clienti activi anul trecut nu au mai cumparat',
|
||||
recomandare='Investigheaza motivele pierderii si incearca recuperarea',
|
||||
vezi_detalii='Sheet: Trending Clienti'
|
||||
)
|
||||
|
||||
if len(scadere) > 5:
|
||||
self._add_recommendation(
|
||||
categorie='Clienti',
|
||||
indicator='Clienti in scadere',
|
||||
valoare=f'{len(scadere)} clienti',
|
||||
status='ATENTIE',
|
||||
explicatie=f'{len(scadere)} clienti au scazut achizitiile cu peste 20%',
|
||||
recomandare='Contacteaza clientii pentru a intelege motivele',
|
||||
vezi_detalii='Sheet: Trending Clienti'
|
||||
)
|
||||
|
||||
def _analyze_stock(self, results: Dict[str, pd.DataFrame]):
|
||||
"""Analyze stock-related indicators"""
|
||||
|
||||
# 1. Check slow moving stock
|
||||
stoc_lent = results.get('stoc_lent')
|
||||
if stoc_lent is not None and not stoc_lent.empty:
|
||||
stoc_zile_max = self.thresholds.get('stoc_zile_max', 90)
|
||||
count = len(stoc_lent)
|
||||
total_valoare = stoc_lent['VALOARE'].sum() if 'VALOARE' in stoc_lent.columns else 0
|
||||
|
||||
if count > 10 or total_valoare > 50000:
|
||||
self._add_recommendation(
|
||||
categorie='Stoc',
|
||||
indicator='Stoc lent',
|
||||
valoare=f'{total_valoare:,.0f} RON',
|
||||
status='ATENTIE',
|
||||
explicatie=f'{count} produse fara miscare de peste {stoc_zile_max} zile, valoare {total_valoare:,.0f} RON',
|
||||
recomandare='Lichideaza stocul lent prin promotii sau retururi la furnizori',
|
||||
vezi_detalii='Sheet: Stoc Lent'
|
||||
)
|
||||
|
||||
# 2. Check stock rotation
|
||||
rotatie = results.get('rotatie_stocuri')
|
||||
if rotatie is not None and not rotatie.empty and 'ZILE_STOC' in rotatie.columns:
|
||||
stoc_foarte_lent = rotatie[rotatie['ZILE_STOC'] > 365]
|
||||
if len(stoc_foarte_lent) > 5:
|
||||
total_val = stoc_foarte_lent['VALOARE_STOC'].sum() if 'VALOARE_STOC' in stoc_foarte_lent.columns else 0
|
||||
self._add_recommendation(
|
||||
categorie='Stoc',
|
||||
indicator='Rotatie foarte lenta',
|
||||
valoare=f'{len(stoc_foarte_lent)} produse',
|
||||
status='ALERTA',
|
||||
explicatie=f'{len(stoc_foarte_lent)} produse cu stoc pentru peste 1 an, valoare {total_val:,.0f} RON',
|
||||
recomandare='Revizuieste politica de aprovizionare si lichideaza surplusul',
|
||||
vezi_detalii='Sheet: Rotatie Stocuri'
|
||||
)
|
||||
|
||||
# 3. Check cash conversion cycle
|
||||
ciclu = results.get('ciclu_conversie_cash')
|
||||
if ciclu is not None and not ciclu.empty:
|
||||
dio_row = ciclu[ciclu['INDICATOR'].str.contains('stoc|DIO', case=False, na=False)]
|
||||
if not dio_row.empty:
|
||||
try:
|
||||
dio_val = float(dio_row['ZILE'].iloc[0])
|
||||
if dio_val > 90:
|
||||
self._add_recommendation(
|
||||
categorie='Stoc',
|
||||
indicator='Zile stoc (DIO)',
|
||||
valoare=f'{dio_val:.0f} zile',
|
||||
status='ATENTIE',
|
||||
explicatie=f'Stocul sta in medie {dio_val:.0f} zile inainte de vanzare',
|
||||
recomandare='Optimizeaza aprovizionarea pentru rotatie mai rapida',
|
||||
vezi_detalii='Sheet: Ciclu Conversie Cash'
|
||||
)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
def _analyze_financial(self, results: Dict[str, pd.DataFrame]):
|
||||
"""Analyze financial indicators"""
|
||||
|
||||
dso_target = self.thresholds.get('dso_target', 45)
|
||||
dso_alert = self.thresholds.get('dso_alert', 60)
|
||||
restante_90_procent = self.thresholds.get('restante_90_procent', 10)
|
||||
|
||||
# 1. Check DSO (Days Sales Outstanding)
|
||||
dso_dpo = results.get('dso_dpo')
|
||||
if dso_dpo is not None and not dso_dpo.empty:
|
||||
dso_row = dso_dpo[dso_dpo['INDICATOR'].str.contains('DSO|incasare', case=False, na=False)]
|
||||
if not dso_row.empty:
|
||||
try:
|
||||
dso_val = float(dso_row['ZILE'].iloc[0])
|
||||
|
||||
if dso_val > dso_alert:
|
||||
self._add_recommendation(
|
||||
categorie='Financiar',
|
||||
indicator='DSO (Zile incasare)',
|
||||
valoare=f'{dso_val:.0f} zile',
|
||||
status='ALERTA',
|
||||
explicatie=f'Incasezi in medie in {dso_val:.0f} zile, cu mult peste tinta de {dso_target} zile',
|
||||
recomandare='Implementeaza urmarire stricta a incasarilor si penalitati',
|
||||
vezi_detalii='Sheet: DSO DPO'
|
||||
)
|
||||
elif dso_val > dso_target:
|
||||
self._add_recommendation(
|
||||
categorie='Financiar',
|
||||
indicator='DSO (Zile incasare)',
|
||||
valoare=f'{dso_val:.0f} zile',
|
||||
status='ATENTIE',
|
||||
explicatie=f'Incasezi in medie in {dso_val:.0f} zile, peste tinta de {dso_target} zile',
|
||||
recomandare='Ofera discount pentru plata rapida (ex: 2% la 10 zile)',
|
||||
vezi_detalii='Sheet: DSO DPO'
|
||||
)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
# 2. Check aging receivables
|
||||
aging = results.get('aging_creante')
|
||||
if aging is not None and not aging.empty:
|
||||
if 'PESTE_90_ZILE' in aging.columns and 'TOTAL_SOLD' in aging.columns:
|
||||
total_creante = aging['TOTAL_SOLD'].sum()
|
||||
peste_90 = aging['PESTE_90_ZILE'].sum()
|
||||
|
||||
if total_creante > 0:
|
||||
procent_90 = (peste_90 / total_creante) * 100
|
||||
|
||||
if procent_90 > restante_90_procent:
|
||||
self._add_recommendation(
|
||||
categorie='Financiar',
|
||||
indicator='Creante >90 zile',
|
||||
valoare=f'{peste_90:,.0f} RON ({procent_90:.1f}%)',
|
||||
status='ALERTA',
|
||||
explicatie=f'{procent_90:.1f}% din creante sunt restante peste 90 zile',
|
||||
recomandare='Initiaza proceduri de recuperare pentru restantele vechi',
|
||||
vezi_detalii='Sheet: Aging Creante'
|
||||
)
|
||||
|
||||
# 3. Check overdue invoices
|
||||
facturi_restante = results.get('facturi_restante')
|
||||
if facturi_restante is not None and not facturi_restante.empty:
|
||||
count = len(facturi_restante)
|
||||
total_restant = facturi_restante['SUMA_RESTANTA'].sum() if 'SUMA_RESTANTA' in facturi_restante.columns else 0
|
||||
|
||||
if count > 0:
|
||||
zile_max = facturi_restante['ZILE_INTARZIERE'].max() if 'ZILE_INTARZIERE' in facturi_restante.columns else 0
|
||||
|
||||
status = 'ALERTA' if zile_max > 90 or total_restant > 100000 else 'ATENTIE'
|
||||
self._add_recommendation(
|
||||
categorie='Financiar',
|
||||
indicator='Facturi restante',
|
||||
valoare=f'{total_restant:,.0f} RON',
|
||||
status=status,
|
||||
explicatie=f'{count} facturi restante, cea mai veche de {zile_max:.0f} zile',
|
||||
recomandare='Prioritizeaza incasarea facturilor cele mai vechi',
|
||||
vezi_detalii='Sheet: Facturi Restante'
|
||||
)
|
||||
|
||||
# 4. Check cash conversion cycle
|
||||
ciclu = results.get('ciclu_conversie_cash')
|
||||
if ciclu is not None and not ciclu.empty:
|
||||
ciclu_row = ciclu[ciclu['INDICATOR'].str.contains('ciclu|total|CCC', case=False, na=False)]
|
||||
if not ciclu_row.empty:
|
||||
try:
|
||||
ciclu_val = float(ciclu_row['ZILE'].iloc[0])
|
||||
|
||||
if ciclu_val > 90:
|
||||
self._add_recommendation(
|
||||
categorie='Financiar',
|
||||
indicator='Ciclu conversie cash',
|
||||
valoare=f'{ciclu_val:.0f} zile',
|
||||
status='ALERTA',
|
||||
explicatie=f'Dureaza {ciclu_val:.0f} zile de la plata furnizor pana la incasare',
|
||||
recomandare='Negociaza termene mai lungi cu furnizorii si mai scurte cu clientii',
|
||||
vezi_detalii='Sheet: Ciclu Conversie Cash'
|
||||
)
|
||||
elif ciclu_val > 60:
|
||||
self._add_recommendation(
|
||||
categorie='Financiar',
|
||||
indicator='Ciclu conversie cash',
|
||||
valoare=f'{ciclu_val:.0f} zile',
|
||||
status='ATENTIE',
|
||||
explicatie=f'Ciclul de conversie cash este de {ciclu_val:.0f} zile',
|
||||
recomandare='Optimizeaza stocurile si accelereaza incasarile',
|
||||
vezi_detalii='Sheet: Ciclu Conversie Cash'
|
||||
)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
# 5. Check client balances for large debtors
|
||||
solduri_clienti = results.get('solduri_clienti')
|
||||
if solduri_clienti is not None and not solduri_clienti.empty:
|
||||
total_creante = solduri_clienti['SOLD_CURENT'].sum() if 'SOLD_CURENT' in solduri_clienti.columns else 0
|
||||
|
||||
if total_creante > 0 and len(solduri_clienti) > 0:
|
||||
top_client = solduri_clienti.iloc[0]
|
||||
top_sold = top_client.get('SOLD_CURENT', 0)
|
||||
top_name = top_client.get('CLIENT', 'N/A')
|
||||
|
||||
procent_top = (top_sold / total_creante) * 100 if total_creante > 0 else 0
|
||||
|
||||
if procent_top > 30:
|
||||
self._add_recommendation(
|
||||
categorie='Financiar',
|
||||
indicator='Concentrare creante',
|
||||
valoare=f'{procent_top:.1f}% la un client',
|
||||
status='ATENTIE',
|
||||
explicatie=f'{top_name[:30]} datoreaza {top_sold:,.0f} RON ({procent_top:.1f}% din total)',
|
||||
recomandare='Monitorizeaza indeaproape si seteaza limite de credit',
|
||||
vezi_detalii='Sheet: Solduri Clienti'
|
||||
)
|
||||
|
||||
def _analyze_general(self, results: Dict[str, pd.DataFrame]):
|
||||
"""Analyze general business indicators (grad indatorare, ROA, autonomie financiara)"""
|
||||
|
||||
indicatori_generali = results.get('indicatori_generali')
|
||||
if indicatori_generali is None or indicatori_generali.empty:
|
||||
return
|
||||
|
||||
# Iterate through each indicator
|
||||
for _, row in indicatori_generali.iterrows():
|
||||
try:
|
||||
indicator = row.get('INDICATOR', '')
|
||||
valoare = row.get('VALOARE', 0)
|
||||
status = row.get('STATUS', 'OK')
|
||||
recomandare_db = row.get('RECOMANDARE', '')
|
||||
|
||||
# Skip if no alert needed
|
||||
if status == 'OK':
|
||||
continue
|
||||
|
||||
# Map indicator to category
|
||||
if isinstance(valoare, (int, float)):
|
||||
valoare_str = f'{valoare:.2f}'
|
||||
else:
|
||||
valoare_str = str(valoare)
|
||||
|
||||
self._add_recommendation(
|
||||
categorie='General',
|
||||
indicator=indicator,
|
||||
valoare=valoare_str,
|
||||
status=status,
|
||||
explicatie=row.get('INTERPRETARE', ''),
|
||||
recomandare=recomandare_db,
|
||||
vezi_detalii='Sheet: Indicatori Generali'
|
||||
)
|
||||
except (ValueError, IndexError, KeyError):
|
||||
pass
|
||||
|
||||
def _analyze_liquidity(self, results: Dict[str, pd.DataFrame]):
|
||||
"""Analyze liquidity indicators"""
|
||||
|
||||
# 1. Check basic liquidity indicators
|
||||
indicatori_lichiditate = results.get('indicatori_lichiditate')
|
||||
if indicatori_lichiditate is not None and not indicatori_lichiditate.empty:
|
||||
for _, row in indicatori_lichiditate.iterrows():
|
||||
try:
|
||||
indicator = row.get('INDICATOR', '')
|
||||
valoare = row.get('VALOARE', 0)
|
||||
status = row.get('STATUS', 'OK')
|
||||
recomandare_db = row.get('RECOMANDARE', '')
|
||||
|
||||
if status == 'OK':
|
||||
continue
|
||||
|
||||
if isinstance(valoare, (int, float)):
|
||||
valoare_str = f'{valoare:.2f}'
|
||||
else:
|
||||
valoare_str = str(valoare)
|
||||
|
||||
self._add_recommendation(
|
||||
categorie='Lichiditate',
|
||||
indicator=indicator,
|
||||
valoare=valoare_str,
|
||||
status=status,
|
||||
explicatie=row.get('INTERPRETARE', ''),
|
||||
recomandare=recomandare_db,
|
||||
vezi_detalii='Sheet: Indicatori Lichiditate'
|
||||
)
|
||||
except (ValueError, IndexError, KeyError):
|
||||
pass
|
||||
|
||||
# 2. Check coverage ratio
|
||||
grad_acoperire = results.get('grad_acoperire_datorii')
|
||||
if grad_acoperire is not None and not grad_acoperire.empty:
|
||||
# Look for the coverage ratio row
|
||||
acoperire_row = grad_acoperire[grad_acoperire['INDICATOR'].str.contains('GRAD ACOPERIRE', case=False, na=False)]
|
||||
if not acoperire_row.empty:
|
||||
try:
|
||||
acoperire_val = float(acoperire_row['VALOARE'].iloc[0])
|
||||
acoperire_status = acoperire_row['ACOPERIRE'].iloc[0]
|
||||
|
||||
if acoperire_status == 'DEFICIT':
|
||||
# Look for financing need
|
||||
necesar_row = grad_acoperire[grad_acoperire['INDICATOR'].str.contains('NECESAR FINANTARE', case=False, na=False)]
|
||||
necesar_val = float(necesar_row['VALOARE'].iloc[0]) if not necesar_row.empty else 0
|
||||
|
||||
self._add_recommendation(
|
||||
categorie='Lichiditate',
|
||||
indicator='Grad acoperire datorii 30 zile',
|
||||
valoare=f'{acoperire_val:.2f}',
|
||||
status='ALERTA',
|
||||
explicatie=f'Cash + incasari asteptate nu acopera datoriile scadente. Deficit: {necesar_val:,.0f} RON',
|
||||
recomandare='Urgentati incasarile sau obtineti finantare pe termen scurt',
|
||||
vezi_detalii='Sheet: Grad Acoperire Datorii'
|
||||
)
|
||||
elif acoperire_status == 'ATENTIE':
|
||||
self._add_recommendation(
|
||||
categorie='Lichiditate',
|
||||
indicator='Grad acoperire datorii 30 zile',
|
||||
valoare=f'{acoperire_val:.2f}',
|
||||
status='ATENTIE',
|
||||
explicatie='Acoperirea datoriilor este la limita',
|
||||
recomandare='Mentineti rezerve suplimentare pentru imprevaute',
|
||||
vezi_detalii='Sheet: Grad Acoperire Datorii'
|
||||
)
|
||||
except (ValueError, IndexError, KeyError):
|
||||
pass
|
||||
|
||||
# 3. Check cash projection
|
||||
proiectie = results.get('proiectie_lichiditate')
|
||||
if proiectie is not None and not proiectie.empty:
|
||||
for _, row in proiectie.iterrows():
|
||||
try:
|
||||
perioada = row.get('PERIOADA', '')
|
||||
sold = row.get('SOLD_PROIECTAT', 0)
|
||||
status = row.get('STATUS', 'OK')
|
||||
|
||||
if status == 'ALERTA' and 'Proiectie' in perioada:
|
||||
self._add_recommendation(
|
||||
categorie='Lichiditate',
|
||||
indicator=f'Proiectie cash: {perioada}',
|
||||
valoare=f'{sold:,.0f} RON',
|
||||
status='ALERTA',
|
||||
explicatie=f'Sold cash proiectat negativ la {perioada}',
|
||||
recomandare='Planificati finantare sau accelerati incasarile pentru a evita criza de lichiditate',
|
||||
vezi_detalii='Sheet: Proiectie Lichiditate'
|
||||
)
|
||||
break # Only report first negative projection
|
||||
except (ValueError, IndexError, KeyError):
|
||||
pass
|
||||
Reference in New Issue
Block a user