Spaces:
Sleeping
Sleeping
File size: 39,129 Bytes
83fea6f 84f13c3 c0bd89c 1b64f01 84f13c3 c99bfb4 fea56cb c99bfb4 98dac45 5b371e3 98dac45 d98b2ae a144f86 98dac45 5b371e3 c99bfb4 98dac45 60c7fbe c99bfb4 6c16a11 c99bfb4 000859d f2f9d9b 5b6021e e0ca25c c99bfb4 bb1ef81 e0ca25c 98dac45 e0ca25c c99bfb4 000859d e0ca25c ed39443 e0ca25c c99bfb4 1560109 c99bfb4 c0bd89c 84f13c3 a144f86 84f13c3 c0bd89c 000859d c0bd89c 000859d c0bd89c 84f13c3 c0bd89c 84f13c3 c0bd89c 000859d 84f13c3 0ff9e41 c0bd89c 84bf975 000859d ed39443 000859d 84bf975 000859d 0ff9e41 c0bd89c ed39443 84f13c3 c399438 000859d c399438 84bf975 93a3f26 f2f9d9b bbe1a0c 84f13c3 5b6021e 84f13c3 1b64f01 5b6021e 0ff9e41 c399438 c99bfb4 84bf975 5b6021e c399438 84bf975 c99bfb4 b300dd8 c99bfb4 f2f9d9b 0ff9e41 000859d 0ff9e41 000859d c99bfb4 000859d 6c16a11 c99bfb4 84bf975 b968882 84bf975 c99bfb4 93a3f26 84bf975 6c16a11 93a3f26 84bf975 0ff9e41 84bf975 93a3f26 84bf975 cbf3873 6c16a11 f2f9d9b 0ff9e41 2cf4de5 000859d 0ff9e41 2cf4de5 84bf975 0ff9e41 2cf4de5 ed39443 2cf4de5 c99bfb4 84bf975 ed39443 c399438 c99bfb4 84bf975 c99bfb4 000859d c99bfb4 bbe1a0c c399438 84bf975 ed39443 c0bd89c bbe1a0c c99bfb4 84f13c3 84bf975 c99bfb4 84f13c3 84bf975 bbe1a0c ed39443 c399438 84bf975 c0bd89c 6c16a11 84bf975 5b6021e 84f13c3 c99bfb4 84f13c3 c99bfb4 84f13c3 84bf975 1b64f01 84bf975 84f13c3 c0bd89c 84bf975 84f13c3 84bf975 c0bd89c 1b64f01 c0bd89c 84bf975 000859d 84bf975 84f13c3 c0bd89c 84f13c3 c0bd89c 84bf975 1b64f01 c0bd89c c99bfb4 6c16a11 5b6021e 6c16a11 84bf975 ed39443 6c16a11 c99bfb4 84bf975 c99bfb4 84bf975 c99bfb4 84bf975 c99bfb4 ed39443 c99bfb4 ed39443 c99bfb4 84bf975 c99bfb4 84bf975 6c16a11 c99bfb4 c0bd89c 73c5c32 c0bd89c 73c5c32 1b64f01 c0bd89c 93a3f26 84bf975 6c16a11 c0bd89c 84bf975 c0bd89c 84bf975 6c16a11 84bf975 6c16a11 c0bd89c 6c16a11 c0bd89c 84bf975 93a3f26 000859d 84bf975 93a3f26 84bf975 6c16a11 000859d 84bf975 000859d c0bd89c 93a3f26 a144f86 93a3f26 8c00147 c0bd89c c99bfb4 a144f86 c0bd89c 84bf975 c0bd89c 2cf4de5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 | 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() |