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()