pasquale991 commited on
Commit
84bf975
·
verified ·
1 Parent(s): 6c16a11

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. 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}, # Ora fisso
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}, # Ora fisso
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
- # Nota: per nomi con punti come 'consegnee.partner.address.address', .lower() li mantiene così.
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'] # Added 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
- # Applica il mapping delle colonne a df_shipment_order.
129
- # Le chiavi del mapping DEVONO corrispondere esattamente ai nomi delle colonne (in minuscolo)
130
- # nel tuo file Excel.
131
- excel_col_mapping = {
132
- 'orderdate': 'order_date', # Per Campo 7 - Still here for robustness even if not primary source
133
- 'consegnee.partner.businessname': 'ragione_sociale_destinatario_from_excel', # Changed name to avoid conflict if 'consegnee.partner.name' is used directly
134
- 'consegnee.partner.name': 'contatto_destinatario', # Campo 21 and 20 are both using this source
135
- 'consegnee.partner.address.streettype': 'tipo_via_destinatario_from_excel', # Campo 22 - now fixed, but still good to have a mapping for potential future use or debugging
136
- 'consegnee.partner.address.address': 'via_destinatario', # Campo 23
137
- 'consegnee.partner.address.addressnumber': 'numero_civico_destinatario', # Campo 24
138
- 'consegnee.partner.phonenumber': 'numero_telefono_destinatario', # Campo 25
139
- 'consegnee.partner.address.zipcode': 'cap_destinatario', # Campo 26
140
- 'consegnee.partner.address.city': 'localita_destinatario', # Campo 27
141
- 'consegnee.partner.address.country': 'provincia_destinatario_from_excel', # Campo 28 - now fixed, but keeping mapping
142
- }
143
- df_shipment_order = df_shipment_order.rename(columns=lambda x: excel_col_mapping.get(x, x))
144
-
145
- # Assicurati che 'orderref' sia presente in df_shipment_order per la join
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
- # Fai la join: cruciale 'how=inner' per la join "secca"
156
- df_merged = pd.merge(df_shipping_mark, df_shipment_order, on='orderref', how='inner')
 
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 in Excel (mappata)
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
- value_to_format = row.get(excel_col_name)
 
 
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): # Se è già un oggetto datetime
190
  value_to_format = value_to_format.strftime("%Y%m%d")
191
- elif isinstance(value_to_format, str) and len(value_to_format) == 8 and value_to_format.isdigit():
192
- # Se è già una stringa nel formato giusto, usala così com'è
193
- pass
194
- else: # Prova a convertirla (e.g., da Excel data numerica)
 
 
 
 
 
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
- except Exception:
201
- value_to_format = '' # Fallback se la conversione fallisce
202
- else:
203
- value_to_format = '' # Se NaN, usa vuoto
 
 
 
204
  elif source_type == 'shipment_order_file':
205
- value_to_format = row.get(excel_col_name)
 
 
 
 
 
 
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 mandatory and not value_str:
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(',', '.') # Sostituisce virgole con punti
228
- num_val = float(clean_value) * (10**decimals)
 
229
  formatted_value = str(int(round(num_val))).rjust(length, '0')
230
  except ValueError:
231
- formatted_value = '0'.rjust(length, '0') # Fallback se la conversione fallisce
 
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
- # Usa la directory temporanea /tmp che è scrivibile su Hugging Face Spaces
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
- with open(dat_filepath, 'w', encoding='utf-8') as f: # Specifica encoding utf-8
 
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
- obligatory = "*" if field_spec['mandatory'] else ""
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
- # Aggiusta la colonna Descrizione per non essere troppo larga nella tabella di testo
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
- # Aggiungi un pulsante per innescare la generazione
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
- st.error(f"Errore: {e}")
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.success("File generati con successo! Puoi scaricarli qui sotto:")
426
- # Per consentire il download in Streamlit, dobbiamo leggere il contenuto del file
427
- # in modalità binaria ('rb') e passarlo al download_button.
 
 
 
 
 
 
 
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', 'requestedShipment', 'totalLdv', 'totalWeight') corrispondano **esattamente** ai nomi completi con la notazione a punti (o CamelCase originali se è il caso dopo il `header=1`) una volta letti da Excel con l'intestazione alla seconda riga.
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()