createManifestXL / src /streamlit_app.py
pasquale991's picture
Update src/streamlit_app.py
2cf4de5 verified
Raw History Blame Contribute Delete
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()