Files
vending_data_intelligence_r…/report_generator.py
Marius Mutu 0b732f7a7a 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>
2025-12-02 15:41:56 +02:00

774 lines
32 KiB
Python

"""
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