|
from reportlab.lib.pagesizes import A4 |
|
from reportlab.lib import colors |
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
|
from reportlab.lib.units import inch, cm |
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle |
|
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER |
|
from reportlab.pdfgen import canvas |
|
from reportlab.platypus import Image |
|
|
|
import streamlit as st |
|
import pandas as pd |
|
from io import BytesIO |
|
import xlsxwriter |
|
from datetime import date, datetime |
|
from st_aggrid import AgGrid |
|
from st_aggrid.grid_options_builder import GridOptionsBuilder |
|
|
|
from helpertools_TM import helpers |
|
|
|
|
|
class ReportGenerator: |
|
|
|
def __init__(self, processor, trova_errori, interface, no_zero=None): |
|
"""Initialize ReportGenerator with required components. |
|
|
|
processor: Handles data processing and error collection |
|
trova_errori: Contains error detection logic and dataframes |
|
interface: Handles UI interactions and grid displays |
|
no_zero: Flag for handling zero values |
|
""" |
|
self.df = trova_errori.df |
|
self.df_bambini = trova_errori.df_bambini_che_impediscono_controllo_superamento_ore |
|
|
|
self.df_calcolo_666 = trova_errori.df_calcolo_666 |
|
self.dffinal = trova_errori.dffinal |
|
self.processor = processor |
|
self.no_zero = no_zero |
|
self.interface = interface |
|
self.trova_errori = trova_errori |
|
|
|
self.ERRORDICT = trova_errori.ERRORDICT |
|
self.helper = helpers(interface,self.ERRORDICT) |
|
|
|
def get_version(self): |
|
return "ReportGenerator v1.0.1" |
|
|
|
@st.fragment() |
|
def display_final_table(self) -> None: |
|
""" |
|
Displays the final table with all data and a separate table for errors only. |
|
|
|
This method creates two expanders: one for displaying all data in a grid format, |
|
and another for showing only records that contain errors. It also provides options |
|
to download these tables as Excel files. |
|
|
|
Args: |
|
None |
|
|
|
Returns: |
|
None |
|
""" |
|
|
|
no_zero = self.no_zero |
|
|
|
|
|
expndr = st.expander("TABELLA FINALE ELABORATA - TUTTI I DATI") |
|
with expndr: |
|
|
|
|
|
self.interface.download_excel_file( |
|
self.df.sort_values(by=["Cognome", "Nome"]), |
|
"TabellaFinaleElaborata_TuttiDati.xlsx", |
|
'mantieniColonneErrore' |
|
) |
|
|
|
|
|
|
|
gridOptions = self.interface.buildGrid(self.df.sort_values(by=["Cognome", "Nome"])) |
|
AgGrid( |
|
self.df.sort_values(by=["Cognome", "Nome"]), |
|
gridOptions=gridOptions, |
|
enable_enterprise_modules=True, |
|
) |
|
|
|
|
|
|
|
|
|
expndr = st.expander("TABELLA FINALE ELABORATA - SOLO ERRORI") |
|
with expndr: |
|
|
|
self.dffinal = self.helper.make_df_solo_errori(self.df) |
|
|
|
|
|
self.interface.download_excel_file( |
|
self.dffinal.sort_values(by=["Cognome", "Nome"]), |
|
"TabellaFinaleElaborata_SoloErrori.xlsx", |
|
"mantieniColonneErrore" |
|
) |
|
|
|
|
|
gridOptions = self.interface.buildGrid(self.dffinal.sort_values(by=["Cognome", "Nome"])) |
|
AgGrid( |
|
self.dffinal.sort_values(by=["Cognome", "Nome"]), |
|
gridOptions=gridOptions, |
|
enable_enterprise_modules=True, |
|
key="tabfinalex" |
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def genera_report(self) -> None: |
|
""" |
|
Generates detailed reports in Excel and PDF formats and provides download options via Streamlit. |
|
|
|
This method filters errors, computes unique children and municipalities, and generates |
|
Excel and PDF reports if errors are present. It handles various exceptions that may occur |
|
during the report generation process. |
|
|
|
Args: |
|
None |
|
|
|
Returns: |
|
None |
|
""" |
|
|
|
try: |
|
|
|
error_keys, errors_present = self._filter_errors(excluded_error="errMassimo543") |
|
|
|
|
|
if not error_keys: |
|
st.warning("Nessun controllo da eseguire.") |
|
return |
|
|
|
|
|
nr_bambini = self._compute_unique_children() |
|
nr_comuni = self._compute_unique_gemeinden() |
|
|
|
|
|
if not errors_present: |
|
st.info("Nessun errore rilevato nei controlli selezionati.") |
|
return |
|
|
|
|
|
try: |
|
excel_data = self._generate_excel_report(nr_bambini, nr_comuni, errors_present) |
|
except (xlsxwriter.exceptions.FileCreateError, PermissionError) as e: |
|
st.error(f"File access error: {str(e)}") |
|
st.stop() |
|
except ValueError as ve: |
|
st.error(f"Invalid data format: {str(ve)}") |
|
st.stop() |
|
except KeyError as ke: |
|
st.error(f"Missing required column: {str(ke)}") |
|
st.stop() |
|
self._provide_downloads(excel_data) |
|
st.success("Generato file riassunto analisi") |
|
|
|
except KeyError as ke: |
|
|
|
st.error(f"Chiave mancante o struttura dati non valida: {ke}") |
|
st.stop() |
|
except ValueError as ve: |
|
st.error(f"Problema di tipo o formato dati: {ve}") |
|
st.stop() |
|
except FileNotFoundError as fnf: |
|
st.error(f"File di riferimento non trovato: {fnf}") |
|
except PermissionError as pe: |
|
st.error(f"Permessi insufficienti per accedere al file: {pe}") |
|
except Exception as e: |
|
st.error(f"Errore imprevisto durante la generazione del report: {e}") |
|
st.stop() |
|
|
|
def _filter_errors(self, excluded_error: str): |
|
""" |
|
Excludes a specific error from the error dictionary and computes present errors. |
|
|
|
Args: |
|
excluded_error (str): The error key to be excluded from the error dictionary. |
|
|
|
Returns: |
|
Tuple[List[str], Dict[str, int]]: Filtered error keys and errors with counts > 0. |
|
""" |
|
|
|
|
|
error_keys = [k for k in self.ERRORDICT.keys() if k != excluded_error] |
|
if not error_keys: |
|
return error_keys, {} |
|
|
|
|
|
error_counts = self.df[error_keys].sum() |
|
errors_present = error_counts[error_counts > 0].to_dict() |
|
return error_keys, errors_present |
|
|
|
def _compute_unique_children(self) -> int: |
|
""" |
|
Computes the number of unique children based on 'Codice fiscale'. |
|
|
|
Args: |
|
None |
|
|
|
Returns: |
|
int: The number of unique children. |
|
""" |
|
|
|
return self.df["Codice fiscale"].nunique() |
|
|
|
def _compute_unique_gemeinden(self) -> int: |
|
""" |
|
Computes the number of unique municipalities based on 'Com_code'. |
|
|
|
Args: |
|
None |
|
|
|
Returns: |
|
int: The number of unique municipalities. |
|
""" |
|
|
|
return self.df["Com_code"].nunique() |
|
|
|
def _generate_excel_report(self, nr_bambini: int, nr_comuni: int, errors_present: dict) -> bytes: |
|
""" |
|
Generates the Excel report and returns the binary data. |
|
|
|
Args: |
|
nr_bambini (int): Number of unique children. |
|
nr_comuni (int): Number of unique municipalities. |
|
errors_present (dict): Dictionary of errors with their counts. |
|
|
|
Returns: |
|
bytes: The binary data of the generated Excel report. |
|
""" |
|
|
|
excel_buffer = BytesIO() |
|
with xlsxwriter.Workbook(excel_buffer) as workbook: |
|
worksheet = workbook.add_worksheet("Riassunto") |
|
try: |
|
self._write_excel_summary(worksheet, nr_bambini, nr_comuni, errors_present, workbook) |
|
except: |
|
st.error("Error during _write_excel_summary") |
|
try: |
|
self._write_excel_detailed_sections(worksheet, errors_present, workbook) |
|
except: |
|
st.error("Error during _write_excel_detailed") |
|
return excel_buffer.getvalue() |
|
|
|
def _write_excel_summary(self, worksheet, nr_bambini: int, nr_comuni: int, errors_present: dict, workbook): |
|
""" |
|
Writes the summary section of the Excel report. |
|
|
|
Args: |
|
worksheet: The worksheet object to write the summary to. |
|
nr_bambini (int): Number of unique children. |
|
nr_comuni (int): Number of unique municipalities. |
|
errors_present (dict): Dictionary of errors with their counts. |
|
workbook: The workbook object to format the cells. |
|
|
|
Returns: |
|
None |
|
""" |
|
|
|
|
|
bold_format = workbook.add_format({'bold': True, 'font_size': 12}) |
|
bold_format2 = workbook.add_format({'bold': True, 'font_size': 11}) |
|
header_format = workbook.add_format({'bold': True, 'bg_color': '#D7E4BC'}) |
|
title_format = workbook.add_format({'bold': True, 'font_size': 18}) |
|
|
|
row, col = 3, 0 |
|
|
|
|
|
try: |
|
worksheet.write(row, col, "Riassunto analisi resoconti Tagesmuetter", title_format) |
|
worksheet.set_row(row, 30) |
|
row += 4 |
|
except xlsxwriter.exceptions.XlsxWriterException as xlse: |
|
st.error(f"Worksheet write error: {str(xlse)}") |
|
raise |
|
|
|
try: |
|
|
|
worksheet.write(row, col, f"Sono stati elaborati i dati di {nr_bambini} bambini in {nr_comuni} comuni") |
|
row += 2 |
|
except: |
|
st.error("Error during Sono stati elaborati i dati di {nr_bambini} bambini in {nr_comuni} comuni") |
|
|
|
|
|
try: |
|
for error_key, count in errors_present.items(): |
|
if error_key not in self.df.columns: |
|
raise KeyError(f"Error column {error_key} not found in dataframe") |
|
|
|
if error_key not in self.ERRORDICT: |
|
raise ValueError(f"Undocumented error code: {error_key}") |
|
|
|
error_description = self.ERRORDICT[error_key] |
|
worksheet.write( |
|
row, col, |
|
f"Trovate {int(count)} occorrenze per il controllo {error_description}") |
|
row += 1 |
|
row += 2 |
|
except: |
|
st.error("Error during Document all errors found in the analysis") |
|
|
|
|
|
try: |
|
for key in self.processor.errori.keys(): |
|
|
|
worksheet.write( |
|
row, col, |
|
f"Errori {key}:" + (" (file non usati)" if key == "critici" else ""), |
|
bold_format |
|
) |
|
row += 1 |
|
for filename in self.processor.errori[key]: |
|
|
|
worksheet.write( |
|
row, col+1, |
|
filename.split('/', 1)[1] if '/' in filename else filename, |
|
|
|
) |
|
|
|
for error in self.processor.errori[key][filename]: |
|
|
|
|
|
worksheet.write( |
|
row, col+2, |
|
error, |
|
) |
|
row += 1 |
|
row += 1 |
|
row += 3 |
|
except Exception as e: |
|
st.error("Error during Document processing errors from file handling: " + str(e)) |
|
|
|
|
|
try: |
|
if not self.df_bambini.empty: |
|
worksheet.write( |
|
row, col, |
|
"Bambini per i quali non e' possibile eseguire il controllo superamento ore nel periodo contrattuale", |
|
bold_format |
|
) |
|
row += 1 |
|
|
|
|
|
for header_col, header in enumerate(self.df_bambini.columns): |
|
worksheet.write(row, header_col, header, header_format) |
|
row += 1 |
|
|
|
|
|
worksheet.set_column(0, len(self.df_bambini.columns)-1, 25) |
|
|
|
|
|
for _, r in self.df_bambini.iterrows(): |
|
for header_col, header in enumerate(self.df_bambini.columns): |
|
value = r.get(header, "") |
|
if isinstance(value, pd.Timestamp): |
|
value = value.strftime("%d/%m/%Y") |
|
worksheet.write(row, header_col, value) |
|
row += 1 |
|
row += 3 |
|
except: |
|
st.error("Error during Handle special cases: children without hour control") |
|
|
|
|
|
if not self.df_calcolo_666.empty: |
|
worksheet.set_column(0, len(self.df_calcolo_666.columns)-1, 25) |
|
|
|
filtered_df_ore = self.df_calcolo_666[self.df_calcolo_666['Confronto ore'] == False] |
|
filtered_df_bambini = self.df_calcolo_666[self.df_calcolo_666['Confronto bambini'] == False] |
|
|
|
if not filtered_df_ore.empty: |
|
worksheet.write( |
|
row, col, |
|
"Riscontrate incongruenze nel numero delle ore per l'allegato 666", bold_format) |
|
row += 2 |
|
|
|
|
|
headers = ["Comune provenienza bambino", "Ore di servizio allegato 666", "Ore di assistenza report Comuni"] |
|
for i, header in enumerate(headers): |
|
worksheet.write(row, col + i + 1, header, header_format) |
|
row += 1 |
|
|
|
for _, r in filtered_df_ore.iterrows(): |
|
worksheet.write(row, col + 1, r.get("Comune provenienza bambino", "")) |
|
worksheet.write(row, col + 2, r.get("Ore di servizio allegato 666", "")) |
|
worksheet.write(row, col + 3, r.get("Ore di assistenza report Comuni", "")) |
|
row += 1 |
|
row +=1 |
|
|
|
if not filtered_df_bambini.empty: |
|
worksheet.write( |
|
row, col, |
|
"Riscontrate incongruenze nel numero bambini per l'allegato 666", bold_format) |
|
row += 2 |
|
|
|
|
|
headers = ["Comune provenienza bambino", "Numero utenti allegato 666", "Numero bambini report Comuni"] |
|
for i, header in enumerate(headers): |
|
worksheet.write(row, col + i + 1, header, header_format) |
|
row += 1 |
|
|
|
for _, r in filtered_df_bambini.iterrows(): |
|
worksheet.write(row, col + 1, r.get("Comune provenienza bambino", "")) |
|
worksheet.write(row, col + 2, r.get("Numero utenti allegato 666", "")) |
|
worksheet.write(row, col + 3, r.get("Numero bambini report Comuni", "")) |
|
row += 1 |
|
|
|
def _write_excel_detailed_sections(self, worksheet, errors_present: dict, workbook): |
|
""" |
|
Writes detailed sections per error in the Excel report. |
|
|
|
Args: |
|
worksheet: The worksheet object to write the detailed sections to. |
|
errors_present (dict): Dictionary of errors with their counts. |
|
workbook: The workbook object to format the cells. |
|
|
|
Returns: |
|
None |
|
""" |
|
|
|
title_format2 = workbook.add_format({'bold': True, 'font_size': 12}) |
|
header_format = workbook.add_format({'bold': True, 'bg_color': '#D7E4BC'}) |
|
date_format = workbook.add_format({'num_format': 'dd/mm/yyyy'}) |
|
summary_format = workbook.add_format({'italic': False, 'font_size': 10}) |
|
|
|
row, col = worksheet.dim_rowmax, 0 |
|
row += 4 |
|
|
|
for error_key, count in errors_present.items(): |
|
error_description = self.ERRORDICT.get(error_key, error_key) |
|
worksheet.write(row, col, error_description, title_format2) |
|
row += 2 |
|
|
|
|
|
headers = [ |
|
"Cognome", |
|
"Nome", |
|
"Codice fiscale", |
|
"Data di nascita", |
|
"Comune", |
|
"Data inizio contratto", |
|
"Data fine contratto", |
|
] |
|
for i, header in enumerate(headers): |
|
worksheet.write(row, col + i + 1, header, header_format) |
|
row += 1 |
|
|
|
for header_col, header in enumerate(headers, start=1): |
|
max_length = max( |
|
self.df[header].astype(str).map(len).max() |
|
if header in self.df.columns else 0, |
|
len(header) |
|
) + 2 |
|
worksheet.set_column(col + header_col, col + header_col, max_length + 5) |
|
|
|
df_bambini = self.df[self.df[error_key] == True].sort_values(by=["Cognome", "Nome"]) |
|
|
|
for _, r in df_bambini.iterrows(): |
|
worksheet.write(row, col + 1, r.get("Cognome", "")) |
|
worksheet.write(row, col + 2, r.get("Nome", "")) |
|
worksheet.write(row, col + 3, r.get("Codice fiscale", "")) |
|
|
|
data_nascita = r.get("Data di nascita", "") |
|
if pd.notnull(data_nascita) and isinstance(data_nascita, pd.Timestamp): |
|
worksheet.write_datetime(row, col + 4, data_nascita, date_format) |
|
else: |
|
worksheet.write(row, col + 4, str(data_nascita)) |
|
|
|
worksheet.write(row, col + 5, r.get("Comune", "")) |
|
|
|
data_inizio = r.get("Data inizio contratto (o data inizio assistenza se diversa)", "") |
|
if pd.notnull(data_inizio) and isinstance(data_inizio, pd.Timestamp): |
|
worksheet.write_datetime(row, col + 6, data_inizio, date_format) |
|
else: |
|
worksheet.write(row, col + 6, str(data_inizio)) |
|
|
|
data_fine = r.get("Data fine contratto (o data fine assistenza se diversa)", "") |
|
if pd.notnull(data_fine) and isinstance(data_fine, pd.Timestamp): |
|
worksheet.write_datetime(row, col + 7, data_fine, date_format) |
|
else: |
|
worksheet.write(row, col + 7, str(data_fine)) |
|
|
|
row += 1 |
|
|
|
|
|
row += 1 |
|
worksheet.write(row, col, "Riepilogo:", summary_format) |
|
|
|
|
|
|
|
summary_entries = [] |
|
for _, r in df_bambini.iterrows(): |
|
|
|
cognome_parts = [part.capitalize() for part in r.get("Cognome", "").split()] |
|
nome_parts = [part.capitalize() for part in r.get("Nome", "").split()] |
|
|
|
cognome = " ".join(cognome_parts).rstrip() |
|
nome = " ".join(nome_parts).rstrip() |
|
comune = r.get("Comune", "").rstrip() |
|
summary_entries.append(f"{cognome} {nome} ({comune})") |
|
|
|
|
|
summary_line = ", ".join(summary_entries) |
|
worksheet.write(row, col + 1, summary_line, summary_format) |
|
row += 1 |
|
|
|
row += 3 |
|
|
|
def _prepare_additional_df(self) -> pd.DataFrame: |
|
""" |
|
Prepares the additional DataFrame for the PDF report by renaming and dropping columns. |
|
|
|
Args: |
|
None |
|
|
|
Returns: |
|
pd.DataFrame: The prepared additional DataFrame. |
|
""" |
|
|
|
additional_df = self.df_bambini.copy() |
|
|
|
column_list = list(additional_df.columns) |
|
column_list[3] = 'Data Inizio Contratto' |
|
column_list[4] = 'Data Fine Contratto' |
|
additional_df.columns = column_list |
|
|
|
position_to_delete = 5 |
|
if position_to_delete < len(additional_df.columns): |
|
column_name = additional_df.columns[position_to_delete] |
|
additional_df.drop(columns=column_name, inplace=True) |
|
|
|
return additional_df |
|
|
|
@st.fragment() |
|
def _provide_downloads(self, excel_data: bytes): |
|
""" |
|
Provides download buttons for the generated Excel and PDF reports. |
|
|
|
Args: |
|
excel_data (bytes): The binary data of the generated Excel report. |
|
# pdf_data (bytes): The binary data of the generated PDF report. |
|
|
|
Returns: |
|
None |
|
""" |
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") |
|
excel_file_name = f"riassuntoAutomatico_{timestamp}.xlsx" |
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
st.download_button( |
|
label="📥 Scarica report riassuntivo dell'analisi in formato Excel", |
|
data=excel_data, |
|
file_name=excel_file_name, |
|
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", |
|
key="fileRiassuntoExcel", |
|
) |
|
|