Initial commit: Data Intelligence Report Generator

- Oracle ERP ROA integration with sales analytics and margin analysis
- Excel multi-sheet reports with conditional formatting
- PDF executive summaries with charts via ReportLab
- Optimized SQL queries (no cartesian products)
- Docker support for cross-platform deployment
- Configurable alert thresholds for business intelligence

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-02 15:41:56 +02:00
commit 0b732f7a7a
15 changed files with 5420 additions and 0 deletions

773
report_generator.py Normal file
View File

@@ -0,0 +1,773 @@
"""
Report Generator Module
Generates Excel and PDF reports from query results
"""
import pandas as pd
from datetime import datetime
from pathlib import Path
import matplotlib
matplotlib.use('Agg') # Non-interactive backend
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.chart import BarChart, LineChart, PieChart, Reference
from openpyxl.utils import get_column_letter
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
PageBreak, Image, KeepTogether
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
import io
class ExcelReportGenerator:
"""Generate Excel reports with multiple sheets and formatting"""
def __init__(self, output_path: Path):
self.output_path = output_path
self.wb = Workbook()
# Remove default sheet
self.wb.remove(self.wb.active)
# Define styles
self.header_font = Font(bold=True, color='FFFFFF', size=11)
self.header_fill = PatternFill(start_color='366092', end_color='366092', fill_type='solid')
self.alert_fill = PatternFill(start_color='FF6B6B', end_color='FF6B6B', fill_type='solid')
self.warning_fill = PatternFill(start_color='FFE66D', end_color='FFE66D', fill_type='solid')
self.good_fill = PatternFill(start_color='4ECDC4', end_color='4ECDC4', fill_type='solid')
self.border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
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"""
# Truncate sheet name to 31 chars (Excel limit)
sheet_name = name[:31]
ws = self.wb.create_sheet(title=sheet_name)
start_row = 1
# Add title if provided
if title:
ws.cell(row=start_row, column=1, value=title)
ws.cell(row=start_row, column=1).font = Font(bold=True, size=14)
start_row += 1
# Add description if provided
if description:
ws.cell(row=start_row, column=1, value=description)
ws.cell(row=start_row, column=1).font = Font(italic=True, size=10, color='666666')
start_row += 1
# Add timestamp
ws.cell(row=start_row, column=1, value=f"Generat: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
ws.cell(row=start_row, column=1).font = Font(size=9, color='999999')
start_row += 1
# Add legend if provided
if legend:
start_row += 1
ws.cell(row=start_row, column=1, value="Explicații calcule:")
ws.cell(row=start_row, column=1).font = Font(bold=True, size=9, color='336699')
start_row += 1
for col_name, explanation in legend.items():
ws.cell(row=start_row, column=1, value=f"{col_name}: {explanation}")
ws.cell(row=start_row, column=1).font = Font(size=8, color='666666')
start_row += 1
start_row += 1
if df is None or df.empty:
ws.cell(row=start_row, column=1, value="Nu există date pentru această analiză.")
return
# Write headers
for col_idx, col_name in enumerate(df.columns, 1):
cell = ws.cell(row=start_row, column=col_idx, value=col_name)
cell.font = self.header_font
cell.fill = self.header_fill
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
cell.border = self.border
# Write data
for row_idx, row in enumerate(df.itertuples(index=False), start_row + 1):
for col_idx, value in enumerate(row, 1):
cell = ws.cell(row=row_idx, column=col_idx, value=value)
cell.border = self.border
# Format numbers
if isinstance(value, (int, float)):
cell.number_format = '#,##0.00' if isinstance(value, float) else '#,##0'
cell.alignment = Alignment(horizontal='right')
# Highlight negative values
if isinstance(value, (int, float)) and value < 0:
cell.fill = self.alert_fill
# Highlight low margins
col_name = df.columns[col_idx - 1].lower()
if 'procent' in col_name or 'marja' in col_name:
if isinstance(value, (int, float)):
if value < 10:
cell.fill = self.alert_fill
elif value < 15:
cell.fill = self.warning_fill
elif value > 25:
cell.fill = self.good_fill
# Highlight TREND column for YoY sheets
if col_name == 'trend':
if isinstance(value, str):
if value in ('CRESTERE', 'IMBUNATATIRE', 'DIVERSIFICARE'):
cell.fill = self.good_fill
elif value in ('SCADERE', 'DETERIORARE', 'CONCENTRARE', 'PIERDUT'):
cell.fill = self.alert_fill
elif value == 'ATENTIE':
cell.fill = self.warning_fill
# Highlight STATUS column
if col_name == 'status' or col_name == 'acoperire':
if isinstance(value, str):
if value == 'OK':
cell.fill = self.good_fill
elif value in ('ATENTIE', 'NECESAR'):
cell.fill = self.warning_fill
elif value in ('ALERTA', 'DEFICIT', 'RISC MARE'):
cell.fill = self.alert_fill
# Highlight variatie columns (positive = green, negative = red)
if 'variatie' in col_name:
if isinstance(value, (int, float)):
if value > 0:
cell.fill = self.good_fill
elif value < 0:
cell.fill = self.alert_fill
# Auto-adjust column widths
for col_idx, col_name in enumerate(df.columns, 1):
max_length = len(str(col_name))
for row in df.itertuples(index=False):
cell_value = row[col_idx - 1]
if cell_value:
max_length = max(max_length, len(str(cell_value)))
adjusted_width = min(max_length + 2, 50)
ws.column_dimensions[get_column_letter(col_idx)].width = adjusted_width
# Freeze header row
ws.freeze_panes = ws.cell(row=start_row + 1, column=1)
def add_sheet_with_recommendations(self, name: str, df: pd.DataFrame,
recommendations_df: pd.DataFrame,
title: str = None, description: str = None,
legend: dict = None, top_n_recommendations: int = 5):
"""Adauga sheet formatat cu KPIs si top recomandari dedesubt"""
sheet_name = name[:31]
ws = self.wb.create_sheet(title=sheet_name)
start_row = 1
# Adauga titlu
if title:
ws.cell(row=start_row, column=1, value=title)
ws.cell(row=start_row, column=1).font = Font(bold=True, size=14)
start_row += 1
# Adauga descriere
if description:
ws.cell(row=start_row, column=1, value=description)
ws.cell(row=start_row, column=1).font = Font(italic=True, size=10, color='666666')
start_row += 1
# Adauga timestamp
ws.cell(row=start_row, column=1, value=f"Generat: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
ws.cell(row=start_row, column=1).font = Font(size=9, color='999999')
start_row += 2
# === SECTIUNEA 1: KPIs ===
if df is not None and not df.empty:
# Header
for col_idx, col_name in enumerate(df.columns, 1):
cell = ws.cell(row=start_row, column=col_idx, value=col_name)
cell.font = self.header_font
cell.fill = self.header_fill
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
cell.border = self.border
# Date
for row_idx, row in enumerate(df.itertuples(index=False), start_row + 1):
for col_idx, value in enumerate(row, 1):
cell = ws.cell(row=row_idx, column=col_idx, value=value)
cell.border = self.border
if isinstance(value, (int, float)):
cell.number_format = '#,##0.00' if isinstance(value, float) else '#,##0'
cell.alignment = Alignment(horizontal='right')
start_row = start_row + len(df) + 3
# === SECTIUNEA 2: TOP RECOMANDARI ===
if recommendations_df is not None and not recommendations_df.empty:
ws.cell(row=start_row, column=1, value="Top Recomandari Prioritare")
ws.cell(row=start_row, column=1).font = Font(bold=True, size=12, color='366092')
start_row += 1
# Sorteaza dupa prioritate (ALERTA primul, apoi ATENTIE, apoi OK)
df_sorted = recommendations_df.copy()
status_order = {'ALERTA': 0, 'ATENTIE': 1, 'OK': 2}
df_sorted['_order'] = df_sorted['STATUS'].map(status_order).fillna(3)
df_sorted = df_sorted.sort_values('_order').head(top_n_recommendations)
df_sorted = df_sorted.drop(columns=['_order'])
# Coloane de afisat
display_cols = ['STATUS', 'CATEGORIE', 'INDICATOR', 'VALOARE', 'RECOMANDARE']
display_cols = [c for c in display_cols if c in df_sorted.columns]
# Header cu background mov
rec_header_fill = PatternFill(start_color='8E44AD', end_color='8E44AD', fill_type='solid')
for col_idx, col_name in enumerate(display_cols, 1):
cell = ws.cell(row=start_row, column=col_idx, value=col_name)
cell.font = self.header_font
cell.fill = rec_header_fill
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
cell.border = self.border
# Randuri cu colorare dupa status
for row_idx, (_, row) in enumerate(df_sorted.iterrows(), start_row + 1):
status = row.get('STATUS', 'OK')
for col_idx, col_name in enumerate(display_cols, 1):
value = row.get(col_name, '')
cell = ws.cell(row=row_idx, column=col_idx, value=value)
cell.border = self.border
cell.alignment = Alignment(wrap_text=True)
# Colorare conditionala
if status == 'ALERTA':
cell.fill = PatternFill(start_color='FADBD8', end_color='FADBD8', fill_type='solid')
elif status == 'ATENTIE':
cell.fill = PatternFill(start_color='FCF3CF', end_color='FCF3CF', fill_type='solid')
else:
cell.fill = PatternFill(start_color='D5F5E3', end_color='D5F5E3', fill_type='solid')
# Auto-adjust latime coloane
for col_idx in range(1, 8):
ws.column_dimensions[get_column_letter(col_idx)].width = 22
ws.freeze_panes = ws.cell(row=5, column=1)
def save(self):
"""Save the workbook"""
self.wb.save(self.output_path)
print(f"✓ Excel salvat: {self.output_path}")
class PDFReportGenerator:
"""Generate PDF executive summary with charts"""
def __init__(self, output_path: Path, company_name: str = "Data Intelligence Report"):
self.output_path = output_path
self.company_name = company_name
self.elements = []
self.styles = getSampleStyleSheet()
# Custom styles
self.styles.add(ParagraphStyle(
name='CustomTitle',
parent=self.styles['Title'],
fontSize=24,
spaceAfter=30,
alignment=TA_CENTER
))
self.styles.add(ParagraphStyle(
name='SectionHeader',
parent=self.styles['Heading1'],
fontSize=14,
spaceBefore=20,
spaceAfter=10,
textColor=colors.HexColor('#366092')
))
self.styles.add(ParagraphStyle(
name='AlertHeader',
parent=self.styles['Heading2'],
fontSize=12,
textColor=colors.red,
spaceBefore=15,
spaceAfter=8
))
self.styles.add(ParagraphStyle(
name='SmallText',
parent=self.styles['Normal'],
fontSize=8,
textColor=colors.gray
))
def add_title_page(self, report_date: datetime = None):
"""Add title page"""
if report_date is None:
report_date = datetime.now()
self.elements.append(Spacer(1, 3*cm))
self.elements.append(Paragraph(self.company_name, self.styles['CustomTitle']))
self.elements.append(Spacer(1, 1*cm))
self.elements.append(Paragraph(
f"Raport generat: {report_date.strftime('%d %B %Y, %H:%M')}",
self.styles['Normal']
))
self.elements.append(Paragraph(
"Perioada analizată: Ultimele 12 luni",
self.styles['Normal']
))
self.elements.append(PageBreak())
def add_kpi_section(self, kpi_df: pd.DataFrame):
"""Add KPI summary section"""
self.elements.append(Paragraph("📊 Sumar Executiv - KPIs", self.styles['SectionHeader']))
if kpi_df is not None and not kpi_df.empty:
data = [['Indicator', 'Valoare', 'UM']]
for _, row in kpi_df.iterrows():
data.append([
str(row.get('INDICATOR', '')),
str(row.get('VALOARE', '')),
str(row.get('UM', ''))
])
table = Table(data, colWidths=[8*cm, 4*cm, 2*cm])
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#366092')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('ALIGN', (1, 1), (1, -1), 'RIGHT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 10),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('GRID', (0, 0), (-1, -1), 0.5, colors.gray),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f0f0f0')])
]))
self.elements.append(table)
self.elements.append(Spacer(1, 0.5*cm))
def add_alerts_section(self, alerts_data: dict):
"""Add critical alerts section"""
self.elements.append(Paragraph("🚨 Alerte Critice", self.styles['SectionHeader']))
# Vânzări sub cost
if 'vanzari_sub_cost' in alerts_data and not alerts_data['vanzari_sub_cost'].empty:
df = alerts_data['vanzari_sub_cost']
count = len(df)
total_loss = df['PIERDERE'].sum() if 'PIERDERE' in df.columns else 0
self.elements.append(Paragraph(
f"⛔ VÂNZĂRI SUB COST: {count} tranzacții cu pierdere totală de {abs(total_loss):,.2f} RON",
self.styles['AlertHeader']
))
# Show top 5
top5 = df.head(5)
if not top5.empty:
cols_to_show = ['FACTURA', 'CLIENT', 'PRODUS', 'PIERDERE']
cols_to_show = [c for c in cols_to_show if c in top5.columns]
if cols_to_show:
data = [cols_to_show] + top5[cols_to_show].values.tolist()
table = Table(data, colWidths=[3*cm, 4*cm, 5*cm, 2*cm])
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#c0392b')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTSIZE', (0, 0), (-1, -1), 8),
('GRID', (0, 0), (-1, -1), 0.5, colors.gray),
]))
self.elements.append(table)
# Clienți cu marjă mică
if 'clienti_marja_mica' in alerts_data and not alerts_data['clienti_marja_mica'].empty:
df = alerts_data['clienti_marja_mica']
count = len(df)
self.elements.append(Spacer(1, 0.3*cm))
self.elements.append(Paragraph(
f"⚠️ CLIENȚI CU MARJĂ MICĂ (<15%): {count} clienți necesită renegociere",
self.styles['AlertHeader']
))
top5 = df.head(5)
if not top5.empty:
cols_to_show = ['CLIENT', 'VANZARI_FARA_TVA', 'PROCENT_MARJA']
cols_to_show = [c for c in cols_to_show if c in top5.columns]
if cols_to_show:
data = [cols_to_show]
for _, row in top5.iterrows():
data.append([
str(row.get('CLIENT', ''))[:30],
f"{row.get('VANZARI_FARA_TVA', 0):,.0f}",
f"{row.get('PROCENT_MARJA', 0):.1f}%"
])
table = Table(data, colWidths=[6*cm, 3*cm, 2*cm])
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#e67e22')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTSIZE', (0, 0), (-1, -1), 8),
('GRID', (0, 0), (-1, -1), 0.5, colors.gray),
]))
self.elements.append(table)
self.elements.append(Spacer(1, 0.5*cm))
def add_chart_image(self, fig, title: str):
"""Add a matplotlib figure as image"""
self.elements.append(Paragraph(title, self.styles['SectionHeader']))
# Save figure to buffer
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
buf.seek(0)
# Add to PDF
img = Image(buf, width=16*cm, height=10*cm)
self.elements.append(img)
self.elements.append(Spacer(1, 0.5*cm))
plt.close(fig)
def add_table_section(self, title: str, df: pd.DataFrame, columns: list = None, max_rows: int = 15):
"""Add a data table section"""
self.elements.append(Paragraph(title, self.styles['SectionHeader']))
if df is None or df.empty:
self.elements.append(Paragraph("Nu există date.", self.styles['Normal']))
return
# Select columns
if columns:
cols = [c for c in columns if c in df.columns]
else:
cols = list(df.columns)[:6] # Max 6 columns for PDF
if not cols:
return
# Prepare data
data = [cols]
for _, row in df.head(max_rows).iterrows():
row_data = []
for col in cols:
val = row.get(col, '')
if isinstance(val, float):
row_data.append(f"{val:,.2f}")
elif isinstance(val, int):
row_data.append(f"{val:,}")
else:
row_data.append(str(val)[:25]) # Truncate long strings
data.append(row_data)
# Calculate column widths
n_cols = len(cols)
col_width = 16*cm / n_cols
table = Table(data, colWidths=[col_width] * n_cols)
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#366092')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 7),
('BOTTOMPADDING', (0, 0), (-1, 0), 8),
('GRID', (0, 0), (-1, -1), 0.5, colors.gray),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f5f5f5')])
]))
self.elements.append(table)
if len(df) > max_rows:
self.elements.append(Paragraph(
f"... și încă {len(df) - max_rows} înregistrări (vezi Excel pentru lista completă)",
self.styles['SmallText']
))
self.elements.append(Spacer(1, 0.5*cm))
def add_page_break(self):
"""Add page break"""
self.elements.append(PageBreak())
def add_recommendations_section(self, recommendations_df: pd.DataFrame):
"""Add recommendations section with status colors"""
self.elements.append(Paragraph("Recomandari Cheie", self.styles['SectionHeader']))
if recommendations_df is None or recommendations_df.empty:
self.elements.append(Paragraph("Nu au fost generate recomandari.", self.styles['Normal']))
return
# Show top 7 most important recommendations (ALERTA first, then ATENTIE)
df_sorted = recommendations_df.copy()
status_order = {'ALERTA': 0, 'ATENTIE': 1, 'OK': 2}
df_sorted['_order'] = df_sorted['STATUS'].map(status_order)
df_sorted = df_sorted.sort_values('_order').head(7)
for _, row in df_sorted.iterrows():
status = row.get('STATUS', 'OK')
indicator = row.get('INDICATOR', '')
valoare = row.get('VALOARE', '')
explicatie = row.get('EXPLICATIE', '')
recomandare = row.get('RECOMANDARE', '')
# Color based on status
if status == 'ALERTA':
status_color = colors.HexColor('#c0392b')
bg_color = colors.HexColor('#fadbd8')
elif status == 'ATENTIE':
status_color = colors.HexColor('#d68910')
bg_color = colors.HexColor('#fef9e7')
else:
status_color = colors.HexColor('#27ae60')
bg_color = colors.HexColor('#d5f5e3')
# Create a small table for each recommendation
data = [
[f"[{status}] {indicator}: {valoare}"],
[explicatie],
[f"Actiune: {recomandare}"]
]
table = Table(data, colWidths=[16*cm])
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), status_color),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('BACKGROUND', (0, 1), (-1, -1), bg_color),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 8),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('TOPPADDING', (0, 0), (-1, -1), 4),
('LEFTPADDING', (0, 0), (-1, -1), 8),
]))
self.elements.append(table)
self.elements.append(Spacer(1, 0.2*cm))
self.elements.append(Spacer(1, 0.3*cm))
def save(self):
"""Generate and save PDF"""
doc = SimpleDocTemplate(
str(self.output_path),
pagesize=A4,
rightMargin=2*cm,
leftMargin=2*cm,
topMargin=2*cm,
bottomMargin=2*cm
)
doc.build(self.elements)
print(f"✓ PDF salvat: {self.output_path}")
def create_monthly_chart(df: pd.DataFrame) -> plt.Figure:
"""Create monthly sales and margin chart"""
if df is None or df.empty:
fig, ax = plt.subplots(figsize=(12, 6))
ax.text(0.5, 0.5, 'Nu există date', ha='center', va='center')
return fig
fig, ax1 = plt.subplots(figsize=(12, 6))
x = range(len(df))
# Bar chart for sales
bars = ax1.bar(x, df['VANZARI_FARA_TVA'], color='#366092', alpha=0.7, label='Vânzări')
ax1.set_xlabel('Luna')
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
ax2 = ax1.twinx()
line = ax2.plot(x, df['MARJA_BRUTA'], color='#e74c3c', linewidth=2, marker='o', label='Marja')
ax2.set_ylabel('Marja (RON)', color='#e74c3c')
ax2.tick_params(axis='y', labelcolor='#e74c3c')
ax2.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f'{x/1000:,.0f}k'))
# X-axis labels
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()
return fig
def create_client_concentration_chart(df: pd.DataFrame) -> plt.Figure:
"""Create client concentration pie chart"""
if df is None or df.empty:
fig, ax = plt.subplots(figsize=(10, 8))
ax.text(0.5, 0.5, 'Nu există date', ha='center', va='center')
return fig
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
# Pie chart - Top 10 vs Others
top10 = df.head(10)
others_pct = 100 - top10['PROCENT_CUMULAT'].iloc[-1] if len(top10) >= 10 else 0
sizes = list(top10['PROCENT_DIN_TOTAL'])
if others_pct > 0:
sizes.append(others_pct)
labels = list(top10['CLIENT'].str[:20]) # Truncate names
if others_pct > 0:
labels.append('Alții')
colors_list = plt.cm.Set3(range(len(sizes)))
ax1.pie(sizes, labels=None, colors=colors_list, autopct='%1.1f%%', startangle=90)
ax1.set_title('Concentrare Top 10 Clienți')
ax1.legend(labels, loc='center left', bbox_to_anchor=(1, 0.5), fontsize=8)
# Bar chart - Pareto
ax2.bar(range(len(top10)), top10['VANZARI'], color='#366092', alpha=0.7)
ax2_twin = ax2.twinx()
ax2_twin.plot(range(len(top10)), top10['PROCENT_CUMULAT'], 'r-o', linewidth=2)
ax2_twin.axhline(y=80, color='green', linestyle='--', alpha=0.5, label='80%')
ax2.set_xticks(range(len(top10)))
ax2.set_xticklabels([c[:15] for c in top10['CLIENT']], rotation=45, ha='right', fontsize=8)
ax2.set_ylabel('Vânzări (RON)')
ax2_twin.set_ylabel('% Cumulat')
ax2.set_title('Analiză Pareto Clienți')
plt.tight_layout()
return fig
def create_production_chart(df: pd.DataFrame) -> plt.Figure:
"""Create production vs resale comparison chart"""
if df is None or df.empty:
fig, ax = plt.subplots(figsize=(10, 6))
ax.text(0.5, 0.5, 'Nu există date', ha='center', va='center')
return fig
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# Bar chart - Sales by type
x = range(len(df))
ax1.bar(x, df['VANZARI_FARA_TVA'], color=['#366092', '#e74c3c', '#2ecc71'][:len(df)])
ax1.set_xticks(x)
ax1.set_xticklabels(df['TIP_PRODUS'], rotation=15)
ax1.set_ylabel('Vânzări (RON)')
ax1.set_title('Vânzări per Tip Produs')
ax1.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f'{x/1000:,.0f}k'))
# Bar chart - Margin %
colors = ['#2ecc71' if m > 20 else '#e67e22' if m > 15 else '#e74c3c' for m in df['PROCENT_MARJA']]
ax2.bar(x, df['PROCENT_MARJA'], color=colors)
ax2.set_xticks(x)
ax2.set_xticklabels(df['TIP_PRODUS'], rotation=15)
ax2.set_ylabel('Marjă (%)')
ax2.set_title('Marjă per Tip Produs')
ax2.axhline(y=15, color='red', linestyle='--', alpha=0.5, label='Prag minim 15%')
ax2.legend()
plt.tight_layout()
return fig
def create_cash_cycle_chart(df: pd.DataFrame) -> plt.Figure:
"""Create cash conversion cycle visualization"""
if df is None or df.empty:
fig, ax = plt.subplots(figsize=(10, 6))
ax.text(0.5, 0.5, 'Nu exista date', ha='center', va='center')
return fig
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
# Extract values
indicators = df['INDICATOR'].tolist() if 'INDICATOR' in df.columns else []
zile = df['ZILE'].tolist() if 'ZILE' in df.columns else []
if not indicators or not zile:
ax1.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
# Colors for each component
colors_map = {
'DIO': '#3498db', # Blue for inventory
'DSO': '#e74c3c', # Red for receivables
'DPO': '#2ecc71', # Green for payables
'CCC': '#9b59b6' # Purple for total cycle
}
bar_colors = []
for ind in indicators:
for key, color in colors_map.items():
if key in ind.upper():
bar_colors.append(color)
break
else:
bar_colors.append('#95a5a6')
# Bar chart
x = range(len(indicators))
bars = ax1.bar(x, zile, color=bar_colors, alpha=0.8)
ax1.set_xticks(x)
ax1.set_xticklabels([ind[:20] for ind in indicators], rotation=45, ha='right', fontsize=9)
ax1.set_ylabel('Zile')
ax1.set_title('Ciclu Conversie Cash - Componente')
# Add value labels on bars
for bar, val in zip(bars, zile):
height = bar.get_height()
ax1.text(bar.get_x() + bar.get_width()/2., height,
f'{int(val)}',
ha='center', va='bottom', fontsize=10, fontweight='bold')
# Waterfall-style visualization
# DIO + DSO - DPO = CCC
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)
dpo = next((z for i, z in zip(indicators, zile) if 'DPO' in i.upper()), 0)
ccc = dio + dso - dpo
waterfall_labels = ['DIO\n(Zile Stoc)', 'DSO\n(Zile Incasare)', 'DPO\n(Zile Plata)', 'CCC\n(Ciclu Total)']
waterfall_values = [dio, dso, -dpo, ccc]
waterfall_colors = ['#3498db', '#e74c3c', '#2ecc71', '#9b59b6']
# Calculate positions for waterfall
cumulative = [0]
for i, v in enumerate(waterfall_values[:-1]):
cumulative.append(cumulative[-1] + v)
ax2.bar([0, 1, 2], [dio, dso, dpo], color=['#3498db', '#e74c3c', '#2ecc71'], alpha=0.8)
ax2.axhline(y=ccc, color='#9b59b6', linewidth=3, linestyle='--', label=f'CCC = {int(ccc)} zile')
ax2.set_xticks([0, 1, 2])
ax2.set_xticklabels(['DIO\n(+Stoc)', 'DSO\n(+Incasare)', 'DPO\n(-Plata)'], fontsize=9)
ax2.set_ylabel('Zile')
ax2.set_title('Formula: DIO + DSO - DPO = CCC')
ax2.legend(loc='upper right')
# Add annotation explaining the result
if ccc > 60:
verdict = "Ciclu lung - capital blocat mult timp"
verdict_color = '#c0392b'
elif ccc > 30:
verdict = "Ciclu moderat - poate fi optimizat"
verdict_color = '#d68910'
else:
verdict = "Ciclu eficient - capital rotit rapid"
verdict_color = '#27ae60'
ax2.text(0.5, -0.15, verdict, transform=ax2.transAxes,
ha='center', fontsize=10, color=verdict_color, fontweight='bold')
plt.tight_layout()
return fig