Add manager-friendly explanations and dynamic generators

main.py:
- Add PDF_EXPLANATIONS dict with Romanian explanations for all report sections
- Add dynamic explanation generators:
  - generate_indicatori_generali_explanation() - financial ratios with values
  - generate_indicatori_lichiditate_explanation() - liquidity ratios
  - generate_ciclu_cash_explanation() - cash conversion cycle
  - generate_solduri_clienti/furnizori_explanation() - balance summaries

report_generator.py:
- Add diacritics removal for PDF compatibility (Helvetica font)
- Add sanitize_for_pdf() helper function
- Add explanation_fill and explanation_border styles for info boxes
- Enhance add_consolidated_sheet() with explanation parameter
- Add merged explanation boxes with light grey background

These changes complement the FORMULA column additions, providing
managers with contextual explanations throughout the reports.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-12 16:20:30 +02:00
parent 38d8f9c6d2
commit bc05d02319
2 changed files with 596 additions and 199 deletions

223
main.py
View File

@@ -63,6 +63,156 @@ from report_generator import (
from recommendations import RecommendationsEngine
# Manager-friendly explanations for PDF and Excel sections (Romanian without diacritics)
PDF_EXPLANATIONS = {
# KPIs and Dashboard
'kpis': "Indicatorii cheie (KPIs) ofera o imagine de ansamblu a performantei. Vanzarile totale arata volumul de business, marja bruta arata profitabilitatea (ideal peste 20%), iar TREND-ul compara cu anul trecut - CRESTERE e pozitiv, SCADERE necesita investigare.",
'recomandari': "Sistemul analizeaza automat datele si genereaza alerte prioritizate. ALERTA (rosu) = problema critica ce necesita actiune imediata; ATENTIE (galben) = risc de monitorizat; OK (verde) = situatie normala.",
# Charts
'evolutie_lunara': "Graficul arata tendinta vanzarilor pe ultimele 12 luni. Urmariti sezonalitatea (luni slabe/puternice) si tendinta generala. Daca volumul creste dar marja scade, exista presiune pe preturi.",
'concentrare_clienti': "Dependenta de clientii mari - regula 80/20: ideal max 60-70% din vanzari de la top 20% clienti. Daca un singur client depaseste 25% din vanzari, exista risc major - diversificati portofoliul.",
'ciclu_cash': "Ciclul de conversie cash (CCC) arata cate zile sunt blocati banii: DIO (zile stoc) + DSO (zile incasare) - DPO (zile plata furnizori). Sub 30 zile = excelent, 30-60 = acceptabil, peste 60 = problema de lichiditate.",
# Alerts and tables
'alerte_critice': "Vanzarile sub cost sunt tranzactii in pierdere - verificati preturile imediat. Clientii cu marja sub 15% consuma resurse fara profit adecvat - renegociati sau reduceti prioritatea.",
'top_clienti_produse': "Clasamentele arata cei mai importanti clienti si produse dupa vanzari si marja. Concentrati-va pe clientii cu marja mare si investigati pe cei cu marja mica (<15%).",
'aging_creante': "Analiza vechimii creantelor arata suma facturilor neincasate pe intervale de timp. Creantele peste 90 de zile reprezinta risc major de neincasare - contactati urgent acesti clienti.",
'stoc_lent': "Stocul lent blocheaza capital si ocupa spatiu. Produsele fara miscare peste 90 zile trebuie lichidate prin promotii sau trecute in pierdere daca sunt expirate/depasate.",
# Excel-specific sections
'venituri': "Vanzarile pe linii de business arata contributia fiecarui segment. Productia proprie are de obicei marja mai mare (25-40%), revanzarea are marja mai mica (10-20%) dar volum mai mare.",
'portofoliu_clienti': "Structura portofoliului de clienti pe segmente. Un portofoliu sanatos are diversificare pe mai multe segmente, fara dependenta excesiva de o singura categorie.",
'risc_concentrare': "Indicatorii de concentrare arata cat de dependenti sunteti de putini clienti. Top 5 clienti ideal sub 50% din vanzari, top 10 ideal sub 70%. Urmariti trendul YoY.",
'indicatori_generali': "Indicatori financiari structurali: grad indatorare (Datorii/Capital - ideal <1), autonomie financiara (Capital/Active - ideal >0.5), rata datoriilor (Datorii/Active - ideal <0.5), marja neta (Profit/Vanzari %), ROA (Profit/Active %), rotatia activelor.",
'indicatori_lichiditate': "Ratii de lichiditate: Lichiditate curenta (Active curente/Datorii curente - ideal >=1.5), Quick Ratio (fara stocuri - ideal >=1.0), Cash Ratio (Cash/Datorii - ideal >=0.2), Fond de rulment (Active curente - Datorii curente).",
'ciclu_conversie_cash': "Ciclul de conversie cash (CCC) = DIO + DSO - DPO. DIO = zile stoc (cat timp sta marfa pe stoc), DSO = zile incasare (cat timp asteptam banii de la clienti), DPO = zile plata (in cat timp platim furnizorii). CCC sub 30 zile = excelent, 30-60 = acceptabil, peste 60 = probleme de lichiditate.",
'solduri_clienti': "Solduri de incasat de la clienti - creante comerciale din cont 4111. Verificati vechimea si urgentati incasarile peste 30 zile pentru a imbunatati cash-flow-ul.",
'solduri_furnizori': "Datorii catre furnizori din cont 401. Prioritizati platile in functie de scadente si relatia comerciala pentru a evita penalizari si pastrarea relatiilor bune.",
'clasificare_datorii': "Datoriile pe intervale de intarziere. Datoriile sub 30 zile sunt normale, 30-60 zile necesita atentie, peste 60 zile afecteaza relatia cu furnizorii.",
'proiectie_lichiditate': "Previziunea de cash pe 30 zile bazata pe scadente. Sold negativ = risc de lipsă numerar - asigurati finantare suplimentara sau urgentati incasarile."
}
# =============================================================================
# DYNAMIC EXPLANATION GENERATORS - Create explanations with actual values
# =============================================================================
def generate_indicatori_generali_explanation(df):
"""Generate dynamic explanation with actual values and formulas for financial indicators."""
if df is None or df.empty:
return "Nu exista date pentru indicatorii financiari generali."
parts = ["INDICATORI FINANCIARI:"]
for _, row in df.iterrows():
ind = row.get('INDICATOR', '')
val = row.get('VALOARE', 0)
status = row.get('STATUS', '')
if val is None:
val = 0
if 'indatorare' in ind.lower():
parts.append(f"Grad indatorare = {val:.2f} (Datorii/Capital propriu) - {status}")
elif 'autonomie' in ind.lower():
parts.append(f"Autonomie financiara = {val:.2f} (Capital/Active) - {status}")
elif 'rata datoriilor' in ind.lower():
parts.append(f"Rata datoriilor = {val:.2f} (Datorii/Active) - {status}")
elif 'marja' in ind.lower():
parts.append(f"Marja neta = {val:.1f}% - {status}")
elif 'roa' in ind.lower():
parts.append(f"ROA = {val:.1f}% (Profit/Active) - {status}")
elif 'rotatia' in ind.lower():
parts.append(f"Rotatia activelor = {val:.2f} - {status}")
return " | ".join(parts) if len(parts) > 1 else parts[0]
def generate_indicatori_lichiditate_explanation(df):
"""Generate dynamic explanation for liquidity ratios with actual values."""
if df is None or df.empty:
return "Nu exista date pentru indicatorii de lichiditate."
parts = ["RATII LICHIDITATE:"]
for _, row in df.iterrows():
ind = row.get('INDICATOR', '')
val = row.get('VALOARE', 0)
status = row.get('STATUS', '')
if val is None:
val = 0
if 'curenta' in ind.lower() and 'lichiditate' in ind.lower():
parts.append(f"Lichiditate curenta = {val:.2f} (Active curente/Datorii curente, ideal >= 1.5) - {status}")
elif 'rapida' in ind.lower() or 'quick' in ind.lower():
parts.append(f"Quick Ratio = {val:.2f} (fara stocuri, ideal >= 1.0) - {status}")
elif 'cash' in ind.lower():
parts.append(f"Cash Ratio = {val:.2f} (Cash/Datorii, ideal >= 0.2) - {status}")
elif 'fond' in ind.lower() or 'rulment' in ind.lower():
parts.append(f"Fond de rulment = {val:,.0f} RON - {status}")
return " | ".join(parts) if len(parts) > 1 else parts[0]
def generate_ciclu_cash_explanation(df):
"""Generate dynamic explanation for cash conversion cycle with actual values."""
if df is None or df.empty:
return "Nu exista date pentru ciclul de conversie cash."
dio = dso = dpo = ccc = 0
for _, row in df.iterrows():
ind = str(row.get('INDICATOR', '')).upper()
zile = row.get('ZILE', 0)
if zile is None:
zile = 0
if 'DIO' in ind and 'DSO' not in ind:
dio = zile
elif 'DSO' in ind and 'DIO' not in ind:
dso = zile
elif 'DPO' in ind:
dpo = zile
elif 'CCC' in ind or 'CICLU' in ind:
ccc = zile
# Calculate CCC if not directly available
if ccc == 0 and (dio > 0 or dso > 0):
ccc = dio + dso - dpo
status = "EXCELENT" if ccc < 30 else "ACCEPTABIL" if ccc < 60 else "PROBLEME LICHIDITATE"
return (f"CICLU CONVERSIE CASH: DIO = {dio:.0f} zile (stoc -> vanzare) + "
f"DSO = {dso:.0f} zile (factura -> incasare) - "
f"DPO = {dpo:.0f} zile (achizitie -> plata) = "
f"CCC = {ccc:.0f} zile. STATUS: {status}")
def generate_solduri_clienti_explanation(df):
"""Generate dynamic explanation for customer balances with actual totals."""
if df is None or df.empty:
return "Nu exista solduri de incasat de la clienti."
total = df['SOLD_CURENT'].sum() if 'SOLD_CURENT' in df.columns else 0
count = len(df)
top1_pct = (df['SOLD_CURENT'].iloc[0] / total * 100) if total > 0 and len(df) > 0 else 0
return (f"CREANTE CLIENTI: Total de incasat = {total:,.0f} RON de la {count} clienti. "
f"Top 1 client = {top1_pct:.1f}% din total. "
f"Verificati vechimea creantelor si urgentati incasarile peste 30 zile.")
def generate_solduri_furnizori_explanation(df):
"""Generate dynamic explanation for supplier balances with actual totals."""
if df is None or df.empty:
return "Nu exista datorii catre furnizori."
total = df['SOLD_CURENT'].sum() if 'SOLD_CURENT' in df.columns else 0
count = len(df)
return (f"DATORII FURNIZORI: Total de platit = {total:,.0f} RON catre {count} furnizori. "
f"Verificati scadentele si prioritizati platile pentru a evita penalizari.")
class PerformanceLogger:
"""Tracks execution time for each operation to identify bottlenecks."""
@@ -131,18 +281,23 @@ class PerformanceLogger:
class OracleConnection:
"""Context manager for Oracle database connection"""
def __init__(self):
self.connection = None
def __enter__(self):
try:
print(f"🔌 Conectare la Oracle: {get_dsn()}...")
# Add TCP keepalive and timeout settings to prevent connection drops
self.connection = oracledb.connect(
user=ORACLE_CONFIG['user'],
password=ORACLE_CONFIG['password'],
dsn=get_dsn()
dsn=get_dsn(),
tcp_connect_timeout=30,
expire_time=5 # Send TCP keepalive every 5 minutes
)
# Set call timeout to 10 minutes for slow queries
self.connection.call_timeout = 600000 # milliseconds
print("✓ Conectat cu succes!")
return self.connection
except oracledb.Error as e:
@@ -634,7 +789,7 @@ def generate_reports(args):
# =========================================================================
# --- Sheet 0: DASHBOARD COMPLET (toate secțiunile într-o singură vedere) ---
perf.start("EXCEL: Dashboard Complet sheet (9 sections)")
perf.start("EXCEL: Dashboard Complet sheet (12 sections)")
excel_gen.add_consolidated_sheet(
name='Dashboard Complet',
sheet_title='Dashboard Executiv - Vedere Completă',
@@ -644,50 +799,80 @@ def generate_reports(args):
{
'title': 'KPIs cu Comparație YoY',
'df': results.get('kpi_consolidated', pd.DataFrame()),
'description': 'Indicatori cheie de performanță - curent vs anterior'
'description': 'Indicatori cheie de performanță - curent vs anterior',
'explanation': PDF_EXPLANATIONS['kpis']
},
{
'title': 'Recomandări Prioritare',
'df': results.get('recomandari', pd.DataFrame()).head(10),
'description': 'Top 10 acțiuni sugerate bazate pe analiză'
'description': 'Top 10 acțiuni sugerate bazate pe analiză',
'explanation': PDF_EXPLANATIONS['recomandari']
},
# Venituri
{
'title': 'Venituri per Linie Business',
'df': results.get('venituri_consolidated', pd.DataFrame()),
'description': 'Producție proprie, Materii prime, Marfă revândută'
'description': 'Producție proprie, Materii prime, Marfă revândută',
'explanation': PDF_EXPLANATIONS['venituri']
},
# Clienți și Risc
{
'title': 'Portofoliu Clienți',
'df': results.get('portofoliu_clienti', pd.DataFrame()),
'description': 'Structura și segmentarea clienților'
'description': 'Structura și segmentarea clienților',
'explanation': PDF_EXPLANATIONS['portofoliu_clienti']
},
{
'title': 'Concentrare Risc YoY',
'df': results.get('risc_consolidated', pd.DataFrame()),
'description': 'Dependența de clienții mari - curent vs anterior'
'description': 'Dependența de clienții mari - curent vs anterior',
'explanation': PDF_EXPLANATIONS['risc_concentrare']
},
# Tablou Financiar
# Tablou Financiar - with DYNAMIC explanations
{
'title': 'Indicatori Generali',
'df': results.get('indicatori_generali', pd.DataFrame()),
'description': 'Sold clienți, furnizori, cifra afaceri'
'description': 'Ratii financiare: indatorare, autonomie, datorii, rentabilitate',
'explanation': generate_indicatori_generali_explanation(results.get('indicatori_generali'))
},
{
'title': 'Indicatori Lichiditate',
'df': results.get('indicatori_lichiditate', pd.DataFrame()),
'description': 'Zile rotație stoc, creanțe, datorii'
'description': 'Capacitatea de plata pe termen scurt',
'explanation': generate_indicatori_lichiditate_explanation(results.get('indicatori_lichiditate'))
},
# NEW: Ciclu Conversie Cash (was missing from Dashboard)
{
'title': 'Ciclu Conversie Cash',
'df': results.get('ciclu_conversie_cash', pd.DataFrame()),
'description': 'DIO (zile stoc) + DSO (zile incasare) - DPO (zile plata) = CCC',
'explanation': generate_ciclu_cash_explanation(results.get('ciclu_conversie_cash'))
},
# NEW: Solduri Clienti (Top 10 - was missing from Dashboard)
{
'title': 'Solduri Clienti (Top 10)',
'df': results.get('solduri_clienti', pd.DataFrame()).head(10) if results.get('solduri_clienti') is not None and not results.get('solduri_clienti', pd.DataFrame()).empty else pd.DataFrame(),
'description': 'Creante de incasat din cont 4111',
'explanation': generate_solduri_clienti_explanation(results.get('solduri_clienti'))
},
# NEW: Solduri Furnizori (Top 10 - was missing from Dashboard)
{
'title': 'Solduri Furnizori (Top 10)',
'df': results.get('solduri_furnizori', pd.DataFrame()).head(10) if results.get('solduri_furnizori') is not None and not results.get('solduri_furnizori', pd.DataFrame()).empty else pd.DataFrame(),
'description': 'Datorii de platit din cont 401',
'explanation': generate_solduri_furnizori_explanation(results.get('solduri_furnizori'))
},
{
'title': 'Clasificare Datorii',
'df': results.get('clasificare_datorii', pd.DataFrame()),
'description': 'Datorii pe intervale de întârziere'
'description': 'Datorii pe intervale de întârziere',
'explanation': PDF_EXPLANATIONS['clasificare_datorii']
},
{
'title': 'Proiecție Lichiditate',
'df': results.get('proiectie_lichiditate', pd.DataFrame()),
'description': 'Previziune încasări și plăți pe 30 zile'
'description': 'Previziune încasări și plăți pe 30 zile',
'explanation': PDF_EXPLANATIONS['proiectie_lichiditate']
}
]
)
@@ -745,6 +930,7 @@ def generate_reports(args):
# Pagina 2-3: DASHBOARD COMPLET (toate secțiunile într-o vedere unificată)
perf.start("PDF: Dashboard Complet page (4 sections)")
pdf_gen.add_explanation(PDF_EXPLANATIONS['kpis'])
pdf_gen.add_consolidated_page(
'Dashboard Complet',
sections=[
@@ -783,6 +969,7 @@ def generate_reports(args):
# Alerte (vânzări sub cost, clienți marjă mică)
perf.start("PDF: Alerts section")
pdf_gen.add_explanation(PDF_EXPLANATIONS['alerte_critice'])
pdf_gen.add_alerts_section({
'vanzari_sub_cost': results.get('vanzari_sub_cost', pd.DataFrame()),
'clienti_marja_mica': results.get('clienti_marja_mica', pd.DataFrame())
@@ -798,6 +985,7 @@ def generate_reports(args):
# Grafic: Evoluția Vânzărilor Lunare
if 'vanzari_lunare' in results and not results['vanzari_lunare'].empty:
perf.start("PDF: Chart - vanzari_lunare")
pdf_gen.add_explanation(PDF_EXPLANATIONS['evolutie_lunara'])
fig = create_monthly_chart(results['vanzari_lunare'])
pdf_gen.add_chart_image(fig, "Evoluția Vânzărilor și Marjei")
perf.stop()
@@ -805,15 +993,17 @@ def generate_reports(args):
# Grafic: Concentrare Clienți
if 'concentrare_clienti' in results and not results['concentrare_clienti'].empty:
perf.start("PDF: Chart - concentrare_clienti")
pdf_gen.add_explanation(PDF_EXPLANATIONS['concentrare_clienti'])
fig = create_client_concentration_chart(results['concentrare_clienti'])
pdf_gen.add_chart_image(fig, "Concentrare Clienți")
perf.stop()
pdf_gen.add_page_break()
# Grafic: Ciclu Conversie Cash
# Grafic: Ciclu Conversie Cash - with DYNAMIC explanation showing actual values
if 'ciclu_conversie_cash' in results and not results['ciclu_conversie_cash'].empty:
perf.start("PDF: Chart - ciclu_conversie_cash")
pdf_gen.add_explanation(generate_ciclu_cash_explanation(results.get('ciclu_conversie_cash')))
fig = create_cash_cycle_chart(results['ciclu_conversie_cash'])
pdf_gen.add_chart_image(fig, "Ciclu Conversie Cash (DIO + DSO - DPO)")
perf.stop()
@@ -826,6 +1016,7 @@ def generate_reports(args):
perf.stop()
# Tabel: Top clienți
pdf_gen.add_explanation(PDF_EXPLANATIONS['top_clienti_produse'])
pdf_gen.add_table_section(
"Top 15 Clienți după Vânzări",
results.get('marja_per_client'),
@@ -854,6 +1045,7 @@ def generate_reports(args):
# Tabel: Aging Creanțe
if 'aging_creante' in results and not results['aging_creante'].empty:
pdf_gen.add_page_break()
pdf_gen.add_explanation(PDF_EXPLANATIONS['aging_creante'])
pdf_gen.add_table_section(
"Aging Creanțe (Vechime Facturi Neîncasate)",
results.get('aging_creante'),
@@ -864,6 +1056,7 @@ def generate_reports(args):
# Tabel: Stoc lent
if 'stoc_lent' in results and not results['stoc_lent'].empty:
pdf_gen.add_page_break()
pdf_gen.add_explanation(PDF_EXPLANATIONS['stoc_lent'])
pdf_gen.add_table_section(
"Stoc Lent (>90 zile fără mișcare)",
results.get('stoc_lent'),