main.py: - Add PDF_EXPLANATIONS dict with Romanian explanations for all report sections - Add dynamic explanation generators: - generate_indicatori_generali_explanation() - financial ratios with values - generate_indicatori_lichiditate_explanation() - liquidity ratios - generate_ciclu_cash_explanation() - cash conversion cycle - generate_solduri_clienti/furnizori_explanation() - balance summaries report_generator.py: - Add diacritics removal for PDF compatibility (Helvetica font) - Add sanitize_for_pdf() helper function - Add explanation_fill and explanation_border styles for info boxes - Enhance add_consolidated_sheet() with explanation parameter - Add merged explanation boxes with light grey background These changes complement the FORMULA column additions, providing managers with contextual explanations throughout the reports. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1131 lines
50 KiB
Python
1131 lines
50 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
|
||
|
||
|
||
# Manager-friendly explanations for PDF and Excel sections (Romanian without diacritics)
|
||
PDF_EXPLANATIONS = {
|
||
# KPIs and Dashboard
|
||
'kpis': "Indicatorii cheie (KPIs) ofera o imagine de ansamblu a performantei. Vanzarile totale arata volumul de business, marja bruta arata profitabilitatea (ideal peste 20%), iar TREND-ul compara cu anul trecut - CRESTERE e pozitiv, SCADERE necesita investigare.",
|
||
'recomandari': "Sistemul analizeaza automat datele si genereaza alerte prioritizate. ALERTA (rosu) = problema critica ce necesita actiune imediata; ATENTIE (galben) = risc de monitorizat; OK (verde) = situatie normala.",
|
||
|
||
# Charts
|
||
'evolutie_lunara': "Graficul arata tendinta vanzarilor pe ultimele 12 luni. Urmariti sezonalitatea (luni slabe/puternice) si tendinta generala. Daca volumul creste dar marja scade, exista presiune pe preturi.",
|
||
'concentrare_clienti': "Dependenta de clientii mari - regula 80/20: ideal max 60-70% din vanzari de la top 20% clienti. Daca un singur client depaseste 25% din vanzari, exista risc major - diversificati portofoliul.",
|
||
'ciclu_cash': "Ciclul de conversie cash (CCC) arata cate zile sunt blocati banii: DIO (zile stoc) + DSO (zile incasare) - DPO (zile plata furnizori). Sub 30 zile = excelent, 30-60 = acceptabil, peste 60 = problema de lichiditate.",
|
||
|
||
# Alerts and tables
|
||
'alerte_critice': "Vanzarile sub cost sunt tranzactii in pierdere - verificati preturile imediat. Clientii cu marja sub 15% consuma resurse fara profit adecvat - renegociati sau reduceti prioritatea.",
|
||
'top_clienti_produse': "Clasamentele arata cei mai importanti clienti si produse dupa vanzari si marja. Concentrati-va pe clientii cu marja mare si investigati pe cei cu marja mica (<15%).",
|
||
'aging_creante': "Analiza vechimii creantelor arata suma facturilor neincasate pe intervale de timp. Creantele peste 90 de zile reprezinta risc major de neincasare - contactati urgent acesti clienti.",
|
||
'stoc_lent': "Stocul lent blocheaza capital si ocupa spatiu. Produsele fara miscare peste 90 zile trebuie lichidate prin promotii sau trecute in pierdere daca sunt expirate/depasate.",
|
||
|
||
# Excel-specific sections
|
||
'venituri': "Vanzarile pe linii de business arata contributia fiecarui segment. Productia proprie are de obicei marja mai mare (25-40%), revanzarea are marja mai mica (10-20%) dar volum mai mare.",
|
||
'portofoliu_clienti': "Structura portofoliului de clienti pe segmente. Un portofoliu sanatos are diversificare pe mai multe segmente, fara dependenta excesiva de o singura categorie.",
|
||
'risc_concentrare': "Indicatorii de concentrare arata cat de dependenti sunteti de putini clienti. Top 5 clienti ideal sub 50% din vanzari, top 10 ideal sub 70%. Urmariti trendul YoY.",
|
||
'indicatori_generali': "Indicatori financiari structurali: grad indatorare (Datorii/Capital - ideal <1), autonomie financiara (Capital/Active - ideal >0.5), rata datoriilor (Datorii/Active - ideal <0.5), marja neta (Profit/Vanzari %), ROA (Profit/Active %), rotatia activelor.",
|
||
'indicatori_lichiditate': "Ratii de lichiditate: Lichiditate curenta (Active curente/Datorii curente - ideal >=1.5), Quick Ratio (fara stocuri - ideal >=1.0), Cash Ratio (Cash/Datorii - ideal >=0.2), Fond de rulment (Active curente - Datorii curente).",
|
||
'ciclu_conversie_cash': "Ciclul de conversie cash (CCC) = DIO + DSO - DPO. DIO = zile stoc (cat timp sta marfa pe stoc), DSO = zile incasare (cat timp asteptam banii de la clienti), DPO = zile plata (in cat timp platim furnizorii). CCC sub 30 zile = excelent, 30-60 = acceptabil, peste 60 = probleme de lichiditate.",
|
||
'solduri_clienti': "Solduri de incasat de la clienti - creante comerciale din cont 4111. Verificati vechimea si urgentati incasarile peste 30 zile pentru a imbunatati cash-flow-ul.",
|
||
'solduri_furnizori': "Datorii catre furnizori din cont 401. Prioritizati platile in functie de scadente si relatia comerciala pentru a evita penalizari si pastrarea relatiilor bune.",
|
||
'clasificare_datorii': "Datoriile pe intervale de intarziere. Datoriile sub 30 zile sunt normale, 30-60 zile necesita atentie, peste 60 zile afecteaza relatia cu furnizorii.",
|
||
'proiectie_lichiditate': "Previziunea de cash pe 30 zile bazata pe scadente. Sold negativ = risc de lipsă numerar - asigurati finantare suplimentara sau urgentati incasarile."
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# DYNAMIC EXPLANATION GENERATORS - Create explanations with actual values
|
||
# =============================================================================
|
||
|
||
def generate_indicatori_generali_explanation(df):
|
||
"""Generate dynamic explanation with actual values and formulas for financial indicators."""
|
||
if df is None or df.empty:
|
||
return "Nu exista date pentru indicatorii financiari generali."
|
||
|
||
parts = ["INDICATORI FINANCIARI:"]
|
||
for _, row in df.iterrows():
|
||
ind = row.get('INDICATOR', '')
|
||
val = row.get('VALOARE', 0)
|
||
status = row.get('STATUS', '')
|
||
|
||
if val is None:
|
||
val = 0
|
||
|
||
if 'indatorare' in ind.lower():
|
||
parts.append(f"Grad indatorare = {val:.2f} (Datorii/Capital propriu) - {status}")
|
||
elif 'autonomie' in ind.lower():
|
||
parts.append(f"Autonomie financiara = {val:.2f} (Capital/Active) - {status}")
|
||
elif 'rata datoriilor' in ind.lower():
|
||
parts.append(f"Rata datoriilor = {val:.2f} (Datorii/Active) - {status}")
|
||
elif 'marja' in ind.lower():
|
||
parts.append(f"Marja neta = {val:.1f}% - {status}")
|
||
elif 'roa' in ind.lower():
|
||
parts.append(f"ROA = {val:.1f}% (Profit/Active) - {status}")
|
||
elif 'rotatia' in ind.lower():
|
||
parts.append(f"Rotatia activelor = {val:.2f} - {status}")
|
||
|
||
return " | ".join(parts) if len(parts) > 1 else parts[0]
|
||
|
||
|
||
def generate_indicatori_lichiditate_explanation(df):
|
||
"""Generate dynamic explanation for liquidity ratios with actual values."""
|
||
if df is None or df.empty:
|
||
return "Nu exista date pentru indicatorii de lichiditate."
|
||
|
||
parts = ["RATII LICHIDITATE:"]
|
||
for _, row in df.iterrows():
|
||
ind = row.get('INDICATOR', '')
|
||
val = row.get('VALOARE', 0)
|
||
status = row.get('STATUS', '')
|
||
|
||
if val is None:
|
||
val = 0
|
||
|
||
if 'curenta' in ind.lower() and 'lichiditate' in ind.lower():
|
||
parts.append(f"Lichiditate curenta = {val:.2f} (Active curente/Datorii curente, ideal >= 1.5) - {status}")
|
||
elif 'rapida' in ind.lower() or 'quick' in ind.lower():
|
||
parts.append(f"Quick Ratio = {val:.2f} (fara stocuri, ideal >= 1.0) - {status}")
|
||
elif 'cash' in ind.lower():
|
||
parts.append(f"Cash Ratio = {val:.2f} (Cash/Datorii, ideal >= 0.2) - {status}")
|
||
elif 'fond' in ind.lower() or 'rulment' in ind.lower():
|
||
parts.append(f"Fond de rulment = {val:,.0f} RON - {status}")
|
||
|
||
return " | ".join(parts) if len(parts) > 1 else parts[0]
|
||
|
||
|
||
def generate_ciclu_cash_explanation(df):
|
||
"""Generate dynamic explanation for cash conversion cycle with actual values."""
|
||
if df is None or df.empty:
|
||
return "Nu exista date pentru ciclul de conversie cash."
|
||
|
||
dio = dso = dpo = ccc = 0
|
||
for _, row in df.iterrows():
|
||
ind = str(row.get('INDICATOR', '')).upper()
|
||
zile = row.get('ZILE', 0)
|
||
if zile is None:
|
||
zile = 0
|
||
|
||
if 'DIO' in ind and 'DSO' not in ind:
|
||
dio = zile
|
||
elif 'DSO' in ind and 'DIO' not in ind:
|
||
dso = zile
|
||
elif 'DPO' in ind:
|
||
dpo = zile
|
||
elif 'CCC' in ind or 'CICLU' in ind:
|
||
ccc = zile
|
||
|
||
# Calculate CCC if not directly available
|
||
if ccc == 0 and (dio > 0 or dso > 0):
|
||
ccc = dio + dso - dpo
|
||
|
||
status = "EXCELENT" if ccc < 30 else "ACCEPTABIL" if ccc < 60 else "PROBLEME LICHIDITATE"
|
||
|
||
return (f"CICLU CONVERSIE CASH: DIO = {dio:.0f} zile (stoc -> vanzare) + "
|
||
f"DSO = {dso:.0f} zile (factura -> incasare) - "
|
||
f"DPO = {dpo:.0f} zile (achizitie -> plata) = "
|
||
f"CCC = {ccc:.0f} zile. STATUS: {status}")
|
||
|
||
|
||
def generate_solduri_clienti_explanation(df):
|
||
"""Generate dynamic explanation for customer balances with actual totals."""
|
||
if df is None or df.empty:
|
||
return "Nu exista solduri de incasat de la clienti."
|
||
|
||
total = df['SOLD_CURENT'].sum() if 'SOLD_CURENT' in df.columns else 0
|
||
count = len(df)
|
||
top1_pct = (df['SOLD_CURENT'].iloc[0] / total * 100) if total > 0 and len(df) > 0 else 0
|
||
|
||
return (f"CREANTE CLIENTI: Total de incasat = {total:,.0f} RON de la {count} clienti. "
|
||
f"Top 1 client = {top1_pct:.1f}% din total. "
|
||
f"Verificati vechimea creantelor si urgentati incasarile peste 30 zile.")
|
||
|
||
|
||
def generate_solduri_furnizori_explanation(df):
|
||
"""Generate dynamic explanation for supplier balances with actual totals."""
|
||
if df is None or df.empty:
|
||
return "Nu exista datorii catre furnizori."
|
||
|
||
total = df['SOLD_CURENT'].sum() if 'SOLD_CURENT' in df.columns else 0
|
||
count = len(df)
|
||
|
||
return (f"DATORII FURNIZORI: Total de platit = {total:,.0f} RON catre {count} furnizori. "
|
||
f"Verificati scadentele si prioritizati platile pentru a evita penalizari.")
|
||
|
||
|
||
class PerformanceLogger:
|
||
"""Tracks execution time for each operation to identify bottlenecks."""
|
||
|
||
def __init__(self):
|
||
self.timings = []
|
||
self.start_time = time.perf_counter()
|
||
self.phase_start = None
|
||
self.phase_name = None
|
||
|
||
def start(self, name: str):
|
||
"""Start timing a named operation."""
|
||
self.phase_name = name
|
||
self.phase_start = time.perf_counter()
|
||
print(f"⏱️ [{self._timestamp()}] START: {name}")
|
||
|
||
def stop(self, rows: int = None):
|
||
"""Stop timing and record duration."""
|
||
if self.phase_start is None:
|
||
return
|
||
duration = time.perf_counter() - self.phase_start
|
||
self.timings.append({
|
||
'name': self.phase_name,
|
||
'duration': duration,
|
||
'rows': rows
|
||
})
|
||
rows_info = f" ({rows} rows)" if rows else ""
|
||
print(f"✅ [{self._timestamp()}] DONE: {self.phase_name} - {duration:.2f}s{rows_info}")
|
||
self.phase_start = None
|
||
|
||
def _timestamp(self):
|
||
return datetime.now().strftime("%H:%M:%S")
|
||
|
||
def summary(self, output_path: str = None):
|
||
"""Print summary sorted by duration (slowest first)."""
|
||
total = time.perf_counter() - self.start_time
|
||
|
||
print("\n" + "="*70)
|
||
print("📊 PERFORMANCE SUMMARY (sorted by duration, slowest first)")
|
||
print("="*70)
|
||
|
||
sorted_timings = sorted(self.timings, key=lambda x: x['duration'], reverse=True)
|
||
|
||
lines = []
|
||
for t in sorted_timings:
|
||
pct = (t['duration'] / total) * 100 if total > 0 else 0
|
||
rows_info = f" [{t['rows']} rows]" if t['rows'] else ""
|
||
line = f"{t['duration']:8.2f}s ({pct:5.1f}%) - {t['name']}{rows_info}"
|
||
print(line)
|
||
lines.append(line)
|
||
|
||
print("-"*70)
|
||
print(f"TOTAL: {total:.2f}s ({total/60:.1f} minutes)")
|
||
|
||
# Save to file
|
||
if output_path:
|
||
log_file = f"{output_path}/performance_log.txt"
|
||
with open(log_file, 'w', encoding='utf-8') as f:
|
||
f.write(f"Performance Log - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||
f.write("="*70 + "\n\n")
|
||
for line in lines:
|
||
f.write(line + "\n")
|
||
f.write("\n" + "-"*70 + "\n")
|
||
f.write(f"TOTAL: {total:.2f}s ({total/60:.1f} minutes)\n")
|
||
print(f"\n📝 Log saved to: {log_file}")
|
||
|
||
|
||
class OracleConnection:
|
||
"""Context manager for Oracle database connection"""
|
||
|
||
def __init__(self):
|
||
self.connection = None
|
||
|
||
def __enter__(self):
|
||
try:
|
||
print(f"🔌 Conectare la Oracle: {get_dsn()}...")
|
||
# Add TCP keepalive and timeout settings to prevent connection drops
|
||
self.connection = oracledb.connect(
|
||
user=ORACLE_CONFIG['user'],
|
||
password=ORACLE_CONFIG['password'],
|
||
dsn=get_dsn(),
|
||
tcp_connect_timeout=30,
|
||
expire_time=5 # Send TCP keepalive every 5 minutes
|
||
)
|
||
# Set call timeout to 10 minutes for slow queries
|
||
self.connection.call_timeout = 600000 # milliseconds
|
||
print("✓ Conectat cu succes!")
|
||
return self.connection
|
||
except oracledb.Error as e:
|
||
print(f"❌ Eroare conexiune Oracle: {e}")
|
||
raise
|
||
|
||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
if self.connection:
|
||
self.connection.close()
|
||
print("✓ Conexiune închisă.")
|
||
|
||
|
||
def execute_query(connection, query_name: str, query_info: dict) -> pd.DataFrame:
|
||
"""Execute a query and return results as DataFrame"""
|
||
try:
|
||
sql = query_info['sql']
|
||
params = query_info.get('params', {})
|
||
|
||
print(f" 📊 Executare: {query_name}...", end=" ")
|
||
|
||
with connection.cursor() as cursor:
|
||
cursor.execute(sql, params)
|
||
columns = [col[0] for col in cursor.description]
|
||
rows = cursor.fetchall()
|
||
|
||
df = pd.DataFrame(rows, columns=columns)
|
||
print(f"✓ ({len(df)} rânduri)")
|
||
return df
|
||
|
||
except oracledb.Error as e:
|
||
print(f"❌ Eroare: {e}")
|
||
return pd.DataFrame()
|
||
|
||
|
||
def generate_reports(args):
|
||
"""Main function to generate all reports"""
|
||
|
||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||
excel_path = OUTPUT_DIR / f"data_intelligence_report_{timestamp}.xlsx"
|
||
pdf_path = OUTPUT_DIR / f"data_intelligence_report_{timestamp}.pdf"
|
||
|
||
print("\n" + "="*60)
|
||
print(" DATA INTELLIGENCE REPORT GENERATOR")
|
||
print("="*60)
|
||
print(f" Perioada: Ultimele {args.months} luni")
|
||
print(f" Output: {OUTPUT_DIR}")
|
||
print("="*60 + "\n")
|
||
|
||
# Update parameters with command line arguments
|
||
for query_info in QUERIES.values():
|
||
if 'months' in query_info.get('params', {}):
|
||
query_info['params']['months'] = args.months
|
||
|
||
# Calculate reporting period string
|
||
end_date = datetime.now()
|
||
start_date = end_date - relativedelta(months=args.months)
|
||
period_str = f"Perioada: {start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}"
|
||
|
||
# Add period to descriptions for queries with months parameter
|
||
for query_name, query_info in QUERIES.items():
|
||
if 'months' in query_info.get('params', {}):
|
||
original_desc = query_info.get('description', '')
|
||
query_info['description'] = f"{original_desc}\n{period_str}"
|
||
|
||
# Connect and execute queries
|
||
results = {}
|
||
perf = PerformanceLogger() # Initialize performance logger
|
||
|
||
with OracleConnection() as conn:
|
||
print("\n📥 Extragere date din Oracle:\n")
|
||
|
||
for query_name, query_info in QUERIES.items():
|
||
perf.start(f"QUERY: {query_name}")
|
||
df = execute_query(conn, query_name, query_info)
|
||
results[query_name] = df
|
||
perf.stop(rows=len(df) if df is not None and not df.empty else 0)
|
||
|
||
# Generate Excel Report
|
||
print("\n📝 Generare raport Excel...")
|
||
excel_gen = ExcelReportGenerator(excel_path)
|
||
|
||
# Generate recommendations based on all data
|
||
perf.start("RECOMMENDATIONS: analyze_all")
|
||
recommendations_engine = RecommendationsEngine(RECOMMENDATION_THRESHOLDS)
|
||
recommendations_df = recommendations_engine.analyze_all(results)
|
||
results['recomandari'] = recommendations_df
|
||
perf.stop(rows=len(recommendations_df))
|
||
print(f"✓ {len(recommendations_df)} recomandări generate")
|
||
|
||
# =========================================================================
|
||
# CONSOLIDARE DATE PENTRU VEDERE DE ANSAMBLU
|
||
# =========================================================================
|
||
print("\n📊 Consolidare date pentru vedere de ansamblu...")
|
||
|
||
# --- Consolidare 1: Vedere Executivă (KPIs + YoY) ---
|
||
perf.start("CONSOLIDATION: kpi_consolidated")
|
||
# Folosim direct sumar_executiv_yoy care are deja toate coloanele necesare:
|
||
# INDICATOR, VALOARE_CURENTA, VALOARE_ANTERIOARA, VARIATIE_PROCENT, TREND
|
||
if 'sumar_executiv_yoy' in results and not results['sumar_executiv_yoy'].empty:
|
||
df_kpi = results['sumar_executiv_yoy'].copy()
|
||
# Adaugă coloana UM bazată pe tipul indicatorului
|
||
df_kpi['UM'] = df_kpi['INDICATOR'].apply(lambda x:
|
||
'%' if '%' in x or 'marja' in x.lower() else
|
||
'buc' if 'numar' in x.lower() else 'RON'
|
||
)
|
||
results['kpi_consolidated'] = df_kpi
|
||
else:
|
||
# Fallback la sumar_executiv simplu (fără YoY)
|
||
results['kpi_consolidated'] = results.get('sumar_executiv', pd.DataFrame())
|
||
perf.stop()
|
||
|
||
# --- Consolidare 2: Indicatori Venituri (Current + YoY) ---
|
||
perf.start("CONSOLIDATION: venituri_consolidated")
|
||
if 'indicatori_agregati_venituri' in results and 'indicatori_agregati_venituri_yoy' in results:
|
||
df_venituri = results['indicatori_agregati_venituri'].copy()
|
||
df_venituri_yoy = results['indicatori_agregati_venituri_yoy'].copy()
|
||
|
||
if not df_venituri.empty and not df_venituri_yoy.empty:
|
||
# Merge pe LINIE_BUSINESS
|
||
df_venituri_yoy = df_venituri_yoy.rename(columns={
|
||
'VANZARI': 'VANZARI_ANTERIOARE',
|
||
'MARJA': 'MARJA_ANTERIOARA'
|
||
})
|
||
df_venituri_combined = pd.merge(
|
||
df_venituri,
|
||
df_venituri_yoy[['LINIE_BUSINESS', 'VANZARI_ANTERIOARE', 'VARIATIE_PROCENT', 'TREND']],
|
||
on='LINIE_BUSINESS',
|
||
how='left'
|
||
)
|
||
df_venituri_combined = df_venituri_combined.rename(columns={'VANZARI': 'VANZARI_CURENTE'})
|
||
results['venituri_consolidated'] = df_venituri_combined
|
||
else:
|
||
results['venituri_consolidated'] = df_venituri
|
||
else:
|
||
results['venituri_consolidated'] = results.get('indicatori_agregati_venituri', pd.DataFrame())
|
||
perf.stop()
|
||
|
||
# --- Consolidare 3: Clienți și Risc (Portofoliu + Concentrare + YoY) ---
|
||
perf.start("CONSOLIDATION: risc_consolidated")
|
||
if 'concentrare_risc' in results and 'concentrare_risc_yoy' in results:
|
||
df_risc = results['concentrare_risc'].copy()
|
||
df_risc_yoy = results['concentrare_risc_yoy'].copy()
|
||
|
||
if not df_risc.empty and not df_risc_yoy.empty:
|
||
# Merge pe INDICATOR
|
||
df_risc = df_risc.rename(columns={'PROCENT': 'PROCENT_CURENT'})
|
||
df_risc_combined = pd.merge(
|
||
df_risc,
|
||
df_risc_yoy[['INDICATOR', 'PROCENT_ANTERIOR', 'VARIATIE', 'TREND']],
|
||
on='INDICATOR',
|
||
how='left'
|
||
)
|
||
results['risc_consolidated'] = df_risc_combined
|
||
else:
|
||
results['risc_consolidated'] = df_risc
|
||
else:
|
||
results['risc_consolidated'] = results.get('concentrare_risc', pd.DataFrame())
|
||
perf.stop()
|
||
|
||
print("✓ Consolidări finalizate")
|
||
|
||
# Add sheets in logical order - CONSOLIDAT primul, apoi detalii
|
||
sheet_order = [
|
||
# CONSOLIDAT - Vedere de Ansamblu (înlocuiește sheet-urile individuale)
|
||
'vedere_ansamblu', # KPIs + YoY + Recomandări
|
||
'indicatori_venituri', # Venituri Current + YoY merged
|
||
'clienti_risc', # Portofoliu + Concentrare + YoY
|
||
'tablou_financiar', # 5 secțiuni financiare
|
||
|
||
# DETALII - Sheet-uri individuale pentru analiză profundă
|
||
'sezonalitate_lunara',
|
||
|
||
# ALERTE
|
||
'vanzari_sub_cost',
|
||
'clienti_marja_mica',
|
||
|
||
# CICLU CASH
|
||
'ciclu_conversie_cash',
|
||
|
||
# ANALIZA CLIENTI
|
||
'marja_per_client',
|
||
'clienti_ranking_profit',
|
||
'frecventa_clienti',
|
||
'concentrare_clienti',
|
||
'trending_clienti',
|
||
'marja_client_categorie',
|
||
|
||
# PRODUSE
|
||
'top_produse',
|
||
'marja_per_categorie',
|
||
'marja_per_gestiune',
|
||
'articole_negestionabile',
|
||
'productie_vs_revanzare',
|
||
|
||
# PRETURI
|
||
'dispersie_preturi',
|
||
'clienti_sub_medie',
|
||
'evolutie_discount',
|
||
|
||
# FINANCIAR
|
||
'dso_dpo',
|
||
'dso_dpo_yoy',
|
||
'solduri_clienti',
|
||
'aging_creante',
|
||
'facturi_restante',
|
||
'solduri_furnizori',
|
||
'aging_datorii',
|
||
'facturi_restante_furnizori',
|
||
'pozitia_cash',
|
||
|
||
# ISTORIC
|
||
'vanzari_lunare',
|
||
|
||
# STOC
|
||
'stoc_curent',
|
||
'stoc_lent',
|
||
'rotatie_stocuri',
|
||
|
||
# PRODUCTIE
|
||
'analiza_prajitorie',
|
||
]
|
||
|
||
# Legends for each sheet explaining column calculations
|
||
legends = {
|
||
'recomandari': {
|
||
'CATEGORIE': 'Domeniu: Marja, Clienti, Stoc, Financiar',
|
||
'STATUS': 'OK = bine, ATENTIE = necesita atentie, ALERTA = actiune urgenta',
|
||
'VEZI_DETALII': 'Sheet-ul cu date detaliate pentru acest indicator'
|
||
},
|
||
'marja_per_client': {
|
||
'VANZARI_FARA_TVA': 'SUM(cantitate × preț) din fact_vfacturi_detalii',
|
||
'COST_TOTAL': 'SUM(cantitate × pret_achizitie)',
|
||
'MARJA_BRUTA': 'Vânzări - Cost = SUM(cantitate × (preț - pret_achizitie))',
|
||
'PROCENT_MARJA': 'Marja Brută / Vânzări × 100'
|
||
},
|
||
'clienti_marja_mica': {
|
||
'VANZARI_FARA_TVA': 'SUM(cantitate × preț) pentru client',
|
||
'MARJA_BRUTA': 'Vânzări - Cost',
|
||
'PROCENT_MARJA': 'Marja / Vânzări × 100 (sub 15% = alertă)'
|
||
},
|
||
'vanzari_sub_cost': {
|
||
'PRET_VANZARE': 'Preț unitar din factură (fact_vfacturi_detalii.pret)',
|
||
'COST': 'Preț achiziție (pret_achizitie)',
|
||
'PIERDERE': '(Preț vânzare - Cost) × Cantitate (negativ = pierdere)'
|
||
},
|
||
'stoc_curent': {
|
||
'TIP_GESTIUNE': 'Preț vânzare (nr_pag=7) sau Preț achiziție',
|
||
'VALOARE_STOC_ACHIZITIE': '(cants+cant-cante) × pret din vstoc',
|
||
'VALOARE_STOC_VANZARE': 'Doar pentru gestiuni preț vânzare, altfel gol'
|
||
},
|
||
'stoc_lent': {
|
||
'CANTITATE': 'Stoc final = cants + cant - cante',
|
||
'VALOARE': 'Cantitate × preț achiziție',
|
||
'ZILE_FARA_MISCARE': 'Zile de la ultima ieșire (dataout) sau intrare'
|
||
},
|
||
'rotatie_stocuri': {
|
||
'VALOARE_STOC': 'Stoc curent (cants+cant-cante) × preț achiziție',
|
||
'VANZARI_12_LUNI': 'Doar vânzări (nu transferuri/consumuri) din ultimele 12 luni',
|
||
'ROTATIE': 'Vânzări / Stoc (de câte ori s-a rotit stocul)',
|
||
'ZILE_STOC': 'La ritmul actual, în câte zile se epuizează'
|
||
},
|
||
'dispersie_preturi': {
|
||
'NR_TRANZACTII': 'Număr total linii factură pentru acest produs',
|
||
'VARIATIE_PROCENT': '(Preț Max - Preț Min) / Preț Mediu × 100',
|
||
'NR_LA_PRET_MIN': 'Câte tranzacții au fost la prețul minim',
|
||
'CLIENT_PRET_MIN': 'Primul client care a cumpărat la preț minim'
|
||
},
|
||
'top_produse': {
|
||
'VALOARE_VANZARI': 'SUM(cantitate × preț)',
|
||
'MARJA_BRUTA': 'SUM(cantitate × (preț - pret_achizitie))',
|
||
'PROCENT_MARJA': 'Marja / Vânzări × 100'
|
||
},
|
||
'marja_per_categorie': {
|
||
'VANZARI_FARA_TVA': 'Total vânzări pe subgrupă',
|
||
'COST_TOTAL': 'Total cost achiziție pe subgrupă',
|
||
'PROCENT_MARJA': 'Marja / Vânzări × 100'
|
||
},
|
||
'marja_per_gestiune': {
|
||
'VANZARI_FARA_TVA': 'Total vânzări pe gestiune (doar articole gestionabile)',
|
||
'MARJA_BRUTA': 'Total marjă pe gestiune',
|
||
'PROCENT_MARJA': 'Marja / Vânzări × 100'
|
||
},
|
||
'articole_negestionabile': {
|
||
'DENUMIRE': 'Nume articol negestionabil (in_stoc=0)',
|
||
'VANZARI_FARA_TVA': 'Total vânzări pentru articole care nu se țin pe stoc',
|
||
'MARJA_BRUTA': 'Vânzări - Cost',
|
||
'PROCENT_MARJA': 'Marja / Vânzări × 100'
|
||
},
|
||
'vanzari_lunare': {
|
||
'VANZARI_FARA_TVA': 'Total vânzări în lună',
|
||
'MARJA_BRUTA': 'Total marjă în lună',
|
||
'NR_FACTURI': 'Număr facturi emise',
|
||
'NR_CLIENTI': 'Clienți unici activi'
|
||
},
|
||
# NEW legends for financial and aggregated sheets
|
||
'indicatori_agregati_venituri': {
|
||
'LINIE_BUSINESS': 'Producție proprie / Materii prime / Marfă revândută',
|
||
'PROCENT_VENITURI': 'Contribuția la totalul vânzărilor',
|
||
'CONTRIBUTIE_PROFIT': 'Contribuția la profitul total (%)'
|
||
},
|
||
'sezonalitate_lunara': {
|
||
'MEDIE_VANZARI': 'Media vânzărilor pe 24 luni pentru această lună',
|
||
'DEVIERE_PROCENT': 'Cât de mult deviază de la media globală',
|
||
'CLASIFICARE': 'LUNĂ PUTERNICĂ / LUNĂ SLABĂ / NORMAL'
|
||
},
|
||
'portofoliu_clienti': {
|
||
'VALOARE': 'Numărul de clienți în fiecare categorie',
|
||
'EXPLICATIE': 'Definiția categoriei de clienți'
|
||
},
|
||
'concentrare_risc': {
|
||
'PROCENT': 'Procentul din vânzări pentru Top N clienți',
|
||
'STATUS': 'OK / ATENTIE / RISC MARE'
|
||
},
|
||
'ciclu_conversie_cash': {
|
||
'INDICATOR': 'DIO (zile stoc) + DSO (zile încasare) - DPO (zile plată)',
|
||
'ZILE': 'Numărul de zile pentru fiecare component',
|
||
'EXPLICATIE': 'Ce reprezintă fiecare indicator'
|
||
},
|
||
'clienti_ranking_profit': {
|
||
'RANG_PROFIT': 'Poziția clientului după profit (nu vânzări)',
|
||
'RANG_VANZARI': 'Poziția clientului după vânzări',
|
||
'PROFIT_BRUT': 'Vânzări - Cost = profitul efectiv adus'
|
||
},
|
||
'frecventa_clienti': {
|
||
'COMENZI_PE_LUNA': 'Media comenzilor pe lună',
|
||
'VALOARE_MEDIE_COMANDA': 'Valoarea medie per comandă',
|
||
'EVOLUTIE_FRECVENTA_YOY': 'Schimbarea frecvenței față de anul trecut'
|
||
},
|
||
'marja_client_categorie': {
|
||
'STATUS_MARJA': 'OK / MARJĂ MICĂ (<15%) / PIERDERE (negativă)',
|
||
'CATEGORIA': 'Grupa de produse',
|
||
'PROCENT_MARJA': 'Marja pentru acest client la această categorie'
|
||
},
|
||
'evolutie_discount': {
|
||
'PRET_INITIAL': 'Prețul mediu în primele 6 luni',
|
||
'PRET_ACTUAL': 'Prețul mediu în ultimele 6 luni',
|
||
'VARIATIE_PRET_PROCENT': 'Scăderea/creșterea prețului (negativ = discount)'
|
||
},
|
||
'dso_dpo': {
|
||
'DSO': 'Days Sales Outstanding - zile medii încasare clienți',
|
||
'DPO': 'Days Payables Outstanding - zile medii plată furnizori',
|
||
'STATUS': 'OK / ATENTIE / ALERTA'
|
||
},
|
||
'solduri_clienti': {
|
||
'SOLD_CURENT': 'Suma de încasat de la client (din cont 4111)',
|
||
'TIP_SOLD': 'Creanță (ne datorează) sau Avans client (am încasat în avans)'
|
||
},
|
||
'aging_creante': {
|
||
'NEAJUNS_SCADENTA': 'Facturi nescadente încă',
|
||
'ZILE_1_30': 'Restanțe 1-30 zile',
|
||
'PESTE_90_ZILE': 'Restanțe critice >90 zile - risc de neîncasare'
|
||
},
|
||
'facturi_restante': {
|
||
'ZILE_INTARZIERE': 'Zile de la scadență',
|
||
'SUMA_RESTANTA': 'Valoarea rămasă de încasat'
|
||
},
|
||
'aging_datorii': {
|
||
'NEAJUNS_SCADENTA': 'Datorii neajunse la scadență',
|
||
'ZILE_1_30': 'Restanțe 1-30 zile',
|
||
'ZILE_31_60': 'Restanțe 31-60 zile',
|
||
'ZILE_61_90': 'Restanțe 61-90 zile',
|
||
'PESTE_90_ZILE': 'Restanțe critice >90 zile',
|
||
'TOTAL_SOLD': 'Total datorii către furnizor'
|
||
},
|
||
'facturi_restante_furnizori': {
|
||
'ZILE_INTARZIERE': 'Zile de la scadență',
|
||
'SUMA_RESTANTA': 'Valoarea rămasă de plătit'
|
||
},
|
||
'solduri_furnizori': {
|
||
'SOLD_CURENT': 'Suma de plătit furnizorului (din cont 401)',
|
||
'TIP_SOLD': 'Datorie (trebuie să plătim) sau Avans (am plătit în avans)'
|
||
},
|
||
'pozitia_cash': {
|
||
'SOLD_CURENT': 'Disponibilul curent în cont/casă',
|
||
'DESCRIERE': 'Tipul contului (bancă/casă, lei/valută)'
|
||
},
|
||
# =====================================================================
|
||
# NEW: Legends for Indicatori Generali, Lichiditate, YoY sheets
|
||
# =====================================================================
|
||
'indicatori_generali': {
|
||
'INDICATOR': 'Grad îndatorare, autonomie financiară, ROA, marjă netă',
|
||
'VALOARE': 'Valoarea calculată a indicatorului',
|
||
'STATUS': 'OK / ATENȚIE / ALERTĂ bazat pe praguri standard',
|
||
'RECOMANDARE': 'Acțiune sugerată pentru îmbunătățire'
|
||
},
|
||
'indicatori_lichiditate': {
|
||
'INDICATOR': 'Lichiditate curentă, rapidă, cash ratio, fond de rulment',
|
||
'VALOARE': 'Valoarea calculată (rată sau sumă RON)',
|
||
'STATUS': 'OK / ATENȚIE / ALERTĂ',
|
||
'INTERPRETARE': 'Ce înseamnă valoarea pentru business'
|
||
},
|
||
'clasificare_datorii': {
|
||
'CATEGORIE': 'Termen scurt (<30z) / mediu (31-90z) / lung (>90z)',
|
||
'VALOARE': 'Suma datoriilor în categoria respectivă',
|
||
'NR_FACTURI': 'Numărul facturilor în acea categorie'
|
||
},
|
||
'grad_acoperire_datorii': {
|
||
'VALOARE': 'Cash disponibil + încasări așteptate vs plăți scadente',
|
||
'ACOPERIRE': 'OK / ATENȚIE / DEFICIT - dacă puteți plăti datoriile',
|
||
'EXPLICATIE': 'Ce înseamnă pentru fluxul de numerar'
|
||
},
|
||
'proiectie_lichiditate': {
|
||
'PERIOADA': 'Azi / 30 zile / 60 zile / 90 zile',
|
||
'SOLD_PROIECTAT': 'Cash estimat la sfârșitul perioadei',
|
||
'FLUX_NET': 'Încasări - Plăți pentru perioada respectivă',
|
||
'STATUS': 'OK dacă sold pozitiv, ALERTĂ dacă negativ'
|
||
},
|
||
'sumar_executiv_yoy': {
|
||
'VALOARE_CURENTA': 'Valoarea din ultimele 12 luni',
|
||
'VALOARE_ANTERIOARA': 'Valoarea din anul anterior (12-24 luni)',
|
||
'VARIATIE_PROCENT': 'Creștere/scădere procentuală',
|
||
'TREND': 'CREȘTERE / SCĂDERE / STABIL'
|
||
},
|
||
'dso_dpo_yoy': {
|
||
'VALOARE_CURENTA': 'Zile încasare/plată actuale',
|
||
'VALOARE_ANTERIOARA': 'Zile în perioada anterioară',
|
||
'VARIATIE_ZILE': 'Diferența în zile (+ = mai rău pentru DSO, mai bine pentru DPO)',
|
||
'TREND': 'ÎMBUNĂTĂȚIRE / DETERIORARE / STABIL'
|
||
},
|
||
'concentrare_risc_yoy': {
|
||
'PROCENT_CURENT': '% vânzări la Top N clienți - an curent',
|
||
'PROCENT_ANTERIOR': '% vânzări la Top N clienți - an trecut',
|
||
'VARIATIE': 'Schimbarea în puncte procentuale',
|
||
'TREND': 'DIVERSIFICARE (bine) / CONCENTRARE (risc) / STABIL'
|
||
},
|
||
'indicatori_agregati_venituri_yoy': {
|
||
'LINIE_BUSINESS': 'Producție proprie / Materii prime / Marfă',
|
||
'VANZARI_CURENTE': 'Vânzări în ultimele 12 luni',
|
||
'VANZARI_ANTERIOARE': 'Vânzări în perioada anterioară',
|
||
'VARIATIE_PROCENT': 'Creștere/scădere procentuală',
|
||
'TREND': 'CREȘTERE / SCĂDERE / STABIL'
|
||
},
|
||
'analiza_prajitorie': {
|
||
'CANTITATE_INTRARI': 'Cantitate intrata (cant > 0, cante = 0)',
|
||
'VALOARE_INTRARI': 'Valoare intrari = cantitate x pret',
|
||
'CANTITATE_IESIRI': 'Cantitate iesita (cant = 0, cante > 0)',
|
||
'VALOARE_IESIRI': 'Valoare iesiri = cantitate x pret',
|
||
'CANTITATE_TRANSFORMARI_IN': 'Cantitate intrata in transformari',
|
||
'CANTITATE_TRANSFORMARI_OUT': 'Cantitate iesita din transformari',
|
||
'SOLD_NET_CANTITATE': 'Sold net = Total intrari - Total iesiri',
|
||
'SOLD_NET_VALOARE': 'Valoare neta a soldului'
|
||
},
|
||
# =====================================================================
|
||
# LEGENDS FOR CONSOLIDATED SHEETS
|
||
# =====================================================================
|
||
'vedere_ansamblu': {
|
||
'INDICATOR': 'Denumirea indicatorului de business',
|
||
'VALOARE_CURENTA': 'Valoare în perioada curentă (ultimele 12 luni)',
|
||
'UM': 'Unitate de măsură',
|
||
'VALOARE_ANTERIOARA': 'Valoare în perioada anterioară (12-24 luni)',
|
||
'VARIATIE_PROCENT': 'Variație procentuală YoY',
|
||
'TREND': 'CREȘTERE/SCĂDERE/STABIL',
|
||
'STATUS': 'OK = bine, ATENȚIE = necesită atenție, ALERTĂ = acțiune urgentă',
|
||
'CATEGORIE': 'Domeniu: Marja, Clienți, Stoc, Financiar',
|
||
'RECOMANDARE': 'Acțiune sugerată'
|
||
},
|
||
'indicatori_venituri': {
|
||
'LINIE_BUSINESS': 'Producție proprie / Materii prime / Marfă revândută',
|
||
'VANZARI_CURENTE': 'Vânzări în ultimele 12 luni',
|
||
'PROCENT_VENITURI': 'Contribuția la totalul vânzărilor (%)',
|
||
'MARJA': 'Marja brută pe linia de business',
|
||
'PROCENT_MARJA': 'Marja procentuală',
|
||
'VANZARI_ANTERIOARE': 'Vânzări în perioada anterioară',
|
||
'VARIATIE_PROCENT': 'Creștere/scădere procentuală YoY',
|
||
'TREND': 'CREȘTERE / SCĂDERE / STABIL'
|
||
},
|
||
'clienti_risc': {
|
||
'CATEGORIE': 'Tipul de categorie clienți',
|
||
'VALOARE': 'Numărul de clienți sau valoarea',
|
||
'EXPLICATIE': 'Explicația categoriei',
|
||
'INDICATOR': 'Top 1/5/10 clienți',
|
||
'PROCENT_CURENT': '% vânzări la Top N clienți - an curent',
|
||
'PROCENT_ANTERIOR': '% vânzări la Top N clienți - an trecut',
|
||
'VARIATIE': 'Schimbarea în puncte procentuale',
|
||
'TREND': 'DIVERSIFICARE (bine) / CONCENTRARE (risc) / STABIL',
|
||
'STATUS': 'OK / ATENTIE / RISC MARE'
|
||
},
|
||
'tablou_financiar': {
|
||
'INDICATOR': 'Denumirea indicatorului financiar',
|
||
'VALOARE': 'Valoarea calculată',
|
||
'STATUS': 'OK / ATENȚIE / ALERTĂ',
|
||
'RECOMANDARE': 'Acțiune sugerată pentru îmbunătățire',
|
||
'INTERPRETARE': 'Ce înseamnă valoarea pentru business'
|
||
}
|
||
}
|
||
|
||
# =========================================================================
|
||
# GENERARE SHEET-URI CONSOLIDATE EXCEL
|
||
# =========================================================================
|
||
|
||
# --- Sheet 0: DASHBOARD COMPLET (toate secțiunile într-o singură vedere) ---
|
||
perf.start("EXCEL: Dashboard Complet sheet (12 sections)")
|
||
excel_gen.add_consolidated_sheet(
|
||
name='Dashboard Complet',
|
||
sheet_title='Dashboard Executiv - Vedere Completă',
|
||
sheet_description='Toate indicatorii cheie consolidați într-o singură vedere rapidă',
|
||
sections=[
|
||
# KPIs și Recomandări
|
||
{
|
||
'title': 'KPIs cu Comparație YoY',
|
||
'df': results.get('kpi_consolidated', pd.DataFrame()),
|
||
'description': 'Indicatori cheie de performanță - curent vs anterior',
|
||
'explanation': PDF_EXPLANATIONS['kpis']
|
||
},
|
||
{
|
||
'title': 'Recomandări Prioritare',
|
||
'df': results.get('recomandari', pd.DataFrame()).head(10),
|
||
'description': 'Top 10 acțiuni sugerate bazate pe analiză',
|
||
'explanation': PDF_EXPLANATIONS['recomandari']
|
||
},
|
||
# Venituri
|
||
{
|
||
'title': 'Venituri per Linie Business',
|
||
'df': results.get('venituri_consolidated', pd.DataFrame()),
|
||
'description': 'Producție proprie, Materii prime, Marfă revândută',
|
||
'explanation': PDF_EXPLANATIONS['venituri']
|
||
},
|
||
# Clienți și Risc
|
||
{
|
||
'title': 'Portofoliu Clienți',
|
||
'df': results.get('portofoliu_clienti', pd.DataFrame()),
|
||
'description': 'Structura și segmentarea clienților',
|
||
'explanation': PDF_EXPLANATIONS['portofoliu_clienti']
|
||
},
|
||
{
|
||
'title': 'Concentrare Risc YoY',
|
||
'df': results.get('risc_consolidated', pd.DataFrame()),
|
||
'description': 'Dependența de clienții mari - curent vs anterior',
|
||
'explanation': PDF_EXPLANATIONS['risc_concentrare']
|
||
},
|
||
# Tablou Financiar - with DYNAMIC explanations
|
||
{
|
||
'title': 'Indicatori Generali',
|
||
'df': results.get('indicatori_generali', pd.DataFrame()),
|
||
'description': 'Ratii financiare: indatorare, autonomie, datorii, rentabilitate',
|
||
'explanation': generate_indicatori_generali_explanation(results.get('indicatori_generali'))
|
||
},
|
||
{
|
||
'title': 'Indicatori Lichiditate',
|
||
'df': results.get('indicatori_lichiditate', pd.DataFrame()),
|
||
'description': 'Capacitatea de plata pe termen scurt',
|
||
'explanation': generate_indicatori_lichiditate_explanation(results.get('indicatori_lichiditate'))
|
||
},
|
||
# NEW: Ciclu Conversie Cash (was missing from Dashboard)
|
||
{
|
||
'title': 'Ciclu Conversie Cash',
|
||
'df': results.get('ciclu_conversie_cash', pd.DataFrame()),
|
||
'description': 'DIO (zile stoc) + DSO (zile incasare) - DPO (zile plata) = CCC',
|
||
'explanation': generate_ciclu_cash_explanation(results.get('ciclu_conversie_cash'))
|
||
},
|
||
# NEW: Solduri Clienti (Top 10 - was missing from Dashboard)
|
||
{
|
||
'title': 'Solduri Clienti (Top 10)',
|
||
'df': results.get('solduri_clienti', pd.DataFrame()).head(10) if results.get('solduri_clienti') is not None and not results.get('solduri_clienti', pd.DataFrame()).empty else pd.DataFrame(),
|
||
'description': 'Creante de incasat din cont 4111',
|
||
'explanation': generate_solduri_clienti_explanation(results.get('solduri_clienti'))
|
||
},
|
||
# NEW: Solduri Furnizori (Top 10 - was missing from Dashboard)
|
||
{
|
||
'title': 'Solduri Furnizori (Top 10)',
|
||
'df': results.get('solduri_furnizori', pd.DataFrame()).head(10) if results.get('solduri_furnizori') is not None and not results.get('solduri_furnizori', pd.DataFrame()).empty else pd.DataFrame(),
|
||
'description': 'Datorii de platit din cont 401',
|
||
'explanation': generate_solduri_furnizori_explanation(results.get('solduri_furnizori'))
|
||
},
|
||
{
|
||
'title': 'Clasificare Datorii',
|
||
'df': results.get('clasificare_datorii', pd.DataFrame()),
|
||
'description': 'Datorii pe intervale de întârziere',
|
||
'explanation': PDF_EXPLANATIONS['clasificare_datorii']
|
||
},
|
||
{
|
||
'title': 'Proiecție Lichiditate',
|
||
'df': results.get('proiectie_lichiditate', pd.DataFrame()),
|
||
'description': 'Previziune încasări și plăți pe 30 zile',
|
||
'explanation': PDF_EXPLANATIONS['proiectie_lichiditate']
|
||
}
|
||
]
|
||
)
|
||
perf.stop()
|
||
|
||
# NOTE: Sheet-urile individuale (Vedere Ansamblu, Indicatori Venituri, Clienti si Risc,
|
||
# Tablou Financiar) au fost eliminate - toate datele sunt acum în Dashboard Complet
|
||
|
||
# --- Adaugă restul sheet-urilor de detaliu ---
|
||
# Skip sheet-urile care sunt acum în view-urile consolidate
|
||
consolidated_sheets = {
|
||
'vedere_ansamblu', 'indicatori_venituri', 'clienti_risc', 'tablou_financiar',
|
||
# Sheet-uri incluse în consolidări (nu mai sunt separate):
|
||
'sumar_executiv', 'sumar_executiv_yoy', 'recomandari',
|
||
'indicatori_agregati_venituri', 'indicatori_agregati_venituri_yoy',
|
||
'portofoliu_clienti', 'concentrare_risc', 'concentrare_risc_yoy',
|
||
'indicatori_generali', 'indicatori_lichiditate', 'clasificare_datorii',
|
||
'grad_acoperire_datorii', 'proiectie_lichiditate'
|
||
}
|
||
|
||
for query_name in sheet_order:
|
||
# Skip consolidated sheets and their source sheets
|
||
if query_name in consolidated_sheets:
|
||
continue
|
||
|
||
if query_name in results and query_name in QUERIES:
|
||
query_info = QUERIES[query_name]
|
||
# Create short sheet name from query name
|
||
sheet_name = query_name.replace('_', ' ').title()[:31]
|
||
perf.start(f"EXCEL: {query_name} detail sheet")
|
||
excel_gen.add_sheet(
|
||
name=sheet_name,
|
||
df=results[query_name],
|
||
title=query_info.get('title', query_name),
|
||
description=query_info.get('description', ''),
|
||
legend=legends.get(query_name)
|
||
)
|
||
df_rows = len(results[query_name]) if results[query_name] is not None else 0
|
||
perf.stop(rows=df_rows)
|
||
|
||
perf.start("EXCEL: Save workbook")
|
||
excel_gen.save()
|
||
perf.stop()
|
||
|
||
# =========================================================================
|
||
# GENERARE PDF - PAGINI CONSOLIDATE
|
||
# =========================================================================
|
||
print("\n📄 Generare raport PDF...")
|
||
pdf_gen = PDFReportGenerator(pdf_path, company_name=COMPANY_NAME)
|
||
|
||
# Pagina 1: Titlu
|
||
perf.start("PDF: Title page")
|
||
pdf_gen.add_title_page()
|
||
perf.stop()
|
||
|
||
# Pagina 2-3: DASHBOARD COMPLET (toate secțiunile într-o vedere unificată)
|
||
perf.start("PDF: Dashboard Complet page (4 sections)")
|
||
pdf_gen.add_explanation(PDF_EXPLANATIONS['kpis'])
|
||
pdf_gen.add_consolidated_page(
|
||
'Dashboard Complet',
|
||
sections=[
|
||
{
|
||
'title': 'KPIs cu Comparație YoY',
|
||
'df': results.get('kpi_consolidated', pd.DataFrame()),
|
||
'columns': ['INDICATOR', 'VALOARE_CURENTA', 'UM', 'VALOARE_ANTERIOARA', 'VARIATIE_PROCENT', 'TREND'],
|
||
'max_rows': 6
|
||
},
|
||
{
|
||
'title': 'Recomandări Prioritare',
|
||
'df': results.get('recomandari', pd.DataFrame()),
|
||
'columns': ['STATUS', 'CATEGORIE', 'INDICATOR', 'RECOMANDARE'],
|
||
'max_rows': 5
|
||
},
|
||
{
|
||
'title': 'Venituri per Linie Business',
|
||
'df': results.get('venituri_consolidated', pd.DataFrame()),
|
||
'columns': ['LINIE_BUSINESS', 'VANZARI_CURENTE', 'PROCENT_VENITURI', 'VARIATIE_PROCENT', 'TREND'],
|
||
'max_rows': 5
|
||
},
|
||
{
|
||
'title': 'Concentrare Risc YoY',
|
||
'df': results.get('risc_consolidated', pd.DataFrame()),
|
||
'columns': ['INDICATOR', 'PROCENT_CURENT', 'PROCENT_ANTERIOR', 'TREND'],
|
||
'max_rows': 4
|
||
}
|
||
]
|
||
)
|
||
perf.stop()
|
||
|
||
# NOTE: Paginile individuale (Vedere Executivă, Indicatori Venituri, Clienți și Risc,
|
||
# Tablou Financiar) au fost eliminate - toate datele sunt acum în Dashboard Complet
|
||
|
||
pdf_gen.add_page_break()
|
||
|
||
# Alerte (vânzări sub cost, clienți marjă mică)
|
||
perf.start("PDF: Alerts section")
|
||
pdf_gen.add_explanation(PDF_EXPLANATIONS['alerte_critice'])
|
||
pdf_gen.add_alerts_section({
|
||
'vanzari_sub_cost': results.get('vanzari_sub_cost', pd.DataFrame()),
|
||
'clienti_marja_mica': results.get('clienti_marja_mica', pd.DataFrame())
|
||
})
|
||
perf.stop()
|
||
|
||
pdf_gen.add_page_break()
|
||
|
||
# =========================================================================
|
||
# PAGINI DE GRAFICE ȘI DETALII
|
||
# =========================================================================
|
||
|
||
# Grafic: Evoluția Vânzărilor Lunare
|
||
if 'vanzari_lunare' in results and not results['vanzari_lunare'].empty:
|
||
perf.start("PDF: Chart - vanzari_lunare")
|
||
pdf_gen.add_explanation(PDF_EXPLANATIONS['evolutie_lunara'])
|
||
fig = create_monthly_chart(results['vanzari_lunare'])
|
||
pdf_gen.add_chart_image(fig, "Evoluția Vânzărilor și Marjei")
|
||
perf.stop()
|
||
|
||
# Grafic: Concentrare Clienți
|
||
if 'concentrare_clienti' in results and not results['concentrare_clienti'].empty:
|
||
perf.start("PDF: Chart - concentrare_clienti")
|
||
pdf_gen.add_explanation(PDF_EXPLANATIONS['concentrare_clienti'])
|
||
fig = create_client_concentration_chart(results['concentrare_clienti'])
|
||
pdf_gen.add_chart_image(fig, "Concentrare Clienți")
|
||
perf.stop()
|
||
|
||
pdf_gen.add_page_break()
|
||
|
||
# Grafic: Ciclu Conversie Cash - with DYNAMIC explanation showing actual values
|
||
if 'ciclu_conversie_cash' in results and not results['ciclu_conversie_cash'].empty:
|
||
perf.start("PDF: Chart - ciclu_conversie_cash")
|
||
pdf_gen.add_explanation(generate_ciclu_cash_explanation(results.get('ciclu_conversie_cash')))
|
||
fig = create_cash_cycle_chart(results['ciclu_conversie_cash'])
|
||
pdf_gen.add_chart_image(fig, "Ciclu Conversie Cash (DIO + DSO - DPO)")
|
||
perf.stop()
|
||
|
||
# Grafic: Producție vs Revânzare
|
||
if 'productie_vs_revanzare' in results and not results['productie_vs_revanzare'].empty:
|
||
perf.start("PDF: Chart - productie_vs_revanzare")
|
||
fig = create_production_chart(results['productie_vs_revanzare'])
|
||
pdf_gen.add_chart_image(fig, "Producție Proprie vs Revânzare")
|
||
perf.stop()
|
||
|
||
# Tabel: Top clienți
|
||
pdf_gen.add_explanation(PDF_EXPLANATIONS['top_clienti_produse'])
|
||
pdf_gen.add_table_section(
|
||
"Top 15 Clienți după Vânzări",
|
||
results.get('marja_per_client'),
|
||
columns=['CLIENT', 'VANZARI_FARA_TVA', 'MARJA_BRUTA', 'PROCENT_MARJA'],
|
||
max_rows=15
|
||
)
|
||
|
||
pdf_gen.add_page_break()
|
||
|
||
# Tabel: Top produse
|
||
pdf_gen.add_table_section(
|
||
"Top 15 Produse după Vânzări",
|
||
results.get('top_produse'),
|
||
columns=['PRODUS', 'VALOARE_VANZARI', 'MARJA_BRUTA', 'PROCENT_MARJA'],
|
||
max_rows=15
|
||
)
|
||
|
||
# Tabel: Trending clienți
|
||
pdf_gen.add_table_section(
|
||
"Trending Clienți (YoY)",
|
||
results.get('trending_clienti'),
|
||
columns=['CLIENT', 'VANZARI_12_LUNI', 'VANZARI_AN_ANTERIOR', 'VARIATIE_PROCENT', 'TREND'],
|
||
max_rows=15
|
||
)
|
||
|
||
# Tabel: Aging Creanțe
|
||
if 'aging_creante' in results and not results['aging_creante'].empty:
|
||
pdf_gen.add_page_break()
|
||
pdf_gen.add_explanation(PDF_EXPLANATIONS['aging_creante'])
|
||
pdf_gen.add_table_section(
|
||
"Aging Creanțe (Vechime Facturi Neîncasate)",
|
||
results.get('aging_creante'),
|
||
columns=['CLIENT', 'NEAJUNS_SCADENTA', 'ZILE_1_30', 'ZILE_31_60', 'PESTE_90_ZILE', 'TOTAL_SOLD'],
|
||
max_rows=15
|
||
)
|
||
|
||
# Tabel: Stoc lent
|
||
if 'stoc_lent' in results and not results['stoc_lent'].empty:
|
||
pdf_gen.add_page_break()
|
||
pdf_gen.add_explanation(PDF_EXPLANATIONS['stoc_lent'])
|
||
pdf_gen.add_table_section(
|
||
"Stoc Lent (>90 zile fără mișcare)",
|
||
results.get('stoc_lent'),
|
||
columns=['PRODUS', 'NUME_GESTIUNE', 'CANTITATE', 'VALOARE', 'ZILE_FARA_MISCARE'],
|
||
max_rows=20
|
||
)
|
||
|
||
perf.start("PDF: Save document")
|
||
pdf_gen.save()
|
||
perf.stop()
|
||
|
||
# Performance Summary
|
||
perf.summary(output_path=str(args.output_dir))
|
||
|
||
# Summary
|
||
print("\n" + "="*60)
|
||
print(" ✅ RAPOARTE GENERATE CU SUCCES!")
|
||
print("="*60)
|
||
print(f"\n 📊 Excel: {excel_path}")
|
||
print(f" 📄 PDF: {pdf_path}")
|
||
print("\n" + "="*60)
|
||
|
||
return excel_path, pdf_path
|
||
|
||
|
||
def main():
|
||
"""Entry point"""
|
||
parser = argparse.ArgumentParser(
|
||
description='Data Intelligence Report Generator',
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
Exemple:
|
||
python main.py # Raport pentru ultimele 12 luni
|
||
python main.py --months 6 # Raport pentru ultimele 6 luni
|
||
python main.py --output-dir /tmp # Salvare în alt director
|
||
"""
|
||
)
|
||
|
||
parser.add_argument(
|
||
'--months', '-m',
|
||
type=int,
|
||
default=ANALYSIS_MONTHS,
|
||
help=f'Numărul de luni pentru analiză (default: {ANALYSIS_MONTHS})'
|
||
)
|
||
|
||
parser.add_argument(
|
||
'--output-dir', '-o',
|
||
type=Path,
|
||
default=OUTPUT_DIR,
|
||
help=f'Directorul pentru output (default: {OUTPUT_DIR})'
|
||
)
|
||
|
||
args = parser.parse_args()
|
||
|
||
# Ensure output directory exists
|
||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
try:
|
||
generate_reports(args)
|
||
except KeyboardInterrupt:
|
||
print("\n\n⚠️ Întrerupt de utilizator.")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(f"\n❌ Eroare: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|