Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +282 -168
src/streamlit_app.py
CHANGED
|
@@ -4,6 +4,96 @@ import os
|
|
| 4 |
import streamlit as st
|
| 5 |
import numpy as np # Importa numpy per np.nan
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
| 8 |
"""
|
| 9 |
Genera un file .dat per Poste Italiane unendo i dati da due DataFrame.
|
|
@@ -23,45 +113,38 @@ def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
|
| 23 |
df_shipment_order = df_shipment_order_input.copy()
|
| 24 |
|
| 25 |
# Assicurati che i nomi delle colonne siano in minuscolo per una maggiore robustezza
|
|
|
|
| 26 |
df_shipping_mark.columns = df_shipping_mark.columns.str.lower()
|
| 27 |
df_shipment_order.columns = df_shipment_order.columns.str.lower()
|
| 28 |
|
| 29 |
# Verifica la presenza delle colonne necessarie nel file shipping mark
|
| 30 |
-
# Ora include totalweight e totalvolume
|
| 31 |
required_shipping_mark_cols = ['shippingmark', 'orderref', 'totalweight', 'totalvolume']
|
| 32 |
if not all(col in df_shipping_mark.columns for col in required_shipping_mark_cols):
|
| 33 |
-
st.error(f"Il file delle shipping mark deve contenere le colonne: {required_shipping_mark_cols}")
|
| 34 |
return None, None
|
| 35 |
|
| 36 |
-
#
|
| 37 |
-
#
|
| 38 |
-
#
|
| 39 |
-
#
|
| 40 |
-
|
| 41 |
-
column_mapping = {
|
| 42 |
-
# Campi destinatario e data che si aspettano dal file ShipmentOrder_.
|
| 43 |
'orderdate': 'order_date', # Per Campo 7
|
| 44 |
'consegnee.partner.businessname': 'ragione_sociale_destinatario', # Campo 20
|
| 45 |
-
'consegnee.partner.
|
| 46 |
-
'consegnee.partner.address.streettype': '
|
| 47 |
-
'consegnee.partner.address.address': '
|
| 48 |
-
'consegnee.partner.address.addressnumber': '
|
| 49 |
-
'consegnee.partner.phone.number': '
|
| 50 |
-
'consegnee.partner.address.zipcode': '
|
| 51 |
-
'consegnee.partner.address.city': '
|
| 52 |
-
'consegnee.partner.address.
|
| 53 |
-
# Nota: 'consegnee.partner.address.country' non è nel tracciato .dat fornito.
|
| 54 |
}
|
| 55 |
-
|
| 56 |
-
# Applica il mapping delle colonne a df_shipment_order.
|
| 57 |
-
# La funzione lambda `x` viene applicata ai nomi delle colonne attuali (già in minuscolo)
|
| 58 |
-
# e poi cerca nel `column_mapping`.
|
| 59 |
-
df_shipment_order = df_shipment_order.rename(columns=lambda x: column_mapping.get(x, x))
|
| 60 |
|
| 61 |
# Assicurati che 'orderref' e 'order_date' siano presenti in df_shipment_order dopo il rename
|
| 62 |
required_shipment_order_cols = ['orderref', 'order_date']
|
| 63 |
if not all(col in df_shipment_order.columns for col in required_shipment_order_cols):
|
| 64 |
-
st.error(f"Il file degli ordini deve contenere le colonne: {required_shipment_order_cols} (anche dopo il mapping).")
|
| 65 |
return None, None
|
| 66 |
|
| 67 |
# Pulisci le colonne di join per una corrispondenza robusta (rimuovi spazi e assicurati che siano stringhe)
|
|
@@ -78,82 +161,6 @@ def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
|
| 78 |
st.warning("Nessun record corrispondente trovato dopo la join. Assicurati che 'orderRef' sia presente e corrisponda in entrambi i file.")
|
| 79 |
return None, None
|
| 80 |
|
| 81 |
-
# Dizionario per i valori di default/fissi per i campi obbligatori o predeterminati
|
| 82 |
-
# Basato sul NUOVO tracciato fornito.
|
| 83 |
-
fixed_and_default_values = {
|
| 84 |
-
# Campi fissi Mittente (Campi 1-18) - Valori dalla tua ultima tabella
|
| 85 |
-
'codice_identificativo_cliente_fixed': 'CLI000000009', # Campo 1
|
| 86 |
-
'id_ldv_fixed': 'FIL01', # Campo 2
|
| 87 |
-
'tipo_spedizione_fixed': 'N', # Campo 3
|
| 88 |
-
'attivazione_ritiro_fixed': 'N', # Campo 4
|
| 89 |
-
'stampa_ldv_fixed': 'N', # Campo 5
|
| 90 |
-
# Campo 6 (orderref) dal file
|
| 91 |
-
# Campo 7 (data_spedizione_derived) è derivato da order_date dal file
|
| 92 |
-
'ritiro_mp_fixed': 'P', # Campo 8
|
| 93 |
-
'codice_indirizzo_mittente_fixed': 'IND001', # Campo 9
|
| 94 |
-
'ragione_sociale_mittente_fixed': 'Poste Logistics', # Campo 10
|
| 95 |
-
'contatto_mittente_fixed': 'postelogistics@poste.it', # Campo 11
|
| 96 |
-
'tipo_via_mittente_fixed': 'Via', # Campo 12
|
| 97 |
-
'via_mittente_fixed': 'Via da Inserire', # Campo 13
|
| 98 |
-
'numero_civico_mittente_fixed': '54', # Campo 14
|
| 99 |
-
'numero_telefono_mittente_fixed': '60898001', # Campo 15
|
| 100 |
-
'cap_mittente_fixed': '70019', # Campo 16
|
| 101 |
-
'localita_mittente_fixed': 'Vidigulfo', # Campo 17
|
| 102 |
-
'provincia_mittente_fixed': 'PV', # Campo 18
|
| 103 |
-
|
| 104 |
-
# Altri campi fissi o con default obbligatorio dal NUOVO tracciato
|
| 105 |
-
'service_code_fixed': 'STD', # Campo 34
|
| 106 |
-
'branch_code_fixed': 'FIL05', # Campo 43
|
| 107 |
-
'client_type_fixed': 'MGCS', # Campo 49
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
# Definizione dettagliata del layout del file .dat.
|
| 111 |
-
# INCLUDE SOLO I CAMPI DALL'ULTIMO TRACCIATO FORNITO DALL'UTENTE.
|
| 112 |
-
# 'source_type': 'fixed' (valore dal dizionario fixed_and_default_values),
|
| 113 |
-
# 'file' (valore dal DataFrame unito df_merged),
|
| 114 |
-
# 'derived' (calcolato, es. data_spedizione_derived)
|
| 115 |
-
dat_format_spec = [
|
| 116 |
-
# BLOCCO MITTENTE FISSO (Campi 1-18 dal NUOVO tracciato)
|
| 117 |
-
{'name': 'codice_identificativo_cliente_fixed', 'length': 12, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 1
|
| 118 |
-
{'name': 'id_ldv_fixed', 'length': 30, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 2
|
| 119 |
-
{'name': 'tipo_spedizione_fixed', 'length': 1, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 3
|
| 120 |
-
{'name': 'attivazione_ritiro_fixed', 'length': 1, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 4
|
| 121 |
-
{'name': 'stampa_ldv_fixed', 'length': 1, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 5
|
| 122 |
-
{'name': 'orderref', 'length': 25, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 6 (Numero Riferimento Spedizione - dal file shipping mark)
|
| 123 |
-
{'name': 'order_date', 'length': 8, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 7 (Data della spedizione - da orderDate dal file shipmentOrder)
|
| 124 |
-
{'name': 'ritiro_mp_fixed', 'length': 1, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 8
|
| 125 |
-
{'name': 'codice_indirizzo_mittente_fixed', 'length': 20, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 9
|
| 126 |
-
{'name': 'ragione_sociale_mittente_fixed', 'length': 40, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 10
|
| 127 |
-
{'name': 'contatto_mittente_fixed', 'length': 20, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 11
|
| 128 |
-
{'name': 'tipo_via_mittente_fixed', 'length': 5, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 12
|
| 129 |
-
{'name': 'via_mittente_fixed', 'length': 30, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 13
|
| 130 |
-
{'name': 'numero_civico_mittente_fixed','length': 5, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 14
|
| 131 |
-
{'name': 'numero_telefono_mittente_fixed', 'length': 15, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 15
|
| 132 |
-
{'name': 'cap_mittente_fixed', 'length': 9, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 16
|
| 133 |
-
{'name': 'localita_mittente_fixed', 'length': 30, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 17
|
| 134 |
-
{'name': 'provincia_mittente_fixed', 'length': 2, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 18
|
| 135 |
-
{'name': 'filler_19_gap', 'length': 60, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': False}, # Gap tra 18 e 20
|
| 136 |
-
|
| 137 |
-
# Campi 20-30 (Destinatario e Colli)
|
| 138 |
-
{'name': 'ragione_sociale_destinatario','length': 40, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 20 (da file shipmentOrder)
|
| 139 |
-
{'name': 'consignee_contact', 'length': 20, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 21 (da file shipmentOrder)
|
| 140 |
-
{'name': 'consignee_street_type', 'length': 5, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 22 (da file shipmentOrder)
|
| 141 |
-
{'name': 'consignee_address', 'length': 30, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 23 (da file shipmentOrder)
|
| 142 |
-
{'name': 'consignee_house_number', 'length': 5, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 24 (da file shipmentOrder)
|
| 143 |
-
{'name': 'consignee_phone', 'length': 15, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 25 (da file shipmentOrder)
|
| 144 |
-
{'name': 'consignee_zip_code', 'length': 9, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 26 (da file shipmentOrder)
|
| 145 |
-
{'name': 'consignee_city', 'length': 30, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 27 (da file shipmentOrder)
|
| 146 |
-
{'name': 'consignee_province', 'length': 2, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 28 (da file shipmentOrder)
|
| 147 |
-
{'name': 'totalvolume', 'length': 3, 'numeric_pad_zero': True, 'decimals': 0, 'mandatory': True}, # 29 (Numero colli - da totalVolume da shippingMark)
|
| 148 |
-
{'name': 'totalweight', 'length': 7, 'numeric_pad_zero': True, 'decimals': 0, 'mandatory': True}, # 30 (Peso colli - da totalWeight da shippingMark)
|
| 149 |
-
|
| 150 |
-
# Campi 34, 42, 43, 49 (Servizio, Identificativo, Filiale, Tipo Cliente)
|
| 151 |
-
{'name': 'service_code_fixed', 'length': 3, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 34 (STD)
|
| 152 |
-
{'name': 'shippingmark', 'length': 25, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 42 (Identificativo collo - shippingMark)
|
| 153 |
-
{'name': 'branch_code_fixed', 'length': 2, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 43 (FIL05)
|
| 154 |
-
{'name': 'client_type_fixed', 'length': 4, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True}, # 49 (MGCS)
|
| 155 |
-
]
|
| 156 |
-
|
| 157 |
# Genera il contenuto del file .dat
|
| 158 |
dat_content = []
|
| 159 |
for index, row in df_merged.iterrows():
|
|
@@ -162,62 +169,63 @@ def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
|
| 162 |
for field_spec in dat_format_spec:
|
| 163 |
field_name = field_spec['name']
|
| 164 |
length = field_spec['length']
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
| 168 |
|
| 169 |
value_to_format = ''
|
| 170 |
|
| 171 |
-
if
|
| 172 |
value_to_format = fixed_and_default_values.get(field_name, '')
|
| 173 |
-
elif
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
if pd.notna(value_to_format):
|
| 177 |
-
# Tenta di convertire la data nel formato AAAAMMGG
|
| 178 |
try:
|
| 179 |
-
# Se è già
|
| 180 |
-
|
|
|
|
|
|
|
| 181 |
pass
|
| 182 |
-
else: #
|
| 183 |
value_to_format = pd.to_datetime(value_to_format).strftime("%Y%m%d")
|
| 184 |
except Exception:
|
| 185 |
value_to_format = '' # Fallback se la conversione fallisce
|
| 186 |
else:
|
| 187 |
value_to_format = '' # Se NaN, usa vuoto
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
value_to_format = datetime.now().strftime("%Y%m%d")
|
| 191 |
-
else:
|
| 192 |
-
value_to_format = '' # Fallback per altri derivati
|
| 193 |
-
elif field_name == 'filler_19_gap':
|
| 194 |
-
value_to_format = ' ' * length
|
| 195 |
-
else: # Campi da prendere direttamente dal DataFrame unito (file)
|
| 196 |
-
value_to_format = row.get(field_name)
|
| 197 |
-
# Gestisci NaN dopo la merge, se il campo dal file non è presente per quel record
|
| 198 |
-
if pd.isna(value_to_format):
|
| 199 |
-
value_to_format = ''
|
| 200 |
|
| 201 |
# Conversione in stringa e pulizia iniziale
|
| 202 |
value_str = str(value_to_format).strip()
|
| 203 |
|
| 204 |
-
#
|
| 205 |
-
if
|
| 206 |
-
|
|
|
|
|
|
|
|
|
|
| 207 |
value_str = '0' * length
|
| 208 |
else:
|
| 209 |
-
value_str = ' ' * length
|
| 210 |
|
| 211 |
# Formattazione finale (allineamento, padding)
|
| 212 |
-
if
|
| 213 |
try:
|
| 214 |
clean_value = value_str.replace(',', '.') # Sostituisce virgole con punti
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
else:
|
| 220 |
-
formatted_value = str(int(float(clean_value))).rjust(length, '0')
|
| 221 |
except ValueError:
|
| 222 |
formatted_value = '0'.rjust(length, '0') # Fallback se la conversione fallisce
|
| 223 |
else:
|
|
@@ -228,11 +236,12 @@ def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
|
| 228 |
|
| 229 |
dat_content.append("".join(record_parts))
|
| 230 |
|
| 231 |
-
# Genera il nome del file con timestamp
|
| 232 |
now = datetime.now()
|
| 233 |
filename_timestamp = now.strftime("%Y%m%d%H%M%S%f")[:-4] # AAAAMMGGhhmmsscc
|
| 234 |
-
|
| 235 |
-
|
|
|
|
| 236 |
|
| 237 |
# Usa la directory temporanea /tmp che è scrivibile su Hugging Face Spaces
|
| 238 |
temp_dir = "/tmp"
|
|
@@ -244,12 +253,10 @@ def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
|
| 244 |
with open(dat_filepath, 'w', encoding='utf-8') as f: # Specifica encoding utf-8
|
| 245 |
for line in dat_content:
|
| 246 |
f.write(line + '\n')
|
| 247 |
-
|
| 248 |
-
|
| 249 |
with open(dis_filepath, 'w') as f:
|
| 250 |
pass # Crea un file vuoto
|
| 251 |
-
|
| 252 |
-
|
| 253 |
return dat_filepath, dis_filepath
|
| 254 |
|
| 255 |
except pd.errors.EmptyDataError:
|
|
@@ -263,6 +270,110 @@ def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
|
| 263 |
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.")
|
| 264 |
return None, None
|
| 265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
# --- Sezione App Streamlit ---
|
| 267 |
def app():
|
| 268 |
st.set_page_config(layout="centered", page_title="Generatore File Poste .DAT")
|
|
@@ -289,55 +400,58 @@ def app():
|
|
| 289 |
df_shipping_mark = pd.read_excel(shipping_mark_file, header=1)
|
| 290 |
df_shipment_order = pd.read_excel(shipment_order_file, header=1)
|
| 291 |
|
| 292 |
-
st.write("Colonne lette da ShippingMark_:", df_shipping_mark.columns.tolist())
|
| 293 |
-
st.write("Colonne lette da ShipmentOrder_:", df_shipment_order.columns.tolist())
|
| 294 |
-
|
| 295 |
-
# DEBUGGING: Mostra il DataFrame ShipmentOrder_ dopo la lettura e il mapping
|
| 296 |
-
st.subheader("Anteprima del DataFrame ShipmentOrder_ (dopo la lettura e il mapping delle colonne):")
|
| 297 |
-
st.dataframe(df_shipment_order)
|
| 298 |
-
|
| 299 |
# Esegui la funzione di generazione
|
| 300 |
dat_filepath, dis_filepath = generate_poste_dat_file(df_shipping_mark, df_shipment_order)
|
| 301 |
|
| 302 |
-
# DEBUGGING: Per mostrare df_merged, la variabile deve essere accessibile qui.
|
| 303 |
-
# Questo richiede una piccola modifica nella funzione generate_poste_dat_file
|
| 304 |
-
# per restituire anche df_merged, oppure duplicare la logica di merge qui solo per il debug.
|
| 305 |
-
# Per semplicità, la lascio commentata qui a meno che non ci sia una richiesta specifica.
|
| 306 |
-
# Oppure puoi ispezionare df_shipment_order e le colonne mappate per vedere i dati.
|
| 307 |
-
|
| 308 |
-
|
| 309 |
except Exception as e:
|
| 310 |
-
st.error(f"Errore
|
| 311 |
st.warning("Assicurati che i file siano in formato .xlsx, non siano corrotti e che l'intestazione sia effettivamente alla seconda riga.")
|
| 312 |
|
| 313 |
if dat_filepath and dis_filepath:
|
| 314 |
-
st.subheader("
|
|
|
|
| 315 |
|
|
|
|
|
|
|
|
|
|
| 316 |
# Per consentire il download in Streamlit, dobbiamo leggere il contenuto del file
|
| 317 |
# in modalità binaria ('rb') e passarlo al download_button.
|
| 318 |
with open(dat_filepath, "rb") as f:
|
| 319 |
st.download_button(
|
| 320 |
-
label=f"Scarica {
|
| 321 |
data=f.read(),
|
| 322 |
-
file_name=
|
| 323 |
-
mime="text/plain" # Tipo MIME per file di testo generici
|
|
|
|
| 324 |
)
|
| 325 |
|
| 326 |
with open(dis_filepath, "rb") as f:
|
| 327 |
st.download_button(
|
| 328 |
-
label=f"Scarica {
|
| 329 |
data=f.read(),
|
| 330 |
-
file_name=
|
| 331 |
-
mime="text/plain"
|
|
|
|
| 332 |
)
|
| 333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
st.markdown("---")
|
| 335 |
st.info("""
|
| 336 |
-
**Nota
|
| 337 |
-
Assicurati che
|
| 338 |
-
- Il file `ShippingMark_` deve contenere le colonne 'shippingMark', 'orderRef', 'totalWeight' e 'totalVolume'.
|
| 339 |
-
- Il file `ShipmentOrder_` deve contenere 'orderRef', 'orderDate' e i campi specifici del destinatario (es. 'consegnee.partner.businessname', 'consegnee.partner.address.address', ecc.).
|
| 340 |
-
I nomi delle colonne nel tuo file `ShipmentOrder_` devono corrispondere **esattamente** (ignorando maiuscole/minuscole) ai nomi completi con la notazione a punti (es. 'consegnee.partner.address.address') per un corretto mapping.
|
| 341 |
""")
|
| 342 |
|
| 343 |
if __name__ == "__main__":
|
|
|
|
| 4 |
import streamlit as st
|
| 5 |
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 nell'immagine.
|
| 10 |
+
fixed_and_default_values = {
|
| 11 |
+
# Campi fissi Mittente (Campi 1-18) - Valori dalla tabella utente
|
| 12 |
+
'codice_identificativo_cliente': 'CLI 000000009', # Campo 1
|
| 13 |
+
'id_ldv': 'FIL01', # Campo 2
|
| 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': 'IND001', # Campo 9
|
| 21 |
+
'ragione_sociale_mittente': 'Poste Logistics + Technolt', # Campo 10
|
| 22 |
+
'contatto_mittente': 'postelogistics@poste.it', # Campo 11
|
| 23 |
+
'tipo_via_mittente': 'Viale', # Campo 12
|
| 24 |
+
'via_mittente': 'delle Industrie', # Campo 13
|
| 25 |
+
'numero_civico_mittente': '9-11-13', # Campo 14
|
| 26 |
+
'numero_telefono_mittente': '60898001', # Campo 15
|
| 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 |
+
'codice_servizio': 'EXT', # Campo 34
|
| 33 |
+
'filiale': 'MD', # Campo 43
|
| 34 |
+
'tipo_cliente': 'MGCS', # Campo 49
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
# Definizione dettagliata del layout del file .dat.
|
| 38 |
+
# INCLUDE SOLO I CAMPI DALL'ULTIMO TRACCIATO FORNITO DALL'UTENTE.
|
| 39 |
+
# 'name': Nome interno per il campo nel codice.
|
| 40 |
+
# 'length': Lunghezza fissa del campo.
|
| 41 |
+
# 'numeric_pad_zero': True se il campo è numerico e va riempito con '0' a sinistra.
|
| 42 |
+
# 'decimals': Numero di decimali (per campi numerici di valuta, non usati nel nuovo tracciato).
|
| 43 |
+
# 'mandatory': True se il campo è obbligatorio (*) o (**) nel tracciato.
|
| 44 |
+
# 'source': Indica la fonte del valore: 'fixed' (da fixed_and_default_values),
|
| 45 |
+
# 'shipping_mark_file' (dal DataFrame shippingMark),
|
| 46 |
+
# 'shipment_order_file' (dal DataFrame shipmentOrder),
|
| 47 |
+
# 'derived' (calcolato, es. data corrente o campi concatenati),
|
| 48 |
+
# 'filler' (gap nel tracciato).
|
| 49 |
+
# 'excel_col': Il nome esatto della colonna nel file Excel di origine (se 'source' è 'shipping_mark_file' o 'shipment_order_file').
|
| 50 |
+
# 'description': Descrizione del campo (per il file di specifiche).
|
| 51 |
+
# 'start_pos': Posizione iniziale del campo (per il file di specifiche).
|
| 52 |
+
dat_format_spec = [
|
| 53 |
+
# Id | Nome | Obbl | Tipo | Lungh | Inizio | Descrizione | Default | Campi
|
| 54 |
+
{'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},
|
| 55 |
+
{'id': '2', 'name': 'id_ldv', 'length': 30, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'fixed', 'excel_col': None, 'description': "Fillare con la numerazione della ldv", 'start_pos': 13},
|
| 56 |
+
{'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},
|
| 57 |
+
{'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},
|
| 58 |
+
{'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},
|
| 59 |
+
{'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},
|
| 60 |
+
{'id': '7', 'name': 'data_spedizione', 'length': 8, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipment_order_file', 'excel_col': 'orderdate', 'description': "Data della spedizione in formato AAAAMMGG (Mese, Giorno)", 'start_pos': 71},
|
| 61 |
+
{'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},
|
| 62 |
+
{'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},
|
| 63 |
+
{'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},
|
| 64 |
+
{'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},
|
| 65 |
+
{'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},
|
| 66 |
+
{'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},
|
| 67 |
+
{'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},
|
| 68 |
+
{'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},
|
| 69 |
+
{'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},
|
| 70 |
+
{'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},
|
| 71 |
+
{'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},
|
| 72 |
+
|
| 73 |
+
# Filler per il gap tra Campo 18 (fine a 255) e Campo 20 (inizio a 315)
|
| 74 |
+
{'id': '', 'name': 'filler_gap_18_20', 'length': 60, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': False, 'source': 'filler', 'excel_col': None, 'description': "Gap tra i campi 18 e 20", 'start_pos': 256}, # 315 - 255 = 60
|
| 75 |
+
|
| 76 |
+
# Campi Destinatario e Colli
|
| 77 |
+
{'id': '20', 'name': 'ragione_sociale_destinatario', 'length': 40, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipment_order_file', 'excel_col': 'consegnee.partner.businessname', 'description': "Ragione sociale del destinatario", 'start_pos': 315},
|
| 78 |
+
{'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': 355},
|
| 79 |
+
{'id': '22', 'name': 'tipo_via_destinatario', 'length': 5, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipment_order_file', 'excel_col': 'consegnee.partner.address.streettype', 'description': "Descrizione tipo via", 'start_pos': 375},
|
| 80 |
+
{'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': 380},
|
| 81 |
+
{'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': 410},
|
| 82 |
+
{'id': '25', 'name': 'numero_telefono_destinatario','length': 15, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipment_order_file', 'excel_col': 'consegnee.partner.phone.number', 'description': "Numero di telefono Destinatario", 'start_pos': 415},
|
| 83 |
+
{'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': 430},
|
| 84 |
+
{'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': 439},
|
| 85 |
+
{'id': '28', 'name': 'provincia_destinatario', 'length': 2, 'numeric_pad_zero': False, 'decimals': 0, 'mandatory': True, 'source': 'shipment_order_file', 'excel_col': 'consegnee.partner.address.country', 'description': "Provincia del destinatario", 'start_pos': 469}, # Assumo 'country' è provincia 2 char
|
| 86 |
+
{'id': '29', 'name': 'numero_colli', 'length': 3, 'numeric_pad_zero': True, 'decimals': 0, 'mandatory': True, 'source': 'shipping_mark_file', 'excel_col': 'totalvolume', 'description': "Numero dei colli totali che compongono la spedizione", 'start_pos': 471},
|
| 87 |
+
{'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': 474},
|
| 88 |
+
|
| 89 |
+
# Altri Campi Obbligatori dal tracciato
|
| 90 |
+
{'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': 481},
|
| 91 |
+
{'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': 484},
|
| 92 |
+
{'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': 509},
|
| 93 |
+
{'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': 511},
|
| 94 |
+
]
|
| 95 |
+
|
| 96 |
+
# --- Funzione per generare il file DAT e DIS ---
|
| 97 |
def generate_poste_dat_file(df_shipping_mark_input, df_shipment_order_input):
|
| 98 |
"""
|
| 99 |
Genera un file .dat per Poste Italiane unendo i dati da due DataFrame.
|
|
|
|
| 113 |
df_shipment_order = df_shipment_order_input.copy()
|
| 114 |
|
| 115 |
# Assicurati che i nomi delle colonne siano in minuscolo per una maggiore robustezza
|
| 116 |
+
# Nota: per nomi con punti come 'consegnee.partner.address.address', .lower() li mantiene così.
|
| 117 |
df_shipping_mark.columns = df_shipping_mark.columns.str.lower()
|
| 118 |
df_shipment_order.columns = df_shipment_order.columns.str.lower()
|
| 119 |
|
| 120 |
# Verifica la presenza delle colonne necessarie nel file shipping mark
|
|
|
|
| 121 |
required_shipping_mark_cols = ['shippingmark', 'orderref', 'totalweight', 'totalvolume']
|
| 122 |
if not all(col in df_shipping_mark.columns for col in required_shipping_mark_cols):
|
| 123 |
+
st.error(f"Errore: Il file delle shipping mark deve contenere le colonne: {required_shipping_mark_cols}")
|
| 124 |
return None, None
|
| 125 |
|
| 126 |
+
# Applica il mapping delle colonne a df_shipment_order.
|
| 127 |
+
# Le chiavi del mapping DEVONO corrispondere esattamente ai nomi delle colonne (in minuscolo)
|
| 128 |
+
# nel tuo file Excel.
|
| 129 |
+
# Esempio: se la colonna in Excel è "consegnee.partner.businessName", la chiave qui deve essere "consegnee.partner.businessname".
|
| 130 |
+
excel_col_mapping = {
|
|
|
|
|
|
|
| 131 |
'orderdate': 'order_date', # Per Campo 7
|
| 132 |
'consegnee.partner.businessname': 'ragione_sociale_destinatario', # Campo 20
|
| 133 |
+
'consegnee.partner.name': 'contatto_destinatario', # Campo 21
|
| 134 |
+
'consegnee.partner.address.streettype': 'tipo_via_destinatario', # Campo 22
|
| 135 |
+
'consegnee.partner.address.address': 'via_destinatario', # Campo 23
|
| 136 |
+
'consegnee.partner.address.addressnumber': 'numero_civico_destinatario', # Campo 24
|
| 137 |
+
'consegnee.partner.phone.number': 'numero_telefono_destinatario', # Campo 25
|
| 138 |
+
'consegnee.partner.address.zipcode': 'cap_destinatario', # Campo 26
|
| 139 |
+
'consegnee.partner.address.city': 'localita_destinatario', # Campo 27
|
| 140 |
+
'consegnee.partner.address.country': 'provincia_destinatario', # Campo 28 (assunto essere codice provincia 2 char)
|
|
|
|
| 141 |
}
|
| 142 |
+
df_shipment_order = df_shipment_order.rename(columns=lambda x: excel_col_mapping.get(x, x))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
# Assicurati che 'orderref' e 'order_date' siano presenti in df_shipment_order dopo il rename
|
| 145 |
required_shipment_order_cols = ['orderref', 'order_date']
|
| 146 |
if not all(col in df_shipment_order.columns for col in required_shipment_order_cols):
|
| 147 |
+
st.error(f"Errore: Il file degli ordini deve contenere le colonne: {required_shipment_order_cols} (anche dopo il mapping).")
|
| 148 |
return None, None
|
| 149 |
|
| 150 |
# Pulisci le colonne di join per una corrispondenza robusta (rimuovi spazi e assicurati che siano stringhe)
|
|
|
|
| 161 |
st.warning("Nessun record corrispondente trovato dopo la join. Assicurati che 'orderRef' sia presente e corrisponda in entrambi i file.")
|
| 162 |
return None, None
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
# Genera il contenuto del file .dat
|
| 165 |
dat_content = []
|
| 166 |
for index, row in df_merged.iterrows():
|
|
|
|
| 169 |
for field_spec in dat_format_spec:
|
| 170 |
field_name = field_spec['name']
|
| 171 |
length = field_spec['length']
|
| 172 |
+
is_numeric_pad_zero = field_spec['numeric_pad_zero']
|
| 173 |
+
decimals = field_spec['decimals']
|
| 174 |
+
mandatory = field_spec['mandatory']
|
| 175 |
+
source_type = field_spec['source']
|
| 176 |
+
excel_col_name = field_spec['excel_col'] # Nome della colonna in Excel (mappata)
|
| 177 |
|
| 178 |
value_to_format = ''
|
| 179 |
|
| 180 |
+
if source_type == 'fixed':
|
| 181 |
value_to_format = fixed_and_default_values.get(field_name, '')
|
| 182 |
+
elif source_type == 'shipping_mark_file':
|
| 183 |
+
value_to_format = row.get(excel_col_name)
|
| 184 |
+
if pd.isna(value_to_format):
|
| 185 |
+
value_to_format = ''
|
| 186 |
+
elif source_type == 'shipment_order_file':
|
| 187 |
+
value_to_format = row.get(excel_col_name)
|
| 188 |
+
if pd.isna(value_to_format):
|
| 189 |
+
value_to_format = ''
|
| 190 |
+
# Gestione specifica per 'order_date' (Campo 7) per formattazione AAAAMMGG
|
| 191 |
+
if field_name == 'data_spedizione':
|
| 192 |
if pd.notna(value_to_format):
|
|
|
|
| 193 |
try:
|
| 194 |
+
if isinstance(value_to_format, datetime): # Se è già un oggetto datetime
|
| 195 |
+
value_to_format = value_to_format.strftime("%Y%m%d")
|
| 196 |
+
elif isinstance(value_to_format, str) and len(value_to_format) == 8 and value_to_format.isdigit():
|
| 197 |
+
# Se è già una stringa nel formato giusto, usala così com'è
|
| 198 |
pass
|
| 199 |
+
else: # Prova a convertirla
|
| 200 |
value_to_format = pd.to_datetime(value_to_format).strftime("%Y%m%d")
|
| 201 |
except Exception:
|
| 202 |
value_to_format = '' # Fallback se la conversione fallisce
|
| 203 |
else:
|
| 204 |
value_to_format = '' # Se NaN, usa vuoto
|
| 205 |
+
elif source_type == 'filler':
|
| 206 |
+
value_to_format = ' ' * length # Riempie il filler con spazi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
# Conversione in stringa e pulizia iniziale
|
| 209 |
value_str = str(value_to_format).strip()
|
| 210 |
|
| 211 |
+
# Gestione specifica per Campo 25 (Numero di telefono Destinatario) "se vuoto mettere tanti zeri"
|
| 212 |
+
if field_name == 'numero_telefono_destinatario' and mandatory and not value_str:
|
| 213 |
+
value_str = '0' * length
|
| 214 |
+
# Se il campo è obbligatorio e il valore è ancora vuoto dopo aver cercato/gestito casi speciali, usa il default (spazi o zeri)
|
| 215 |
+
elif mandatory and not value_str:
|
| 216 |
+
if is_numeric_pad_zero:
|
| 217 |
value_str = '0' * length
|
| 218 |
else:
|
| 219 |
+
value_str = ' ' * length
|
| 220 |
|
| 221 |
# Formattazione finale (allineamento, padding)
|
| 222 |
+
if is_numeric_pad_zero:
|
| 223 |
try:
|
| 224 |
clean_value = value_str.replace(',', '.') # Sostituisce virgole con punti
|
| 225 |
+
# Converti in int dopo aver gestito i decimali (se presenti, altrimenti 0)
|
| 226 |
+
# Nota: nel nuovo tracciato non ci sono campi con 'decimals' > 0, ma la logica è robusta.
|
| 227 |
+
num_val = float(clean_value) * (10**decimals)
|
| 228 |
+
formatted_value = str(int(round(num_val))).rjust(length, '0')
|
|
|
|
|
|
|
| 229 |
except ValueError:
|
| 230 |
formatted_value = '0'.rjust(length, '0') # Fallback se la conversione fallisce
|
| 231 |
else:
|
|
|
|
| 236 |
|
| 237 |
dat_content.append("".join(record_parts))
|
| 238 |
|
| 239 |
+
# Genera il nome del file con timestamp (uguale per .dat e .dis)
|
| 240 |
now = datetime.now()
|
| 241 |
filename_timestamp = now.strftime("%Y%m%d%H%M%S%f")[:-4] # AAAAMMGGhhmmsscc
|
| 242 |
+
base_filename = f"SDA{filename_timestamp}"
|
| 243 |
+
dat_filename = f"{base_filename}.dat"
|
| 244 |
+
dis_filename = f"{base_filename}.dis"
|
| 245 |
|
| 246 |
# Usa la directory temporanea /tmp che è scrivibile su Hugging Face Spaces
|
| 247 |
temp_dir = "/tmp"
|
|
|
|
| 253 |
with open(dat_filepath, 'w', encoding='utf-8') as f: # Specifica encoding utf-8
|
| 254 |
for line in dat_content:
|
| 255 |
f.write(line + '\n')
|
| 256 |
+
|
|
|
|
| 257 |
with open(dis_filepath, 'w') as f:
|
| 258 |
pass # Crea un file vuoto
|
| 259 |
+
|
|
|
|
| 260 |
return dat_filepath, dis_filepath
|
| 261 |
|
| 262 |
except pd.errors.EmptyDataError:
|
|
|
|
| 270 |
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.")
|
| 271 |
return None, None
|
| 272 |
|
| 273 |
+
# --- Funzione per generare il testo delle specifiche del tracciato DAT ---
|
| 274 |
+
def generate_dat_format_spec_text():
|
| 275 |
+
spec_text = "Specifiche del Tracciato File .DAT\n\n"
|
| 276 |
+
spec_text += "Id\tNome Campo\tObbligatorio\tTipo\tLunghezza\tInizio\tDescrizione\tDefault / Fonte Dati\n"
|
| 277 |
+
spec_text += "--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n"
|
| 278 |
+
|
| 279 |
+
for field_spec in dat_format_spec:
|
| 280 |
+
field_id = field_spec.get('id', '')
|
| 281 |
+
display_name = field_spec['name'].replace('_', ' ').replace('fixed', '').replace('derived', '').strip().title()
|
| 282 |
+
|
| 283 |
+
# Gestione speciale per nomi lunghi per migliorare la leggibilità
|
| 284 |
+
if display_name == 'Numero Riferimento Spedizione':
|
| 285 |
+
display_name = 'Num. Rif. Spedizione'
|
| 286 |
+
elif display_name == 'Numero Civico Destinatario':
|
| 287 |
+
display_name = 'Num. Civico Dest.'
|
| 288 |
+
elif display_name == 'Numero Telefono Destinatario':
|
| 289 |
+
display_name = 'Tel. Dest.'
|
| 290 |
+
elif display_name == 'Numero Telefono Mittente':
|
| 291 |
+
display_name = 'Tel. Mittente'
|
| 292 |
+
elif display_name == 'Codice Identificativo Cliente':
|
| 293 |
+
display_name = 'Cod. Ident. Cliente'
|
| 294 |
+
elif display_name == 'Codice Indirizzo Mittente':
|
| 295 |
+
display_name = 'Cod. Ind. Mittente'
|
| 296 |
+
elif display_name == 'Ragione Sociale Mittente':
|
| 297 |
+
display_name = 'Rag. Soc. Mittente'
|
| 298 |
+
elif display_name == 'Contatto Mittente':
|
| 299 |
+
display_name = 'Contatto Mittente'
|
| 300 |
+
elif display_name == 'Tipo Via Mittente':
|
| 301 |
+
display_name = 'Tipo Via Mittente'
|
| 302 |
+
elif display_name == 'Via Mittente':
|
| 303 |
+
display_name = 'Via Mittente'
|
| 304 |
+
elif display_name == 'Numero Civico Mittente':
|
| 305 |
+
display_name = 'Num. Civico Mitt.'
|
| 306 |
+
elif display_name == 'Cap Mittente':
|
| 307 |
+
display_name = 'CAP Mittente'
|
| 308 |
+
elif display_name == 'Localita Mittente':
|
| 309 |
+
display_name = 'Località Mittente'
|
| 310 |
+
elif display_name == 'Provincia Mittente':
|
| 311 |
+
display_name = 'Prov. Mittente'
|
| 312 |
+
elif display_name == 'Ragione Sociale Destinatario':
|
| 313 |
+
display_name = 'Rag. Soc. Destinatario'
|
| 314 |
+
elif display_name == 'Contatto Destinatario':
|
| 315 |
+
display_name = 'Contatto Destinatario'
|
| 316 |
+
elif display_name == 'Tipo Via Destinatario':
|
| 317 |
+
display_name = 'Tipo Via Destinatario'
|
| 318 |
+
elif display_name == 'Via Destinatario':
|
| 319 |
+
display_name = 'Via Destinatario'
|
| 320 |
+
elif display_name == 'Cap Destinatario':
|
| 321 |
+
display_name = 'CAP Destinatario'
|
| 322 |
+
elif display_name == 'Localita Destinatario':
|
| 323 |
+
display_name = 'Località Destinatario'
|
| 324 |
+
elif display_name == 'Provincia Destinatario':
|
| 325 |
+
display_name = 'Prov. Destinatario'
|
| 326 |
+
elif display_name == 'Numero Colli':
|
| 327 |
+
display_name = 'Num. Colli'
|
| 328 |
+
elif display_name == 'Peso Colli':
|
| 329 |
+
display_name = 'Peso Colli'
|
| 330 |
+
elif display_name == 'Codice Servizio':
|
| 331 |
+
display_name = 'Cod. Servizio'
|
| 332 |
+
elif display_name == 'Identificativo Collo':
|
| 333 |
+
display_name = 'ID Collo'
|
| 334 |
+
elif display_name == 'Filiale':
|
| 335 |
+
display_name = 'Filiale'
|
| 336 |
+
elif display_name == 'Tipo Cliente':
|
| 337 |
+
display_name = 'Tipo Cliente'
|
| 338 |
+
elif display_name == 'Id Ldv':
|
| 339 |
+
display_name = 'ID LdV'
|
| 340 |
+
elif display_name == 'Data Spedizione':
|
| 341 |
+
display_name = 'Data Spedizione'
|
| 342 |
+
elif display_name == 'Ritiro M/P':
|
| 343 |
+
display_name = 'Ritiro M/P'
|
| 344 |
+
elif display_name == 'Filler Gap 18 20':
|
| 345 |
+
display_name = 'Gap'
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
obligatory = "*" if field_spec['mandatory'] else ""
|
| 349 |
+
field_type = "char" # Tutte sono char
|
| 350 |
+
length = field_spec['length']
|
| 351 |
+
start_pos = field_spec['start_pos']
|
| 352 |
+
description = field_spec['description']
|
| 353 |
+
|
| 354 |
+
source_info = ""
|
| 355 |
+
source_type = field_spec['source']
|
| 356 |
+
if source_type == 'fixed':
|
| 357 |
+
source_info = fixed_and_default_values.get(field_spec['name'], 'N/A')
|
| 358 |
+
elif source_type == 'shipping_mark_file':
|
| 359 |
+
source_info = f"Da ShippingMark_.xlsx: '{field_spec['excel_col']}'"
|
| 360 |
+
elif source_type == 'shipment_order_file':
|
| 361 |
+
source_info = f"Da ShipmentOrder_.xlsx: '{field_spec['excel_col']}'"
|
| 362 |
+
if field_spec['name'] == 'data_spedizione':
|
| 363 |
+
source_info += " (formato AAAAMMGG)"
|
| 364 |
+
elif field_spec['name'] == 'numero_telefono_destinatario':
|
| 365 |
+
source_info += " (riempi con '0' se vuoto)"
|
| 366 |
+
elif source_type == 'filler':
|
| 367 |
+
source_info = "Spazi vuoti (Gap)"
|
| 368 |
+
|
| 369 |
+
# Aggiusta la colonna Descrizione per non essere troppo larga nella tabella di testo
|
| 370 |
+
if len(description) > 50:
|
| 371 |
+
description = description[:47] + "..."
|
| 372 |
+
|
| 373 |
+
spec_text += f"{field_id}\t{display_name:<25}\t{obligatory:<12}\t{field_type:<4}\t{length:<9}\t{start_pos:<7}\t{description:<50}\t{source_info}\n"
|
| 374 |
+
|
| 375 |
+
return spec_text
|
| 376 |
+
|
| 377 |
# --- Sezione App Streamlit ---
|
| 378 |
def app():
|
| 379 |
st.set_page_config(layout="centered", page_title="Generatore File Poste .DAT")
|
|
|
|
| 400 |
df_shipping_mark = pd.read_excel(shipping_mark_file, header=1)
|
| 401 |
df_shipment_order = pd.read_excel(shipment_order_file, header=1)
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
# Esegui la funzione di generazione
|
| 404 |
dat_filepath, dis_filepath = generate_poste_dat_file(df_shipping_mark, df_shipment_order)
|
| 405 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
except Exception as e:
|
| 407 |
+
st.error(f"Errore: {e}")
|
| 408 |
st.warning("Assicurati che i file siano in formato .xlsx, non siano corrotti e che l'intestazione sia effettivamente alla seconda riga.")
|
| 409 |
|
| 410 |
if dat_filepath and dis_filepath:
|
| 411 |
+
st.subheader("File generati:")
|
| 412 |
+
st.success("I file .DAT e .DIS sono stati generati con successo e sono pronti per il download.")
|
| 413 |
|
| 414 |
+
# Ottieni il nome base del file (es. SDA20240101123456789)
|
| 415 |
+
base_filename = os.path.basename(dat_filepath).replace('.dat', '')
|
| 416 |
+
|
| 417 |
# Per consentire il download in Streamlit, dobbiamo leggere il contenuto del file
|
| 418 |
# in modalità binaria ('rb') e passarlo al download_button.
|
| 419 |
with open(dat_filepath, "rb") as f:
|
| 420 |
st.download_button(
|
| 421 |
+
label=f"Scarica {base_filename}.dat",
|
| 422 |
data=f.read(),
|
| 423 |
+
file_name=f"{base_filename}.dat",
|
| 424 |
+
mime="text/plain", # Tipo MIME per file di testo generici
|
| 425 |
+
key="download_dat_btn"
|
| 426 |
)
|
| 427 |
|
| 428 |
with open(dis_filepath, "rb") as f:
|
| 429 |
st.download_button(
|
| 430 |
+
label=f"Scarica {base_filename}.dis",
|
| 431 |
data=f.read(),
|
| 432 |
+
file_name=f"{base_filename}.dis",
|
| 433 |
+
mime="text/plain",
|
| 434 |
+
key="download_dis_btn"
|
| 435 |
)
|
| 436 |
|
| 437 |
+
st.markdown("---")
|
| 438 |
+
st.subheader("Specifiche del Tracciato .DAT")
|
| 439 |
+
st.markdown("Qui puoi scaricare un file di testo con la descrizione dettagliata del formato del file .DAT generato:")
|
| 440 |
+
|
| 441 |
+
spec_text_content = generate_dat_format_spec_text()
|
| 442 |
+
|
| 443 |
+
st.download_button(
|
| 444 |
+
label="Scarica Specifiche Tracciato DAT",
|
| 445 |
+
data=spec_text_content.encode('utf-8'),
|
| 446 |
+
file_name="DAT_Format_Specification.txt",
|
| 447 |
+
mime="text/plain",
|
| 448 |
+
key="download_spec_btn"
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
st.markdown("---")
|
| 452 |
st.info("""
|
| 453 |
+
**Nota importante sui nomi delle colonne nei tuoi file Excel:**
|
| 454 |
+
Assicurati che le colonne nel tuo file `ShipmentOrder_` corrispondano **esattamente** ai nomi con la notazione a punti (es. 'consegnee.partner.businessname', 'consegnee.partner.address.address', 'orderdate') una volta letti da Excel con l'intestazione alla seconda riga.
|
|
|
|
|
|
|
|
|
|
| 455 |
""")
|
| 456 |
|
| 457 |
if __name__ == "__main__":
|