Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +159 -115
src/streamlit_app.py
CHANGED
|
@@ -6,16 +6,12 @@ import numpy as np # Importa numpy per np.nan
|
|
| 6 |
|
| 7 |
# --- Dati di Configurazione del Tracciato ---
|
| 8 |
# Dizionario per i valori di default/fissi per i campi specifici del .dat
|
| 9 |
-
# Basato sull'ultimo tracciato fornito dall'utente.
|
| 10 |
fixed_and_default_values = {
|
| 11 |
# Campi fissi Mittente (Campi 1-18) - Valori dalla tabella utente
|
| 12 |
'codice_identificativo_cliente': '1040453', # Campo 1
|
| 13 |
-
# 'id_ldv': non è più fisso, viene da shippingMark
|
| 14 |
'tipo_spedizione': 'N', # Campo 3
|
| 15 |
'attivazione_ritiro': 'N', # Campo 4
|
| 16 |
'stampa_ldv': 'N', # Campo 5
|
| 17 |
-
# Campo 6 (Numero Riferimento Spedizione) dal file shippingMark
|
| 18 |
-
# Campo 7 (Data Spedizione) dal file shipmentOrder.orderDate
|
| 19 |
'ritiro_mp': 'P', # Campo 8
|
| 20 |
'codice_indirizzo_mittente': '', # Campo 9
|
| 21 |
'ragione_sociale_mittente': 'Poste Logistics x Technolt', # Campo 10
|
|
@@ -27,7 +23,6 @@ fixed_and_default_values = {
|
|
| 27 |
'cap_mittente': '27018', # Campo 16
|
| 28 |
'localita_mittente': 'Vidigulfo', # Campo 17
|
| 29 |
'provincia_mittente': 'PV', # Campo 18
|
| 30 |
-
|
| 31 |
# Altri campi fissi o con default obbligatorio
|
| 32 |
'tipo_via_destinatario': '-', # Campo 22 (nuovo fisso)
|
| 33 |
'provincia_destinatario': '-', # Campo 28 (nuovo fisso)
|
|
@@ -37,20 +32,6 @@ fixed_and_default_values = {
|
|
| 37 |
}
|
| 38 |
|
| 39 |
# Definizione dettagliata del layout del file .dat.
|
| 40 |
-
# INCLUDE SOLO I CAMPI DALL'ULTIMO TRACCIATO FORNITO DALL'UTENTE.
|
| 41 |
-
# 'name': Nome interno per il campo nel codice.
|
| 42 |
-
# 'length': Lunghezza fissa del campo.
|
| 43 |
-
# 'numeric_pad_zero': True se il campo è numerico e va riempito con '0' a sinistra.
|
| 44 |
-
# 'decimals': Numero di decimali (per campi numerici di valuta, non usati nel nuovo tracciato).
|
| 45 |
-
# 'mandatory': True se il campo è obbligatorio (*) o (**) nel tracciato.
|
| 46 |
-
# 'source': Indica la fonte del valore: 'fixed' (da fixed_and_default_values),
|
| 47 |
-
# 'shipping_mark_file' (dal DataFrame shippingMark),
|
| 48 |
-
# 'shipment_order_file' (dal DataFrame shipmentOrder),
|
| 49 |
-
# 'filler' (gap nel tracciato).
|
| 50 |
-
# 'excel_col': Il nome esatto della colonna nel file Excel di origine (se 'source' è 'shipping_mark_file' o 'shipment_order_file').
|
| 51 |
-
# 'description': Descrizione del campo (per il file di specifiche).
|
| 52 |
-
# 'start_pos': Posizione iniziale del campo (per il file di specifiche).
|
| 53 |
-
# 'end_pos': Posizione finale del campo (per il file di specifiche).
|
| 54 |
dat_format_spec = [
|
| 55 |
# Id | Nome | Obbl | Tipo | Lungh | Inizio | Descrizione | Default | Campi
|
| 56 |
{'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},
|
|
@@ -71,23 +52,23 @@ dat_format_spec = [
|
|
| 71 |
{'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': 214},
|
| 72 |
{'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': 223},
|
| 73 |
{'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': 253},
|
| 74 |
-
|
| 75 |
# Filler per il gap tra Campo 18 (fine a 254) e Campo 20 (inizio a 275)
|
| 76 |
{'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': 255},
|
| 77 |
-
|
| 78 |
# Campi Destinatario e Colli
|
|
|
|
|
|
|
|
|
|
| 79 |
{'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': 275},
|
| 80 |
{'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': 315},
|
| 81 |
-
{'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': 335},
|
| 82 |
{'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': 340},
|
| 83 |
{'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': 370},
|
| 84 |
{'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': 375},
|
| 85 |
{'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': 390},
|
| 86 |
{'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': 399},
|
| 87 |
-
{'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': 429},
|
| 88 |
{'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': 431},
|
| 89 |
{'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': 434},
|
| 90 |
-
|
| 91 |
# Altri Campi Obbligatori dal tracciato
|
| 92 |
{'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': 456},
|
| 93 |
{'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': 553},
|
|
@@ -99,13 +80,11 @@ dat_format_spec = [
|
|
| 99 |
def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
| 100 |
"""
|
| 101 |
Genera un file .dat per Poste Italiane unendo i dati da due DataFrame.
|
| 102 |
-
|
| 103 |
Args:
|
| 104 |
df_shipping_mark_input (pd.DataFrame): DataFrame contenente le shipping mark e i riferimenti d'ordine.
|
| 105 |
Deve contenere le colonne 'shippingMark', 'totalWeight', 'totalVolume'.
|
| 106 |
df_shipment_order_input (pd.DataFrame): DataFrame contenente i dettagli dell'ordine.
|
| 107 |
Deve contenere 'orderRef', 'orderDate' e i campi relativi al destinatario.
|
| 108 |
-
|
| 109 |
Returns:
|
| 110 |
tuple: (str, str) contenente i percorsi dei file .dat e .dis generati, o (None, None) in caso di errore.
|
| 111 |
"""
|
|
@@ -115,45 +94,61 @@ def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
|
| 115 |
df_shipment_order = df_shipment_order_input.copy()
|
| 116 |
|
| 117 |
# Assicurati che i nomi delle colonne siano in minuscolo per una maggiore robustezza
|
| 118 |
-
#
|
| 119 |
df_shipping_mark.columns = df_shipping_mark.columns.str.lower()
|
| 120 |
df_shipment_order.columns = df_shipment_order.columns.str.lower()
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
# Verifica la presenza delle colonne necessarie nel file shipping mark
|
| 123 |
-
required_shipping_mark_cols = ['shippingmark', 'orderref', 'totalweight', 'totalldv', 'requestedshipment']
|
| 124 |
if not all(col in df_shipping_mark.columns for col in required_shipping_mark_cols):
|
| 125 |
st.error(f"Errore: Il file delle shipping mark deve contenere le colonne: {required_shipping_mark_cols}")
|
| 126 |
return None, None
|
| 127 |
|
| 128 |
-
#
|
| 129 |
-
#
|
| 130 |
-
#
|
| 131 |
-
|
| 132 |
-
'
|
| 133 |
-
'consegnee.partner.
|
| 134 |
-
'
|
| 135 |
-
'
|
| 136 |
-
'
|
| 137 |
-
'
|
| 138 |
-
'
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
required_shipment_order_cols = ['orderref'] # 'order_date' is now from shipping_mark_file
|
| 147 |
-
if not all(col in df_shipment_order.columns for col in required_shipment_order_cols):
|
| 148 |
-
st.error(f"Errore: Il file degli ordini deve contenere la colonna: {required_shipment_order_cols} (anche dopo il mapping).")
|
| 149 |
return None, None
|
| 150 |
|
|
|
|
| 151 |
# Pulisci le colonne di join per una corrispondenza robusta (rimuovi spazi e assicurati che siano stringhe)
|
| 152 |
df_shipping_mark['orderref'] = df_shipping_mark['orderref'].astype(str).str.strip()
|
| 153 |
df_shipment_order['orderref'] = df_shipment_order['orderref'].astype(str).str.strip()
|
| 154 |
|
| 155 |
-
#
|
| 156 |
-
|
|
|
|
| 157 |
|
| 158 |
if df_merged.empty:
|
| 159 |
st.warning("Nessun record corrispondente trovato dopo la join. Assicurati che 'orderRef' sia presente e corrisponda in entrambi i file.")
|
|
@@ -163,8 +158,6 @@ def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
|
| 163 |
dat_content = []
|
| 164 |
for index, row in df_merged.iterrows():
|
| 165 |
record_parts = []
|
| 166 |
-
current_cumulative_length = 0 # Per tracciare la posizione corretta
|
| 167 |
-
|
| 168 |
for field_spec in dat_format_spec:
|
| 169 |
field_name = field_spec['name']
|
| 170 |
length = field_spec['length']
|
|
@@ -172,70 +165,87 @@ def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
|
| 172 |
decimals = field_spec['decimals']
|
| 173 |
mandatory = field_spec['mandatory']
|
| 174 |
source_type = field_spec['source']
|
| 175 |
-
excel_col_name = field_spec['excel_col'] # Nome della colonna
|
| 176 |
-
|
| 177 |
-
value_to_format = ''
|
| 178 |
|
|
|
|
| 179 |
if source_type == 'fixed':
|
| 180 |
value_to_format = fixed_and_default_values.get(field_name, '')
|
| 181 |
elif source_type == 'shipping_mark_file':
|
| 182 |
-
|
|
|
|
|
|
|
| 183 |
if pd.isna(value_to_format):
|
| 184 |
value_to_format = ''
|
| 185 |
# Gestione specifica per 'data_spedizione' (Campo 7) per formattazione AAAAMMGG
|
| 186 |
if field_name == 'data_spedizione':
|
| 187 |
-
if pd.notna(value_to_format):
|
| 188 |
try:
|
| 189 |
-
if isinstance(value_to_format, datetime):
|
| 190 |
value_to_format = value_to_format.strftime("%Y%m%d")
|
| 191 |
-
elif isinstance(value_to_format,
|
| 192 |
-
#
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
value_to_format = pd.to_datetime(value_to_format, errors='coerce')
|
| 196 |
if pd.notna(value_to_format):
|
| 197 |
value_to_format = value_to_format.strftime("%Y%m%d")
|
| 198 |
else:
|
| 199 |
value_to_format = ''
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
| 204 |
elif source_type == 'shipment_order_file':
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
if pd.isna(value_to_format):
|
| 207 |
value_to_format = ''
|
| 208 |
elif source_type == 'filler':
|
| 209 |
value_to_format = ' ' * length # Riempie il filler con spazi
|
| 210 |
-
|
| 211 |
-
# Conversione in stringa e pulizia iniziale
|
| 212 |
value_str = str(value_to_format).strip()
|
| 213 |
|
| 214 |
# Gestione specifica per Campo 25 (Numero di telefono Destinatario) "se vuoto mettere tanti zeri"
|
| 215 |
-
if field_name == 'numero_telefono_destinatario' and
|
| 216 |
value_str = '0' * length
|
| 217 |
# Se il campo è obbligatorio e il valore è ancora vuoto dopo aver cercato/gestito casi speciali, usa il default (spazi o zeri)
|
| 218 |
elif mandatory and not value_str:
|
| 219 |
if is_numeric_pad_zero:
|
| 220 |
value_str = '0' * length
|
| 221 |
else:
|
| 222 |
-
value_str = ' ' * length
|
| 223 |
|
| 224 |
# Formattazione finale (allineamento, padding)
|
| 225 |
if is_numeric_pad_zero:
|
| 226 |
try:
|
| 227 |
-
clean_value = value_str.replace(',', '.')
|
| 228 |
-
num_val = float(clean_value)
|
|
|
|
| 229 |
formatted_value = str(int(round(num_val))).rjust(length, '0')
|
| 230 |
except ValueError:
|
| 231 |
-
|
|
|
|
| 232 |
else:
|
|
|
|
| 233 |
formatted_value = value_str.ljust(length)
|
| 234 |
-
|
| 235 |
# Trunca se la stringa è più lunga della lunghezza specificata
|
| 236 |
final_field_value = formatted_value[:length]
|
| 237 |
record_parts.append(final_field_value)
|
| 238 |
-
|
| 239 |
dat_content.append("".join(record_parts))
|
| 240 |
|
| 241 |
# Genera il nome del file con timestamp (uguale per .dat e .dis)
|
|
@@ -245,30 +255,38 @@ def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
|
| 245 |
dat_filename = f"{base_filename}.dat"
|
| 246 |
dis_filename = f"{base_filename}.dis"
|
| 247 |
|
| 248 |
-
#
|
| 249 |
temp_dir = "/tmp"
|
| 250 |
-
os.makedirs(temp_dir, exist_ok=True)
|
| 251 |
|
| 252 |
dat_filepath = os.path.join(temp_dir, dat_filename)
|
| 253 |
dis_filepath = os.path.join(temp_dir, dis_filename)
|
| 254 |
|
| 255 |
-
|
|
|
|
| 256 |
for line in dat_content:
|
| 257 |
f.write(line + '\n')
|
| 258 |
-
|
| 259 |
-
with open(dis_filepath, 'w') as f:
|
| 260 |
-
pass # Crea un file vuoto
|
| 261 |
-
|
| 262 |
-
return dat_filepath, dis_filepath
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
except pd.errors.EmptyDataError:
|
| 265 |
st.error("Errore: Uno dei file Excel è vuoto o non contiene dati validi. Assicurati che non siano completamente vuoti.")
|
| 266 |
return None, None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
except FileNotFoundError as e:
|
| 268 |
st.error(f"Errore: File non trovato - {e}. Assicurati che i file siano caricati correttamente.")
|
| 269 |
return None, None
|
| 270 |
except Exception as e:
|
| 271 |
st.error(f"Si è verificato un errore inatteso durante l'elaborazione: {e}")
|
|
|
|
|
|
|
|
|
|
| 272 |
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.")
|
| 273 |
return None, None
|
| 274 |
|
|
@@ -277,19 +295,20 @@ def generate_dat_format_spec_text():
|
|
| 277 |
spec_text = "Specifiche del Tracciato File .DAT\n\n"
|
| 278 |
spec_text += "Id\tNome Campo\tObbligatorio\tTipo\tLunghezza\tInizio\tDescrizione\tDefault / Fonte Dati\n"
|
| 279 |
spec_text += "--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n"
|
| 280 |
-
|
| 281 |
# Ricalcolo delle start_pos per la documentazione basata sulle lunghezze
|
| 282 |
current_start_pos_for_spec = 1
|
| 283 |
for field_spec in dat_format_spec:
|
|
|
|
| 284 |
field_spec['start_pos_for_spec_output'] = current_start_pos_for_spec
|
|
|
|
| 285 |
current_start_pos_for_spec += field_spec['length']
|
| 286 |
|
| 287 |
-
|
| 288 |
for field_spec in dat_format_spec:
|
| 289 |
field_id = field_spec.get('id', '')
|
|
|
|
| 290 |
display_name = field_spec['name'].replace('_', ' ').replace('fixed', '').replace('derived', '').strip().title()
|
| 291 |
-
|
| 292 |
-
# Gestione speciale per nomi lunghi per migliorare la leggibilità
|
| 293 |
if display_name == 'Numero Riferimento Spedizione':
|
| 294 |
display_name = 'Num. Rif. Spedizione'
|
| 295 |
elif display_name == 'Numero Civico Destinatario':
|
|
@@ -353,14 +372,12 @@ def generate_dat_format_spec_text():
|
|
| 353 |
elif display_name == 'Filler Gap 18 20':
|
| 354 |
display_name = 'Gap'
|
| 355 |
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
field_type = "char" # Tutte sono char
|
| 359 |
length = field_spec['length']
|
| 360 |
# Usa la start_pos ricalcolata per la documentazione
|
| 361 |
start_pos_output = field_spec['start_pos_for_spec_output']
|
| 362 |
description = field_spec['description']
|
| 363 |
-
|
| 364 |
source_info = ""
|
| 365 |
source_type = field_spec['source']
|
| 366 |
if source_type == 'fixed':
|
|
@@ -375,21 +392,18 @@ def generate_dat_format_spec_text():
|
|
| 375 |
source_info += " (riempi con '0' se vuoto)"
|
| 376 |
elif source_type == 'filler':
|
| 377 |
source_info = "Spazi vuoti (Gap)"
|
| 378 |
-
|
| 379 |
-
#
|
| 380 |
if len(description) > 50:
|
| 381 |
description = description[:47] + "..."
|
| 382 |
-
|
| 383 |
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"
|
| 384 |
-
|
| 385 |
return spec_text
|
| 386 |
|
| 387 |
# --- Sezione App Streamlit ---
|
| 388 |
def app():
|
| 389 |
st.set_page_config(layout="centered", page_title="Generatore File Poste .DAT")
|
| 390 |
-
|
| 391 |
st.title("📦 Generatore File .DAT per Poste Italiane")
|
| 392 |
-
|
| 393 |
st.markdown("""
|
| 394 |
Carica i tuoi due file Excel per generare il file .DAT nel formato richiesto da Poste Italiane.
|
| 395 |
Ogni "shipping mark" (LDV) nel file `ShippingMark_` genererà un record nel file di output.
|
|
@@ -397,15 +411,19 @@ def app():
|
|
| 397 |
**Nota**: I file Excel devono avere l'intestazione sulla **seconda riga**.
|
| 398 |
""")
|
| 399 |
|
|
|
|
| 400 |
shipping_mark_file = st.file_uploader("Carica il file ShippingMark_ (con 'shippingMark', 'orderRef', 'totalWeight', 'totalLdv', 'requestedShipment')", type=["xlsx"])
|
| 401 |
shipment_order_file = st.file_uploader("Carica il file ShipmentOrder_ (con 'orderRef' e dettagli destinatario)", type=["xlsx"])
|
| 402 |
|
| 403 |
dat_filepath = None
|
| 404 |
dis_filepath = None
|
| 405 |
-
|
| 406 |
-
#
|
| 407 |
if shipping_mark_file and shipment_order_file:
|
|
|
|
|
|
|
| 408 |
if st.button("Crea File DAT e DIS"):
|
|
|
|
| 409 |
try:
|
| 410 |
# Leggi i file Excel, specificando che l'intestazione è alla seconda riga (indice 1)
|
| 411 |
df_shipping_mark = pd.read_excel(shipping_mark_file, header=1)
|
|
@@ -414,54 +432,80 @@ def app():
|
|
| 414 |
# Esegui la funzione di generazione
|
| 415 |
dat_filepath, dis_filepath = generate_poste_dat_file(df_shipping_mark, df_shipment_order)
|
| 416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
except Exception as e:
|
| 418 |
-
|
| 419 |
st.warning("Assicurati che i file siano in formato .xlsx, non siano corrotti e che l'intestazione sia effettivamente alla seconda riga.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
|
|
|
|
| 421 |
if dat_filepath and dis_filepath:
|
| 422 |
# Ottieni il nome base del file (es. SDA20240101123456789)
|
| 423 |
base_filename = os.path.basename(dat_filepath).replace('.dat', '')
|
| 424 |
-
|
| 425 |
-
st.
|
| 426 |
-
|
| 427 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
with open(dat_filepath, "rb") as f:
|
| 429 |
st.download_button(
|
| 430 |
label=f"Scarica {base_filename}.dat",
|
| 431 |
data=f.read(),
|
| 432 |
file_name=f"{base_filename}.dat",
|
| 433 |
mime="text/plain", # Tipo MIME per file di testo generici
|
| 434 |
-
key="download_dat_btn"
|
|
|
|
| 435 |
)
|
| 436 |
-
|
|
|
|
| 437 |
with open(dis_filepath, "rb") as f:
|
| 438 |
st.download_button(
|
| 439 |
label=f"Scarica {base_filename}.dis",
|
| 440 |
data=f.read(),
|
| 441 |
file_name=f"{base_filename}.dis",
|
| 442 |
mime="text/plain",
|
| 443 |
-
key="download_dis_btn"
|
|
|
|
| 444 |
)
|
| 445 |
|
| 446 |
st.markdown("---")
|
| 447 |
st.subheader("Specifiche del Tracciato .DAT")
|
| 448 |
st.markdown("Qui puoi scaricare un file di testo con la descrizione dettagliata del formato del file .DAT generato:")
|
| 449 |
-
|
|
|
|
| 450 |
spec_text_content = generate_dat_format_spec_text()
|
| 451 |
-
|
|
|
|
| 452 |
st.download_button(
|
| 453 |
label="Scarica Specifiche Tracciato DAT",
|
| 454 |
-
data=spec_text_content.encode('utf-8'),
|
| 455 |
file_name="DAT_Format_Specification.txt",
|
| 456 |
mime="text/plain",
|
| 457 |
-
key="download_spec_btn"
|
|
|
|
| 458 |
)
|
| 459 |
-
|
| 460 |
st.markdown("---")
|
| 461 |
st.info("""
|
| 462 |
**Nota importante sui nomi delle colonne nei tuoi file Excel:**
|
| 463 |
-
Assicurati che i nomi delle colonne nel tuo file `ShipmentOrder_` (es. 'consegnee.partner.name', 'consegnee.partner.address.address') e `ShippingMark_` (es. 'shippingMark', 'orderRef', '
|
| 464 |
""")
|
| 465 |
|
|
|
|
| 466 |
if __name__ == "__main__":
|
| 467 |
app()
|
|
|
|
| 6 |
|
| 7 |
# --- Dati di Configurazione del Tracciato ---
|
| 8 |
# Dizionario per i valori di default/fissi per i campi specifici del .dat
|
|
|
|
| 9 |
fixed_and_default_values = {
|
| 10 |
# Campi fissi Mittente (Campi 1-18) - Valori dalla tabella utente
|
| 11 |
'codice_identificativo_cliente': '1040453', # Campo 1
|
|
|
|
| 12 |
'tipo_spedizione': 'N', # Campo 3
|
| 13 |
'attivazione_ritiro': 'N', # Campo 4
|
| 14 |
'stampa_ldv': 'N', # Campo 5
|
|
|
|
|
|
|
| 15 |
'ritiro_mp': 'P', # Campo 8
|
| 16 |
'codice_indirizzo_mittente': '', # Campo 9
|
| 17 |
'ragione_sociale_mittente': 'Poste Logistics x Technolt', # Campo 10
|
|
|
|
| 23 |
'cap_mittente': '27018', # Campo 16
|
| 24 |
'localita_mittente': 'Vidigulfo', # Campo 17
|
| 25 |
'provincia_mittente': 'PV', # Campo 18
|
|
|
|
| 26 |
# Altri campi fissi o con default obbligatorio
|
| 27 |
'tipo_via_destinatario': '-', # Campo 22 (nuovo fisso)
|
| 28 |
'provincia_destinatario': '-', # Campo 28 (nuovo fisso)
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
# Definizione dettagliata del layout del file .dat.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
dat_format_spec = [
|
| 36 |
# Id | Nome | Obbl | Tipo | Lungh | Inizio | Descrizione | Default | Campi
|
| 37 |
{'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},
|
|
|
|
| 52 |
{'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': 214},
|
| 53 |
{'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': 223},
|
| 54 |
{'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': 253},
|
|
|
|
| 55 |
# Filler per il gap tra Campo 18 (fine a 254) e Campo 20 (inizio a 275)
|
| 56 |
{'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': 255},
|
|
|
|
| 57 |
# Campi Destinatario e Colli
|
| 58 |
+
# NOTA: Per risolvere il problema di recupero dei dati del destinatario,
|
| 59 |
+
# 'consegnee.partner.name' verrà utilizzato direttamente per entrambi i campi 20 e 21,
|
| 60 |
+
# e non verrà rinominato nella mappatura iniziale.
|
| 61 |
{'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': 275},
|
| 62 |
{'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': 315},
|
| 63 |
+
{'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': 335},
|
| 64 |
{'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': 340},
|
| 65 |
{'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': 370},
|
| 66 |
{'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': 375},
|
| 67 |
{'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': 390},
|
| 68 |
{'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': 399},
|
| 69 |
+
{'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': 429},
|
| 70 |
{'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': 431},
|
| 71 |
{'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': 434},
|
|
|
|
| 72 |
# Altri Campi Obbligatori dal tracciato
|
| 73 |
{'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': 456},
|
| 74 |
{'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': 553},
|
|
|
|
| 80 |
def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
| 81 |
"""
|
| 82 |
Genera un file .dat per Poste Italiane unendo i dati da due DataFrame.
|
|
|
|
| 83 |
Args:
|
| 84 |
df_shipping_mark_input (pd.DataFrame): DataFrame contenente le shipping mark e i riferimenti d'ordine.
|
| 85 |
Deve contenere le colonne 'shippingMark', 'totalWeight', 'totalVolume'.
|
| 86 |
df_shipment_order_input (pd.DataFrame): DataFrame contenente i dettagli dell'ordine.
|
| 87 |
Deve contenere 'orderRef', 'orderDate' e i campi relativi al destinatario.
|
|
|
|
| 88 |
Returns:
|
| 89 |
tuple: (str, str) contenente i percorsi dei file .dat e .dis generati, o (None, None) in caso di errore.
|
| 90 |
"""
|
|
|
|
| 94 |
df_shipment_order = df_shipment_order_input.copy()
|
| 95 |
|
| 96 |
# Assicurati che i nomi delle colonne siano in minuscolo per una maggiore robustezza
|
| 97 |
+
# I nomi con punti come 'consegnee.partner.address.address' rimarranno con i punti dopo .lower()
|
| 98 |
df_shipping_mark.columns = df_shipping_mark.columns.str.lower()
|
| 99 |
df_shipment_order.columns = df_shipment_order.columns.str.lower()
|
| 100 |
|
| 101 |
+
# Mappatura delle colonne di df_shipment_order che verranno rinominate PRIMA della join.
|
| 102 |
+
# 'consegnee.partner.name' non è incluso qui per permettere l'accesso diretto ai campi 20 e 21.
|
| 103 |
+
excel_col_mapping_for_rename = {
|
| 104 |
+
'orderdate': 'order_date',
|
| 105 |
+
'consegnee.partner.businessname': 'ragione_sociale_destinatario_from_excel',
|
| 106 |
+
'consegnee.partner.address.streettype': 'tipo_via_destinatario_from_excel',
|
| 107 |
+
'consegnee.partner.address.address': 'via_destinatario',
|
| 108 |
+
'consegnee.partner.address.addressnumber': 'numero_civico_destinatario',
|
| 109 |
+
'consegnee.partner.phonenumber': 'numero_telefono_destinatario',
|
| 110 |
+
'consegnee.partner.address.zipcode': 'cap_destinatario',
|
| 111 |
+
'consegnee.partner.address.city': 'localita_destinatario',
|
| 112 |
+
'consegnee.partner.address.country': 'provincia_destinatario_from_excel',
|
| 113 |
+
}
|
| 114 |
+
# Applica il mapping solo se la colonna originale (in minuscolo) esiste nel DataFrame
|
| 115 |
+
df_shipment_order.rename(columns={k.lower(): v for k, v in excel_col_mapping_for_rename.items() if k.lower() in df_shipment_order.columns}, inplace=True)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
# Verifica la presenza delle colonne necessarie nel file shipping mark
|
| 119 |
+
required_shipping_mark_cols = ['shippingmark', 'orderref', 'totalweight', 'totalldv', 'requestedshipment']
|
| 120 |
if not all(col in df_shipping_mark.columns for col in required_shipping_mark_cols):
|
| 121 |
st.error(f"Errore: Il file delle shipping mark deve contenere le colonne: {required_shipping_mark_cols}")
|
| 122 |
return None, None
|
| 123 |
|
| 124 |
+
# Verifica le colonne necessarie nel file shipment order
|
| 125 |
+
# Queste sono le colonne che saranno ACCESSE dopo il rename (se applicato) e la join.
|
| 126 |
+
# 'consegnee.partner.name' è incluso qui con il suo nome originale perché non viene rinominato.
|
| 127 |
+
required_shipment_order_final_cols = [
|
| 128 |
+
'orderref', # Per la join
|
| 129 |
+
'consegnee.partner.name', # Per ragione_sociale_destinatario e contatto_destinatario
|
| 130 |
+
'via_destinatario',
|
| 131 |
+
'numero_civico_destinatario',
|
| 132 |
+
'numero_telefono_destinatario',
|
| 133 |
+
'cap_destinatario',
|
| 134 |
+
'localita_destinatario',
|
| 135 |
+
]
|
| 136 |
+
# Converte i nomi delle colonne finali che dovrebbero essere presenti nel df_shipment_order
|
| 137 |
+
# dopo il renaming (ma prima della join) e verifica la loro esistenza.
|
| 138 |
+
df_shipment_order_cols_after_rename = df_shipment_order.columns.tolist()
|
| 139 |
+
if not all(col in df_shipment_order_cols_after_rename for col in required_shipment_order_final_cols):
|
| 140 |
+
missing_cols = [col for col in required_shipment_order_final_cols if col not in df_shipment_order_cols_after_rename]
|
| 141 |
+
st.error(f"Errore: Il file degli ordini (ShipmentOrder_) deve contenere le seguenti colonne: {missing_cols}. Verifica attentamente la corrispondenza dei nomi (anche con punti o underscore). Ricorda che l'intestazione è alla seconda riga.")
|
|
|
|
|
|
|
|
|
|
| 142 |
return None, None
|
| 143 |
|
| 144 |
+
|
| 145 |
# Pulisci le colonne di join per una corrispondenza robusta (rimuovi spazi e assicurati che siano stringhe)
|
| 146 |
df_shipping_mark['orderref'] = df_shipping_mark['orderref'].astype(str).str.strip()
|
| 147 |
df_shipment_order['orderref'] = df_shipment_order['orderref'].astype(str).str.strip()
|
| 148 |
|
| 149 |
+
# Esegue la join: cruciale 'how=inner' per una join "secca"
|
| 150 |
+
# Utilizza suffissi per differenziare le colonne con lo stesso nome provenienti dai due DataFrame
|
| 151 |
+
df_merged = pd.merge(df_shipping_mark, df_shipment_order, on='orderref', how='inner', suffixes=('_sm', '_so'))
|
| 152 |
|
| 153 |
if df_merged.empty:
|
| 154 |
st.warning("Nessun record corrispondente trovato dopo la join. Assicurati che 'orderRef' sia presente e corrisponda in entrambi i file.")
|
|
|
|
| 158 |
dat_content = []
|
| 159 |
for index, row in df_merged.iterrows():
|
| 160 |
record_parts = []
|
|
|
|
|
|
|
| 161 |
for field_spec in dat_format_spec:
|
| 162 |
field_name = field_spec['name']
|
| 163 |
length = field_spec['length']
|
|
|
|
| 165 |
decimals = field_spec['decimals']
|
| 166 |
mandatory = field_spec['mandatory']
|
| 167 |
source_type = field_spec['source']
|
| 168 |
+
excel_col_name = field_spec['excel_col'] # Nome della colonna come specificato nel dat_format_spec
|
|
|
|
|
|
|
| 169 |
|
| 170 |
+
value_to_format = ''
|
| 171 |
if source_type == 'fixed':
|
| 172 |
value_to_format = fixed_and_default_values.get(field_name, '')
|
| 173 |
elif source_type == 'shipping_mark_file':
|
| 174 |
+
# Accede alle colonne di shipping_mark utilizzando il suffisso '_sm' dopo la join
|
| 175 |
+
# e il nome della colonna come specificato nel dat_format_spec
|
| 176 |
+
value_to_format = row.get(f"{excel_col_name}_sm")
|
| 177 |
if pd.isna(value_to_format):
|
| 178 |
value_to_format = ''
|
| 179 |
# Gestione specifica per 'data_spedizione' (Campo 7) per formattazione AAAAMMGG
|
| 180 |
if field_name == 'data_spedizione':
|
| 181 |
+
if pd.notna(value_to_format) and value_to_format != '':
|
| 182 |
try:
|
| 183 |
+
if isinstance(value_to_format, datetime):
|
| 184 |
value_to_format = value_to_format.strftime("%Y%m%d")
|
| 185 |
+
elif isinstance(value_to_format, (int, float)): # Le date di Excel possono essere numeri
|
| 186 |
+
# L'epoch di Excel è 1899-12-30 per le date
|
| 187 |
+
value_to_format = pd.to_datetime(value_to_format, unit='D', origin='1899-12-30', errors='coerce')
|
| 188 |
+
if pd.notna(value_to_format):
|
| 189 |
+
value_to_format = value_to_format.strftime("%Y%m%d")
|
| 190 |
+
else:
|
| 191 |
+
value_to_format = ''
|
| 192 |
+
elif isinstance(value_to_format, str):
|
| 193 |
+
# Tenta di parsare formati di data stringa comuni
|
| 194 |
value_to_format = pd.to_datetime(value_to_format, errors='coerce')
|
| 195 |
if pd.notna(value_to_format):
|
| 196 |
value_to_format = value_to_format.strftime("%Y%m%d")
|
| 197 |
else:
|
| 198 |
value_to_format = ''
|
| 199 |
+
else: # Fallback per tipi inaspettati
|
| 200 |
+
value_to_format = ''
|
| 201 |
+
except Exception as e:
|
| 202 |
+
st.warning(f"Errore durante la conversione della data per campo '{field_name}': {value_to_format} - {e}")
|
| 203 |
+
value_to_format = ''
|
| 204 |
+
else: # Se NaN o vuoto, usa stringa vuota
|
| 205 |
+
value_to_format = ''
|
| 206 |
elif source_type == 'shipment_order_file':
|
| 207 |
+
# Accede alle colonne di shipment_order utilizzando il suffisso '_so' dopo la join.
|
| 208 |
+
# Se la colonna originale è stata rinominata prima della join (e.g. 'orderdate' -> 'order_date'),
|
| 209 |
+
# usa il nome rinominato per costruire il nome della colonna fusa.
|
| 210 |
+
# Altrimenti, usa il nome originale di excel_col_name.
|
| 211 |
+
col_to_access = excel_col_mapping_for_rename.get(excel_col_name, excel_col_name)
|
| 212 |
+
value_to_format = row.get(f"{col_to_access}_so")
|
| 213 |
+
|
| 214 |
if pd.isna(value_to_format):
|
| 215 |
value_to_format = ''
|
| 216 |
elif source_type == 'filler':
|
| 217 |
value_to_format = ' ' * length # Riempie il filler con spazi
|
| 218 |
+
|
|
|
|
| 219 |
value_str = str(value_to_format).strip()
|
| 220 |
|
| 221 |
# Gestione specifica per Campo 25 (Numero di telefono Destinatario) "se vuoto mettere tanti zeri"
|
| 222 |
+
if field_name == 'numero_telefono_destinatario' and (not value_str or value_str.lower() == 'nan'):
|
| 223 |
value_str = '0' * length
|
| 224 |
# Se il campo è obbligatorio e il valore è ancora vuoto dopo aver cercato/gestito casi speciali, usa il default (spazi o zeri)
|
| 225 |
elif mandatory and not value_str:
|
| 226 |
if is_numeric_pad_zero:
|
| 227 |
value_str = '0' * length
|
| 228 |
else:
|
| 229 |
+
value_str = ' ' * length
|
| 230 |
|
| 231 |
# Formattazione finale (allineamento, padding)
|
| 232 |
if is_numeric_pad_zero:
|
| 233 |
try:
|
| 234 |
+
clean_value = value_str.replace(',', '.')
|
| 235 |
+
num_val = float(clean_value)
|
| 236 |
+
# Arrotonda e converte in stringa, riempiendo con '0' a sinistra
|
| 237 |
formatted_value = str(int(round(num_val))).rjust(length, '0')
|
| 238 |
except ValueError:
|
| 239 |
+
# Fallback se la conversione numerica fallisce
|
| 240 |
+
formatted_value = '0'.rjust(length, '0')
|
| 241 |
else:
|
| 242 |
+
# Allinea a sinistra e riempi con spazi
|
| 243 |
formatted_value = value_str.ljust(length)
|
| 244 |
+
|
| 245 |
# Trunca se la stringa è più lunga della lunghezza specificata
|
| 246 |
final_field_value = formatted_value[:length]
|
| 247 |
record_parts.append(final_field_value)
|
| 248 |
+
|
| 249 |
dat_content.append("".join(record_parts))
|
| 250 |
|
| 251 |
# Genera il nome del file con timestamp (uguale per .dat e .dis)
|
|
|
|
| 255 |
dat_filename = f"{base_filename}.dat"
|
| 256 |
dis_filename = f"{base_filename}.dis"
|
| 257 |
|
| 258 |
+
# Utilizza la directory temporanea /tmp che è scrivibile su Hugging Face Spaces
|
| 259 |
temp_dir = "/tmp"
|
| 260 |
+
os.makedirs(temp_dir, exist_ok=True)
|
| 261 |
|
| 262 |
dat_filepath = os.path.join(temp_dir, dat_filename)
|
| 263 |
dis_filepath = os.path.join(temp_dir, dis_filename)
|
| 264 |
|
| 265 |
+
# Scrivi il contenuto nel file .dat con encoding UTF-8
|
| 266 |
+
with open(dat_filepath, 'w', encoding='utf-8') as f:
|
| 267 |
for line in dat_content:
|
| 268 |
f.write(line + '\n')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
+
# Crea un file .dis vuoto con encoding UTF-8
|
| 271 |
+
with open(dis_filepath, 'w', encoding='utf-8') as f:
|
| 272 |
+
pass
|
| 273 |
+
|
| 274 |
+
return dat_filepath, dis_filepath
|
| 275 |
except pd.errors.EmptyDataError:
|
| 276 |
st.error("Errore: Uno dei file Excel è vuoto o non contiene dati validi. Assicurati che non siano completamente vuoti.")
|
| 277 |
return None, None
|
| 278 |
+
except KeyError as e:
|
| 279 |
+
st.error(f"Errore: Colonna mancante nel file Excel. Assicurati che tutte le colonne necessarie ('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-insensitive) a quelli previsti. Errore specifico: {e}")
|
| 280 |
+
st.warning("Ricorda che l'intestazione dei file Excel deve essere alla **seconda riga**.")
|
| 281 |
+
return None, None
|
| 282 |
except FileNotFoundError as e:
|
| 283 |
st.error(f"Errore: File non trovato - {e}. Assicurati che i file siano caricati correttamente.")
|
| 284 |
return None, None
|
| 285 |
except Exception as e:
|
| 286 |
st.error(f"Si è verificato un errore inatteso durante l'elaborazione: {e}")
|
| 287 |
+
import traceback
|
| 288 |
+
st.error("Traceback completo per il debug:")
|
| 289 |
+
st.error(traceback.format_exc()) # Stampa il traceback completo per il debug
|
| 290 |
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.")
|
| 291 |
return None, None
|
| 292 |
|
|
|
|
| 295 |
spec_text = "Specifiche del Tracciato File .DAT\n\n"
|
| 296 |
spec_text += "Id\tNome Campo\tObbligatorio\tTipo\tLunghezza\tInizio\tDescrizione\tDefault / Fonte Dati\n"
|
| 297 |
spec_text += "--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n"
|
|
|
|
| 298 |
# Ricalcolo delle start_pos per la documentazione basata sulle lunghezze
|
| 299 |
current_start_pos_for_spec = 1
|
| 300 |
for field_spec in dat_format_spec:
|
| 301 |
+
# Assegna la posizione iniziale calcolata per l'output della documentazione
|
| 302 |
field_spec['start_pos_for_spec_output'] = current_start_pos_for_spec
|
| 303 |
+
# Incrementa la posizione iniziale per il campo successivo
|
| 304 |
current_start_pos_for_spec += field_spec['length']
|
| 305 |
|
|
|
|
| 306 |
for field_spec in dat_format_spec:
|
| 307 |
field_id = field_spec.get('id', '')
|
| 308 |
+
# Formatta il nome del campo per la visualizzazione, rimuovendo underscore e capitalizzando
|
| 309 |
display_name = field_spec['name'].replace('_', ' ').replace('fixed', '').replace('derived', '').strip().title()
|
| 310 |
+
|
| 311 |
+
# Gestione speciale per nomi lunghi per migliorare la leggibilità nella tabella di testo
|
| 312 |
if display_name == 'Numero Riferimento Spedizione':
|
| 313 |
display_name = 'Num. Rif. Spedizione'
|
| 314 |
elif display_name == 'Numero Civico Destinatario':
|
|
|
|
| 372 |
elif display_name == 'Filler Gap 18 20':
|
| 373 |
display_name = 'Gap'
|
| 374 |
|
| 375 |
+
obligatory = "*" if field_spec['mandatory'] else "" # Indica se il campo è obbligatorio
|
| 376 |
+
field_type = "char" # Tutte le colonne sono considerate di tipo carattere
|
|
|
|
| 377 |
length = field_spec['length']
|
| 378 |
# Usa la start_pos ricalcolata per la documentazione
|
| 379 |
start_pos_output = field_spec['start_pos_for_spec_output']
|
| 380 |
description = field_spec['description']
|
|
|
|
| 381 |
source_info = ""
|
| 382 |
source_type = field_spec['source']
|
| 383 |
if source_type == 'fixed':
|
|
|
|
| 392 |
source_info += " (riempi con '0' se vuoto)"
|
| 393 |
elif source_type == 'filler':
|
| 394 |
source_info = "Spazi vuoti (Gap)"
|
| 395 |
+
|
| 396 |
+
# Adatta la colonna Descrizione per non essere troppo larga nella tabella di testo
|
| 397 |
if len(description) > 50:
|
| 398 |
description = description[:47] + "..."
|
| 399 |
+
# Aggiungi la riga formattata al testo delle specifiche
|
| 400 |
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"
|
|
|
|
| 401 |
return spec_text
|
| 402 |
|
| 403 |
# --- Sezione App Streamlit ---
|
| 404 |
def app():
|
| 405 |
st.set_page_config(layout="centered", page_title="Generatore File Poste .DAT")
|
|
|
|
| 406 |
st.title("📦 Generatore File .DAT per Poste Italiane")
|
|
|
|
| 407 |
st.markdown("""
|
| 408 |
Carica i tuoi due file Excel per generare il file .DAT nel formato richiesto da Poste Italiane.
|
| 409 |
Ogni "shipping mark" (LDV) nel file `ShippingMark_` genererà un record nel file di output.
|
|
|
|
| 411 |
**Nota**: I file Excel devono avere l'intestazione sulla **seconda riga**.
|
| 412 |
""")
|
| 413 |
|
| 414 |
+
# Caricamento dei file
|
| 415 |
shipping_mark_file = st.file_uploader("Carica il file ShippingMark_ (con 'shippingMark', 'orderRef', 'totalWeight', 'totalLdv', 'requestedShipment')", type=["xlsx"])
|
| 416 |
shipment_order_file = st.file_uploader("Carica il file ShipmentOrder_ (con 'orderRef' e dettagli destinatario)", type=["xlsx"])
|
| 417 |
|
| 418 |
dat_filepath = None
|
| 419 |
dis_filepath = None
|
| 420 |
+
|
| 421 |
+
# Pulsante per innescare la generazione dei file
|
| 422 |
if shipping_mark_file and shipment_order_file:
|
| 423 |
+
# Aggiungi un placeholder per il messaggio di caricamento/stato
|
| 424 |
+
status_message_placeholder = st.empty()
|
| 425 |
if st.button("Crea File DAT e DIS"):
|
| 426 |
+
status_message_placeholder.info("Elaborazione in corso... Attendere.")
|
| 427 |
try:
|
| 428 |
# Leggi i file Excel, specificando che l'intestazione è alla seconda riga (indice 1)
|
| 429 |
df_shipping_mark = pd.read_excel(shipping_mark_file, header=1)
|
|
|
|
| 432 |
# Esegui la funzione di generazione
|
| 433 |
dat_filepath, dis_filepath = generate_poste_dat_file(df_shipping_mark, df_shipment_order)
|
| 434 |
|
| 435 |
+
if dat_filepath and dis_filepath:
|
| 436 |
+
status_message_placeholder.success("File generati con successo!")
|
| 437 |
+
else:
|
| 438 |
+
# Se generate_poste_dat_file ha restituito None, un errore è già stato mostrato
|
| 439 |
+
# Questo garantisce che il messaggio di errore rimanga visibile
|
| 440 |
+
pass
|
| 441 |
+
|
| 442 |
except Exception as e:
|
| 443 |
+
status_message_placeholder.error(f"Si è verificato un errore durante la lettura dei file Excel: {e}")
|
| 444 |
st.warning("Assicurati che i file siano in formato .xlsx, non siano corrotti e che l'intestazione sia effettivamente alla seconda riga.")
|
| 445 |
+
finally:
|
| 446 |
+
# La logica del finally assicura che il messaggio di caricamento venga rimosso,
|
| 447 |
+
# ma i messaggi di successo/errore specifici rimangano.
|
| 448 |
+
if status_message_placeholder.empty: # Se non ci sono messaggi attivi
|
| 449 |
+
status_message_placeholder.empty() # Assicurati che sia vuoto
|
| 450 |
|
| 451 |
+
# Mostra i pulsanti di download se i file sono stati generati con successo
|
| 452 |
if dat_filepath and dis_filepath:
|
| 453 |
# Ottieni il nome base del file (es. SDA20240101123456789)
|
| 454 |
base_filename = os.path.basename(dat_filepath).replace('.dat', '')
|
| 455 |
+
st.write("---") # Separatore visivo
|
| 456 |
+
st.subheader("Scarica i File Generati")
|
| 457 |
+
st.markdown(f"I file `{base_filename}.dat` e `{base_filename}.dis` sono stati generati.")
|
| 458 |
+
st.markdown("""
|
| 459 |
+
**Nota sul download automatico:**
|
| 460 |
+
Per motivi di sicurezza del browser, Streamlit non consente il download automatico di file
|
| 461 |
+
senza un'interazione esplicita dell'utente dopo il click sul pulsante "Crea File".
|
| 462 |
+
È necessario cliccare sui pulsanti qui sotto per avviare il download dei file.
|
| 463 |
+
""")
|
| 464 |
+
|
| 465 |
+
# Pulsante per scaricare il file .dat
|
| 466 |
with open(dat_filepath, "rb") as f:
|
| 467 |
st.download_button(
|
| 468 |
label=f"Scarica {base_filename}.dat",
|
| 469 |
data=f.read(),
|
| 470 |
file_name=f"{base_filename}.dat",
|
| 471 |
mime="text/plain", # Tipo MIME per file di testo generici
|
| 472 |
+
key="download_dat_btn",
|
| 473 |
+
help="Clicca per scaricare il file .dat generato."
|
| 474 |
)
|
| 475 |
+
|
| 476 |
+
# Pulsante per scaricare il file .dis
|
| 477 |
with open(dis_filepath, "rb") as f:
|
| 478 |
st.download_button(
|
| 479 |
label=f"Scarica {base_filename}.dis",
|
| 480 |
data=f.read(),
|
| 481 |
file_name=f"{base_filename}.dis",
|
| 482 |
mime="text/plain",
|
| 483 |
+
key="download_dis_btn",
|
| 484 |
+
help="Clicca per scaricare il file .dis generato."
|
| 485 |
)
|
| 486 |
|
| 487 |
st.markdown("---")
|
| 488 |
st.subheader("Specifiche del Tracciato .DAT")
|
| 489 |
st.markdown("Qui puoi scaricare un file di testo con la descrizione dettagliata del formato del file .DAT generato:")
|
| 490 |
+
|
| 491 |
+
# Genera il testo delle specifiche
|
| 492 |
spec_text_content = generate_dat_format_spec_text()
|
| 493 |
+
|
| 494 |
+
# Pulsante per scaricare le specifiche
|
| 495 |
st.download_button(
|
| 496 |
label="Scarica Specifiche Tracciato DAT",
|
| 497 |
+
data=spec_text_content.encode('utf-8'), # Codifica il testo in UTF-8
|
| 498 |
file_name="DAT_Format_Specification.txt",
|
| 499 |
mime="text/plain",
|
| 500 |
+
key="download_spec_btn",
|
| 501 |
+
help="Clicca per scaricare il documento con le specifiche del formato DAT."
|
| 502 |
)
|
|
|
|
| 503 |
st.markdown("---")
|
| 504 |
st.info("""
|
| 505 |
**Nota importante sui nomi delle colonne nei tuoi file Excel:**
|
| 506 |
+
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).
|
| 507 |
""")
|
| 508 |
|
| 509 |
+
# Punto di ingresso dell'applicazione Streamlit
|
| 510 |
if __name__ == "__main__":
|
| 511 |
app()
|