Compare commits

...

2 Commits

Author SHA1 Message Date
bc05d02319 Add manager-friendly explanations and dynamic generators
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>
2025-12-12 16:20:30 +02:00
38d8f9c6d2 Add FORMULA column to financial indicator queries
Added detailed calculation formulas with accounting references for managers
to understand and verify financial indicators:

- INDICATORI_GENERALI (6 formulas): grad indatorare, autonomie financiara,
  rata datoriilor, marja neta, ROA, rotatia activelor
- INDICATORI_LICHIDITATE (4 formulas): lichiditate curenta, quick ratio,
  cash ratio, fond de rulment
- CICLU_CONVERSIE_CASH (4 formulas): DIO, DSO, DPO, CCC
- DSO_DPO (2 formulas): DSO, DPO with detailed sources

Each formula includes:
- Mathematical calculation (e.g., "Datorii / Capital Propriu")
- Account references (e.g., "cont 16x,40x,42x,44x,46x / cont 10x,11x,12x,117")
- Data sources for verification (e.g., "jurnal vanzari vjv2025")

Also fixed ORA-32039 error by renaming CTE 'vanzari' to 'vanzari_calc'
to avoid conflict with table name.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 16:19:53 +02:00
3 changed files with 826 additions and 388 deletions

219
main.py
View File

