Spaces:
Sleeping
Sleeping
Download src/streamlit_app.py from supportDTO/createManifestXL: direct link, hf CLI and curl.
- Browser
- Download file 39.1 kB
-
https://huggingface.co/spaces/supportDTO/createManifestXL/resolve/main/src/streamlit_app.py
- Command line
-
hf download hf://spaces/supportDTO/createManifestXL/src/streamlit_app.py
-
curl -L -o streamlit_app.py https://huggingface.co/spaces/supportDTO/createManifestXL/resolve/main/src/streamlit_app.py
39.1 kB
| 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() |