#!/usr/bin/env python3 """ Data Intelligence Report Generator =================================== Generates comprehensive Excel and PDF reports with financial analytics for ERP data stored in Oracle Database. Usage: python main.py [--months 12] [--output-dir ./output] Author: Claude AI for ROMFAST SRL """ import sys import argparse from datetime import datetime from pathlib import Path import time import warnings warnings.filterwarnings('ignore') # Check dependencies def check_dependencies(): """Check if all required packages are installed""" missing = [] packages = { 'oracledb': 'oracledb', 'pandas': 'pandas', 'openpyxl': 'openpyxl', 'matplotlib': 'matplotlib', 'reportlab': 'reportlab', 'dotenv': 'python-dotenv' } for import_name, pip_name in packages.items(): try: __import__(import_name) except ImportError: missing.append(pip_name) if missing: print("❌ Pachete lipsă. Rulează:") print(f" pip install {' '.join(missing)} --break-system-packages") sys.exit(1) check_dependencies() import oracledb import pandas as pd from dateutil.relativedelta import relativedelta from config import ( ORACLE_CONFIG, get_dsn, OUTPUT_DIR, COMPANY_NAME, ANALYSIS_MONTHS, MIN_SALES_FOR_ANALYSIS, LOW_MARGIN_THRESHOLD, RECOMMENDATION_THRESHOLDS ) from queries import QUERIES from report_generator import ( ExcelReportGenerator, PDFReportGenerator, create_monthly_chart, create_client_concentration_chart, create_production_chart, create_cash_cycle_chart ) 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.""" 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""" 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(), 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: print(f"❌ Eroare conexiune Oracle: {e}") raise def __exit__(self, exc_type, exc_val, exc_tb): if self.connection: self.connection.close() print("✓ Conexiune închisă.") def execute_query(connection, query_name: str, query_info: dict) -> pd.DataFrame: """Execute a query and return results as DataFrame""" try: sql = query_info['sql'] params = query_info.get('params', {}) print(f" 📊 Executare: {query_name}...", end=" ") with connection.cursor() as cursor: cursor.execute(sql, params) columns = [col[0] for col in cursor.description] rows = cursor.fetchall() df = pd.DataFrame(rows, columns=columns) print(f"✓ ({len(df)} rânduri)") return df except oracledb.Error as e: print(f"❌ Eroare: {e}") return pd.DataFrame() def generate_reports(args): """Main function to generate all reports""" timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') excel_path = OUTPUT_DIR / f"data_intelligence_report_{timestamp}.xlsx" pdf_path = OUTPUT_DIR / f"data_intelligence_report_{timestamp}.pdf" print("\n" + "="*60) print(" DATA INTELLIGENCE REPORT GENERATOR") print("="*60) print(f" Perioada: Ultimele {args.months} luni") print(f" Output: {OUTPUT_DIR}") print("="*60 + "\n") # Update parameters with command line arguments for query_info in QUERIES.values(): if 'months' in query_info.get('params', {}): query_info['params']['months'] = args.months # Calculate reporting period string end_date = datetime.now() start_date = end_date - relativedelta(months=args.months) period_str = f"Perioada: {start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}" # Add period to descriptions for queries with months parameter for query_name, query_info in QUERIES.items(): if 'months' in query_info.get('params', {}): original_desc = query_info.get('description', '') query_info['description'] = f"{original_desc}\n{period_str}" # 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 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") # ========================================================================= # 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 = [ # 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 # DETALII - Sheet-uri individuale pentru analiză profundă 'sezonalitate_lunara', # ALERTE 'vanzari_sub_cost', 'clienti_marja_mica', # CICLU CASH 'ciclu_conversie_cash', # ANALIZA CLIENTI 'marja_per_client', 'clienti_ranking_profit', 'frecventa_clienti', 'concentrare_clienti', 'trending_clienti', 'marja_client_categorie', # PRODUSE 'top_produse', 'marja_per_categorie', 'marja_per_gestiune', 'articole_negestionabile', 'productie_vs_revanzare', # PRETURI 'dispersie_preturi', 'clienti_sub_medie', 'evolutie_discount', # FINANCIAR 'dso_dpo', 'dso_dpo_yoy', 'solduri_clienti', 'aging_creante', 'facturi_restante', 'solduri_furnizori', 'aging_datorii', 'facturi_restante_furnizori', 'pozitia_cash', # ISTORIC 'vanzari_lunare', # STOC 'stoc_curent', 'stoc_lent', 'rotatie_stocuri', # PRODUCTIE 'analiza_prajitorie', ] # Legends for each sheet explaining column calculations legends = { 'recomandari': { 'CATEGORIE': 'Domeniu: Marja, Clienti, Stoc, Financiar', 'STATUS': 'OK = bine, ATENTIE = necesita atentie, ALERTA = actiune urgenta', 'VEZI_DETALII': 'Sheet-ul cu date detaliate pentru acest indicator' }, 'marja_per_client': { 'VANZARI_FARA_TVA': 'SUM(cantitate × preț) din fact_vfacturi_detalii', 'COST_TOTAL': 'SUM(cantitate × pret_achizitie)', 'MARJA_BRUTA': 'Vânzări - Cost = SUM(cantitate × (preț - pret_achizitie))', 'PROCENT_MARJA': 'Marja Brută / Vânzări × 100' }, 'clienti_marja_mica': { 'VANZARI_FARA_TVA': 'SUM(cantitate × preț) pentru client', 'MARJA_BRUTA': 'Vânzări - Cost', 'PROCENT_MARJA': 'Marja / Vânzări × 100 (sub 15% = alertă)' }, 'vanzari_sub_cost': { 'PRET_VANZARE': 'Preț unitar din factură (fact_vfacturi_detalii.pret)', 'COST': 'Preț achiziție (pret_achizitie)', 'PIERDERE': '(Preț vânzare - Cost) × Cantitate (negativ = pierdere)' }, 'stoc_curent': { 'TIP_GESTIUNE': 'Preț vânzare (nr_pag=7) sau Preț achiziție', 'VALOARE_STOC_ACHIZITIE': '(cants+cant-cante) × pret din vstoc', 'VALOARE_STOC_VANZARE': 'Doar pentru gestiuni preț vânzare, altfel gol' }, 'stoc_lent': { 'CANTITATE': 'Stoc final = cants + cant - cante', 'VALOARE': 'Cantitate × preț achiziție', 'ZILE_FARA_MISCARE': 'Zile de la ultima ieșire (dataout) sau intrare' }, 'rotatie_stocuri': { 'VALOARE_STOC': 'Stoc curent (cants+cant-cante) × preț achiziție', 'VANZARI_12_LUNI': 'Doar vânzări (nu transferuri/consumuri) din ultimele 12 luni', 'ROTATIE': 'Vânzări / Stoc (de câte ori s-a rotit stocul)', 'ZILE_STOC': 'La ritmul actual, în câte zile se epuizează' }, 'dispersie_preturi': { 'NR_TRANZACTII': 'Număr total linii factură pentru acest produs', 'VARIATIE_PROCENT': '(Preț Max - Preț Min) / Preț Mediu × 100', 'NR_LA_PRET_MIN': 'Câte tranzacții au fost la prețul minim', 'CLIENT_PRET_MIN': 'Primul client care a cumpărat la preț minim' }, 'top_produse': { 'VALOARE_VANZARI': 'SUM(cantitate × preț)', 'MARJA_BRUTA': 'SUM(cantitate × (preț - pret_achizitie))', 'PROCENT_MARJA': 'Marja / Vânzări × 100' }, 'marja_per_categorie': { 'VANZARI_FARA_TVA': 'Total vânzări pe subgrupă', 'COST_TOTAL': 'Total cost achiziție pe subgrupă', 'PROCENT_MARJA': 'Marja / Vânzări × 100' }, 'marja_per_gestiune': { 'VANZARI_FARA_TVA': 'Total vânzări pe gestiune (doar articole gestionabile)', 'MARJA_BRUTA': 'Total marjă pe gestiune', 'PROCENT_MARJA': 'Marja / Vânzări × 100' }, 'articole_negestionabile': { 'DENUMIRE': 'Nume articol negestionabil (in_stoc=0)', 'VANZARI_FARA_TVA': 'Total vânzări pentru articole care nu se țin pe stoc', 'MARJA_BRUTA': 'Vânzări - Cost', 'PROCENT_MARJA': 'Marja / Vânzări × 100' }, 'vanzari_lunare': { 'VANZARI_FARA_TVA': 'Total vânzări în lună', 'MARJA_BRUTA': 'Total marjă în lună', 'NR_FACTURI': 'Număr facturi emise', 'NR_CLIENTI': 'Clienți unici activi' }, # NEW legends for financial and aggregated sheets 'indicatori_agregati_venituri': { 'LINIE_BUSINESS': 'Producție proprie / Materii prime / Marfă revândută', 'PROCENT_VENITURI': 'Contribuția la totalul vânzărilor', 'CONTRIBUTIE_PROFIT': 'Contribuția la profitul total (%)' }, 'sezonalitate_lunara': { 'MEDIE_VANZARI': 'Media vânzărilor pe 24 luni pentru această lună', 'DEVIERE_PROCENT': 'Cât de mult deviază de la media globală', 'CLASIFICARE': 'LUNĂ PUTERNICĂ / LUNĂ SLABĂ / NORMAL' }, 'portofoliu_clienti': { 'VALOARE': 'Numărul de clienți în fiecare categorie', 'EXPLICATIE': 'Definiția categoriei de clienți' }, 'concentrare_risc': { 'PROCENT': 'Procentul din vânzări pentru Top N clienți', 'STATUS': 'OK / ATENTIE / RISC MARE' }, 'ciclu_conversie_cash': { 'INDICATOR': 'DIO (zile stoc) + DSO (zile încasare) - DPO (zile plată)', 'ZILE': 'Numărul de zile pentru fiecare component', 'EXPLICATIE': 'Ce reprezintă fiecare indicator' }, 'clienti_ranking_profit': { 'RANG_PROFIT': 'Poziția clientului după profit (nu vânzări)', 'RANG_VANZARI': 'Poziția clientului după vânzări', 'PROFIT_BRUT': 'Vânzări - Cost = profitul efectiv adus' }, 'frecventa_clienti': { 'COMENZI_PE_LUNA': 'Media comenzilor pe lună', 'VALOARE_MEDIE_COMANDA': 'Valoarea medie per comandă', 'EVOLUTIE_FRECVENTA_YOY': 'Schimbarea frecvenței față de anul trecut' }, 'marja_client_categorie': { 'STATUS_MARJA': 'OK / MARJĂ MICĂ (<15%) / PIERDERE (negativă)', 'CATEGORIA': 'Grupa de produse', 'PROCENT_MARJA': 'Marja pentru acest client la această categorie' }, 'evolutie_discount': { 'PRET_INITIAL': 'Prețul mediu în primele 6 luni', 'PRET_ACTUAL': 'Prețul mediu în ultimele 6 luni', 'VARIATIE_PRET_PROCENT': 'Scăderea/creșterea prețului (negativ = discount)' }, 'dso_dpo': { 'DSO': 'Days Sales Outstanding - zile medii încasare clienți', 'DPO': 'Days Payables Outstanding - zile medii plată furnizori', 'STATUS': 'OK / ATENTIE / ALERTA' }, 'solduri_clienti': { 'SOLD_CURENT': 'Suma de încasat de la client (din cont 4111)', 'TIP_SOLD': 'Creanță (ne datorează) sau Avans client (am încasat în avans)' }, 'aging_creante': { 'NEAJUNS_SCADENTA': 'Facturi nescadente încă', 'ZILE_1_30': 'Restanțe 1-30 zile', 'PESTE_90_ZILE': 'Restanțe critice >90 zile - risc de neîncasare' }, 'facturi_restante': { 'ZILE_INTARZIERE': 'Zile de la scadență', 'SUMA_RESTANTA': 'Valoarea rămasă de încasat' }, 'aging_datorii': { 'NEAJUNS_SCADENTA': 'Datorii neajunse la scadență', 'ZILE_1_30': 'Restanțe 1-30 zile', 'ZILE_31_60': 'Restanțe 31-60 zile', 'ZILE_61_90': 'Restanțe 61-90 zile', 'PESTE_90_ZILE': 'Restanțe critice >90 zile', 'TOTAL_SOLD': 'Total datorii către furnizor' }, 'facturi_restante_furnizori': { 'ZILE_INTARZIERE': 'Zile de la scadență', 'SUMA_RESTANTA': 'Valoarea rămasă de plătit' }, 'solduri_furnizori': { 'SOLD_CURENT': 'Suma de plătit furnizorului (din cont 401)', 'TIP_SOLD': 'Datorie (trebuie să plătim) sau Avans (am plătit în avans)' }, 'pozitia_cash': { 'SOLD_CURENT': 'Disponibilul curent în cont/casă', 'DESCRIERE': 'Tipul contului (bancă/casă, lei/valută)' }, # ===================================================================== # NEW: Legends for Indicatori Generali, Lichiditate, YoY sheets # ===================================================================== 'indicatori_generali': { 'INDICATOR': 'Grad îndatorare, autonomie financiară, ROA, marjă netă', 'VALOARE': 'Valoarea calculată a indicatorului', 'STATUS': 'OK / ATENȚIE / ALERTĂ bazat pe praguri standard', 'RECOMANDARE': 'Acțiune sugerată pentru îmbunătățire' }, 'indicatori_lichiditate': { 'INDICATOR': 'Lichiditate curentă, rapidă, cash ratio, fond de rulment', 'VALOARE': 'Valoarea calculată (rată sau sumă RON)', 'STATUS': 'OK / ATENȚIE / ALERTĂ', 'INTERPRETARE': 'Ce înseamnă valoarea pentru business' }, 'clasificare_datorii': { 'CATEGORIE': 'Termen scurt (<30z) / mediu (31-90z) / lung (>90z)', 'VALOARE': 'Suma datoriilor în categoria respectivă', 'NR_FACTURI': 'Numărul facturilor în acea categorie' }, 'grad_acoperire_datorii': { 'VALOARE': 'Cash disponibil + încasări așteptate vs plăți scadente', 'ACOPERIRE': 'OK / ATENȚIE / DEFICIT - dacă puteți plăti datoriile', 'EXPLICATIE': 'Ce înseamnă pentru fluxul de numerar' }, 'proiectie_lichiditate': { 'PERIOADA': 'Azi / 30 zile / 60 zile / 90 zile', 'SOLD_PROIECTAT': 'Cash estimat la sfârșitul perioadei', 'FLUX_NET': 'Încasări - Plăți pentru perioada respectivă', 'STATUS': 'OK dacă sold pozitiv, ALERTĂ dacă negativ' }, 'sumar_executiv_yoy': { 'VALOARE_CURENTA': 'Valoarea din ultimele 12 luni', 'VALOARE_ANTERIOARA': 'Valoarea din anul anterior (12-24 luni)', 'VARIATIE_PROCENT': 'Creștere/scădere procentuală', 'TREND': 'CREȘTERE / SCĂDERE / STABIL' }, 'dso_dpo_yoy': { 'VALOARE_CURENTA': 'Zile încasare/plată actuale', 'VALOARE_ANTERIOARA': 'Zile în perioada anterioară', 'VARIATIE_ZILE': 'Diferența în zile (+ = mai rău pentru DSO, mai bine pentru DPO)', 'TREND': 'ÎMBUNĂTĂȚIRE / DETERIORARE / STABIL' }, 'concentrare_risc_yoy': { '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' }, 'indicatori_agregati_venituri_yoy': { 'LINIE_BUSINESS': 'Producție proprie / Materii prime / Marfă', 'VANZARI_CURENTE': 'Vânzări în ultimele 12 luni', 'VANZARI_ANTERIOARE': 'Vânzări în perioada anterioară', 'VARIATIE_PROCENT': 'Creștere/scădere procentuală', 'TREND': 'CREȘTERE / SCĂDERE / STABIL' }, 'analiza_prajitorie': { 'CANTITATE_INTRARI': 'Cantitate intrata (cant > 0, cante = 0)', 'VALOARE_INTRARI': 'Valoare intrari = cantitate x pret', 'CANTITATE_IESIRI': 'Cantitate iesita (cant = 0, cante > 0)', 'VALOARE_IESIRI': 'Valoare iesiri = cantitate x pret', 'CANTITATE_TRANSFORMARI_IN': 'Cantitate intrata in transformari', '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 (12 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', '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ă', '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ă', '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', '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', 'explanation': PDF_EXPLANATIONS['risc_concentrare'] }, # Tablou Financiar - with DYNAMIC explanations { 'title': 'Indicatori Generali', 'df': results.get('indicatori_generali', pd.DataFrame()), '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': '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', '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', 'explanation': PDF_EXPLANATIONS['proiectie_lichiditate'] } ] ) 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: # 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() # ========================================================================= # GENERARE PDF - PAGINI CONSOLIDATE # ========================================================================= print("\n📄 Generare raport PDF...") pdf_gen = PDFReportGenerator(pdf_path, company_name=COMPANY_NAME) # Pagina 1: Titlu perf.start("PDF: Title page") pdf_gen.add_title_page() perf.stop() # 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=[ { '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() # NOTE: Paginile individuale (Vedere Executivă, Indicatori Venituri, Clienți și Risc, # Tablou Financiar) au fost eliminate - toate datele sunt acum în Dashboard Complet pdf_gen.add_page_break() # 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()) }) perf.stop() pdf_gen.add_page_break() # ========================================================================= # 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") 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() # 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 - 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() # 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() # 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'), columns=['CLIENT', 'VANZARI_FARA_TVA', 'MARJA_BRUTA', 'PROCENT_MARJA'], max_rows=15 ) pdf_gen.add_page_break() # Tabel: Top produse pdf_gen.add_table_section( "Top 15 Produse după Vânzări", results.get('top_produse'), columns=['PRODUS', 'VALOARE_VANZARI', 'MARJA_BRUTA', 'PROCENT_MARJA'], max_rows=15 ) # Tabel: Trending clienți pdf_gen.add_table_section( "Trending Clienți (YoY)", results.get('trending_clienti'), columns=['CLIENT', 'VANZARI_12_LUNI', 'VANZARI_AN_ANTERIOR', 'VARIATIE_PROCENT', 'TREND'], max_rows=15 ) # 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'), columns=['CLIENT', 'NEAJUNS_SCADENTA', 'ZILE_1_30', 'ZILE_31_60', 'PESTE_90_ZILE', 'TOTAL_SOLD'], max_rows=15 ) # 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'), columns=['PRODUS', 'NUME_GESTIUNE', 'CANTITATE', 'VALOARE', 'ZILE_FARA_MISCARE'], 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!") print("="*60) print(f"\n 📊 Excel: {excel_path}") print(f" 📄 PDF: {pdf_path}") print("\n" + "="*60) return excel_path, pdf_path def main(): """Entry point""" parser = argparse.ArgumentParser( description='Data Intelligence Report Generator', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Exemple: python main.py # Raport pentru ultimele 12 luni python main.py --months 6 # Raport pentru ultimele 6 luni python main.py --output-dir /tmp # Salvare în alt director """ ) parser.add_argument( '--months', '-m', type=int, default=ANALYSIS_MONTHS, help=f'Numărul de luni pentru analiză (default: {ANALYSIS_MONTHS})' ) parser.add_argument( '--output-dir', '-o', type=Path, default=OUTPUT_DIR, help=f'Directorul pentru output (default: {OUTPUT_DIR})' ) args = parser.parse_args() # Ensure output directory exists args.output_dir.mkdir(parents=True, exist_ok=True) try: generate_reports(args) except KeyboardInterrupt: print("\n\n⚠️ Întrerupt de utilizator.") sys.exit(1) except Exception as e: print(f"\n❌ Eroare: {e}") import traceback traceback.print_exc() sys.exit(1) if __name__ == '__main__': main()