@@ -63,6 +63,156 @@ from report_generator import (
from recommendations import RecommendationsEngine 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: class PerformanceLogger:
"""Tracks execution time for each operation to identify bottlenecks.""" """Tracks execution time for each operation to identify bottlenecks."""
@@ -138,11 +288,16 @@ class OracleConnection:
def __enter__(self): def __enter__(self):
try: try:
print(f"🔌 Conectare la Oracle: {get_dsn()}...") print(f"🔌 Conectare la Oracle: {get_dsn()}...")
# Add TCP keepalive and timeout settings to prevent connection drops
self.connection = oracledb.connect( self.connection = oracledb.connect(
user=ORACLE_CONFIG['user'], user=ORACLE_CONFIG['user'],
password=ORACLE_CONFIG['password'], password=ORACLE_CONFIG['password'],
dsn=get_dsn() 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!") print("✓ Conectat cu succes!")
return self.connection return self.connection
except oracledb.Error as e: except oracledb.Error as e:
@@ -634,7 +789,7 @@ def generate_reports(args):
# ========================================================================= # =========================================================================
# --- Sheet 0: DASHBOARD COMPLET (toate secțiunile într-o singură vedere) --- # --- Sheet 0: DASHBOARD COMPLET (toate secțiunile într-o singură vedere) ---
perf.start("EXCEL: Dashboard Complet sheet (9 sections)") perf.start("EXCEL: Dashboard Complet sheet (12 sections)")
excel_gen.add_consolidated_sheet( excel_gen.add_consolidated_sheet(
name='Dashboard Complet', name='Dashboard Complet',
sheet_title='Dashboard Executiv - Vedere Completă', sheet_title='Dashboard Executiv - Vedere Completă',
@@ -644,50 +799,80 @@ def generate_reports(args):
{ {
'title': 'KPIs cu Comparație YoY', 'title': 'KPIs cu Comparație YoY',
'df': results.get('kpi_consolidated', pd.DataFrame()), 'df': results.get('kpi_consolidated', pd.DataFrame()),
'description': 'Indicatori cheie de performanță - curent vs anterior' 'description': 'Indicatori cheie de performanță - curent vs anterior',
'explanation': PDF_EXPLANATIONS['kpis']
}, },
{ {
'title': 'Recomandări Prioritare', 'title': 'Recomandări Prioritare',
'df': results.get('recomandari', pd.DataFrame()).head(10), 'df': results.get('recomandari', pd.DataFrame()).head(10),
'description': 'Top 10 acțiuni sugerate bazate pe analiză' 'description': 'Top 10 acțiuni sugerate bazate pe analiză',
'explanation': PDF_EXPLANATIONS['recomandari']
}, },
# Venituri # Venituri
{ {
'title': 'Venituri per Linie Business', 'title': 'Venituri per Linie Business',
'df': results.get('venituri_consolidated', pd.DataFrame()), 'df': results.get('venituri_consolidated', pd.DataFrame()),
'description': 'Producție proprie, Materii prime, Marfă revândută' 'description': 'Producție proprie, Materii prime, Marfă revândută',
'explanation': PDF_EXPLANATIONS['venituri']
}, },
# Clienți și Risc # Clienți și Risc
{ {
'title': 'Portofoliu Clienți', 'title': 'Portofoliu Clienți',
'df': results.get('portofoliu_clienti', pd.DataFrame()), 'df': results.get('portofoliu_clienti', pd.DataFrame()),
'description': 'Structura și segmentarea clienților' 'description': 'Structura și segmentarea clienților',
'explanation': PDF_EXPLANATIONS['portofoliu_clienti']
}, },
{ {
'title': 'Concentrare Risc YoY', 'title': 'Concentrare Risc YoY',
'df': results.get('risc_consolidated', pd.DataFrame()), 'df': results.get('risc_consolidated', pd.DataFrame()),
'description': 'Dependența de clienții mari - curent vs anterior' 'description': 'Dependența de clienții mari - curent vs anterior',
'explanation': PDF_EXPLANATIONS['risc_concentrare']
}, },
# Tablou Financiar # Tablou Financiar - with DYNAMIC explanations
{ {
'title': 'Indicatori Generali', 'title': 'Indicatori Generali',
'df': results.get('indicatori_generali', pd.DataFrame()), 'df': results.get('indicatori_generali', pd.DataFrame()),
'description': 'Sold clienți, furnizori, cifra afaceri' 'description': 'Ratii financiare: indatorare, autonomie, datorii, rentabilitate',
'explanation': generate_indicatori_generali_explanation(results.get('indicatori_generali'))
}, },
{ {
'title': 'Indicatori Lichiditate', 'title': 'Indicatori Lichiditate',
'df': results.get('indicatori_lichiditate', pd.DataFrame()), 'df': results.get('indicatori_lichiditate', pd.DataFrame()),
'description': 'Zile rotație stoc, creanțe, datorii' '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', 'title': 'Clasificare Datorii',
'df': results.get('clasificare_datorii', pd.DataFrame()), 'df': results.get('clasificare_datorii', pd.DataFrame()),
'description': 'Datorii pe intervale de întârziere' 'description': 'Datorii pe intervale de întârziere',
'explanation': PDF_EXPLANATIONS['clasificare_datorii']
}, },
{ {
'title': 'Proiecție Lichiditate', 'title': 'Proiecție Lichiditate',
'df': results.get('proiectie_lichiditate', pd.DataFrame()), 'df': results.get('proiectie_lichiditate', pd.DataFrame()),
'description': 'Previziune încasări și plăți pe 30 zile' 'description': 'Previziune încasări și plăți pe 30 zile',
'explanation': PDF_EXPLANATIONS['proiectie_lichiditate']
} }
] ]
) )
@@ -745,6 +930,7 @@ def generate_reports(args):
# Pagina 2-3: DASHBOARD COMPLET (toate secțiunile într-o vedere unificată) # Pagina 2-3: DASHBOARD COMPLET (toate secțiunile într-o vedere unificată)
perf.start("PDF: Dashboard Complet page (4 sections)") perf.start("PDF: Dashboard Complet page (4 sections)")
pdf_gen.add_explanation(PDF_EXPLANATIONS['kpis'])
pdf_gen.add_consolidated_page( pdf_gen.add_consolidated_page(
'Dashboard Complet', 'Dashboard Complet',
sections=[ sections=[
@@ -783,6 +969,7 @@ def generate_reports(args):
# Alerte (vânzări sub cost, clienți marjă mică) # Alerte (vânzări sub cost, clienți marjă mică)
perf.start("PDF: Alerts section") perf.start("PDF: Alerts section")
pdf_gen.add_explanation(PDF_EXPLANATIONS['alerte_critice'])
pdf_gen.add_alerts_section({ pdf_gen.add_alerts_section({
'vanzari_sub_cost': results.get('vanzari_sub_cost', pd.DataFrame()), 'vanzari_sub_cost': results.get('vanzari_sub_cost', pd.DataFrame()),
'clienti_marja_mica': results.get('clienti_marja_mica', pd.DataFrame()) 'clienti_marja_mica': results.get('clienti_marja_mica', pd.DataFrame())
@@ -798,6 +985,7 @@ def generate_reports(args):
# Grafic: Evoluția Vânzărilor Lunare # Grafic: Evoluția Vânzărilor Lunare
if 'vanzari_lunare' in results and not results['vanzari_lunare'].empty: if 'vanzari_lunare' in results and not results['vanzari_lunare'].empty:
perf.start("PDF: Chart - vanzari_lunare") perf.start("PDF: Chart - vanzari_lunare")
pdf_gen.add_explanation(PDF_EXPLANATIONS['evolutie_lunara'])
fig = create_monthly_chart(results['vanzari_lunare']) fig = create_monthly_chart(results['vanzari_lunare'])
pdf_gen.add_chart_image(fig, "Evoluția Vânzărilor și Marjei") pdf_gen.add_chart_image(fig, "Evoluția Vânzărilor și Marjei")
perf.stop() perf.stop()
@@ -805,15 +993,17 @@ def generate_reports(args):
# Grafic: Concentrare Clienți # Grafic: Concentrare Clienți
if 'concentrare_clienti' in results and not results['concentrare_clienti'].empty: if 'concentrare_clienti' in results and not results['concentrare_clienti'].empty:
perf.start("PDF: Chart - concentrare_clienti") perf.start("PDF: Chart - concentrare_clienti")
pdf_gen.add_explanation(PDF_EXPLANATIONS['concentrare_clienti'])
fig = create_client_concentration_chart(results['concentrare_clienti']) fig = create_client_concentration_chart(results['concentrare_clienti'])
pdf_gen.add_chart_image(fig, "Concentrare Clienți") pdf_gen.add_chart_image(fig, "Concentrare Clienți")
perf.stop() perf.stop()
pdf_gen.add_page_break() pdf_gen.add_page_break()
# Grafic: Ciclu Conversie Cash # Grafic: Ciclu Conversie Cash - with DYNAMIC explanation showing actual values
if 'ciclu_conversie_cash' in results and not results['ciclu_conversie_cash'].empty: if 'ciclu_conversie_cash' in results and not results['ciclu_conversie_cash'].empty:
perf.start("PDF: Chart - ciclu_conversie_cash") 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']) fig = create_cash_cycle_chart(results['ciclu_conversie_cash'])
pdf_gen.add_chart_image(fig, "Ciclu Conversie Cash (DIO + DSO - DPO)") pdf_gen.add_chart_image(fig, "Ciclu Conversie Cash (DIO + DSO - DPO)")
perf.stop() perf.stop()
@@ -826,6 +1016,7 @@ def generate_reports(args):
perf.stop() perf.stop()
# Tabel: Top clienți # Tabel: Top clienți
pdf_gen.add_explanation(PDF_EXPLANATIONS['top_clienti_produse'])
pdf_gen.add_table_section( pdf_gen.add_table_section(
"Top 15 Clienți după Vânzări", "Top 15 Clienți după Vânzări",
results.get('marja_per_client'), results.get('marja_per_client'),
@@ -854,6 +1045,7 @@ def generate_reports(args):
# Tabel: Aging Creanțe # Tabel: Aging Creanțe
if 'aging_creante' in results and not results['aging_creante'].empty: if 'aging_creante' in results and not results['aging_creante'].empty:
pdf_gen.add_page_break() pdf_gen.add_page_break()
pdf_gen.add_explanation(PDF_EXPLANATIONS['aging_creante'])
pdf_gen.add_table_section( pdf_gen.add_table_section(
"Aging Creanțe (Vechime Facturi Neîncasate)", "Aging Creanțe (Vechime Facturi Neîncasate)",
results.get('aging_creante'), results.get('aging_creante'),
@@ -864,6 +1056,7 @@ def generate_reports(args):
# Tabel: Stoc lent # Tabel: Stoc lent
if 'stoc_lent' in results and not results['stoc_lent'].empty: if 'stoc_lent' in results and not results['stoc_lent'].empty:
pdf_gen.add_page_break() pdf_gen.add_page_break()
pdf_gen.add_explanation(PDF_EXPLANATIONS['stoc_lent'])
pdf_gen.add_table_section( pdf_gen.add_table_section(
"Stoc Lent (>90 zile fără mișcare)", "Stoc Lent (>90 zile fără mișcare)",
results.get('stoc_lent'), results.get('stoc_lent'),

View File

@@ -1,6 +1,8 @@
""" """
SQL Queries for Data Intelligence Report SQL Queries for Data Intelligence Report
All queries use the existing views: fact_vfacturi2, fact_vfacturi_detalii, vstoc, vrul OPTIMIZED: All queries use base tables (vanzari, vanzari_detalii) with Oracle hints
for index usage (IDX_VANZARI_NR) instead of views (fact_vfacturi2, fact_vfacturi_detalii).
Other views: vstoc, vrul are still used where appropriate.
IMPORTANT: Price calculation considers pret_cu_tva flag: IMPORTANT: Price calculation considers pret_cu_tva flag:
- If pret_cu_tva = 1: price includes VAT, must divide by (1 + proc_tvav/100) - If pret_cu_tva = 1: price includes VAT, must divide by (1 + proc_tvav/100)
@@ -9,13 +11,13 @@ Formula: CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.
""" """
# ============================================================================= # =============================================================================
# 1. MARJA PER CLIENT # 1. MARJA PER CLIENT (OPTIMIZAT - folosește tabele de bază + indexuri)
# ============================================================================= # =============================================================================
MARJA_PER_CLIENT = """ MARJA_PER_CLIENT = """
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
f.id_part, f.id_part,
f.client, p.denumire AS client,
f.cod_fiscal, p.cod_fiscal,
COUNT(DISTINCT f.id_vanzare) AS nr_facturi, COUNT(DISTINCT f.id_vanzare) AS nr_facturi,
ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS vanzari_fara_tva, ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS vanzari_fara_tva,
ROUND(SUM(d.cantitate * d.pret_achizitie), 2) AS cost_total, ROUND(SUM(d.cantitate * d.pret_achizitie), 2) AS cost_total,
@@ -25,25 +27,25 @@ SELECT
THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END)
ELSE 0 END ELSE 0 END
, 2) AS procent_marja , 2) AS procent_marja
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
LEFT JOIN nom_parteneri p ON f.id_part = p.id_part
WHERE f.sters = 0 WHERE f.sters = 0
AND d.sters = 0
AND f.tip > 0 AND f.tip > 0
AND f.tip NOT IN (7, 8, 9, 24) AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -:months)
GROUP BY f.id_part, f.client, f.cod_fiscal GROUP BY f.id_part, p.denumire, p.cod_fiscal
ORDER BY marja_bruta DESC ORDER BY marja_bruta DESC
""" """
# ============================================================================= # =============================================================================
# 2. CLIENȚI CU MARJĂ MICĂ (sub prag) # 2. CLIENȚI CU MARJĂ MICĂ (sub prag) - OPTIMIZAT
# ============================================================================= # =============================================================================
CLIENTI_MARJA_MICA = """ CLIENTI_MARJA_MICA = """
SELECT * FROM ( SELECT * FROM (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
f.client, p.denumire AS client,
f.cod_fiscal, p.cod_fiscal,
ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS vanzari_fara_tva, ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS vanzari_fara_tva,
ROUND(SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)), 2) AS marja_bruta, ROUND(SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)), 2) AS marja_bruta,
ROUND( ROUND(
@@ -51,12 +53,13 @@ SELECT * FROM (
THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END)
ELSE 0 END ELSE 0 END
, 2) AS procent_marja , 2) AS procent_marja
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 LEFT JOIN nom_parteneri p ON f.id_part = p.id_part
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -:months)
GROUP BY f.id_part, f.client, f.cod_fiscal GROUP BY f.id_part, p.denumire, p.cod_fiscal
HAVING SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) > :min_sales HAVING SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) > :min_sales
) )
WHERE procent_marja < :margin_threshold WHERE procent_marja < :margin_threshold
@@ -64,12 +67,12 @@ ORDER BY vanzari_fara_tva DESC
""" """
# ============================================================================= # =============================================================================
# 3. MARJA PER CATEGORIE (Grupă + Subgrupă) # 3. MARJA PER CATEGORIE (Grupă + Subgrupă) - OPTIMIZAT
# ============================================================================= # =============================================================================
MARJA_PER_CATEGORIE = """ MARJA_PER_CATEGORIE = """
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
NVL(sg.grupa, 'NECLASIFICAT') AS grupa, NVL(sg.grupa, 'NECLASIFICAT') AS grupa,
NVL(d.subgrupa, 'NECLASIFICAT') AS subgrupa, NVL(sg.subgrupa, 'NECLASIFICAT') AS subgrupa,
COUNT(DISTINCT f.id_vanzare) AS nr_facturi, COUNT(DISTINCT f.id_vanzare) AS nr_facturi,
ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS vanzari_fara_tva, ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS vanzari_fara_tva,
ROUND(SUM(d.cantitate * d.pret_achizitie), 2) AS cost_total, ROUND(SUM(d.cantitate * d.pret_achizitie), 2) AS cost_total,
@@ -79,21 +82,22 @@ SELECT
THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END)
ELSE 0 END ELSE 0 END
, 2) AS procent_marja , 2) AS procent_marja
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
LEFT JOIN vgest_art_sbgr sg ON d.id_subgrupa = sg.id_subgrupa LEFT JOIN nom_articole art ON d.id_articol = art.id_articol
WHERE f.sters = 0 AND d.sters = 0 LEFT JOIN vgest_art_sbgr sg ON art.id_subgrupa = sg.id_subgrupa
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -:months)
GROUP BY sg.id_grupa, sg.grupa, d.id_subgrupa, d.subgrupa GROUP BY sg.id_grupa, sg.grupa, art.id_subgrupa, sg.subgrupa
ORDER BY vanzari_fara_tva DESC ORDER BY vanzari_fara_tva DESC
""" """
# ============================================================================= # =============================================================================
# 4. PRODUCȚIE PROPRIE vs MARFĂ REVÂNDUTĂ # 4. PRODUCȚIE PROPRIE vs MARFĂ REVÂNDUTĂ - OPTIMIZAT
# ============================================================================= # =============================================================================
PRODUCTIE_VS_REVANZARE = """ PRODUCTIE_VS_REVANZARE = """
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
CASE CASE
WHEN d.cont IN ('341', '345') THEN 'Producție proprie' WHEN d.cont IN ('341', '345') THEN 'Producție proprie'
WHEN d.cont = '301' THEN 'Materii prime' WHEN d.cont = '301' THEN 'Materii prime'
@@ -107,11 +111,11 @@ SELECT
THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END)
ELSE 0 END ELSE 0 END
, 2) AS procent_marja , 2) AS procent_marja
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -:months)
GROUP BY CASE GROUP BY CASE
WHEN d.cont IN ('341', '345') THEN 'Producție proprie' WHEN d.cont IN ('341', '345') THEN 'Producție proprie'
WHEN d.cont = '301' THEN 'Materii prime' WHEN d.cont = '301' THEN 'Materii prime'
@@ -125,18 +129,21 @@ ORDER BY vanzari_fara_tva DESC
# ============================================================================= # =============================================================================
DISPERSIE_PRETURI = """ DISPERSIE_PRETURI = """
WITH preturi_detalii AS ( WITH preturi_detalii AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
d.id_articol, d.id_articol,
d.denumire, a.denumire,
d.subgrupa, g.subgrupa,
f.id_part, f.id_part,
f.client, p.denumire as client,
CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END AS pret_fara_tva, CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END AS pret_fara_tva,
d.cantitate, d.cantitate,
MIN(CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) OVER (PARTITION BY d.id_articol) AS pret_min_global MIN(CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) OVER (PARTITION BY d.id_articol) AS pret_min_global
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 LEFT JOIN nom_parteneri p ON f.id_part = p.id_part
LEFT JOIN nom_articole a ON d.id_articol = a.id_articol
LEFT JOIN gest_art_sbgr g ON a.id_subgrupa = g.id_subgrupa
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months)
AND d.pret > 0 AND d.pret > 0
@@ -206,23 +213,25 @@ FETCH FIRST 100 ROWS ONLY
""" """
# ============================================================================= # =============================================================================
# 7. VÂNZĂRI SUB COST (ALERTĂ CRITICĂ) # 7. VÂNZĂRI SUB COST (ALERTĂ CRITICĂ) - OPTIMIZAT
# ============================================================================= # =============================================================================
VANZARI_SUB_COST = """ VANZARI_SUB_COST = """
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
f.data_act, f.data_act,
f.serie_act || ' ' || f.numar_act AS factura, f.serie_act || ' ' || f.numar_act AS factura,
f.client, p.denumire AS client,
d.denumire AS produs, NVL2(d.id_articol, art.denumire, d.explicatie) AS produs,
d.cantitate, d.cantitate,
ROUND(CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END, 2) AS pret_vanzare, ROUND(CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END, 2) AS pret_vanzare,
ROUND(d.pret_achizitie, 2) AS cost, ROUND(d.pret_achizitie, 2) AS cost,
ROUND((CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie) * d.cantitate, 2) AS pierdere ROUND((CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie) * d.cantitate, 2) AS pierdere
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 LEFT JOIN nom_parteneri p ON f.id_part = p.id_part
LEFT JOIN nom_articole art ON d.id_articol = art.id_articol
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -:months)
AND d.pret_achizitie > 0 AND d.pret_achizitie > 0
AND d.pret > 0 AND d.pret > 0
AND CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END < d.pret_achizitie AND CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END < d.pret_achizitie
@@ -235,20 +244,21 @@ FETCH FIRST 100 ROWS ONLY
# ============================================================================= # =============================================================================
TRENDING_CLIENTI = """ TRENDING_CLIENTI = """
WITH vanzari_perioade AS ( WITH vanzari_perioade AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
f.id_part, f.id_part,
f.client, p.denumire AS client,
SUM(CASE WHEN f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12) SUM(CASE WHEN f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12)
THEN d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END ELSE 0 END) AS vanzari_an_curent, THEN d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END ELSE 0 END) AS vanzari_an_curent,
SUM(CASE WHEN f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24) SUM(CASE WHEN f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24)
AND f.data_act < ADD_MONTHS(TRUNC(SYSDATE), -12) AND f.data_act < ADD_MONTHS(TRUNC(SYSDATE), -12)
THEN d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END ELSE 0 END) AS vanzari_an_trecut THEN d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END ELSE 0 END) AS vanzari_an_trecut
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 LEFT JOIN nom_parteneri p ON f.id_part = p.id_part
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24)
GROUP BY f.id_part, f.client GROUP BY f.id_part, p.denumire
) )
SELECT SELECT
client, client,
@@ -276,24 +286,26 @@ ORDER BY variatie_procent DESC NULLS LAST
# ============================================================================= # =============================================================================
CONCENTRARE_CLIENTI = """ CONCENTRARE_CLIENTI = """
WITH total_vanzari AS ( WITH total_vanzari AS (
SELECT SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS total SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
FROM fact_vfacturi2 f SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS total
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare FROM vanzari f
WHERE f.sters = 0 AND d.sters = 0 JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months)
), ),
vanzari_client AS ( vanzari_client AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
f.client, p.denumire AS client,
SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS vanzari, SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS vanzari,
ROW_NUMBER() OVER (ORDER BY SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) DESC) AS rn ROW_NUMBER() OVER (ORDER BY SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) DESC) AS rn
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 LEFT JOIN nom_parteneri p ON f.id_part = p.id_part
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months)
GROUP BY f.id_part, f.client GROUP BY f.id_part, p.denumire
), ),
top_clienti AS ( top_clienti AS (
SELECT SELECT
@@ -556,10 +568,10 @@ WHERE s.cant > 0
# 16. TOP PRODUSE DUPĂ VÂNZĂRI # 16. TOP PRODUSE DUPĂ VÂNZĂRI
# ============================================================================= # =============================================================================
TOP_PRODUSE = """ TOP_PRODUSE = """
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
d.denumire AS produs, NVL2(d.id_articol, a.denumire, d.explicatie) AS produs,
NVL(d.subgrupa, 'NECLASIFICAT') AS subgrupa, NVL(g.subgrupa, 'NECLASIFICAT') AS subgrupa,
d.um, a.um,
ROUND(SUM(d.cantitate), 2) AS cantitate_vanduta, ROUND(SUM(d.cantitate), 2) AS cantitate_vanduta,
ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS valoare_vanzari, ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS valoare_vanzari,
ROUND(SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)), 2) AS marja_bruta, ROUND(SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)), 2) AS marja_bruta,
@@ -568,22 +580,24 @@ SELECT
THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END)
ELSE 0 END ELSE 0 END
, 2) AS procent_marja , 2) AS procent_marja
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 LEFT JOIN nom_articole a ON d.id_articol = a.id_articol
LEFT JOIN gest_art_sbgr g ON a.id_subgrupa = g.id_subgrupa
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months)
GROUP BY d.id_articol, d.denumire, d.subgrupa, d.um GROUP BY d.id_articol, NVL2(d.id_articol, a.denumire, d.explicatie), g.subgrupa, a.um
ORDER BY valoare_vanzari DESC ORDER BY valoare_vanzari DESC
FETCH FIRST 50 ROWS ONLY FETCH FIRST 50 ROWS ONLY
""" """
# ============================================================================= # =============================================================================
# 17. MARJA PER GESTIUNE (doar articole gestionabile) # 17. MARJA PER GESTIUNE (doar articole gestionabile) - OPTIMIZAT
# ============================================================================= # =============================================================================
MARJA_PER_GESTIUNE = """ MARJA_PER_GESTIUNE = """
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
d.nume_gestiune, g.nume_gestiune,
COUNT(DISTINCT f.id_vanzare) AS nr_facturi, COUNT(DISTINCT f.id_vanzare) AS nr_facturi,
ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS vanzari_fara_tva, ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS vanzari_fara_tva,
ROUND(SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)), 2) AS marja_bruta, ROUND(SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)), 2) AS marja_bruta,
@@ -592,25 +606,26 @@ SELECT
THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END)
ELSE 0 END ELSE 0 END
, 2) AS procent_marja , 2) AS procent_marja
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
LEFT JOIN nom_gestiuni g ON d.id_gestiune = g.id_gestiune
JOIN nom_articole a ON d.id_articol = a.id_articol JOIN nom_articole a ON d.id_articol = a.id_articol
WHERE f.sters = 0 AND d.sters = 0 WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -:months)
AND NVL(a.in_stoc, 1) = 1 AND NVL(a.in_stoc, 1) = 1
GROUP BY d.id_gestiune, d.nume_gestiune GROUP BY d.id_gestiune, g.nume_gestiune
ORDER BY vanzari_fara_tva DESC ORDER BY vanzari_fara_tva DESC
""" """
# ============================================================================= # =============================================================================
# 18. ARTICOLE NEGESTIONABILE (servicii, etc.) # 18. ARTICOLE NEGESTIONABILE (servicii, etc.) - OPTIMIZAT
# ============================================================================= # =============================================================================
ARTICOLE_NEGESTIONABILE = """ ARTICOLE_NEGESTIONABILE = """
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
NVL(d.denumire, 'NECUNOSCUT') AS denumire, NVL(art.denumire, d.explicatie) AS denumire,
NVL(d.subgrupa, 'NECLASIFICAT') AS subgrupa, NVL(sg.subgrupa, 'NECLASIFICAT') AS subgrupa,
d.um, art.um,
COUNT(DISTINCT f.id_vanzare) AS nr_facturi, COUNT(DISTINCT f.id_vanzare) AS nr_facturi,
ROUND(SUM(d.cantitate), 2) AS cantitate_vanduta, ROUND(SUM(d.cantitate), 2) AS cantitate_vanduta,
ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS vanzari_fara_tva, ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS vanzari_fara_tva,
@@ -621,14 +636,15 @@ SELECT
THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) THEN SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) * 100.0 / SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END)
ELSE 0 END ELSE 0 END
, 2) AS procent_marja , 2) AS procent_marja
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
JOIN nom_articole a ON d.id_articol = a.id_articol LEFT JOIN nom_articole art ON d.id_articol = art.id_articol
WHERE f.sters = 0 AND d.sters = 0 LEFT JOIN vgest_art_sbgr sg ON art.id_subgrupa = sg.id_subgrupa
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -:months)
AND NVL(a.in_stoc, 0) = 0 AND NVL(art.in_stoc, 0) = 0
GROUP BY d.id_articol, d.denumire, d.subgrupa, d.um GROUP BY d.id_articol, art.denumire, d.explicatie, sg.subgrupa, art.um
ORDER BY vanzari_fara_tva DESC ORDER BY vanzari_fara_tva DESC
""" """
@@ -889,6 +905,7 @@ sold_furnizori AS (
SELECT SELECT
'DSO (Zile incasare clienti)' AS indicator, 'DSO (Zile incasare clienti)' AS indicator,
ROUND(NVL(sc.total_creante, 0) * 365 / NULLIF(v.total_vanzari, 0), 0) AS zile, ROUND(NVL(sc.total_creante, 0) * 365 / NULLIF(v.total_vanzari, 0), 0) AS zile,
'(Creante clienti x 365) / Vanzari anuale (cont 4111 x 365 / jurnal vanzari vjv2025)' AS formula,
CASE CASE
WHEN NVL(sc.total_creante, 0) * 365 / NULLIF(v.total_vanzari, 0) > 60 THEN 'ALERTA' WHEN NVL(sc.total_creante, 0) * 365 / NULLIF(v.total_vanzari, 0) > 60 THEN 'ALERTA'
WHEN NVL(sc.total_creante, 0) * 365 / NULLIF(v.total_vanzari, 0) > 45 THEN 'ATENTIE' WHEN NVL(sc.total_creante, 0) * 365 / NULLIF(v.total_vanzari, 0) > 45 THEN 'ATENTIE'
@@ -899,6 +916,7 @@ UNION ALL
SELECT SELECT
'DPO (Zile plata furnizori)' AS indicator, 'DPO (Zile plata furnizori)' AS indicator,
ROUND(NVL(sf.total_datorii, 0) * 365 / NULLIF(a.total_achizitii, 0), 0) AS zile, ROUND(NVL(sf.total_datorii, 0) * 365 / NULLIF(a.total_achizitii, 0), 0) AS zile,
'(Datorii furnizori x 365) / Achizitii anuale (cont 401 x 365 / jurnal cumparari vjc2025)' AS formula,
CASE CASE
WHEN NVL(sf.total_datorii, 0) * 365 / NULLIF(a.total_achizitii, 0) < 15 THEN 'ATENTIE' WHEN NVL(sf.total_datorii, 0) * 365 / NULLIF(a.total_achizitii, 0) < 15 THEN 'ATENTIE'
ELSE 'OK' ELSE 'OK'
@@ -948,10 +966,11 @@ WITH metrici AS (
AND b.an = EXTRACT(YEAR FROM SYSDATE) AND b.an = EXTRACT(YEAR FROM SYSDATE)
AND b.luna = EXTRACT(MONTH FROM SYSDATE)) AS stoc_curent, AND b.luna = EXTRACT(MONTH FROM SYSDATE)) AS stoc_curent,
-- COGS din facturi (preț achiziție articole vândute) -- COGS din facturi (preț achiziție articole vândute)
(SELECT SUM(d.cantitate * d.pret_achizitie) (SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
FROM fact_vfacturi2 f SUM(d.cantitate * d.pret_achizitie)
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare FROM vanzari f
WHERE f.sters = 0 AND d.sters = 0 JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12)) AS cogs_12_luni, AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12)) AS cogs_12_luni,
-- Creanțe clienți -- Creanțe clienți
@@ -1015,18 +1034,21 @@ WITH metrici AS (
SELECT SELECT
'DIO (Zile stoc)' AS indicator, 'DIO (Zile stoc)' AS indicator,
ROUND(NVL(m.stoc_curent, 0) * 365 / NULLIF(m.cogs_12_luni, 0), 0) AS zile, ROUND(NVL(m.stoc_curent, 0) * 365 / NULLIF(m.cogs_12_luni, 0), 0) AS zile,
'(Stoc x 365) / Cost marfuri vandute (clasa 3xx x 365 / cost achizitie 12 luni)' AS formula,
'Zile medii pentru transformarea stocului in vanzare' AS explicatie 'Zile medii pentru transformarea stocului in vanzare' AS explicatie
FROM metrici m FROM metrici m
UNION ALL UNION ALL
SELECT SELECT
'DSO (Zile incasare)' AS indicator, 'DSO (Zile incasare)' AS indicator,
ROUND(NVL(m.creante, 0) * 365 / NULLIF(m.vanzari_12_luni, 0), 0) AS zile, ROUND(NVL(m.creante, 0) * 365 / NULLIF(m.vanzari_12_luni, 0), 0) AS zile,
'(Creante x 365) / Vanzari anuale (cont 4111 x 365 / jurnal vanzari vjv2025)' AS formula,
'Zile medii pentru incasarea creantelor' AS explicatie 'Zile medii pentru incasarea creantelor' AS explicatie
FROM metrici m FROM metrici m
UNION ALL UNION ALL
SELECT SELECT
'DPO (Zile plata)' AS indicator, 'DPO (Zile plata)' AS indicator,
ROUND(NVL(m.datorii_furnizori, 0) * 365 / NULLIF(m.achizitii_12_luni, 0), 0) AS zile, ROUND(NVL(m.datorii_furnizori, 0) * 365 / NULLIF(m.achizitii_12_luni, 0), 0) AS zile,
'(Datorii furnizori x 365) / Achizitii anuale (cont 401 x 365 / jurnal cumparari vjc2025)' AS formula,
'Zile medii pentru plata furnizorilor' AS explicatie 'Zile medii pentru plata furnizorilor' AS explicatie
FROM metrici m FROM metrici m
UNION ALL UNION ALL
@@ -1037,16 +1059,17 @@ SELECT
NVL(m.creante, 0) * 365 / NULLIF(m.vanzari_12_luni, 0) - NVL(m.creante, 0) * 365 / NULLIF(m.vanzari_12_luni, 0) -
NVL(m.datorii_furnizori, 0) * 365 / NULLIF(m.achizitii_12_luni, 0) NVL(m.datorii_furnizori, 0) * 365 / NULLIF(m.achizitii_12_luni, 0)
, 0) AS zile, , 0) AS zile,
'DIO + DSO - DPO (zile de la plata furnizor pana la incasare client)' AS formula,
'DIO + DSO - DPO = zile de la plata furnizor pana la incasare client' AS explicatie 'DIO + DSO - DPO = zile de la plata furnizor pana la incasare client' AS explicatie
FROM metrici m FROM metrici m
""" """
# ============================================================================= # =============================================================================
# 26. INDICATORI AGREGATI VENITURI (Revenue mix per linie de business) # 26. INDICATORI AGREGATI VENITURI (Revenue mix per linie de business) - OPTIMIZAT
# ============================================================================= # =============================================================================
INDICATORI_AGREGATI_VENITURI = """ INDICATORI_AGREGATI_VENITURI = """
WITH vanzari_detaliate AS ( WITH vanzari_detaliate AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
CASE CASE
WHEN d.cont IN ('341', '345') THEN 'Productie proprie' WHEN d.cont IN ('341', '345') THEN 'Productie proprie'
WHEN d.cont = '301' THEN 'Materii prime' WHEN d.cont = '301' THEN 'Materii prime'
@@ -1054,11 +1077,11 @@ WITH vanzari_detaliate AS (
END AS linie_business, END AS linie_business,
d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END AS vanzare, d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END AS vanzare,
d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie) AS marja d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie) AS marja
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -:months)
), ),
total AS ( total AS (
SELECT SUM(vanzare) AS total_vanzari, SUM(marja) AS total_marja SELECT SUM(vanzare) AS total_vanzari, SUM(marja) AS total_marja
@@ -1082,14 +1105,14 @@ ORDER BY vanzari_ron DESC
# ============================================================================= # =============================================================================
SEZONALITATE_LUNARA = """ SEZONALITATE_LUNARA = """
WITH vanzari_lunare AS ( WITH vanzari_lunare AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
EXTRACT(MONTH FROM f.data_act) AS nr_luna, EXTRACT(MONTH FROM f.data_act) AS nr_luna,
TO_CHAR(f.data_act, 'Month', 'NLS_DATE_LANGUAGE=ROMANIAN') AS luna, TO_CHAR(f.data_act, 'Month', 'NLS_DATE_LANGUAGE=ROMANIAN') AS luna,
EXTRACT(YEAR FROM f.data_act) AS an, EXTRACT(YEAR FROM f.data_act) AS an,
SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS vanzari SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS vanzari
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24)
GROUP BY EXTRACT(MONTH FROM f.data_act), TO_CHAR(f.data_act, 'Month', 'NLS_DATE_LANGUAGE=ROMANIAN'), EXTRACT(YEAR FROM f.data_act) GROUP BY EXTRACT(MONTH FROM f.data_act), TO_CHAR(f.data_act, 'Month', 'NLS_DATE_LANGUAGE=ROMANIAN'), EXTRACT(YEAR FROM f.data_act)
@@ -1129,51 +1152,51 @@ ORDER BY s.nr_luna
# ============================================================================= # =============================================================================
PORTOFOLIU_CLIENTI = """ PORTOFOLIU_CLIENTI = """
WITH clienti_activi_3_luni AS ( WITH clienti_activi_3_luni AS (
SELECT COUNT(DISTINCT f.id_part) AS cnt SELECT /*+ INDEX(f IDX_VANZARI_NR) */ COUNT(DISTINCT f.id_part) AS cnt
FROM fact_vfacturi2 f FROM vanzari f
WHERE f.sters = 0 AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) WHERE f.sters = 0 AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -3) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -3)
), ),
clienti_activi_12_luni AS ( clienti_activi_12_luni AS (
SELECT COUNT(DISTINCT f.id_part) AS cnt SELECT /*+ INDEX(f IDX_VANZARI_NR) */ COUNT(DISTINCT f.id_part) AS cnt
FROM fact_vfacturi2 f FROM vanzari f
WHERE f.sters = 0 AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) WHERE f.sters = 0 AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12)
), ),
clienti_noi AS ( clienti_noi AS (
SELECT COUNT(DISTINCT f.id_part) AS cnt SELECT /*+ INDEX(f IDX_VANZARI_NR) */ COUNT(DISTINCT f.id_part) AS cnt
FROM fact_vfacturi2 f FROM vanzari f
WHERE f.sters = 0 AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) WHERE f.sters = 0 AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12)
AND f.id_part NOT IN ( AND f.id_part NOT IN (
SELECT DISTINCT f2.id_part SELECT /*+ INDEX(f2 IDX_VANZARI_NR) */ DISTINCT f2.id_part
FROM fact_vfacturi2 f2 FROM vanzari f2
WHERE f2.sters = 0 AND f2.tip > 0 AND f2.tip NOT IN (7, 8, 9, 24) WHERE f2.sters = 0 AND f2.tip > 0 AND f2.tip NOT IN (7, 8, 9, 24)
AND f2.data_act < ADD_MONTHS(TRUNC(SYSDATE), -12) AND f2.data_act < ADD_MONTHS(TRUNC(SYSDATE), -12)
) )
), ),
clienti_pierduti AS ( clienti_pierduti AS (
SELECT COUNT(DISTINCT f.id_part) AS cnt SELECT /*+ INDEX(f IDX_VANZARI_NR) */ COUNT(DISTINCT f.id_part) AS cnt
FROM fact_vfacturi2 f FROM vanzari f
WHERE f.sters = 0 AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) WHERE f.sters = 0 AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24)
AND f.data_act < ADD_MONTHS(TRUNC(SYSDATE), -6) AND f.data_act < ADD_MONTHS(TRUNC(SYSDATE), -6)
AND f.id_part NOT IN ( AND f.id_part NOT IN (
SELECT DISTINCT f2.id_part SELECT /*+ INDEX(f2 IDX_VANZARI_NR) */ DISTINCT f2.id_part
FROM fact_vfacturi2 f2 FROM vanzari f2
WHERE f2.sters = 0 AND f2.tip > 0 AND f2.tip NOT IN (7, 8, 9, 24) WHERE f2.sters = 0 AND f2.tip > 0 AND f2.tip NOT IN (7, 8, 9, 24)
AND f2.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -6) AND f2.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -6)
) )
), ),
clienti_inactivi AS ( clienti_inactivi AS (
SELECT COUNT(DISTINCT f.id_part) AS cnt SELECT /*+ INDEX(f IDX_VANZARI_NR) */ COUNT(DISTINCT f.id_part) AS cnt
FROM fact_vfacturi2 f FROM vanzari f
WHERE f.sters = 0 AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) WHERE f.sters = 0 AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -6) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -6)
AND f.data_act < ADD_MONTHS(TRUNC(SYSDATE), -3) AND f.data_act < ADD_MONTHS(TRUNC(SYSDATE), -3)
AND f.id_part NOT IN ( AND f.id_part NOT IN (
SELECT DISTINCT f2.id_part SELECT /*+ INDEX(f2 IDX_VANZARI_NR) */ DISTINCT f2.id_part
FROM fact_vfacturi2 f2 FROM vanzari f2
WHERE f2.sters = 0 AND f2.tip > 0 AND f2.tip NOT IN (7, 8, 9, 24) WHERE f2.sters = 0 AND f2.tip > 0 AND f2.tip NOT IN (7, 8, 9, 24)
AND f2.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -3) AND f2.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -3)
) )
@@ -1194,24 +1217,25 @@ SELECT 'Clienti inactivi (3-6 luni)' AS indicator, cnt AS valoare, 'Risc de pier
# ============================================================================= # =============================================================================
FRECVENTA_CLIENTI = """ FRECVENTA_CLIENTI = """
WITH frecventa_curenta AS ( WITH frecventa_curenta AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
f.id_part, f.id_part,
f.client, p.denumire AS client,
COUNT(DISTINCT f.id_vanzare) AS comenzi_12_luni, COUNT(DISTINCT f.id_vanzare) AS comenzi_12_luni,
ROUND(COUNT(DISTINCT f.id_vanzare) / 12.0, 2) AS comenzi_pe_luna, ROUND(COUNT(DISTINCT f.id_vanzare) / 12.0, 2) AS comenzi_pe_luna,
ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS valoare_12_luni ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS valoare_12_luni
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 LEFT JOIN nom_parteneri p ON f.id_part = p.id_part
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12)
GROUP BY f.id_part, f.client GROUP BY f.id_part, p.denumire
), ),
frecventa_anterioara AS ( frecventa_anterioara AS (
SELECT SELECT /*+ INDEX(f IDX_VANZARI_NR) */
f.id_part, f.id_part,
COUNT(DISTINCT f.id_vanzare) AS comenzi_an_anterior COUNT(DISTINCT f.id_vanzare) AS comenzi_an_anterior
FROM fact_vfacturi2 f FROM vanzari f
WHERE f.sters = 0 WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24)
@@ -1240,21 +1264,22 @@ ORDER BY fc.valoare_12_luni DESC
# ============================================================================= # =============================================================================
CONCENTRARE_RISC = """ CONCENTRARE_RISC = """
WITH total_vanzari AS ( WITH total_vanzari AS (
SELECT SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS total SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
FROM fact_vfacturi2 f SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS total
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare FROM vanzari f
WHERE f.sters = 0 AND d.sters = 0 JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months)
), ),
vanzari_client AS ( vanzari_client AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
f.id_part, f.id_part,
SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS vanzari, SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS vanzari,
ROW_NUMBER() OVER (ORDER BY SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) DESC) AS rn ROW_NUMBER() OVER (ORDER BY SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) DESC) AS rn
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months)
GROUP BY f.id_part GROUP BY f.id_part
@@ -1301,18 +1326,19 @@ GROUP BY tv.total
# ============================================================================= # =============================================================================
CLIENTI_RANKING_PROFIT = """ CLIENTI_RANKING_PROFIT = """
WITH vanzari_client AS ( WITH vanzari_client AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
f.id_part, f.id_part,
f.client, p.denumire AS client,
SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS vanzari_fara_tva, SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS vanzari_fara_tva,
SUM(d.cantitate * d.pret_achizitie) AS cost_total, SUM(d.cantitate * d.pret_achizitie) AS cost_total,
SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) AS profit_brut SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) AS profit_brut
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 LEFT JOIN nom_parteneri p ON f.id_part = p.id_part
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months)
GROUP BY f.id_part, f.client GROUP BY f.id_part, p.denumire
) )
SELECT SELECT
client, client,
@@ -1328,11 +1354,11 @@ ORDER BY profit_brut DESC
""" """
# ============================================================================= # =============================================================================
# 32. MARJA CLIENT CATEGORIE (Marjă per categorie per client) # 32. MARJA CLIENT CATEGORIE (Marjă per categorie per client) - OPTIMIZAT
# ============================================================================= # =============================================================================
MARJA_CLIENT_CATEGORIE = """ MARJA_CLIENT_CATEGORIE = """
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
f.client, p.denumire AS client,
NVL(sg.grupa, 'NECLASIFICAT') AS categoria, NVL(sg.grupa, 'NECLASIFICAT') AS categoria,
ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS vanzari, ROUND(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 2) AS vanzari,
ROUND(SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)), 2) AS marja, ROUND(SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)), 2) AS marja,
@@ -1346,15 +1372,17 @@ SELECT
NULLIF(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 0) < 15 THEN 'MARJA MICA' NULLIF(SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END), 0) < 15 THEN 'MARJA MICA'
ELSE 'OK' ELSE 'OK'
END AS status_marja END AS status_marja
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
LEFT JOIN vgest_art_sbgr sg ON d.id_subgrupa = sg.id_subgrupa LEFT JOIN nom_parteneri p ON f.id_part = p.id_part
WHERE f.sters = 0 AND d.sters = 0 LEFT JOIN nom_articole art ON d.id_articol = art.id_articol
LEFT JOIN vgest_art_sbgr sg ON art.id_subgrupa = sg.id_subgrupa
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -:months) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -:months)
GROUP BY f.id_part, f.client, sg.id_grupa, sg.grupa GROUP BY f.id_part, p.denumire, sg.id_grupa, sg.grupa
HAVING SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) > 1000 HAVING SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) > 1000
ORDER BY f.client, vanzari DESC ORDER BY p.denumire, vanzari DESC
""" """
# ============================================================================= # =============================================================================
@@ -1362,27 +1390,28 @@ ORDER BY f.client, vanzari DESC
# ============================================================================= # =============================================================================
EVOLUTIE_DISCOUNT = """ EVOLUTIE_DISCOUNT = """
WITH preturi_vechi AS ( WITH preturi_vechi AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
d.id_articol, d.id_articol,
d.denumire, a.denumire,
AVG(CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS pret_mediu_vechi AVG(CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS pret_mediu_vechi
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 LEFT JOIN nom_articole a ON d.id_articol = a.id_articol
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12)
AND f.data_act < ADD_MONTHS(TRUNC(SYSDATE), -6) AND f.data_act < ADD_MONTHS(TRUNC(SYSDATE), -6)
AND d.pret > 0 AND d.pret > 0
GROUP BY d.id_articol, d.denumire GROUP BY d.id_articol, a.denumire
HAVING COUNT(*) >= 5 HAVING COUNT(*) >= 5
), ),
preturi_noi AS ( preturi_noi AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
d.id_articol, d.id_articol,
AVG(CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS pret_mediu_nou AVG(CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS pret_mediu_nou
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -6) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -6)
AND d.pret > 0 AND d.pret > 0
@@ -1436,13 +1465,13 @@ activ AS (
AND b.luna = EXTRACT(MONTH FROM SYSDATE) AND b.luna = EXTRACT(MONTH FROM SYSDATE)
), ),
-- Vanzari si profit din ultimele 12 luni -- Vanzari si profit din ultimele 12 luni
vanzari AS ( vanzari_calc AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS total_vanzari, SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS total_vanzari,
SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) AS profit_brut SUM(d.cantitate * (CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END - d.pret_achizitie)) AS profit_brut
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12)
) )
@@ -1454,6 +1483,7 @@ SELECT
WHEN NVL(dt.datorii_totale, 0) / NULLIF(cp.capital_propriu, 0) > 1 THEN 'ATENTIE' WHEN NVL(dt.datorii_totale, 0) / NULLIF(cp.capital_propriu, 0) > 1 THEN 'ATENTIE'
ELSE 'OK' ELSE 'OK'
END AS status, END AS status,
'Datorii / Capital Propriu (cont 16x,40x,42x,44x,46x / cont 10x,11x,12x,117)' AS formula,
'< 1 = bine, > 2 = risc' AS interpretare, '< 1 = bine, > 2 = risc' AS interpretare,
CASE CASE
WHEN NVL(dt.datorii_totale, 0) / NULLIF(cp.capital_propriu, 0) > 2 THEN 'Reduceti datoriile sau cresteti capitalul propriu' WHEN NVL(dt.datorii_totale, 0) / NULLIF(cp.capital_propriu, 0) > 2 THEN 'Reduceti datoriile sau cresteti capitalul propriu'
@@ -1470,6 +1500,7 @@ SELECT
WHEN NVL(cp.capital_propriu, 0) / NULLIF(ac.total_activ, 0) < 0.5 THEN 'ATENTIE' WHEN NVL(cp.capital_propriu, 0) / NULLIF(ac.total_activ, 0) < 0.5 THEN 'ATENTIE'
ELSE 'OK' ELSE 'OK'
END AS status, END AS status,
'Capital Propriu / Total Active (cont 10x,11x,12x,117 / Active clasa 1-5)' AS formula,
'> 0.5 = bine, < 0.3 = risc' AS interpretare, '> 0.5 = bine, < 0.3 = risc' AS interpretare,
CASE CASE
WHEN NVL(cp.capital_propriu, 0) / NULLIF(ac.total_activ, 0) < 0.3 THEN 'Dependenta prea mare de creditori' WHEN NVL(cp.capital_propriu, 0) / NULLIF(ac.total_activ, 0) < 0.3 THEN 'Dependenta prea mare de creditori'
@@ -1486,6 +1517,7 @@ SELECT
WHEN NVL(dt.datorii_totale, 0) / NULLIF(ac.total_activ, 0) > 0.5 THEN 'ATENTIE' WHEN NVL(dt.datorii_totale, 0) / NULLIF(ac.total_activ, 0) > 0.5 THEN 'ATENTIE'
ELSE 'OK' ELSE 'OK'
END AS status, END AS status,
'Datorii / Total Active (cont 16x,40x,42x,44x,46x / Active clasa 1-5)' AS formula,
'< 0.5 = bine, > 0.7 = risc' AS interpretare, '< 0.5 = bine, > 0.7 = risc' AS interpretare,
CASE CASE
WHEN NVL(dt.datorii_totale, 0) / NULLIF(ac.total_activ, 0) > 0.7 THEN 'Risc de insolventa - reduceti datoriile' WHEN NVL(dt.datorii_totale, 0) / NULLIF(ac.total_activ, 0) > 0.7 THEN 'Risc de insolventa - reduceti datoriile'
@@ -1502,13 +1534,14 @@ SELECT
WHEN NVL(v.profit_brut, 0) * 100 / NULLIF(v.total_vanzari, 0) < 5 THEN 'ATENTIE' WHEN NVL(v.profit_brut, 0) * 100 / NULLIF(v.total_vanzari, 0) < 5 THEN 'ATENTIE'
ELSE 'OK' ELSE 'OK'
END AS status, END AS status,
'(Profit Brut / Vanzari) x 100 (din facturi ultimele 12 luni)' AS formula,
'> 5% = bine, < 3% = risc' AS interpretare, '> 5% = bine, < 3% = risc' AS interpretare,
CASE CASE
WHEN NVL(v.profit_brut, 0) * 100 / NULLIF(v.total_vanzari, 0) < 3 THEN 'Marja periculoasa - revizuiti preturile si costurile' WHEN NVL(v.profit_brut, 0) * 100 / NULLIF(v.total_vanzari, 0) < 3 THEN 'Marja periculoasa - revizuiti preturile si costurile'
WHEN NVL(v.profit_brut, 0) * 100 / NULLIF(v.total_vanzari, 0) < 5 THEN 'Optimizati costurile sau cresteti preturile' WHEN NVL(v.profit_brut, 0) * 100 / NULLIF(v.total_vanzari, 0) < 5 THEN 'Optimizati costurile sau cresteti preturile'
ELSE 'Marja acceptabila' ELSE 'Marja acceptabila'
END AS recomandare END AS recomandare
FROM vanzari v FROM vanzari_calc v
UNION ALL UNION ALL
SELECT SELECT
'ROA - Rentabilitatea activelor (%)' AS indicator, 'ROA - Rentabilitatea activelor (%)' AS indicator,
@@ -1518,13 +1551,14 @@ SELECT
WHEN NVL(v.profit_brut, 0) * 100 / NULLIF(ac.total_activ, 0) < 5 THEN 'ATENTIE' WHEN NVL(v.profit_brut, 0) * 100 / NULLIF(ac.total_activ, 0) < 5 THEN 'ATENTIE'
ELSE 'OK' ELSE 'OK'
END AS status, END AS status,
'(Profit Brut / Total Active) x 100 (facturi 12 luni / balanta)' AS formula,
'> 5% = bine, < 2% = slab' AS interpretare, '> 5% = bine, < 2% = slab' AS interpretare,
CASE CASE
WHEN NVL(v.profit_brut, 0) * 100 / NULLIF(ac.total_activ, 0) < 2 THEN 'Activele nu genereaza profit suficient - optimizati utilizarea' WHEN NVL(v.profit_brut, 0) * 100 / NULLIF(ac.total_activ, 0) < 2 THEN 'Activele nu genereaza profit suficient - optimizati utilizarea'
WHEN NVL(v.profit_brut, 0) * 100 / NULLIF(ac.total_activ, 0) < 5 THEN 'Cresteti eficienta utilizarii activelor' WHEN NVL(v.profit_brut, 0) * 100 / NULLIF(ac.total_activ, 0) < 5 THEN 'Cresteti eficienta utilizarii activelor'
ELSE 'Activele sunt utilizate eficient' ELSE 'Activele sunt utilizate eficient'
END AS recomandare END AS recomandare
FROM vanzari v, activ ac FROM vanzari_calc v, activ ac
UNION ALL UNION ALL
SELECT SELECT
'Rotatia activelor' AS indicator, 'Rotatia activelor' AS indicator,
@@ -1533,12 +1567,13 @@ SELECT
WHEN NVL(v.total_vanzari, 0) / NULLIF(ac.total_activ, 0) < 0.5 THEN 'ATENTIE' WHEN NVL(v.total_vanzari, 0) / NULLIF(ac.total_activ, 0) < 0.5 THEN 'ATENTIE'
ELSE 'OK' ELSE 'OK'
END AS status, END AS status,
'Vanzari / Total Active (facturi 12 luni / balanta)' AS formula,
'> 1 = eficient' AS interpretare, '> 1 = eficient' AS interpretare,
CASE CASE
WHEN NVL(v.total_vanzari, 0) / NULLIF(ac.total_activ, 0) < 0.5 THEN 'Active subutilizate - cresteti vanzarile' WHEN NVL(v.total_vanzari, 0) / NULLIF(ac.total_activ, 0) < 0.5 THEN 'Active subutilizate - cresteti vanzarile'
ELSE 'Activele genereaza vanzari eficient' ELSE 'Activele genereaza vanzari eficient'
END AS recomandare END AS recomandare
FROM vanzari v, activ ac FROM vanzari_calc v, activ ac
""" """
# ============================================================================= # =============================================================================
@@ -1586,6 +1621,7 @@ SELECT
WHEN (NVL(c.cash_total, 0) + NVL(cr.creante_total, 0) + NVL(st.stoc_total, 0)) / NULLIF(dc.datorii_total, 0) < 1.5 THEN 'ATENTIE' WHEN (NVL(c.cash_total, 0) + NVL(cr.creante_total, 0) + NVL(st.stoc_total, 0)) / NULLIF(dc.datorii_total, 0) < 1.5 THEN 'ATENTIE'
ELSE 'OK' ELSE 'OK'
END AS status, END AS status,
'(Cash + Creante + Stoc) / Datorii curente (cont 512x,531x + 4111 + vstoc / cont 401x,404x,462x)' AS formula,
'>= 1.5 ideal, >= 1.0 acceptabil' AS interpretare, '>= 1.5 ideal, >= 1.0 acceptabil' AS interpretare,
CASE CASE
WHEN (NVL(c.cash_total, 0) + NVL(cr.creante_total, 0) + NVL(st.stoc_total, 0)) / NULLIF(dc.datorii_total, 0) < 1.0 THEN 'Risc de lichiditate - nu puteti acoperi datoriile curente' WHEN (NVL(c.cash_total, 0) + NVL(cr.creante_total, 0) + NVL(st.stoc_total, 0)) / NULLIF(dc.datorii_total, 0) < 1.0 THEN 'Risc de lichiditate - nu puteti acoperi datoriile curente'
@@ -1602,6 +1638,7 @@ SELECT
WHEN (NVL(c.cash_total, 0) + NVL(cr.creante_total, 0)) / NULLIF(dc.datorii_total, 0) < 1.0 THEN 'ATENTIE' WHEN (NVL(c.cash_total, 0) + NVL(cr.creante_total, 0)) / NULLIF(dc.datorii_total, 0) < 1.0 THEN 'ATENTIE'
ELSE 'OK' ELSE 'OK'
END AS status, END AS status,
'(Cash + Creante) / Datorii curente - fara stocuri (cont 512x,531x + 4111 / cont 401x,404x,462x)' AS formula,
'>= 1.0 ideal, >= 0.7 acceptabil' AS interpretare, '>= 1.0 ideal, >= 0.7 acceptabil' AS interpretare,
CASE CASE
WHEN (NVL(c.cash_total, 0) + NVL(cr.creante_total, 0)) / NULLIF(dc.datorii_total, 0) < 0.7 THEN 'Dependenta de stocuri pentru plata datoriilor' WHEN (NVL(c.cash_total, 0) + NVL(cr.creante_total, 0)) / NULLIF(dc.datorii_total, 0) < 0.7 THEN 'Dependenta de stocuri pentru plata datoriilor'
@@ -1618,6 +1655,7 @@ SELECT
WHEN NVL(c.cash_total, 0) / NULLIF(dc.datorii_total, 0) < 0.2 THEN 'ATENTIE' WHEN NVL(c.cash_total, 0) / NULLIF(dc.datorii_total, 0) < 0.2 THEN 'ATENTIE'
ELSE 'OK' ELSE 'OK'
END AS status, END AS status,
'Cash / Datorii curente (cont 512x,531x / cont 401x,404x,462x)' AS formula,
'>= 0.2 ideal, >= 0.1 minim' AS interpretare, '>= 0.2 ideal, >= 0.1 minim' AS interpretare,
CASE CASE
WHEN NVL(c.cash_total, 0) / NULLIF(dc.datorii_total, 0) < 0.1 THEN 'Cash insuficient - risc la plati urgente' WHEN NVL(c.cash_total, 0) / NULLIF(dc.datorii_total, 0) < 0.1 THEN 'Cash insuficient - risc la plati urgente'
@@ -1633,6 +1671,7 @@ SELECT
WHEN (NVL(c.cash_total, 0) + NVL(cr.creante_total, 0) + NVL(st.stoc_total, 0)) - NVL(dc.datorii_total, 0) < 0 THEN 'ALERTA' WHEN (NVL(c.cash_total, 0) + NVL(cr.creante_total, 0) + NVL(st.stoc_total, 0)) - NVL(dc.datorii_total, 0) < 0 THEN 'ALERTA'
ELSE 'OK' ELSE 'OK'
END AS status, END AS status,
'Active curente - Datorii curente (Cash + Creante + Stoc - Datorii furnizori)' AS formula,
'Pozitiv = OK, Negativ = probleme' AS interpretare, 'Pozitiv = OK, Negativ = probleme' AS interpretare,
CASE CASE
WHEN (NVL(c.cash_total, 0) + NVL(cr.creante_total, 0) + NVL(st.stoc_total, 0)) - NVL(dc.datorii_total, 0) < 0 THEN 'Fond de rulment negativ - necesita finantare' WHEN (NVL(c.cash_total, 0) + NVL(cr.creante_total, 0) + NVL(st.stoc_total, 0)) - NVL(dc.datorii_total, 0) < 0 THEN 'Fond de rulment negativ - necesita finantare'
@@ -1957,10 +1996,11 @@ DSO_DPO_YOY = """
WITH WITH
-- Metrici curente -- Metrici curente
vanzari_curente AS ( vanzari_curente AS (
SELECT SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS total_vanzari SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
FROM fact_vfacturi2 f SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS total_vanzari
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare FROM vanzari f
WHERE f.sters = 0 AND d.sters = 0 JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12)
), ),
@@ -1986,10 +2026,11 @@ sold_furnizori_curent AS (
), ),
-- Metrici anterioare (aproximare - vanzari an anterior) -- Metrici anterioare (aproximare - vanzari an anterior)
vanzari_anterioare AS ( vanzari_anterioare AS (
SELECT SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS total_vanzari SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
FROM fact_vfacturi2 f SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS total_vanzari
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare FROM vanzari f
WHERE f.sters = 0 AND d.sters = 0 JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24)
AND f.data_act < ADD_MONTHS(TRUNC(SYSDATE), -12) AND f.data_act < ADD_MONTHS(TRUNC(SYSDATE), -12)
@@ -2057,12 +2098,12 @@ CONCENTRARE_RISC_YOY = """
WITH WITH
-- Single scan for current year: compute total + per-client with ranking -- Single scan for current year: compute total + per-client with ranking
vanzari_curent AS ( vanzari_curent AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
f.id_part, f.id_part,
SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS vanzari SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS vanzari
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -12)
GROUP BY f.id_part GROUP BY f.id_part
@@ -2081,12 +2122,12 @@ metrics_curent AS (
), ),
-- Single scan for previous year -- Single scan for previous year
vanzari_anterior AS ( vanzari_anterior AS (
SELECT SELECT /*+ LEADING(f d) USE_NL(d) INDEX(f IDX_VANZARI_NR) */
f.id_part, f.id_part,
SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS vanzari SUM(d.cantitate * CASE WHEN d.pret_cu_tva = 1 THEN d.pret / (1 + d.proc_tvav/100) ELSE d.pret END) AS vanzari
FROM fact_vfacturi2 f FROM vanzari f
JOIN fact_vfacturi_detalii d ON d.id_vanzare = f.id_vanzare JOIN vanzari_detalii d ON d.id_vanzare = f.id_vanzare AND d.sters = 0
WHERE f.sters = 0 AND d.sters = 0 WHERE f.sters = 0
AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24) AND f.tip > 0 AND f.tip NOT IN (7, 8, 9, 24)
AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24) AND f.data_act >= ADD_MONTHS(TRUNC(SYSDATE), -24)
AND f.data_act < ADD_MONTHS(TRUNC(SYSDATE), -12) AND f.data_act < ADD_MONTHS(TRUNC(SYSDATE), -12)

