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>
938 lines
38 KiB
Python
938 lines
38 KiB
Python
#!/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()
|