Files
vending_data_intelligence_r…/main.py
Marius Mutu 9e9ddec014 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>
2025-12-11 13:33:02 +02:00

938 lines
38 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
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()}...")
self.connection = oracledb.connect(
user=ORACLE_CONFIG['user'],
password=ORACLE_CONFIG['password'],
dsn=get_dsn()
)
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 (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:
# 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_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_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")
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")
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
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()
# 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_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_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_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()