View File

@@ -5,11 +5,45 @@ Generates Excel and PDF reports from query results
import pandas as pd import pandas as pd
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
import unicodedata
import re
import matplotlib import matplotlib
matplotlib.use('Agg') # Non-interactive backend matplotlib.use('Agg') # Non-interactive backend
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import matplotlib.ticker as ticker import matplotlib.ticker as ticker
from openpyxl import Workbook from openpyxl import Workbook
# Romanian diacritics mapping for PDF (Helvetica doesn't support them)
DIACRITICS_MAP = {
'ă': 'a', 'Ă': 'A',
'â': 'a', 'Â': 'A',
'î': 'i', 'Î': 'I',
'ș': 's', 'Ș': 'S',
'ş': 's', 'Ş': 'S', # Alternative encoding
'ț': 't', 'Ț': 'T',
'ţ': 't', 'Ţ': 'T', # Alternative encoding
}
def remove_diacritics(text):
"""Remove Romanian diacritics from text for PDF compatibility"""
if not isinstance(text, str):
return text
for diacritic, replacement in DIACRITICS_MAP.items():
text = text.replace(diacritic, replacement)
return text
def sanitize_for_pdf(value, max_length=None):
"""Sanitize value for PDF: remove diacritics and optionally truncate"""
if value is None:
return ''
text = str(value)
text = remove_diacritics(text)
if max_length and len(text) > max_length:
text = text[:max_length-3] + '...'
return text
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils.dataframe import dataframe_to_rows from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.chart import BarChart, LineChart, PieChart, Reference from openpyxl.chart import BarChart, LineChart, PieChart, Reference
@@ -47,6 +81,14 @@ class ExcelReportGenerator:
top=Side(style='thin'), top=Side(style='thin'),
bottom=Side(style='thin') bottom=Side(style='thin')
) )
# Style for manager-friendly explanations
self.explanation_fill = PatternFill(start_color='F8F9FA', end_color='F8F9FA', fill_type='solid')
self.explanation_border = Border(
left=Side(style='thin', color='DEE2E6'),
right=Side(style='thin', color='DEE2E6'),
top=Side(style='thin', color='DEE2E6'),
bottom=Side(style='thin', color='DEE2E6')
)
def add_sheet(self, name: str, df: pd.DataFrame, title: str = None, description: str = None, legend: dict = None): def add_sheet(self, name: str, df: pd.DataFrame, title: str = None, description: str = None, legend: dict = None):
"""Add a formatted sheet to the workbook with optional legend""" """Add a formatted sheet to the workbook with optional legend"""
@@ -87,7 +129,7 @@ class ExcelReportGenerator:
start_row += 1 start_row += 1
if df is None or df.empty: if df is None or df.empty:
ws.cell(row=start_row, column=1, value="Nu există date pentru această analiză.") ws.cell(row=start_row, column=1, value="Nu exista date pentru această analiză.")
return return
# Write headers # Write headers
@@ -304,6 +346,7 @@ class ExcelReportGenerator:
section_title = section.get('title', '') section_title = section.get('title', '')
df = section.get('df') df = section.get('df')
description = section.get('description', '') description = section.get('description', '')
explanation = section.get('explanation', '')
legend = section.get('legend', {}) legend = section.get('legend', {})
# Section separator # Section separator
@@ -315,7 +358,23 @@ class ExcelReportGenerator:
cell.font = Font(bold=True, color='FFFFFF', size=11) cell.font = Font(bold=True, color='FFFFFF', size=11)
start_row += 1 start_row += 1
# Section description # Manager-friendly explanation box (if provided)
if explanation:
# Merge cells for explanation box (columns 1-8)
ws.merge_cells(start_row=start_row, start_column=1, end_row=start_row, end_column=8)
cell = ws.cell(row=start_row, column=1, value=f"💡 {explanation}")
cell.fill = self.explanation_fill
cell.border = self.explanation_border
cell.font = Font(size=9, color='555555')
cell.alignment = Alignment(wrap_text=True, vertical='center')
ws.row_dimensions[start_row].height = 40 # Taller row for wrapped text
# Apply border to merged cells
for col in range(1, 9):
ws.cell(row=start_row, column=col).fill = self.explanation_fill
ws.cell(row=start_row, column=col).border = self.explanation_border
start_row += 1
# Section description (technical)
if description: if description:
ws.cell(row=start_row, column=1, value=description) ws.cell(row=start_row, column=1, value=description)
ws.cell(row=start_row, column=1).font = Font(italic=True, size=9, color='666666') ws.cell(row=start_row, column=1).font = Font(italic=True, size=9, color='666666')
@@ -325,7 +384,7 @@ class ExcelReportGenerator:
# Check for empty data # Check for empty data
if df is None or df.empty: if df is None or df.empty:
ws.cell(row=start_row, column=1, value="Nu există date pentru această secțiune.") ws.cell(row=start_row, column=1, value="Nu exista date pentru această secțiune.")
ws.cell(row=start_row, column=1).font = Font(italic=True, color='999999') ws.cell(row=start_row, column=1).font = Font(italic=True, color='999999')
start_row += 3 start_row += 3
continue continue
@@ -457,6 +516,53 @@ class PDFReportGenerator:
fontSize=8, fontSize=8,
textColor=colors.gray textColor=colors.gray
)) ))
# Table cell styles with word wrapping
self.styles.add(ParagraphStyle(
name='TableCell',
parent=self.styles['Normal'],
fontSize=7,
leading=9,
wordWrap='CJK',
))
self.styles.add(ParagraphStyle(
name='TableCellBold',
parent=self.styles['Normal'],
fontSize=7,
leading=9,
fontName='Helvetica-Bold',
))
# Explanation style for manager-friendly text boxes
self.styles.add(ParagraphStyle(
name='Explanation',
parent=self.styles['Normal'],
fontSize=9,
textColor=colors.HexColor('#555555'),
backColor=colors.HexColor('#F8F9FA'),
borderPadding=8,
spaceBefore=5,
spaceAfter=10,
))
def make_cell_paragraph(self, text, bold=False):
"""Create a Paragraph for table cell with word wrapping"""
style = self.styles['TableCellBold'] if bold else self.styles['TableCell']
return Paragraph(sanitize_for_pdf(text), style)
def add_explanation(self, text: str):
"""Add a manager-friendly explanation box before a section"""
# Create a table with background color to simulate a box
explanation_para = Paragraph(sanitize_for_pdf(text), self.styles['Explanation'])
box_table = Table([[explanation_para]], colWidths=[16*cm])
box_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#F8F9FA')),
('BOX', (0, 0), (-1, -1), 0.5, colors.HexColor('#DEE2E6')),
('TOPPADDING', (0, 0), (-1, -1), 8),
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
('LEFTPADDING', (0, 0), (-1, -1), 10),
('RIGHTPADDING', (0, 0), (-1, -1), 10),
]))
self.elements.append(box_table)
self.elements.append(Spacer(1, 0.3*cm))
def add_title_page(self, report_date: datetime = None): def add_title_page(self, report_date: datetime = None):
"""Add title page""" """Add title page"""
@@ -464,29 +570,38 @@ class PDFReportGenerator:
report_date = datetime.now() report_date = datetime.now()
self.elements.append(Spacer(1, 3*cm)) self.elements.append(Spacer(1, 3*cm))
self.elements.append(Paragraph(self.company_name, self.styles['CustomTitle'])) self.elements.append(Paragraph(remove_diacritics(self.company_name), self.styles['CustomTitle']))
self.elements.append(Spacer(1, 1*cm)) self.elements.append(Spacer(1, 1*cm))
self.elements.append(Paragraph( self.elements.append(Paragraph(
f"Raport generat: {report_date.strftime('%d %B %Y, %H:%M')}", f"Raport generat: {report_date.strftime('%d %B %Y, %H:%M')}",
self.styles['Normal'] self.styles['Normal']
)) ))
self.elements.append(Paragraph( self.elements.append(Paragraph(
"Perioada analizată: Ultimele 12 luni", "Perioada analizata: Ultimele 12 luni",
self.styles['Normal'] self.styles['Normal']
)) ))
self.elements.append(PageBreak()) self.elements.append(PageBreak())
def add_kpi_section(self, kpi_df: pd.DataFrame): def add_kpi_section(self, kpi_df: pd.DataFrame):
"""Add KPI summary section""" """Add KPI summary section"""
self.elements.append(Paragraph("📊 Sumar Executiv - KPIs", self.styles['SectionHeader'])) self.elements.append(Paragraph("Sumar Executiv - KPIs", self.styles['SectionHeader']))
if kpi_df is not None and not kpi_df.empty: if kpi_df is not None and not kpi_df.empty:
data = [['Indicator', 'Valoare', 'UM']] # Header row with bold style
header_style = ParagraphStyle(
'TableHeaderCell', parent=self.styles['Normal'],
fontSize=9, fontName='Helvetica-Bold', textColor=colors.white
)
data = [[
Paragraph('Indicator', header_style),
Paragraph('Valoare', header_style),
Paragraph('UM', header_style)
]]
for _, row in kpi_df.iterrows(): for _, row in kpi_df.iterrows():
data.append([ data.append([
str(row.get('INDICATOR', '')), self.make_cell_paragraph(row.get('INDICATOR', '')),
str(row.get('VALOARE', '')), self.make_cell_paragraph(row.get('VALOARE', '')),
str(row.get('UM', '')) self.make_cell_paragraph(row.get('UM', ''))
]) ])
table = Table(data, colWidths=[8*cm, 4*cm, 2*cm]) table = Table(data, colWidths=[8*cm, 4*cm, 2*cm])
@@ -495,9 +610,10 @@ class PDFReportGenerator:
('TEXTCOLOR', (0, 0), (-1, 0), colors.white), ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('ALIGN', (1, 1), (1, -1), 'RIGHT'), ('ALIGN', (1, 1), (1, -1), 'RIGHT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('VALIGN', (0, 0), (-1, -1), 'TOP'),
('FONTSIZE', (0, 0), (-1, 0), 10),
('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('TOPPADDING', (0, 1), (-1, -1), 4),
('BOTTOMPADDING', (0, 1), (-1, -1), 4),
('GRID', (0, 0), (-1, -1), 0.5, colors.gray), ('GRID', (0, 0), (-1, -1), 0.5, colors.gray),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f0f0f0')]) ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f0f0f0')])
])) ]))
@@ -507,16 +623,16 @@ class PDFReportGenerator:
def add_alerts_section(self, alerts_data: dict): def add_alerts_section(self, alerts_data: dict):
"""Add critical alerts section""" """Add critical alerts section"""
self.elements.append(Paragraph("🚨 Alerte Critice", self.styles['SectionHeader'])) self.elements.append(Paragraph("Alerte Critice", self.styles['SectionHeader']))
# Vânzări sub cost # Vanzari sub cost
if 'vanzari_sub_cost' in alerts_data and not alerts_data['vanzari_sub_cost'].empty: if 'vanzari_sub_cost' in alerts_data and not alerts_data['vanzari_sub_cost'].empty:
df = alerts_data['vanzari_sub_cost'] df = alerts_data['vanzari_sub_cost']
count = len(df) count = len(df)
total_loss = df['PIERDERE'].sum() if 'PIERDERE' in df.columns else 0 total_loss = df['PIERDERE'].sum() if 'PIERDERE' in df.columns else 0
self.elements.append(Paragraph( self.elements.append(Paragraph(
f"⛔ VÂNZĂRI SUB COST: {count} tranzacții cu pierdere totală de {abs(total_loss):,.2f} RON", f"VANZARI SUB COST: {count} tranzactii cu pierdere totala de {abs(total_loss):,.2f} RON",
self.styles['AlertHeader'] self.styles['AlertHeader']
)) ))
@@ -526,24 +642,30 @@ class PDFReportGenerator:
cols_to_show = ['FACTURA', 'CLIENT', 'PRODUS', 'PIERDERE'] cols_to_show = ['FACTURA', 'CLIENT', 'PRODUS', 'PIERDERE']
cols_to_show = [c for c in cols_to_show if c in top5.columns] cols_to_show = [c for c in cols_to_show if c in top5.columns]
if cols_to_show: if cols_to_show:
data = [cols_to_show] + top5[cols_to_show].values.tolist() # Header with Paragraph
data = [[self.make_cell_paragraph(c, bold=True) for c in cols_to_show]]
for _, row in top5.iterrows():
row_data = [self.make_cell_paragraph(row.get(c, '')) for c in cols_to_show]
data.append(row_data)
table = Table(data, colWidths=[3*cm, 4*cm, 5*cm, 2*cm]) table = Table(data, colWidths=[3*cm, 4*cm, 5*cm, 2*cm])
table.setStyle(TableStyle([ table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#c0392b')), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#c0392b')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white), ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTSIZE', (0, 0), (-1, -1), 8), ('VALIGN', (0, 0), (-1, -1), 'TOP'),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('GRID', (0, 0), (-1, -1), 0.5, colors.gray), ('GRID', (0, 0), (-1, -1), 0.5, colors.gray),
])) ]))
self.elements.append(table) self.elements.append(table)
# Clienți cu marjă mică # Clienti cu marja mica
if 'clienti_marja_mica' in alerts_data and not alerts_data['clienti_marja_mica'].empty: if 'clienti_marja_mica' in alerts_data and not alerts_data['clienti_marja_mica'].empty:
df = alerts_data['clienti_marja_mica'] df = alerts_data['clienti_marja_mica']
count = len(df) count = len(df)
self.elements.append(Spacer(1, 0.3*cm)) self.elements.append(Spacer(1, 0.3*cm))
self.elements.append(Paragraph( self.elements.append(Paragraph(
f"⚠️ CLIENȚI CU MARJĂ MICĂ (<15%): {count} clienți necesită renegociere", f"CLIENTI CU MARJA MICA (<15%): {count} clienti necesita renegociere",
self.styles['AlertHeader'] self.styles['AlertHeader']
)) ))
@@ -552,18 +674,21 @@ class PDFReportGenerator:
cols_to_show = ['CLIENT', 'VANZARI_FARA_TVA', 'PROCENT_MARJA'] cols_to_show = ['CLIENT', 'VANZARI_FARA_TVA', 'PROCENT_MARJA']
cols_to_show = [c for c in cols_to_show if c in top5.columns] cols_to_show = [c for c in cols_to_show if c in top5.columns]
if cols_to_show: if cols_to_show:
data = [cols_to_show] # Header with Paragraph
data = [[self.make_cell_paragraph(c, bold=True) for c in cols_to_show]]
for _, row in top5.iterrows(): for _, row in top5.iterrows():
data.append([ data.append([
str(row.get('CLIENT', ''))[:30], self.make_cell_paragraph(row.get('CLIENT', '')),
f"{row.get('VANZARI_FARA_TVA', 0):,.0f}", self.make_cell_paragraph(f"{row.get('VANZARI_FARA_TVA', 0):,.0f}"),
f"{row.get('PROCENT_MARJA', 0):.1f}%" self.make_cell_paragraph(f"{row.get('PROCENT_MARJA', 0):.1f}%")
]) ])
table = Table(data, colWidths=[6*cm, 3*cm, 2*cm]) table = Table(data, colWidths=[6*cm, 3*cm, 2*cm])
table.setStyle(TableStyle([ table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#e67e22')), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#e67e22')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white), ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTSIZE', (0, 0), (-1, -1), 8), ('VALIGN', (0, 0), (-1, -1), 'TOP'),
('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('GRID', (0, 0), (-1, -1), 0.5, colors.gray), ('GRID', (0, 0), (-1, -1), 0.5, colors.gray),
])) ]))
self.elements.append(table) self.elements.append(table)
@@ -572,11 +697,11 @@ class PDFReportGenerator:
def add_chart_image(self, fig, title: str): def add_chart_image(self, fig, title: str):
"""Add a matplotlib figure as image""" """Add a matplotlib figure as image"""
self.elements.append(Paragraph(title, self.styles['SectionHeader'])) self.elements.append(Paragraph(remove_diacritics(title), self.styles['SectionHeader']))
# Save figure to buffer # Save figure to buffer
buf = io.BytesIO() buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=150, bbox_inches='tight') fig.savefig(buf, format='png', dpi=200, bbox_inches='tight')
buf.seek(0) buf.seek(0)
# Add to PDF # Add to PDF
@@ -587,11 +712,11 @@ class PDFReportGenerator:
plt.close(fig) plt.close(fig)
def add_table_section(self, title: str, df: pd.DataFrame, columns: list = None, max_rows: int = 15): def add_table_section(self, title: str, df: pd.DataFrame, columns: list = None, max_rows: int = 15):
"""Add a data table section""" """Add a data table section with word-wrapped cells"""
self.elements.append(Paragraph(title, self.styles['SectionHeader'])) self.elements.append(Paragraph(remove_diacritics(title), self.styles['SectionHeader']))
if df is None or df.empty: if df is None or df.empty:
self.elements.append(Paragraph("Nu există date.", self.styles['Normal'])) self.elements.append(Paragraph("Nu exista date.", self.styles['Normal']))
return return
# Select columns # Select columns
@@ -603,18 +728,18 @@ class PDFReportGenerator:
if not cols: if not cols:
return return
# Prepare data # Prepare data with Paragraph cells for word wrapping
data = [cols] data = [[self.make_cell_paragraph(c, bold=True) for c in cols]]
for _, row in df.head(max_rows).iterrows(): for _, row in df.head(max_rows).iterrows():
row_data = [] row_data = []
for col in cols: for col in cols:
val = row.get(col, '') val = row.get(col, '')
if isinstance(val, float): if isinstance(val, float):
row_data.append(f"{val:,.2f}") row_data.append(self.make_cell_paragraph(f"{val:,.2f}"))
elif isinstance(val, int): elif isinstance(val, int):
row_data.append(f"{val:,}") row_data.append(self.make_cell_paragraph(f"{val:,}"))
else: else:
row_data.append(str(val)[:25]) # Truncate long strings row_data.append(self.make_cell_paragraph(val))
data.append(row_data) data.append(row_data)
# Calculate column widths # Calculate column widths
@@ -626,9 +751,9 @@ class PDFReportGenerator:
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#366092')), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#366092')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white), ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('VALIGN', (0, 0), (-1, -1), 'TOP'),
('FONTSIZE', (0, 0), (-1, -1), 7), ('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, 0), 8), ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('GRID', (0, 0), (-1, -1), 0.5, colors.gray), ('GRID', (0, 0), (-1, -1), 0.5, colors.gray),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f5f5f5')]) ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f5f5f5')])
])) ]))
@@ -637,7 +762,7 @@ class PDFReportGenerator:
if len(df) > max_rows: if len(df) > max_rows:
self.elements.append(Paragraph( self.elements.append(Paragraph(
f"... și încă {len(df) - max_rows} înregistrări (vezi Excel pentru lista completă)", f"... si inca {len(df) - max_rows} inregistrari (vezi Excel pentru lista completa)",
self.styles['SmallText'] self.styles['SmallText']
)) ))
@@ -649,7 +774,7 @@ class PDFReportGenerator:
def add_consolidated_page(self, page_title: str, sections: list): def add_consolidated_page(self, page_title: str, sections: list):
""" """
Add a consolidated PDF page with multiple sections. Add a consolidated PDF page with multiple sections and word-wrapped cells.
Args: Args:
page_title: Main title for the page page_title: Main title for the page
@@ -660,7 +785,7 @@ class PDFReportGenerator:
- 'max_rows': Max rows to display (default 15) - 'max_rows': Max rows to display (default 15)
""" """
# Page title # Page title
self.elements.append(Paragraph(page_title, self.styles['SectionHeader'])) self.elements.append(Paragraph(remove_diacritics(page_title), self.styles['SectionHeader']))
self.elements.append(Spacer(1, 0.3*cm)) self.elements.append(Spacer(1, 0.3*cm))
for section in sections: for section in sections:
@@ -678,10 +803,10 @@ class PDFReportGenerator:
spaceAfter=5, spaceAfter=5,
textColor=colors.HexColor('#2C3E50') textColor=colors.HexColor('#2C3E50')
) )
self.elements.append(Paragraph(section_title, subsection_style)) self.elements.append(Paragraph(remove_diacritics(section_title), subsection_style))
if df is None or df.empty: if df is None or df.empty:
self.elements.append(Paragraph("Nu există date.", self.styles['Normal'])) self.elements.append(Paragraph("Nu exista date.", self.styles['Normal']))
self.elements.append(Spacer(1, 0.3*cm)) self.elements.append(Spacer(1, 0.3*cm))
continue continue
@@ -694,18 +819,18 @@ class PDFReportGenerator:
if not cols: if not cols:
continue continue
# Prepare data # Prepare data with Paragraph cells for word wrapping
data = [cols] data = [[self.make_cell_paragraph(c, bold=True) for c in cols]]
for _, row in df.head(max_rows).iterrows(): for _, row in df.head(max_rows).iterrows():
row_data = [] row_data = []
for col in cols: for col in cols:
val = row.get(col, '') val = row.get(col, '')
if isinstance(val, float): if isinstance(val, float):
row_data.append(f"{val:,.2f}") row_data.append(self.make_cell_paragraph(f"{val:,.2f}"))
elif isinstance(val, int): elif isinstance(val, int):
row_data.append(f"{val:,}") row_data.append(self.make_cell_paragraph(f"{val:,}"))
else: else:
row_data.append(str(val)[:30]) # Truncate long strings row_data.append(self.make_cell_paragraph(val))
data.append(row_data) data.append(row_data)
# Calculate column widths # Calculate column widths
@@ -719,9 +844,9 @@ class PDFReportGenerator:
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#366092')), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#366092')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white), ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('VALIGN', (0, 0), (-1, -1), 'TOP'),
('FONTSIZE', (0, 0), (-1, -1), 7), ('TOPPADDING', (0, 0), (-1, -1), 4),
('BOTTOMPADDING', (0, 0), (-1, 0), 6), ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('GRID', (0, 0), (-1, -1), 0.5, colors.gray), ('GRID', (0, 0), (-1, -1), 0.5, colors.gray),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f5f5f5')]) ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f5f5f5')])
] ]
@@ -743,7 +868,7 @@ class PDFReportGenerator:
if len(df) > max_rows: if len(df) > max_rows:
self.elements.append(Paragraph( self.elements.append(Paragraph(
f"... și încă {len(df) - max_rows} înregistrări", f"... si inca {len(df) - max_rows} inregistrari",
self.styles['SmallText'] self.styles['SmallText']
)) ))
@@ -764,11 +889,11 @@ class PDFReportGenerator:
df_sorted = df_sorted.sort_values('_order').head(7) df_sorted = df_sorted.sort_values('_order').head(7)
for _, row in df_sorted.iterrows(): for _, row in df_sorted.iterrows():
status = row.get('STATUS', 'OK') status = sanitize_for_pdf(row.get('STATUS', 'OK'))
indicator = row.get('INDICATOR', '') indicator = sanitize_for_pdf(row.get('INDICATOR', ''))
valoare = row.get('VALOARE', '') valoare = sanitize_for_pdf(row.get('VALOARE', ''))
explicatie = row.get('EXPLICATIE', '') explicatie = sanitize_for_pdf(row.get('EXPLICATIE', ''))
recomandare = row.get('RECOMANDARE', '') recomandare = sanitize_for_pdf(row.get('RECOMANDARE', ''))
# Color based on status # Color based on status
if status == 'ALERTA': if status == 'ALERTA':
@@ -819,128 +944,202 @@ class PDFReportGenerator:
print(f"✓ PDF salvat: {self.output_path}") print(f"✓ PDF salvat: {self.output_path}")
# Modern minimalist chart colors
CHART_COLORS = {
'primary': '#2C3E50', # Dark blue-gray
'secondary': '#7F8C8D', # Gray
'accent': '#E74C3C', # Red for alerts/negative
'positive': '#27AE60', # Green for positive trends
'light': '#ECF0F1', # Light background
}
def setup_chart_style():
"""Apply modern minimalist styling to charts"""
plt.rcParams.update({
'font.family': 'sans-serif',
'font.size': 10,
'axes.titlesize': 12,
'axes.titleweight': 'bold',
'axes.spines.top': False,
'axes.spines.right': False,
'axes.grid': True,
'grid.alpha': 0.3,
'grid.linestyle': '--',
'figure.facecolor': 'white',
'axes.facecolor': 'white',
'axes.edgecolor': '#7F8C8D',
'xtick.color': '#7F8C8D',
'ytick.color': '#7F8C8D',
})
def create_monthly_chart(df: pd.DataFrame) -> plt.Figure: def create_monthly_chart(df: pd.DataFrame) -> plt.Figure:
"""Create monthly sales and margin chart""" """Create monthly sales chart - modern minimalist style"""
setup_chart_style()
if df is None or df.empty: if df is None or df.empty:
fig, ax = plt.subplots(figsize=(12, 6)) fig, ax = plt.subplots(figsize=(12, 5))
ax.text(0.5, 0.5, 'Nu există date', ha='center', va='center') ax.text(0.5, 0.5, 'Nu exista date', ha='center', va='center')
return fig return fig
fig, ax1 = plt.subplots(figsize=(12, 6)) fig, ax = plt.subplots(figsize=(12, 5))
x = range(len(df)) x = range(len(df))
# Bar chart for sales # Single color bars with clean styling
bars = ax1.bar(x, df['VANZARI_FARA_TVA'], color='#366092', alpha=0.7, label='Vânzări') bars = ax.bar(x, df['VANZARI_FARA_TVA'], color=CHART_COLORS['primary'], alpha=0.85,
ax1.set_xlabel('Luna') edgecolor='white', linewidth=0.5)
ax1.set_ylabel('Vânzări (RON)', color='#366092')
ax1.tick_params(axis='y', labelcolor='#366092')
ax1.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f'{x/1000:,.0f}k'))
# Line chart for margin # Add value labels on top of bars
ax2 = ax1.twinx() for bar in bars:
line = ax2.plot(x, df['MARJA_BRUTA'], color='#e74c3c', linewidth=2, marker='o', label='Marja') height = bar.get_height()
ax2.set_ylabel('Marja (RON)', color='#e74c3c') ax.text(bar.get_x() + bar.get_width()/2., height,
ax2.tick_params(axis='y', labelcolor='#e74c3c') f'{height/1000:,.0f}k', ha='center', va='bottom', fontsize=8, color='#555')
# Clean axis formatting
ax.set_xlabel('Luna', fontsize=10, color='#555')
ax.set_ylabel('Vanzari (RON)', fontsize=10, color='#555')
ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f'{x/1000:,.0f}k'))
ax.set_xticks(x)
ax.set_xticklabels(df['LUNA'], rotation=45, ha='right', fontsize=9)
# Add subtle trend line if margin data exists
if 'MARJA_BRUTA' in df.columns:
ax2 = ax.twinx()
ax2.plot(x, df['MARJA_BRUTA'], color=CHART_COLORS['accent'], linewidth=2,
marker='o', markersize=4, label='Marja Bruta', alpha=0.8)
ax2.set_ylabel('Marja (RON)', fontsize=10, color=CHART_COLORS['accent'])
ax2.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f'{x/1000:,.0f}k')) ax2.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f'{x/1000:,.0f}k'))
ax2.spines['right'].set_visible(True)
ax2.spines['right'].set_color(CHART_COLORS['accent'])
ax2.tick_params(axis='y', colors=CHART_COLORS['accent'])
# X-axis labels ax.set_title('Evolutia Vanzarilor Lunare', fontsize=12, fontweight='bold', color='#2C3E50')
ax1.set_xticks(x)
ax1.set_xticklabels(df['LUNA'], rotation=45, ha='right')
# Legend
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')
plt.title('Evoluția Vânzărilor și Marjei Lunare')
plt.tight_layout() plt.tight_layout()
return fig return fig
def create_client_concentration_chart(df: pd.DataFrame) -> plt.Figure: def create_client_concentration_chart(df: pd.DataFrame) -> plt.Figure:
"""Create client concentration pie chart""" """Create client concentration chart - horizontal bars (easier to read than pie)"""
setup_chart_style()
if df is None or df.empty: if df is None or df.empty:
fig, ax = plt.subplots(figsize=(10, 8)) fig, ax = plt.subplots(figsize=(12, 6))
ax.text(0.5, 0.5, 'Nu există date', ha='center', va='center') ax.text(0.5, 0.5, 'Nu exista date', ha='center', va='center')
return fig return fig
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# Pie chart - Top 10 vs Others
top10 = df.head(10) top10 = df.head(10)
others_pct = 100 - top10['PROCENT_CUMULAT'].iloc[-1] if len(top10) >= 10 else 0
sizes = list(top10['PROCENT_DIN_TOTAL']) # Left: Horizontal bar chart showing client share (cleaner than pie)
if others_pct > 0: y_pos = range(len(top10)-1, -1, -1) # Reverse for top-to-bottom
sizes.append(others_pct) colors = [CHART_COLORS['primary'] if pct < 25 else CHART_COLORS['accent']
labels = list(top10['CLIENT'].str[:20]) # Truncate names for pct in top10['PROCENT_DIN_TOTAL']]
if others_pct > 0:
labels.append('Alții')
colors_list = plt.cm.Set3(range(len(sizes))) bars = ax1.barh(y_pos, top10['PROCENT_DIN_TOTAL'], color=colors, alpha=0.85,
edgecolor='white', linewidth=0.5)
ax1.set_yticks(y_pos)
ax1.set_yticklabels([c[:25] for c in top10['CLIENT']], fontsize=9)
ax1.set_xlabel('% din Vanzari Totale', fontsize=10)
ax1.set_title('Top 10 Clienti - Pondere Vanzari', fontsize=11, fontweight='bold')
ax1.pie(sizes, labels=None, colors=colors_list, autopct='%1.1f%%', startangle=90) # Add percentage labels
ax1.set_title('Concentrare Top 10 Clienți') for bar, pct in zip(bars, top10['PROCENT_DIN_TOTAL']):
ax1.legend(labels, loc='center left', bbox_to_anchor=(1, 0.5), fontsize=8) ax1.text(bar.get_width() + 0.5, bar.get_y() + bar.get_height()/2,
f'{pct:.1f}%', va='center', fontsize=8, color='#555')
# Bar chart - Pareto # Add 25% threshold line
ax2.bar(range(len(top10)), top10['VANZARI'], color='#366092', alpha=0.7) ax1.axvline(x=25, color=CHART_COLORS['accent'], linestyle='--', alpha=0.7,
label='Prag risc 25%')
ax1.legend(loc='lower right', fontsize=8)
# Right: Pareto chart with cumulative line
x = range(len(top10))
ax2.bar(x, top10['VANZARI'], color=CHART_COLORS['primary'], alpha=0.85,
edgecolor='white', linewidth=0.5)
# Cumulative line on secondary axis
ax2_twin = ax2.twinx() ax2_twin = ax2.twinx()
ax2_twin.plot(range(len(top10)), top10['PROCENT_CUMULAT'], 'r-o', linewidth=2) ax2_twin.plot(x, top10['PROCENT_CUMULAT'], color=CHART_COLORS['accent'],
ax2_twin.axhline(y=80, color='green', linestyle='--', alpha=0.5, label='80%') linewidth=2, marker='o', markersize=4)
ax2_twin.axhline(y=80, color=CHART_COLORS['positive'], linestyle='--',
alpha=0.7, linewidth=1.5, label='Prag 80%')
ax2.set_xticks(range(len(top10))) ax2.set_xticks(x)
ax2.set_xticklabels([c[:15] for c in top10['CLIENT']], rotation=45, ha='right', fontsize=8) ax2.set_xticklabels([c[:12] for c in top10['CLIENT']], rotation=45, ha='right', fontsize=8)
ax2.set_ylabel('Vânzări (RON)') ax2.set_ylabel('Vanzari (RON)', fontsize=10)
ax2_twin.set_ylabel('% Cumulat') ax2.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f'{x/1000:,.0f}k'))
ax2.set_title('Analiză Pareto Clienți') ax2_twin.set_ylabel('% Cumulat', fontsize=10, color=CHART_COLORS['accent'])
ax2_twin.set_ylim(0, 105)
ax2.set_title('Analiza Pareto - Concentrare Clienti', fontsize=11, fontweight='bold')
ax2_twin.legend(loc='center right', fontsize=8)
plt.tight_layout() plt.tight_layout()
return fig return fig
def create_production_chart(df: pd.DataFrame) -> plt.Figure: def create_production_chart(df: pd.DataFrame) -> plt.Figure:
"""Create production vs resale comparison chart""" """Create production vs resale comparison chart - modern minimalist style"""
setup_chart_style()
if df is None or df.empty: if df is None or df.empty:
fig, ax = plt.subplots(figsize=(10, 6)) fig, ax = plt.subplots(figsize=(10, 5))
ax.text(0.5, 0.5, 'Nu există date', ha='center', va='center') ax.text(0.5, 0.5, 'Nu exista date', ha='center', va='center')
return fig return fig
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# Bar chart - Sales by type
x = range(len(df)) x = range(len(df))
ax1.bar(x, df['VANZARI_FARA_TVA'], color=['#366092', '#e74c3c', '#2ecc71'][:len(df)])
# Left: Sales by type (two-color scheme: primary + secondary)
bar_colors = [CHART_COLORS['primary'], CHART_COLORS['secondary']][:len(df)]
if len(df) > 2:
bar_colors = [CHART_COLORS['primary']] * len(df)
bars = ax1.bar(x, df['VANZARI_FARA_TVA'], color=bar_colors, alpha=0.85,
edgecolor='white', linewidth=0.5)
ax1.set_xticks(x) ax1.set_xticks(x)
ax1.set_xticklabels(df['TIP_PRODUS'], rotation=15) ax1.set_xticklabels(df['TIP_PRODUS'], rotation=15, fontsize=9)
ax1.set_ylabel('Vânzări (RON)') ax1.set_ylabel('Vanzari (RON)', fontsize=10)
ax1.set_title('Vânzări per Tip Produs') ax1.set_title('Vanzari per Tip Produs', fontsize=11, fontweight='bold')
ax1.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f'{x/1000:,.0f}k')) ax1.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f'{x/1000:,.0f}k'))
# Bar chart - Margin % # Add value labels
colors = ['#2ecc71' if m > 20 else '#e67e22' if m > 15 else '#e74c3c' for m in df['PROCENT_MARJA']] for bar in bars:
ax2.bar(x, df['PROCENT_MARJA'], color=colors) height = bar.get_height()
ax1.text(bar.get_x() + bar.get_width()/2., height,
f'{height/1000:,.0f}k', ha='center', va='bottom', fontsize=9, color='#555')
# Right: Margin % with color-coded status
colors = [CHART_COLORS['positive'] if m > 20 else CHART_COLORS['secondary'] if m > 15
else CHART_COLORS['accent'] for m in df['PROCENT_MARJA']]
bars2 = ax2.bar(x, df['PROCENT_MARJA'], color=colors, alpha=0.85,
edgecolor='white', linewidth=0.5)
ax2.set_xticks(x) ax2.set_xticks(x)
ax2.set_xticklabels(df['TIP_PRODUS'], rotation=15) ax2.set_xticklabels(df['TIP_PRODUS'], rotation=15, fontsize=9)
ax2.set_ylabel('Marjă (%)') ax2.set_ylabel('Marja (%)', fontsize=10)
ax2.set_title('Marjă per Tip Produs') ax2.set_title('Marja per Tip Produs', fontsize=11, fontweight='bold')
ax2.axhline(y=15, color='red', linestyle='--', alpha=0.5, label='Prag minim 15%') ax2.axhline(y=15, color=CHART_COLORS['accent'], linestyle='--', alpha=0.7,
ax2.legend() linewidth=1.5, label='Prag minim 15%')
ax2.legend(loc='upper right', fontsize=8)
# Add value labels
for bar, val in zip(bars2, df['PROCENT_MARJA']):
ax2.text(bar.get_x() + bar.get_width()/2., bar.get_height(),
f'{val:.1f}%', ha='center', va='bottom', fontsize=9, color='#555')
plt.tight_layout() plt.tight_layout()
return fig return fig
def create_cash_cycle_chart(df: pd.DataFrame) -> plt.Figure: def create_cash_cycle_chart(df: pd.DataFrame) -> plt.Figure:
"""Create cash conversion cycle visualization""" """Create cash conversion cycle visualization - modern minimalist style"""
setup_chart_style()
if df is None or df.empty: if df is None or df.empty:
fig, ax = plt.subplots(figsize=(10, 6)) fig, ax = plt.subplots(figsize=(10, 5))
ax.text(0.5, 0.5, 'Nu exista date', ha='center', va='center') ax.text(0.5, 0.5, 'Nu exista date', ha='center', va='center')
return fig return fig
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# Extract values # Extract values
indicators = df['INDICATOR'].tolist() if 'INDICATOR' in df.columns else [] indicators = df['INDICATOR'].tolist() if 'INDICATOR' in df.columns else []
@@ -951,12 +1150,12 @@ def create_cash_cycle_chart(df: pd.DataFrame) -> plt.Figure:
ax2.text(0.5, 0.5, 'Date incomplete', ha='center', va='center') ax2.text(0.5, 0.5, 'Date incomplete', ha='center', va='center')
return fig return fig
# Colors for each component # Simplified color scheme
colors_map = { colors_map = {
'DIO': '#3498db', # Blue for inventory 'DIO': CHART_COLORS['primary'], # Dark blue for inventory
'DSO': '#e74c3c', # Red for receivables 'DSO': CHART_COLORS['secondary'], # Gray for receivables
'DPO': '#2ecc71', # Green for payables 'DPO': CHART_COLORS['positive'], # Green for payables (reduces cycle)
'CCC': '#9b59b6' # Purple for total cycle 'CCC': CHART_COLORS['accent'] # Red for total cycle result
} }
bar_colors = [] bar_colors = []
@@ -966,59 +1165,64 @@ def create_cash_cycle_chart(df: pd.DataFrame) -> plt.Figure:
bar_colors.append(color) bar_colors.append(color)
break break
else: else:
bar_colors.append('#95a5a6') bar_colors.append(CHART_COLORS['secondary'])
# Bar chart # Left: Component bars
x = range(len(indicators)) x = range(len(indicators))
bars = ax1.bar(x, zile, color=bar_colors, alpha=0.8) bars = ax1.bar(x, zile, color=bar_colors, alpha=0.85, edgecolor='white', linewidth=0.5)
ax1.set_xticks(x) ax1.set_xticks(x)
ax1.set_xticklabels([ind[:20] for ind in indicators], rotation=45, ha='right', fontsize=9) ax1.set_xticklabels([ind[:20] for ind in indicators], rotation=45, ha='right', fontsize=9)
ax1.set_ylabel('Zile') ax1.set_ylabel('Zile', fontsize=10)
ax1.set_title('Ciclu Conversie Cash - Componente') ax1.set_title('Ciclu Conversie Cash - Componente', fontsize=11, fontweight='bold')
# Add value labels on bars # Add value labels
for bar, val in zip(bars, zile): for bar, val in zip(bars, zile):
height = bar.get_height() ax1.text(bar.get_x() + bar.get_width()/2., bar.get_height(),
ax1.text(bar.get_x() + bar.get_width()/2., height, f'{int(val)}', ha='center', va='bottom', fontsize=10, fontweight='bold', color='#555')
f'{int(val)}',
ha='center', va='bottom', fontsize=10, fontweight='bold')
# Waterfall-style visualization # Right: Formula visualization
# DIO + DSO - DPO = CCC
dio = next((z for i, z in zip(indicators, zile) if 'DIO' in i.upper()), 0) dio = next((z for i, z in zip(indicators, zile) if 'DIO' in i.upper()), 0)
dso = next((z for i, z in zip(indicators, zile) if 'DSO' in i.upper() and 'DIO' not in i.upper()), 0) dso = next((z for i, z in zip(indicators, zile) if 'DSO' in i.upper() and 'DIO' not in i.upper()), 0)
dpo = next((z for i, z in zip(indicators, zile) if 'DPO' in i.upper()), 0) dpo = next((z for i, z in zip(indicators, zile) if 'DPO' in i.upper()), 0)
ccc = dio + dso - dpo ccc = dio + dso - dpo
waterfall_labels = ['DIO\n(Zile Stoc)', 'DSO\n(Zile Incasare)', 'DPO\n(Zile Plata)', 'CCC\n(Ciclu Total)'] bars2 = ax2.bar([0, 1, 2], [dio, dso, dpo],
waterfall_values = [dio, dso, -dpo, ccc] color=[CHART_COLORS['primary'], CHART_COLORS['secondary'], CHART_COLORS['positive']],
waterfall_colors = ['#3498db', '#e74c3c', '#2ecc71', '#9b59b6'] alpha=0.85, edgecolor='white', linewidth=0.5)
# Calculate positions for waterfall # CCC result line with color based on health
cumulative = [0] if ccc > 60:
for i, v in enumerate(waterfall_values[:-1]): ccc_color = CHART_COLORS['accent']
cumulative.append(cumulative[-1] + v) elif ccc > 30:
ccc_color = CHART_COLORS['secondary']
else:
ccc_color = CHART_COLORS['positive']
ax2.bar([0, 1, 2], [dio, dso, dpo], color=['#3498db', '#e74c3c', '#2ecc71'], alpha=0.8) ax2.axhline(y=ccc, color=ccc_color, linewidth=3, linestyle='--',
ax2.axhline(y=ccc, color='#9b59b6', linewidth=3, linestyle='--', label=f'CCC = {int(ccc)} zile') label=f'CCC = {int(ccc)} zile')
ax2.set_xticks([0, 1, 2]) ax2.set_xticks([0, 1, 2])
ax2.set_xticklabels(['DIO\n(+Stoc)', 'DSO\n(+Incasare)', 'DPO\n(-Plata)'], fontsize=9) ax2.set_xticklabels(['DIO\n(+Stoc)', 'DSO\n(+Incasare)', 'DPO\n(-Plata)'], fontsize=9)
ax2.set_ylabel('Zile') ax2.set_ylabel('Zile', fontsize=10)
ax2.set_title('Formula: DIO + DSO - DPO = CCC') ax2.set_title('Formula: DIO + DSO - DPO = CCC', fontsize=11, fontweight='bold')
ax2.legend(loc='upper right') ax2.legend(loc='upper right', fontsize=9)
# Add annotation explaining the result # Add value labels
for bar, val in zip(bars2, [dio, dso, dpo]):
ax2.text(bar.get_x() + bar.get_width()/2., bar.get_height(),
f'{int(val)}', ha='center', va='bottom', fontsize=10, fontweight='bold', color='#555')
# Status annotation
if ccc > 60: if ccc > 60:
verdict = "Ciclu lung - capital blocat mult timp" verdict = "Ciclu lung - capital blocat mult timp"
verdict_color = '#c0392b' verdict_color = CHART_COLORS['accent']
elif ccc > 30: elif ccc > 30:
verdict = "Ciclu moderat - poate fi optimizat" verdict = "Ciclu moderat - poate fi optimizat"
verdict_color = '#d68910' verdict_color = CHART_COLORS['secondary']
else: else:
verdict = "Ciclu eficient - capital rotit rapid" verdict = "Ciclu eficient - capital rotit rapid"
verdict_color = '#27ae60' verdict_color = CHART_COLORS['positive']
ax2.text(0.5, -0.15, verdict, transform=ax2.transAxes, ax2.text(0.5, -0.18, verdict, transform=ax2.transAxes,
ha='center', fontsize=10, color=verdict_color, fontweight='bold') ha='center', fontsize=10, color=verdict_color, fontweight='bold')
plt.tight_layout() plt.tight_layout()