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:
223
main.py
223
main.py
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user