#!/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 datetime import timedelta from config import ( ORACLE_CONFIG, get_dsn, OUTPUT_DIR, COMPANY_NAME, ANALYSIS_MONTHS, MIN_SALES_FOR_ANALYSIS, LOW_MARGIN_THRESHOLD, RECOMMENDATION_THRESHOLDS, REPORT_END_MONTH ) 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}") def parse_aging_dates(raw: str) -> list: """Parse --aging-dates CLI value into sorted list of data_referinta datetimes. Accepts 'YYYY-MM,YYYY-MM,...' (end-of-month), returns list of first-day-next-month datetimes. Raises SystemExit on any invalid token so user sees a clear message, not a crash. """ dates = [] for token in raw.split(','): tok = token.strip() if not tok: continue try: dates.append(compute_data_referinta(tok)) except ValueError as e: print(f"❌ --aging-dates: token invalid '{tok}' (astept YYYY-MM). {e}") sys.exit(1) return sorted(set(dates)) def compute_data_referinta(end_month_str: str = None) -> datetime: """ Compute reference date (first day of month AFTER the last reporting month). If end_month_str is None: default to first day of current month (i.e., report through end of previous month). If end_month_str is 'YYYY-MM': parse it and return first day of next month. """ if end_month_str: parsed = datetime.strptime(end_month_str, '%Y-%m') if parsed.month == 12: return datetime(parsed.year + 1, 1, 1) else: return datetime(parsed.year, parsed.month + 1, 1) else: today = datetime.now() return datetime(today.year, today.month, 1) 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" # Compute reference date data_referinta = compute_data_referinta(args.end_month) end_date = data_referinta - timedelta(days=1) # last day of reporting period start_date = data_referinta - relativedelta(months=args.months) period_str = f"Perioada: {start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}" print("\n" + "="*60) print(" DATA INTELLIGENCE REPORT GENERATOR") print("="*60) print(f" {period_str} ({args.months} luni)") print(f" Output: {OUTPUT_DIR}") print("="*60 + "\n") # Update parameters with command line arguments and inject data_referinta for query_info in QUERIES.values(): if 'months' in query_info.get('params', {}): query_info['params']['months'] = args.months # Inject data_referinta into ALL queries query_info.setdefault('params', {}) query_info['params']['data_referinta'] = data_referinta # 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 # Parse multi-date aging request (validation before DB connection) aging_dates_list = [] if getattr(args, 'aging_dates', None): aging_dates_list = parse_aging_dates(args.aging_dates) 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) # --- Evolutie multi-data stoc 371 >3 ani (optional) --- if aging_dates_list: print(f"\n📥 Extragere evolutie stoc 371 pe {len(aging_dates_list)} date:") evolutie_rows = [] sumar_info = QUERIES['stocuri_371_sumar'] original_param = sumar_info['params'].get('data_referinta') for dref in aging_dates_list: perf.start(f"QUERY: stocuri_371_sumar@{dref.strftime('%Y-%m-%d')}") sumar_info['params']['data_referinta'] = dref df_e = execute_query(conn, f"stocuri_371_sumar@{dref.strftime('%Y-%m')}", sumar_info) perf.stop(rows=len(df_e) if df_e is not None else 0) if df_e is None or df_e.empty: continue grand = df_e[df_e['GROUPING_LEVEL'] == 3] if grand.empty: continue row = grand.iloc[0] val_total = float(row.get('VALOARE_TOTAL', 0) or 0) val_3 = float(row.get('VAL_PESTE_3_ANI', 0) or 0) evolutie_rows.append({ 'DATA_REFERINTA': (dref - timedelta(days=1)).strftime('%Y-%m-%d'), 'VALOARE_TOTAL': round(val_total, 2), 'VALOARE_PESTE_3_ANI': round(val_3, 2), 'PROCENT_PESTE_3_ANI': round(val_3 / val_total * 100, 2) if val_total > 0 else 0.0 }) # restore original data_referinta for downstream consumers sumar_info['params']['data_referinta'] = original_param results['stocuri_371_evolutie'] = pd.DataFrame(evolutie_rows) else: results['stocuri_371_evolutie'] = pd.DataFrame() # 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', 'stocuri_371_sumar', 'stocuri_371_detaliu', 'stocuri_371_evolutie', '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' }, 'stocuri_371_sumar': { 'GROUPING_LEVEL': '0 = detaliu subgrupa, 1 = subtotal per grupa, 3 = grand total (ROLLUP)', 'VALOARE_TOTAL': 'Total valoare stoc cont 371 la data de referinta (cost achizitie fara TVA)', 'VAL_0_6_LUNI': 'Valoare articole cu vechime 0-180 zile', 'VAL_6_12_LUNI': 'Valoare articole cu vechime 181-365 zile', 'VAL_1_2_ANI': 'Valoare articole cu vechime 366-730 zile', 'VAL_2_3_ANI': 'Valoare articole cu vechime 731-1095 zile', 'VAL_PESTE_3_ANI': 'Valoare articole cu vechime peste 1095 zile (risc haircut bancar)' }, 'stocuri_371_detaliu': { 'CANTITATE': 'Stoc final = cants + cant - cante', 'VALOARE': 'Cantitate × pret achizitie (cost, fara TVA)', 'ZILE_VECHIME': '(data_referinta - 1) - NVL(dataout, datain)', 'ANI_VECHIME': 'zile_vechime / 365', 'BUCKET_VECHIME': 'Grupare: 0-6 luni / 6-12 luni / 1-2 ani / 2-3 ani / >3 ani' }, 'stocuri_371_evolutie': { 'DATA_REFERINTA': 'End-of-month pentru care s-a calculat snapshot-ul', 'VALOARE_TOTAL': 'Total stoc 371 la acea data', 'VALOARE_PESTE_3_ANI': 'Portiunea cu vechime >3 ani (trend an-la-an pentru banca)', 'PROCENT_PESTE_3_ANI': '% din total care depaseste 3 ani' }, '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' } # Metadata pentru sheet-uri sintetice (nu sunt in QUERIES dar sunt randate) synthetic_sheets = { 'stocuri_371_evolutie': { 'title': 'Stocuri Marfa 371 - Evolutie >3 ani', 'description': 'Valoare totala 371 si portiune >3 ani la mai multe end-of-month (trend)' } } 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 or query_name in synthetic_sheets): df_sheet = results[query_name] if df_sheet is None or df_sheet.empty: continue if query_name in QUERIES: meta = QUERIES[query_name] else: meta = synthetic_sheets[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=df_sheet, title=meta.get('title', query_name), description=meta.get('description', ''), legend=legends.get(query_name) ) df_rows = len(df_sheet) 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 ) # ========================================================================= # Pagina dedicata BANCA: Stocuri Marfa 371 pe Vechimi + reconciliere # ========================================================================= df_sumar_371 = results.get('stocuri_371_sumar') if df_sumar_371 is not None and not df_sumar_371.empty: perf.start("PDF: Stocuri 371 bank page") pdf_gen.add_page_break() nota_metodologica = ( "METODOLOGIE: Vechimea este calculata ca numar de zile intre data de referinta " f"({end_date.strftime('%d.%m.%Y')}) si ultima miscare a articolului in stoc " "(iesire, sau intrare daca nu a existat nicio iesire). Valoarea este costul de " "achizitie fara TVA (cant × pret din vstoc). Se includ toate pozitiile cu stoc " "nenul (<> 0), inclusiv cele negative (corectii / erori de inventar), ca sa se " "reconcilieze corect cu soldul contabil 371 din balanta (vbal, cont sintetic)." ) pdf_gen.add_explanation(nota_metodologica) pdf_gen.add_table_section( "Raport Stocuri Marfa 371 - Sumar Vechimi (pentru banca)", df_sumar_371, columns=['GRUPA', 'SUBGRUPA', 'VALOARE_TOTAL', 'VAL_0_6_LUNI', 'VAL_6_12_LUNI', 'VAL_1_2_ANI', 'VAL_2_3_ANI', 'VAL_PESTE_3_ANI'], max_rows=30 ) df_evolutie_371 = results.get('stocuri_371_evolutie') if df_evolutie_371 is not None and not df_evolutie_371.empty: pdf_gen.add_table_section( "Evolutie stoc 371 >3 ani (multi-data)", df_evolutie_371, columns=['DATA_REFERINTA', 'VALOARE_TOTAL', 'VALOARE_PESTE_3_ANI', 'PROCENT_PESTE_3_ANI'], max_rows=20 ) df_detaliu_371 = results.get('stocuri_371_detaliu') if df_detaliu_371 is not None and not df_detaliu_371.empty: pdf_gen.add_table_section( "Top 25 articole dupa vechime (detaliu 371)", df_detaliu_371, columns=['COD_ARTICOL', 'DENUMIRE', 'CANTITATE', 'VALOARE', 'ZILE_VECHIME', 'BUCKET_VECHIME'], max_rows=25 ) # Reconciliere contabila df_sold = results.get('stocuri_371_sold_contabil') grand_total_row = df_sumar_371[df_sumar_371['GROUPING_LEVEL'] == 3] total_raport = float(grand_total_row['VALOARE_TOTAL'].iloc[0]) if not grand_total_row.empty else 0.0 sold_contabil = None if df_sold is not None and not df_sold.empty: val = df_sold['SOLD_371'].iloc[0] if val is not None: sold_contabil = float(val) if sold_contabil is None: recon_rows = [ {'INDICATOR': 'Total valoare stoc 371 (raport)', 'VALOARE_RON': f'{total_raport:,.2f}'}, {'INDICATOR': 'Sold contabil 371 (vbal)', 'VALOARE_RON': 'INDISPONIBIL (verifica balanta vbal pe luna ref)'}, {'INDICATOR': 'Diferenta', 'VALOARE_RON': 'N/A'} ] else: diferenta = total_raport - sold_contabil pct = (diferenta / sold_contabil * 100) if sold_contabil else 0.0 marker = ' ⚠' if abs(pct) > 1.0 else '' recon_rows = [ {'INDICATOR': 'Total valoare stoc 371 (raport)', 'VALOARE_RON': f'{total_raport:,.2f}'}, {'INDICATOR': 'Sold contabil 371 (vbal, la data ref)', 'VALOARE_RON': f'{sold_contabil:,.2f}'}, {'INDICATOR': f'Diferenta{marker}', 'VALOARE_RON': f'{diferenta:,.2f} ({pct:+.2f}%)'} ] df_recon = pd.DataFrame(recon_rows) pdf_gen.add_table_section( "RECONCILIERE CONTABILA", df_recon, columns=['INDICATOR', 'VALOARE_RON'], max_rows=5 ) if sold_contabil is not None and abs((total_raport - sold_contabil) / sold_contabil * 100) > 1.0: pdf_gen.add_explanation( "Diferenta >1% intre raport si soldul contabil. Cauze uzuale: evaluare " "pret mediu vs FIFO, inregistrari contabile ulterioare snapshot-ului vstoc, " "sau cont 371 analitic (371.1, 371.2) netotalizat in vbal." ) perf.stop() 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( '--end-month', '-e', type=str, default=REPORT_END_MONTH, help='Luna finala de raportare YYYY-MM (default: luna completa anterioara). Ex: 2026-01' ) parser.add_argument( '--aging-dates', type=str, default=None, help='Lista YYYY-MM separata prin virgula pentru sectiunea evolutie stoc 371 >3 ani. Ex: 2022-12,2023-12,2024-12,2025-12' ) 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()