Implement Dashboard consolidation + Performance logging

Features:
- Add unified "Dashboard Complet" sheet (Excel) with all 9 sections
- Add unified "Dashboard Complet" page (PDF) with key metrics
- Fix VALOARE_ANTERIOARA NULL bug (use sumar_executiv_yoy directly)
- Add PerformanceLogger class for timing analysis
- Remove redundant consolidated sheets (keep only Dashboard Complet)

Bug fixes:
- Fix Excel formula error (=== interpreted as formula, changed to >>>)
- Fix args.output → args.output_dir in perf.summary()

Performance analysis:
- Add PERFORMANCE_ANALYSIS.md with detailed breakdown
- SQL queries take 94% of runtime (31 min), Excel/PDF only 1%
- Identified slow queries for optimization

Documentation:
- Update CLAUDE.md with new structure
- Add context handover for query optimization task

🤖 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-11 13:33:02 +02:00
parent a2ad4c7ed2
commit 9e9ddec014
20 changed files with 2400 additions and 959 deletions

471
main.py
View File

@@ -15,6 +15,7 @@ import sys
import argparse
from datetime import datetime
from pathlib import Path
import time
import warnings
warnings.filterwarnings('ignore')
@@ -62,6 +63,72 @@ from report_generator import (
from recommendations import RecommendationsEngine
class PerformanceLogger:
"""Tracks execution time for each operation to identify bottlenecks."""
def __init__(self):
self.timings = []
self.start_time = time.perf_counter()
self.phase_start = None
self.phase_name = None
def start(self, name: str):
"""Start timing a named operation."""
self.phase_name = name
self.phase_start = time.perf_counter()
print(f"⏱️ [{self._timestamp()}] START: {name}")
def stop(self, rows: int = None):
"""Stop timing and record duration."""
if self.phase_start is None:
return
duration = time.perf_counter() - self.phase_start
self.timings.append({
'name': self.phase_name,
'duration': duration,
'rows': rows
})
rows_info = f" ({rows} rows)" if rows else ""
print(f"✅ [{self._timestamp()}] DONE: {self.phase_name} - {duration:.2f}s{rows_info}")
self.phase_start = None
def _timestamp(self):
return datetime.now().strftime("%H:%M:%S")
def summary(self, output_path: str = None):
"""Print summary sorted by duration (slowest first)."""
total = time.perf_counter() - self.start_time
print("\n" + "="*70)
print("📊 PERFORMANCE SUMMARY (sorted by duration, slowest first)")
print("="*70)
sorted_timings = sorted(self.timings, key=lambda x: x['duration'], reverse=True)
lines = []
for t in sorted_timings:
pct = (t['duration'] / total) * 100 if total > 0 else 0
rows_info = f" [{t['rows']} rows]" if t['rows'] else ""
line = f"{t['duration']:8.2f}s ({pct:5.1f}%) - {t['name']}{rows_info}"
print(line)
lines.append(line)
print("-"*70)
print(f"TOTAL: {total:.2f}s ({total/60:.1f} minutes)")
# Save to file
if output_path:
log_file = f"{output_path}/performance_log.txt"
with open(log_file, 'w', encoding='utf-8') as f:
f.write(f"Performance Log - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write("="*70 + "\n\n")
for line in lines:
f.write(line + "\n")
f.write("\n" + "-"*70 + "\n")
f.write(f"TOTAL: {total:.2f}s ({total/60:.1f} minutes)\n")
print(f"\n📝 Log saved to: {log_file}")
class OracleConnection:
"""Context manager for Oracle database connection"""
@@ -142,47 +209,112 @@ def generate_reports(args):
# Connect and execute queries
results = {}
perf = PerformanceLogger() # Initialize performance logger
with OracleConnection() as conn:
print("\n📥 Extragere date din Oracle:\n")
for query_name, query_info in QUERIES.items():
perf.start(f"QUERY: {query_name}")
df = execute_query(conn, query_name, query_info)
results[query_name] = df
perf.stop(rows=len(df) if df is not None and not df.empty else 0)
# Generate Excel Report
print("\n📝 Generare raport Excel...")
excel_gen = ExcelReportGenerator(excel_path)
# Generate recommendations based on all data
print("\n🔍 Generare recomandări automate...")
perf.start("RECOMMENDATIONS: analyze_all")
recommendations_engine = RecommendationsEngine(RECOMMENDATION_THRESHOLDS)
recommendations_df = recommendations_engine.analyze_all(results)
results['recomandari'] = recommendations_df
perf.stop(rows=len(recommendations_df))
print(f"{len(recommendations_df)} recomandări generate")
# Add sheets in logical order (updated per PLAN_INDICATORI_LICHIDITATE_YOY.md)
# =========================================================================
# CONSOLIDARE DATE PENTRU VEDERE DE ANSAMBLU
# =========================================================================
print("\n📊 Consolidare date pentru vedere de ansamblu...")
# --- Consolidare 1: Vedere Executivă (KPIs + YoY) ---
perf.start("CONSOLIDATION: kpi_consolidated")
# Folosim direct sumar_executiv_yoy care are deja toate coloanele necesare:
# INDICATOR, VALOARE_CURENTA, VALOARE_ANTERIOARA, VARIATIE_PROCENT, TREND
if 'sumar_executiv_yoy' in results and not results['sumar_executiv_yoy'].empty:
df_kpi = results['sumar_executiv_yoy'].copy()
# Adaugă coloana UM bazată pe tipul indicatorului
df_kpi['UM'] = df_kpi['INDICATOR'].apply(lambda x:
'%' if '%' in x or 'marja' in x.lower() else
'buc' if 'numar' in x.lower() else 'RON'
)
results['kpi_consolidated'] = df_kpi
else:
# Fallback la sumar_executiv simplu (fără YoY)
results['kpi_consolidated'] = results.get('sumar_executiv', pd.DataFrame())
perf.stop()
# --- Consolidare 2: Indicatori Venituri (Current + YoY) ---
perf.start("CONSOLIDATION: venituri_consolidated")
if 'indicatori_agregati_venituri' in results and 'indicatori_agregati_venituri_yoy' in results:
df_venituri = results['indicatori_agregati_venituri'].copy()
df_venituri_yoy = results['indicatori_agregati_venituri_yoy'].copy()
if not df_venituri.empty and not df_venituri_yoy.empty:
# Merge pe LINIE_BUSINESS
df_venituri_yoy = df_venituri_yoy.rename(columns={
'VANZARI': 'VANZARI_ANTERIOARE',
'MARJA': 'MARJA_ANTERIOARA'
})
df_venituri_combined = pd.merge(
df_venituri,
df_venituri_yoy[['LINIE_BUSINESS', 'VANZARI_ANTERIOARE', 'VARIATIE_PROCENT', 'TREND']],
on='LINIE_BUSINESS',
how='left'
)
df_venituri_combined = df_venituri_combined.rename(columns={'VANZARI': 'VANZARI_CURENTE'})
results['venituri_consolidated'] = df_venituri_combined
else:
results['venituri_consolidated'] = df_venituri
else:
results['venituri_consolidated'] = results.get('indicatori_agregati_venituri', pd.DataFrame())
perf.stop()
# --- Consolidare 3: Clienți și Risc (Portofoliu + Concentrare + YoY) ---
perf.start("CONSOLIDATION: risc_consolidated")
if 'concentrare_risc' in results and 'concentrare_risc_yoy' in results:
df_risc = results['concentrare_risc'].copy()
df_risc_yoy = results['concentrare_risc_yoy'].copy()
if not df_risc.empty and not df_risc_yoy.empty:
# Merge pe INDICATOR
df_risc = df_risc.rename(columns={'PROCENT': 'PROCENT_CURENT'})
df_risc_combined = pd.merge(
df_risc,
df_risc_yoy[['INDICATOR', 'PROCENT_ANTERIOR', 'VARIATIE', 'TREND']],
on='INDICATOR',
how='left'
)
results['risc_consolidated'] = df_risc_combined
else:
results['risc_consolidated'] = df_risc
else:
results['risc_consolidated'] = results.get('concentrare_risc', pd.DataFrame())
perf.stop()
print("✓ Consolidări finalizate")
# Add sheets in logical order - CONSOLIDAT primul, apoi detalii
sheet_order = [
# SUMAR EXECUTIV
'sumar_executiv',
'sumar_executiv_yoy',
'recomandari',
# CONSOLIDAT - Vedere de Ansamblu (înlocuiește sheet-urile individuale)
'vedere_ansamblu', # KPIs + YoY + Recomandări
'indicatori_venituri', # Venituri Current + YoY merged
'clienti_risc', # Portofoliu + Concentrare + YoY
'tablou_financiar', # 5 secțiuni financiare
# INDICATORI AGREGATI (MUTATI SUS - imagine de ansamblu)
'indicatori_agregati_venituri',
'indicatori_agregati_venituri_yoy',
'portofoliu_clienti',
'concentrare_risc',
'concentrare_risc_yoy',
# DETALII - Sheet-uri individuale pentru analiză profundă
'sezonalitate_lunara',
# INDICATORI GENERALI & LICHIDITATE
'indicatori_generali',
'indicatori_lichiditate',
'clasificare_datorii',
'grad_acoperire_datorii',
'proiectie_lichiditate',
# ALERTE
'vanzari_sub_cost',
'clienti_marja_mica',
@@ -452,118 +584,248 @@ def generate_reports(args):
'CANTITATE_TRANSFORMARI_OUT': 'Cantitate iesita din transformari',
'SOLD_NET_CANTITATE': 'Sold net = Total intrari - Total iesiri',
'SOLD_NET_VALOARE': 'Valoare neta a soldului'
},
# =====================================================================
# LEGENDS FOR CONSOLIDATED SHEETS
# =====================================================================
'vedere_ansamblu': {
'INDICATOR': 'Denumirea indicatorului de business',
'VALOARE_CURENTA': 'Valoare în perioada curentă (ultimele 12 luni)',
'UM': 'Unitate de măsură',
'VALOARE_ANTERIOARA': 'Valoare în perioada anterioară (12-24 luni)',
'VARIATIE_PROCENT': 'Variație procentuală YoY',
'TREND': 'CREȘTERE/SCĂDERE/STABIL',
'STATUS': 'OK = bine, ATENȚIE = necesită atenție, ALERTĂ = acțiune urgentă',
'CATEGORIE': 'Domeniu: Marja, Clienți, Stoc, Financiar',
'RECOMANDARE': 'Acțiune sugerată'
},
'indicatori_venituri': {
'LINIE_BUSINESS': 'Producție proprie / Materii prime / Marfă revândută',
'VANZARI_CURENTE': 'Vânzări în ultimele 12 luni',
'PROCENT_VENITURI': 'Contribuția la totalul vânzărilor (%)',
'MARJA': 'Marja brută pe linia de business',
'PROCENT_MARJA': 'Marja procentuală',
'VANZARI_ANTERIOARE': 'Vânzări în perioada anterioară',
'VARIATIE_PROCENT': 'Creștere/scădere procentuală YoY',
'TREND': 'CREȘTERE / SCĂDERE / STABIL'
},
'clienti_risc': {
'CATEGORIE': 'Tipul de categorie clienți',
'VALOARE': 'Numărul de clienți sau valoarea',
'EXPLICATIE': 'Explicația categoriei',
'INDICATOR': 'Top 1/5/10 clienți',
'PROCENT_CURENT': '% vânzări la Top N clienți - an curent',
'PROCENT_ANTERIOR': '% vânzări la Top N clienți - an trecut',
'VARIATIE': 'Schimbarea în puncte procentuale',
'TREND': 'DIVERSIFICARE (bine) / CONCENTRARE (risc) / STABIL',
'STATUS': 'OK / ATENTIE / RISC MARE'
},
'tablou_financiar': {
'INDICATOR': 'Denumirea indicatorului financiar',
'VALOARE': 'Valoarea calculată',
'STATUS': 'OK / ATENȚIE / ALERTĂ',
'RECOMANDARE': 'Acțiune sugerată pentru îmbunătățire',
'INTERPRETARE': 'Ce înseamnă valoarea pentru business'
}
}
# =========================================================================
# GENERARE SHEET-URI CONSOLIDATE EXCEL
# =========================================================================
# --- Sheet 0: DASHBOARD COMPLET (toate secțiunile într-o singură vedere) ---
perf.start("EXCEL: Dashboard Complet sheet (9 sections)")
excel_gen.add_consolidated_sheet(
name='Dashboard Complet',
sheet_title='Dashboard Executiv - Vedere Completă',
sheet_description='Toate indicatorii cheie consolidați într-o singură vedere rapidă',
sections=[
# KPIs și Recomandări
{
'title': 'KPIs cu Comparație YoY',
'df': results.get('kpi_consolidated', pd.DataFrame()),
'description': 'Indicatori cheie de performanță - curent vs anterior'
},
{
'title': 'Recomandări Prioritare',
'df': results.get('recomandari', pd.DataFrame()).head(10),
'description': 'Top 10 acțiuni sugerate bazate pe analiză'
},
# Venituri
{
'title': 'Venituri per Linie Business',
'df': results.get('venituri_consolidated', pd.DataFrame()),
'description': 'Producție proprie, Materii prime, Marfă revândută'
},
# Clienți și Risc
{
'title': 'Portofoliu Clienți',
'df': results.get('portofoliu_clienti', pd.DataFrame()),
'description': 'Structura și segmentarea clienților'
},
{
'title': 'Concentrare Risc YoY',
'df': results.get('risc_consolidated', pd.DataFrame()),
'description': 'Dependența de clienții mari - curent vs anterior'
},
# Tablou Financiar
{
'title': 'Indicatori Generali',
'df': results.get('indicatori_generali', pd.DataFrame()),
'description': 'Sold clienți, furnizori, cifra afaceri'
},
{
'title': 'Indicatori Lichiditate',
'df': results.get('indicatori_lichiditate', pd.DataFrame()),
'description': 'Zile rotație stoc, creanțe, datorii'
},
{
'title': 'Clasificare Datorii',
'df': results.get('clasificare_datorii', pd.DataFrame()),
'description': 'Datorii pe intervale de întârziere'
},
{
'title': 'Proiecție Lichiditate',
'df': results.get('proiectie_lichiditate', pd.DataFrame()),
'description': 'Previziune încasări și plăți pe 30 zile'
}
]
)
perf.stop()
# NOTE: Sheet-urile individuale (Vedere Ansamblu, Indicatori Venituri, Clienti si Risc,
# Tablou Financiar) au fost eliminate - toate datele sunt acum în Dashboard Complet
# --- Adaugă restul sheet-urilor de detaliu ---
# Skip sheet-urile care sunt acum în view-urile consolidate
consolidated_sheets = {
'vedere_ansamblu', 'indicatori_venituri', 'clienti_risc', 'tablou_financiar',
# Sheet-uri incluse în consolidări (nu mai sunt separate):
'sumar_executiv', 'sumar_executiv_yoy', 'recomandari',
'indicatori_agregati_venituri', 'indicatori_agregati_venituri_yoy',
'portofoliu_clienti', 'concentrare_risc', 'concentrare_risc_yoy',
'indicatori_generali', 'indicatori_lichiditate', 'clasificare_datorii',
'grad_acoperire_datorii', 'proiectie_lichiditate'
}
for query_name in sheet_order:
if query_name in results:
# Tratare speciala pentru 'sumar_executiv' - adauga recomandari sub KPIs
if query_name == 'sumar_executiv':
query_info = QUERIES.get(query_name, {})
excel_gen.add_sheet_with_recommendations(
name='Sumar Executiv',
df=results['sumar_executiv'],
recommendations_df=results.get('recomandari'),
title=query_info.get('title', 'Sumar Executiv'),
description=query_info.get('description', ''),
legend=legends.get('sumar_executiv'),
top_n_recommendations=5
)
# Pastreaza sheet-ul complet de recomandari
elif query_name == 'recomandari':
excel_gen.add_sheet(
name='RECOMANDARI',
df=results['recomandari'],
title='Recomandari Automate (Lista Completa)',
description='Toate insight-urile si actiunile sugerate bazate pe analiza datelor',
legend=legends.get('recomandari')
)
elif query_name in QUERIES:
query_info = QUERIES[query_name]
# Create short sheet name from query name
sheet_name = query_name.replace('_', ' ').title()[:31]
excel_gen.add_sheet(
name=sheet_name,
df=results[query_name],
title=query_info.get('title', query_name),
description=query_info.get('description', ''),
legend=legends.get(query_name)
)
# Skip consolidated sheets and their source sheets
if query_name in consolidated_sheets:
continue
if query_name in results and query_name in QUERIES:
query_info = QUERIES[query_name]
# Create short sheet name from query name
sheet_name = query_name.replace('_', ' ').title()[:31]
perf.start(f"EXCEL: {query_name} detail sheet")
excel_gen.add_sheet(
name=sheet_name,
df=results[query_name],
title=query_info.get('title', query_name),
description=query_info.get('description', ''),
legend=legends.get(query_name)
)
df_rows = len(results[query_name]) if results[query_name] is not None else 0
perf.stop(rows=df_rows)
perf.start("EXCEL: Save workbook")
excel_gen.save()
perf.stop()
# Generate PDF Report
# =========================================================================
# GENERARE PDF - PAGINI CONSOLIDATE
# =========================================================================
print("\n📄 Generare raport PDF...")
pdf_gen = PDFReportGenerator(pdf_path, company_name=COMPANY_NAME)
# Title page
# Pagina 1: Titlu
perf.start("PDF: Title page")
pdf_gen.add_title_page()
perf.stop()
# KPIs
pdf_gen.add_kpi_section(results.get('sumar_executiv'))
# Pagina 2-3: DASHBOARD COMPLET (toate secțiunile într-o vedere unificată)
perf.start("PDF: Dashboard Complet page (4 sections)")
pdf_gen.add_consolidated_page(
'Dashboard Complet',
sections=[
{
'title': 'KPIs cu Comparație YoY',
'df': results.get('kpi_consolidated', pd.DataFrame()),
'columns': ['INDICATOR', 'VALOARE_CURENTA', 'UM', 'VALOARE_ANTERIOARA', 'VARIATIE_PROCENT', 'TREND'],
'max_rows': 6
},
{
'title': 'Recomandări Prioritare',
'df': results.get('recomandari', pd.DataFrame()),
'columns': ['STATUS', 'CATEGORIE', 'INDICATOR', 'RECOMANDARE'],
'max_rows': 5
},
{
'title': 'Venituri per Linie Business',
'df': results.get('venituri_consolidated', pd.DataFrame()),
'columns': ['LINIE_BUSINESS', 'VANZARI_CURENTE', 'PROCENT_VENITURI', 'VARIATIE_PROCENT', 'TREND'],
'max_rows': 5
},
{
'title': 'Concentrare Risc YoY',
'df': results.get('risc_consolidated', pd.DataFrame()),
'columns': ['INDICATOR', 'PROCENT_CURENT', 'PROCENT_ANTERIOR', 'TREND'],
'max_rows': 4
}
]
)
perf.stop()
# NEW: Indicatori Generali section
if 'indicatori_generali' in results and not results['indicatori_generali'].empty:
pdf_gen.add_table_section(
"Indicatori Generali de Business",
results.get('indicatori_generali'),
columns=['INDICATOR', 'VALOARE', 'STATUS', 'RECOMANDARE'],
max_rows=10
)
# NOTE: Paginile individuale (Vedere Executivă, Indicatori Venituri, Clienți și Risc,
# Tablou Financiar) au fost eliminate - toate datele sunt acum în Dashboard Complet
# NEW: Indicatori Lichiditate section
if 'indicatori_lichiditate' in results and not results['indicatori_lichiditate'].empty:
pdf_gen.add_table_section(
"Indicatori de Lichiditate",
results.get('indicatori_lichiditate'),
columns=['INDICATOR', 'VALOARE', 'STATUS', 'RECOMANDARE'],
max_rows=10
)
pdf_gen.add_page_break()
# NEW: Proiecție Lichiditate
if 'proiectie_lichiditate' in results and not results['proiectie_lichiditate'].empty:
pdf_gen.add_table_section(
"Proiecție Cash Flow 30/60/90 zile",
results.get('proiectie_lichiditate'),
columns=['PERIOADA', 'SOLD_PROIECTAT', 'INCASARI', 'PLATI', 'STATUS'],
max_rows=5
)
# NEW: Recommendations section (top priorities)
if 'recomandari' in results and not results['recomandari'].empty:
pdf_gen.add_recommendations_section(results['recomandari'])
# Alerts
# Alerte (vânzări sub cost, clienți marjă mică)
perf.start("PDF: Alerts section")
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())
})
perf.stop()
pdf_gen.add_page_break()
# Monthly chart
# =========================================================================
# PAGINI DE GRAFICE ȘI DETALII
# =========================================================================
# Grafic: Evoluția Vânzărilor Lunare
if 'vanzari_lunare' in results and not results['vanzari_lunare'].empty:
perf.start("PDF: Chart - vanzari_lunare")
fig = create_monthly_chart(results['vanzari_lunare'])
pdf_gen.add_chart_image(fig, "Evoluția Vânzărilor și Marjei")
perf.stop()
# Client concentration
# Grafic: Concentrare Clienți
if 'concentrare_clienti' in results and not results['concentrare_clienti'].empty:
perf.start("PDF: Chart - 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()
# NEW: Cash Conversion Cycle chart
# Grafic: Ciclu Conversie Cash
if 'ciclu_conversie_cash' in results and not results['ciclu_conversie_cash'].empty:
perf.start("PDF: Chart - 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()
# Production vs Resale
# Grafic: Producție vs Revânzare
if 'productie_vs_revanzare' in results and not results['productie_vs_revanzare'].empty:
perf.start("PDF: Chart - productie_vs_revanzare")
fig = create_production_chart(results['productie_vs_revanzare'])
pdf_gen.add_chart_image(fig, "Producție Proprie vs Revânzare")
perf.stop()
# Top clients table
# Tabel: Top clienți
pdf_gen.add_table_section(
"Top 15 Clienți după Vânzări",
results.get('marja_per_client'),
@@ -573,7 +835,7 @@ def generate_reports(args):
pdf_gen.add_page_break()
# Top products
# Tabel: Top produse
pdf_gen.add_table_section(
"Top 15 Produse după Vânzări",
results.get('top_produse'),
@@ -581,7 +843,7 @@ def generate_reports(args):
max_rows=15
)
# Trending clients
# Tabel: Trending clienți
pdf_gen.add_table_section(
"Trending Clienți (YoY)",
results.get('trending_clienti'),
@@ -589,7 +851,7 @@ def generate_reports(args):
max_rows=15
)
# NEW: Aging Creanțe table
# Tabel: Aging Creanțe
if 'aging_creante' in results and not results['aging_creante'].empty:
pdf_gen.add_page_break()
pdf_gen.add_table_section(
@@ -599,7 +861,7 @@ def generate_reports(args):
max_rows=15
)
# Stoc lent
# Tabel: Stoc lent
if 'stoc_lent' in results and not results['stoc_lent'].empty:
pdf_gen.add_page_break()
pdf_gen.add_table_section(
@@ -609,8 +871,13 @@ def generate_reports(args):
max_rows=20
)
perf.start("PDF: Save document")
pdf_gen.save()
perf.stop()
# Performance Summary
perf.summary(output_path=str(args.output_dir))
# Summary
print("\n" + "="*60)
print(" ✅ RAPOARTE GENERATE CU SUCCES!")
@@ -618,7 +885,7 @@ def generate_reports(args):
print(f"\n 📊 Excel: {excel_path}")
print(f" 📄 PDF: {pdf_path}")
print("\n" + "="*60)
return excel_path, pdf_path