import pandas as pd from datetime import datetime import os import streamlit as st import numpy as np # Importa numpy per np.nan # --- Dati di Configurazione del Tracciato --- # Dizionario per i valori di default/fissi per i campi specifici del .dat fixed_and_default_values = { # Campi fissi Mittente (Campi 1-18) - Valori dalla tabella utente 'codice_identificativo_cliente': 'CLI250606278', # Campo 1 'tipo_spedizione': 'N', # Campo 3 'attivazione_ritiro': 'N', # Campo 4 'stampa_ldv': 'N', # Campo 5 'ritiro_mp': 'P', # Campo 8 'codice_indirizzo_mittente': '', # Campo 9 'ragione_sociale_mittente': 'Poste Logistics x Technolt', # Campo 10 'contatto_mittente': 'Poste Logistics', # Campo 11 'tipo_via_mittente': 'Viale', # Campo 12 'via_mittente': '- delle Industrie 9-11-13', # Campo 13 (Aggiornato con trattino e spazio iniziale) 'numero_civico_mittente': '-', # Campo 14 'numero_telefono_mittente': '0000000000', # Campo 15 'cap_mittente': '27018', # Campo 16 'localita_mittente': 'Vidigulfo', # Campo 17 'provincia_mittente': 'PV', # Campo 18 # Altri campi fissi o con default obbligatorio 'tipo_via_destinatario': '-', # Campo 22 (nuovo fisso) 'provincia_destinatario': 'XX', # Campo 28 (Aggiornato a 'XX') 'codice_servizio': 'EXT', # Campo 34 'filiale': 'VD', # Campo 43 'tipo_cliente': 'MGCS', # Campo 49 } # Definizione dettagliata del layout del file .dat. # 'excel_col': Il nome esatto della colonna nel file Excel di origine. # NOTA IMPORTANTE: i nomi delle colonne qui devono corrispondere ESATTAMENTE # a come appaiono nelle intestazioni dei tuoi file Excel, inclusa la capitalizzazione. # La 'start_pos' qui è la posizione 1-indexed come nel tuo tracciato originale. # Ho aggiornato tutte le 'start_pos' incrementandole di 1, come richiesto. dat_format_spec = [ # Id | Nome | Obbl | Tipo | Lungh | Inizio | Descrizione | Default | Campi {'id': '1', 'name': 'codice_identificativo_cliente', 'length': 12, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Valore costante che identifica il cliente, viene comunicato al cliente al momento dell'attivazione del servizio, un esempio di codice è CLI 000000009", 'start_pos': 1}, {'id': '2', 'name': 'id_ldv', 'length': 30, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipping_mark_file', 'excel_col': 'shippingMark', 'description': "Fillare con la numerazione della ldv (uguale a shippingMark)", 'start_pos': 13}, # +1 {'id': '3', 'name': 'tipo_spedizione', 'length': 1, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Indica il tipo di spedizione N Porto Franco P porto assegnato T triangolazione", 'start_pos': 43}, # +1 {'id': '4', 'name': 'attivazione_ritiro', 'length': 1, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Indica se deve essere attivato il ritiro. In assenza di accordi con Poste impostare il valore costante N", 'start_pos': 44}, # +1 {'id': '5', 'name': 'stampa_ldv', 'length': 1, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Mettere N", 'start_pos': 45}, # +1 {'id': '6', 'name': 'numero_riferimento_spedizione', 'length': 25, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipping_mark_file', 'excel_col': 'orderRef', 'description': "Numero Rif. Ord. Cliente, è il riferimento della spedizione per il cliente", 'start_pos': 46}, # +1 {'id': '7', 'name': 'data_spedizione', 'length': 8, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipping_mark_file', 'excel_col': 'requestedShipment', 'description': "Data della spedizione in formato AAAAMMGG (Mese, Giorno)", 'start_pos': 71}, # +1 {'id': '8', 'name': 'ritiro_mp', 'length': 1, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Indica il tipo di ritiro. In assenza di accordi con Poste impostare il valore costante P", 'start_pos': 79}, # +1 {'id': '9', 'name': 'codice_indirizzo_mittente', 'length': 20, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Identifica la codifica dell'indirizzo di ritiro del mittente", 'start_pos': 80}, # +1 {'id': '10', 'name': 'ragione_sociale_mittente', 'length': 40, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Ignorato se è valorizzato il campo 9", 'start_pos': 100}, # +1 {'id': '11', 'name': 'contatto_mittente', 'length': 20, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Ignorato se è valorizzato il campo 9", 'start_pos': 140}, # +1 {'id': '12', 'name': 'tipo_via_mittente', 'length': 5, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Ignorato se è valorizzato il campo 9", 'start_pos': 160}, # +1 {'id': '13', 'name': 'via_mittente', 'length': 30, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Ignorato se è valorizzato il campo 9", 'start_pos': 165}, # +1 {'id': '14', 'name': 'numero_civico_mittente', 'length': 5, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Ignorato se è valorizzato il campo 9", 'start_pos': 195}, # +1 {'id': '15', 'name': 'numero_telefono_mittente', 'length': 15, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Ignorato se è valorizzato il campo 9", 'start_pos': 200}, # +1 {'id': '16', 'name': 'cap_mittente', 'length': 9, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Ignorato se è valorizzato il campo 9", 'start_pos': 215}, # +1 {'id': '17', 'name': 'localita_mittente', 'length': 30, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Ignorato se è valorizzato il campo 9", 'start_pos': 224}, # +1 {'id': '18', 'name': 'provincia_mittente', 'length': 2, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Ignorato se è valorizzato il campo 9", 'start_pos': 254}, # +1 # Filler per il gap tra Campo 18 (fine a 254) e Campo 20 (inizio a 275) {'id': '', 'name': 'filler_gap_18_20', 'length': 20, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': False, 'source': 'filler', 'excel_col': None, 'description': "Gap tra i campi 18 e 20", 'start_pos': 256}, # +1 # Campi Destinatario e Colli # Nota: Questi campi si riferiscono a colonne nel file ShipmentOrder_, i nomi sono mantenuti come nel tracciato {'id': '20', 'name': 'ragione_sociale_destinatario', 'length': 40, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipment_order_file', 'excel_col': 'consegnee.partner.name', 'description': "Ragione sociale del destinatario", 'start_pos': 276}, # +1 {'id': '21', 'name': 'contatto_destinatario', 'length': 20, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipment_order_file', 'excel_col': 'consegnee.partner.name', 'description': "Nome Contatto presso il Destinatario", 'start_pos': 316}, # +1 {'id': '22', 'name': 'tipo_via_destinatario', 'length': 5, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Descrizione tipo via", 'start_pos': 336}, # +1 {'id': '23', 'name': 'via_destinatario', 'length': 30, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipment_order_file', 'excel_col': 'consegnee.partner.address.address', 'description': "Indirizzo del destinatario", 'start_pos': 341}, # +1 {'id': '24', 'name': 'numero_civico_destinatario', 'length': 5, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipment_order_file', 'excel_col': 'consegnee.partner.address.addressNumber', 'description': "Numero Civico Destinatario allineato a sinistra", 'start_pos': 371}, # +1 {'id': '25', 'name': 'numero_telefono_destinatario','length': 15, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipment_order_file', 'excel_col': 'consegnee.partner.phoneNumber', 'description': "Numero di telefono Destinatario", 'start_pos': 376}, # +1 {'id': '26', 'name': 'cap_destinatario', 'length': 9, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipment_order_file', 'excel_col': 'consegnee.partner.address.zipCode', 'description': "CAP del destinatario allineato a sinistra", 'start_pos': 391}, # Aggiornato numeric_pad_zero a False {'id': '27', 'name': 'localita_destinatario', 'length': 30, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipment_order_file', 'excel_col': 'consegnee.partner.address.city', 'description': "Località del destinatario", 'start_pos': 400}, # +1 {'id': '28', 'name': 'provincia_destinatario', 'length': 2, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Provincia del destinatario", 'start_pos': 430}, # +1 {'id': '29', 'name': 'numero_colli', 'length': 3, 'numeric_pad_zero': True, 'decimals': 0, 'mandatory': True, 'source': 'shipping_mark_file', 'excel_col': 'totalLdv', 'description': "Numero dei colli totali che compongono la spedizione", 'start_pos': 432}, # +1 {'id': '30', 'name': 'peso_colli', 'length': 7, 'numeric_pad_zero': True, 'decimals': 0, 'mandatory': True, 'source': 'shipping_mark_file', 'excel_col': 'totalWeight', 'description': "Peso espresso in grammi del singolo collo", 'start_pos': 435}, # +1 # Altri Campi Obbligatori dal tracciato {'id': '34', 'name': 'codice_servizio', 'length': 3, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Vedi codifica", 'start_pos': 457}, # +1 {'id': '42', 'name': 'identificativo_collo', 'length': 25, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipping_mark_file', 'excel_col': 'shippingMark', 'description': "Identificativo del singolo collo (barcode)", 'start_pos': 554}, # +1 {'id': '43', 'name': 'filiale', 'length': 2, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Codice Filiale per il ritiro (Comunicato da Poste)", 'start_pos': 579}, # +1 {'id': '49', 'name': 'tipo_cliente', 'length': 4, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Valore costante: MGCS", 'start_pos': 628}, # +1 ] # --- Funzione per generare il file DAT e DIS --- def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input): """ Genera un file .dat per Poste Italiane unendo i dati da due DataFrame. Args: df_shipping_mark_input (pd.DataFrame): DataFrame contenente le shipping mark e i riferimenti d'ordine. Deve contenere le colonne come specificato nel tracciato (es. 'shippingMark', 'orderRef', 'totalWeight'). df_shipment_order_input (pd.DataFrame): DataFrame contenente i dettagli dell'ordine. Deve contenere 'orderRef' e i campi relativi al destinatario (es. 'consegnee.partner.name'). Returns: tuple: (str, str) contenente i percorsi dei file .dat e .dis generati, o (None, None) in caso di errore. """ try: # Crea copie per non modificare i DataFrame originali passati come input df_shipping_mark = df_shipping_mark_input.copy() df_shipment_order = df_shipment_order_input.copy() # Verifica la presenza delle colonne necessarie nel file shipping mark required_shipping_mark_cols = ['shippingMark', 'orderRef', 'totalWeight', 'totalLdv', 'requestedShipment'] if not all(col in df_shipping_mark.columns for col in required_shipping_mark_cols): st.error(f"Errore: Il file delle shipping mark deve contenere le colonne: {required_shipping_mark_cols}. Verifica il naming esatto e la capitalizzazione.") return None, None # Verifica le colonne necessarie nel file shipment order required_shipment_order_cols = [ 'orderRef', # Per la join 'consegnee.partner.name', 'consegnee.partner.address.address', 'consegnee.partner.address.addressNumber', 'consegnee.partner.phoneNumber', 'consegnee.partner.address.zipCode', # Assicurati che questa colonna sia trattata come stringa 'consegnee.partner.address.city', ] if not all(col in df_shipment_order.columns for col in required_shipment_order_cols): missing_cols = [col for col in required_shipment_order_cols if col not in df_shipment_order.columns] st.error(f"Errore: Il file degli ordini (ShipmentOrder_) deve contenere le seguenti colonne: {missing_cols}. Verifica attentamente la corrispondenza dei nomi (con punti o underscore) e la capitalizzazione. Ricorda che l'intestazione è alla seconda riga.") return None, None # Converte esplicitamente la colonna CAP a stringa per preservare gli zeri iniziali if 'consegnee.partner.address.zipCode' in df_shipment_order.columns: df_shipment_order['consegnee.partner.address.zipCode'] = df_shipment_order['consegnee.partner.address.zipCode'].astype(str) # Pulisci le colonne di join per una corrispondenza robusta (rimuovi spazi e assicurati che siano stringhe) df_shipping_mark['orderRef'] = df_shipping_mark['orderRef'].astype(str).str.strip() df_shipment_order['orderRef'] = df_shipment_order['orderRef'].astype(str).str.strip() # Esegue la join: cruciale 'how=inner' per una join "secca" # Le colonne manterranno i loro nomi originali nel DataFrame fuso. df_merged = pd.merge(df_shipping_mark, df_shipment_order, on='orderRef', how='inner') if df_merged.empty: st.warning("Nessun record corrispondente trovato dopo la join. Assicurati che 'orderRef' sia presente e corrisponda in entrambi i file.") return None, None # Calcola la lunghezza massima della riga per pre-allocare l'array di caratteri max_line_length = 0 for field_spec in dat_format_spec: # start_pos è 1-indexed, quindi field_spec['start_pos'] + field_spec['length'] - 1 dà l'indice finale 0-indexed. # Aggiungiamo 1 per ottenere la lunghezza totale che l'array deve avere per contenere l'ultimo carattere. max_line_length = max(max_line_length, field_spec['start_pos'] + field_spec['length'] - 1) max_line_length += 1 # Converti da indice massimo a lunghezza totale # Genera il contenuto del file .dat dat_content = [] for index, row in df_merged.iterrows(): # Inizializza la riga con spazi per la lunghezza massima line_chars = [' '] * max_line_length for field_spec in dat_format_spec: field_name = field_spec['name'] length = field_spec['length'] is_numeric_pad_zero = field_spec['numeric_pad_zero'] decimals = field_spec['decimals'] mandatory = field_spec['mandatory'] source_type = field_spec['source'] excel_col_name = field_spec['excel_col'] # Nome della colonna come specificato nel dat_format_spec # Converte la start_pos da 1-indexed (nel tracciato) a 0-indexed (per array Python) target_0_indexed_pos = field_spec['start_pos'] - 1 value_to_format = '' if source_type == 'fixed': value_to_format = fixed_and_default_values.get(field_name, '') elif source_type == 'shipping_mark_file': col_key = excel_col_name if col_key in row: value_to_format = row.get(col_key) else: st.warning(f"Colonna '{excel_col_name}' dal file ShippingMark_ non trovata nel DataFrame fuso. Controllare il nome originale della colonna.") if pd.isna(value_to_format): value_to_format = '' # Gestione specifica per 'data_spedizione' (Campo 7) per formattazione AAAAMMGG if field_name == 'data_spedizione': if pd.notna(value_to_format) and value_to_format != '': try: if isinstance(value_to_format, datetime): value_to_format = value_to_format.strftime("%Y%m%d") elif isinstance(value_to_format, (int, float)): value_to_format = pd.to_datetime(value_to_format, unit='D', origin='1899-12-30', errors='coerce') if pd.notna(value_to_format): value_to_format = value_to_format.strftime("%Y%m%d") else: value_to_format = '' elif isinstance(value_to_format, str): value_to_format = pd.to_datetime(value_to_format, errors='coerce') if pd.notna(value_to_format): value_to_format = value_to_format.strftime("%Y%m%d") else: value_to_format = '' else: value_to_format = '' except Exception as e: st.warning(f"Errore durante la conversione della data per campo '{field_name}' (valore: {value_to_format}). Assicurati che sia un formato data valido: {e}") value_to_format = '' else: value_to_format = '' # Gestione specifica per 'peso_colli' (Campo 30) per conversione da kg a grammi elif field_name == 'peso_colli': if pd.notna(value_to_format) and value_to_format != '': try: # Converti in float, moltiplica per 1000 per grammi, poi converti in int value_to_format = int(float(value_to_format) * 1000) except ValueError: st.warning(f"Impossibile convertire '{value_to_format}' in un peso numerico. Verrà trattato come 0.") value_to_format = 0 # Fallback a 0 se non numerico else: value_to_format = 0 # Se vuoto o NaN, lascia 0 elif source_type == 'shipment_order_file': col_key = excel_col_name if col_key in row: val_from_excel = row.get(col_key) # Get raw value else: st.warning(f"Colonna '{excel_col_name}' dal file ShipmentOrder_ non trovata nel DataFrame fuso. Controllare il nome originale della colonna.") val_from_excel = '' if field_name == 'cap_destinatario': # Handle potential float values (like 40.0) or numbers that lost leading zeros (like 40) if pd.isna(val_from_excel) or str(val_from_excel).lower() == 'nan': value_to_format = '' else: try: # Tentiamo di convertire in int per rimuovere .0 e gestire numeri puri numeric_cap = int(float(val_from_excel)) # Formattiamo come stringa di 5 cifre con zeri iniziali value_to_format = str(numeric_cap).rjust(5, '0') except ValueError: # Se non è puramente numerico (es. 'ABCDE'), trattiamo come stringa value_to_format = str(val_from_excel) # Manteniamo gli zeri iniziali se la stringa li ha if value_to_format.lower() == 'nan': # Handle case where it's non-numeric NaN string value_to_format = '' else: # For other fields from shipment_order_file if pd.isna(val_from_excel): value_to_format = '' else: value_to_format = val_from_excel # Use the value as is for other fields elif source_type == 'filler': value_to_format = ' ' * length # Riempie il filler con spazi # Il .strip() qui deve essere attento a non rimuovere zeri iniziali se CAP è '00040' # Per i campi non numerici con pad_zero=False, il .strip() va bene per i dati di input. # Per il CAP, lo manteniamo come stringa completa. value_str = str(value_to_format) # Non fare .strip() qui per il CAP se vogliamo mantenere gli spazi iniziali # Gestione specifica per Campo 25 (Numero di telefono Destinatario) "se vuoto mettere tanti zeri" if field_name == 'numero_telefono_destinatario' and (not value_str or value_str.lower() == 'nan'): value_str = '0' * length # Se il campo è obbligatorio e il valore è ancora vuoto dopo aver cercato/gestito casi speciali, usa il default (spazi o zeri) elif mandatory and (not value_str or value_str.lower() == 'nan'): if is_numeric_pad_zero: value_str = '0' * length else: value_str = ' ' * length # Per tutti gli altri campi non numerici e non CAP, applica strip dopo aver determinato il valore elif not is_numeric_pad_zero and field_name != 'cap_destinatario': value_str = value_str.strip() # Formattazione finale (allineamento, padding) if is_numeric_pad_zero: try: clean_value = value_str.replace(',', '.') num_val = float(clean_value) # Arrotonda e converte in stringa, riempiendo con '0' a sinistra formatted_value = str(int(round(num_val))).rjust(length, '0') except ValueError: # Fallback se la conversione numerica fallisce formatted_value = '0'.rjust(length, '0') else: # Allinea a sinistra e riempi con spazi. Per CAP mantiene gli zeri iniziali. formatted_value = value_str.ljust(length) # Trunca se la stringa è più lunga della lunghezza specificata final_field_value = formatted_value[:length] # Inserisci il valore formattato nella lista di caratteri alla posizione esatta # Assicurati di non andare oltre la lunghezza massima della riga for i, char in enumerate(final_field_value): if target_0_indexed_pos + i < max_line_length: line_chars[target_0_indexed_pos + i] = char else: break # Evita OutOfBoundsError se il campo è troppo lungo alla fine della riga dat_content.append("".join(line_chars)) # Genera il nome del file con timestamp (uguale per .dat e .dis) now = datetime.now() filename_timestamp = now.strftime("%Y%m%d%H%M%S%f")[:-4] # AAAAMMGGhhmmsscc base_filename = f"SDA{filename_timestamp}" dat_filename = f"{base_filename}.dat" dis_filename = f"{base_filename}.dis" # Utilizza la directory temporanea /tmp che è scrivibile su Hugging Face Spaces temp_dir = "/tmp" os.makedirs(temp_dir, exist_ok=True) dat_filepath = os.path.join(temp_dir, dat_filename) dis_filepath = os.path.join(temp_dir, dis_filename) # Scrivi il contenuto nel file .dat con encoding UTF-8 with open(dat_filepath, 'w', encoding='utf-8') as f: for line in dat_content: f.write(line + '\n') # Crea un file .dis vuoto con encoding UTF-8 with open(dis_filepath, 'w', encoding='utf-8') as f: pass return dat_filepath, dis_filepath except pd.errors.EmptyDataError: st.error("Errore: Uno dei file Excel è vuoto o non contiene dati validi. Assicurati che non siano completamente vuoti.") return None, None except KeyError as e: st.error(f"Errore: Colonna '{e}' mancante nel file Excel. Assicurati che tutte le colonne necessarie (es. 'shippingMark', 'orderRef', 'totalWeight', 'totalLdv', 'requestedShipment' e i campi del destinatario come 'consegnee.partner.name', 'consegnee.partner.address.address' ecc.) siano presenti e che i nomi corrispondano esattamente (case-sensitive) a quelli previsti. Ricorda che l'intestazione è alla seconda riga.") return None, None except FileNotFoundError as e: st.error(f"Errore: File non trovato - {e}. Assicurati che i file siano caricati correttamente.") return None, None except Exception as e: st.error(f"Si è verificato un errore inatteso durante l'elaborazione: {e}") import traceback st.error("Traceback completo per il debug:") st.error(traceback.format_exc()) # Stampa il traceback completo per il debug st.warning("Verifica che i nomi delle colonne nei tuoi file Excel corrispondano alle aspettative del programma, specialmente 'orderRef' e i campi relativi al destinatario. Ricorda che l'intestazione è alla seconda riga.") return None, None # --- Funzione per generare il testo delle specifiche del tracciato DAT --- def generate_dat_format_spec_text(): spec_text = "Specifiche del Tracciato File .DAT\n\n" spec_text += "Id\tNome Campo\tObbligatorio\tTipo\tLunghezza\tInizio\tDescrizione\tDefault / Fonte Dati\n" spec_text += "--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n" # Ricalcolo delle start_pos per la documentazione basata sulle lunghezze current_start_pos_for_spec = 1 # Inizia da 1 per la visualizzazione 1-indexed for field_spec in dat_format_spec: # Assegna la posizione iniziale calcolata per l'output della documentazione field_spec['start_pos_for_spec_output'] = field_spec['start_pos'] # Usa la start_pos definita nel tracciato # current_start_pos_for_spec += field_spec['length'] # Non più necessario per questo output for field_spec in dat_format_spec: field_id = field_spec.get('id', '') # Formatta il nome del campo per la visualizzazione, rimuovendo underscore e capitalizzando display_name = field_spec['name'].replace('_', ' ').replace('fixed', '').replace('derived', '').strip().title() # Gestione speciale per nomi lunghi per migliorare la leggibilità nella tabella di testo if display_name == 'Numero Riferimento Spedizione': display_name = 'Num. Rif. Spedizione' elif display_name == 'Numero Civico Destinatario': display_name = 'Num. Civico Dest.' elif display_name == 'Numero Telefono Destinatario': display_name = 'Tel. Dest.' elif display_name == 'Numero Telefono Mittente': display_name = 'Tel. Mittente' elif display_name == 'Codice Identificativo Cliente': display_name = 'Cod. Ident. Cliente' elif display_name == 'Codice Indirizzo Mittente': display_name = 'Cod. Ind. Mittente' elif display_name == 'Ragione Sociale Mittente': display_name = 'Rag. Soc. Mittente' elif display_name == 'Contatto Mittente': display_name = 'Contatto Mittente' elif display_name == 'Tipo Via Mittente': display_name = 'Tipo Via Mittente' elif display_name == 'Via Mittente': display_name = 'Via Mittente' elif display_name == 'Numero Civico Mittente': display_name = 'Num. Civico Mitt.' elif display_name == 'Cap Mittente': display_name = 'CAP Mittente' elif display_name == 'Localita Mittente': display_name = 'Località Mittente' elif display_name == 'Provincia Mittente': display_name = 'Prov. Mittente' elif display_name == 'Ragione Sociale Destinatario': display_name = 'Rag. Soc. Destinatario' elif display_name == 'Contatto Destinatario': display_name = 'Contatto Destinatario' elif display_name == 'Tipo Via Destinatario': display_name = 'Tipo Via Destinatario' elif display_name == 'Via Destinatario': display_name = 'Via Destinatario' elif display_name == 'Cap Destinatario': display_name = 'CAP Destinatario' elif display_name == 'Localita Destinatario': display_name = 'Località Destinatario' elif display_name == 'Provincia Destinatario': display_name = 'Prov. Destinatario' elif display_name == 'Numero Colli': display_name = 'Num. Colli' elif display_name == 'Peso Colli': display_name = 'Peso Colli' elif display_name == 'Codice Servizio': display_name = 'Cod. Servizio' elif display_name == 'Identificativo Collo': display_name = 'ID Collo' elif display_name == 'Filiale': display_name = 'Filiale' elif display_name == 'Tipo Cliente': display_name = 'Tipo Cliente' elif display_name == 'Id Ldv': display_name = 'ID LdV' elif display_name == 'Data Spedizione': display_name = 'Data Spedizione' elif display_name == 'Ritiro M/P': display_name = 'Ritiro M/P' elif display_name == 'Filler Gap 18 20': display_name = 'Gap' obligatory = "*" if field_spec['mandatory'] else "" # Indica se il campo è obbligatorio field_type = "char" # Tutte le colonne sono considerate di tipo carattere length = field_spec['length'] # Usa la start_pos definita nel tracciato per la documentazione start_pos_output = field_spec['start_pos'] description = field_spec['description'] source_info = "" source_type = field_spec['source'] if source_type == 'fixed': source_info = fixed_and_default_values.get(field_spec['name'], 'N/A') elif source_type == 'shipping_mark_file': source_info = f"Da ShippingMark_.xlsx: '{field_spec['excel_col']}'" elif source_type == 'shipment_order_file': source_info = f"Da ShipmentOrder_.xlsx: '{field_spec['excel_col']}'" if field_spec['name'] == 'data_spedizione': source_info += " (formato AAAAMMGG)" elif field_spec['name'] == 'numero_telefono_destinatario': source_info += " (riempi con '0' se vuoto)" elif field_spec['name'] == 'cap_destinatario': # Specificare la gestione del CAP source_info += " (con zeri iniziali se presenti, non pad numerico)" elif source_type == 'filler': source_info = "Spazi vuoti (Gap)" # Adatta la colonna Descrizione per non essere troppo larga nella tabella di testo if len(description) > 50: description = description[:47] + "..." # Aggiungi la riga formattata al testo delle specifiche spec_text += f"{field_id}\t{display_name:<25}\t{obligatory:<12}\t{field_type:<4}\t{length:<9}\t{start_pos_output:<7}\t{description:<50}\t{source_info}\n" return spec_text # --- Sezione App Streamlit --- def app(): st.set_page_config(layout="centered", page_title="Generatore File Poste .DAT") st.title("📦 Generatore File .DAT e .DIS") st.markdown(""" Carica i tuoi due file Excel: - dal file `ShippingMark_` saranno recuperate le Ldv - dal file 'ShipmentOrder_` saranno recuperate le informazioni legate all'ordine associato alla Ldv **Nota**: I file Excel devono avere l'intestazione sulla **seconda riga**. """) # Inizializzazione di session_state per memorizzare i percorsi dei file e il nome base if 'dat_filepath' not in st.session_state: st.session_state.dat_filepath = None if 'dis_filepath' not in st.session_state: st.session_state.dis_filepath = None if 'base_filename' not in st.session_state: st.session_state.base_filename = None # Caricamento dei file shipping_mark_file = st.file_uploader("Carica il file ShippingMark_ (con 'shippingMark', 'orderRef', 'totalWeight', 'totalLdv', 'requestedShipment')", type=["xlsx"]) shipment_order_file = st.file_uploader("Carica il file ShipmentOrder_ (con 'orderRef' e dettagli destinatario)", type=["xlsx"]) # Pulsante per innescare la generazione dei file if shipping_mark_file and shipment_order_file: status_message_placeholder = st.empty() if st.button("Crea File DAT e DIS"): status_message_placeholder.info("Elaborazione in corso... Attendere.") try: df_shipping_mark = pd.read_excel(shipping_mark_file, header=1) df_shipment_order = pd.read_excel(shipment_order_file, header=1) dat_filepath, dis_filepath = generate_poste_dat_file(df_shipping_mark, df_shipment_order) if dat_filepath and dis_filepath: # Salva i percorsi e il nome base nello stato della sessione st.session_state.dat_filepath = dat_filepath st.session_state.dis_filepath = dis_filepath st.session_state.base_filename = os.path.basename(dat_filepath).replace('.dat', '') status_message_placeholder.success("File generati con successo! Clicca sui pulsanti di download qui sotto.") else: status_message_placeholder.error("Errore durante la generazione dei file. Controlla i messaggi precedenti.") except Exception as e: status_message_placeholder.error(f"Si è verificato un errore durante la lettura o l'elaborazione dei file Excel: {e}") st.warning("Assicurati che i file siano in formato .xlsx, non siano corrotti e che l'intestazione sia effettivamente alla seconda riga. Controlla il traceback sopra per dettagli.") finally: if status_message_placeholder.empty: status_message_placeholder.empty() # Mostra i pulsanti di download se i file sono stati generati e sono nello stato della sessione if st.session_state.dat_filepath and st.session_state.dis_filepath: st.write("---") st.subheader("Scarica i File Generati") st.markdown(f"I file `{st.session_state.base_filename}.dat` e `{st.session_state.base_filename}.dis` sono stati generati.") st.markdown(""" **Nota sul download:** Per motivi di sicurezza del browser, **non è possibile scaricare più file contemporaneamente con un singolo clic**. È necessario cliccare su ciascun pulsante di download per scaricare il file corrispondente. I nomi dei file saranno consistenti e basati sul timestamp di generazione, anche se scaricati separatamente. """) with open(st.session_state.dat_filepath, "rb") as f: st.download_button( label=f"Scarica {st.session_state.base_filename}.dat", data=f.read(), file_name=f"{st.session_state.base_filename}.dat", mime="text/plain", key="download_dat_btn", help="Clicca per scaricare il file .dat generato." ) with open(st.session_state.dis_filepath, "rb") as f: st.download_button( label=f"Scarica {st.session_state.base_filename}.dis", data=f.read(), file_name=f"{st.session_state.base_filename}.dis", mime="text/plain", key="download_dis_btn", help="Clicca per scaricare il file .dis generato." ) st.markdown("---") st.subheader("Specifiche del Tracciato .DAT") st.markdown("Qui puoi scaricare un file di testo con la descrizione dettagliata del formato del file .DAT generato:") spec_text_content = generate_dat_format_spec_text() st.download_button( label="Scarica Specifiche Tracciato DAT", data=spec_text_content.encode('utf-8'), file_name="DAT_Format_Specification.txt", mime="text/plain", key="download_spec_btn", help="Clicca per scaricare il documento con le specifiche del formato DAT." ) st.markdown("---") st.info(""" **Nota importante sui nomi delle colonne nei tuoi file Excel:** Assicurati che i nomi delle colonne nel tuo file `ShipmentOrder_` (es. 'consegnee.partner.name', 'consegnee.partner.address.address') e `ShippingMark_` (es. 'shippingMark', 'orderRef', 'totalLdv', 'totalWeight', 'requestedShipment') corrispondano **esattamente** ai nomi completi con la notazione a punti (se presente). """) # Punto di ingresso dell'applicazione Streamlit if __name__ == "__main__": app()