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