File size: 19,169 Bytes
db2eddf
7da7801
 
 
 
 
15536fc
7da7801
 
15536fc
 
 
 
7da7801
 
 
 
 
 
fb1f8a9
7da7801
 
 
fb1f8a9
7da7801
 
fb1f8a9
7da7801
 
 
fb1f8a9
7da7801
 
 
 
 
 
fb1f8a9
7da7801
 
 
15536fc
7da7801
 
15536fc
 
 
 
 
 
7da7801
 
 
 
 
 
15536fc
7da7801
 
 
15536fc
 
 
7da7801
15536fc
7da7801
15536fc
 
7da7801
15536fc
 
7da7801
15536fc
 
7da7801
 
15536fc
 
 
7da7801
 
 
 
 
 
15536fc
 
 
 
 
 
 
 
7da7801
15536fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7da7801
 
 
 
 
 
 
 
15536fc
7da7801
 
 
15536fc
 
7da7801
 
 
 
15536fc
7da7801
15536fc
7da7801
15536fc
7da7801
 
15536fc
 
7da7801
 
 
 
 
 
 
 
 
fb1f8a9
 
7da7801
 
 
 
fb1f8a9
 
7da7801
 
15536fc
 
 
 
 
 
 
 
 
 
 
 
 
7da7801
15536fc
 
7da7801
 
15536fc
7da7801
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15536fc
 
 
 
 
 
 
 
 
7da7801
15536fc
 
 
 
 
 
 
 
 
 
 
 
 
 
7da7801
15536fc
 
7da7801
 
 
15536fc
7da7801
 
 
 
15536fc
 
7da7801
 
 
15536fc
7da7801
 
 
 
 
 
 
 
 
 
 
15536fc
 
7da7801
fb1f8a9
7da7801
fb1f8a9
7da7801
 
15536fc
7da7801
 
 
 
 
 
 
 
15536fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7da7801
 
 
fb1f8a9
7da7801
 
15536fc
 
 
 
 
7da7801
 
 
15536fc
 
 
7da7801
 
 
 
 
 
 
 
 
15536fc
 
 
 
 
7da7801
15536fc
7da7801
 
15536fc
 
 
 
7da7801
 
 
 
 
 
 
 
 
15536fc
 
 
 
7da7801
15536fc
 
 
7da7801
 
 
fb1f8a9
15536fc
 
 
 
 
 
 
7da7801
 
 
 
 
15536fc
7da7801
 
 
 
 
 
 
15536fc
 
7da7801
 
 
15536fc
7da7801
15536fc
7da7801
15536fc
 
7da7801
 
 
db2eddf
7da7801
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
import streamlit as st
import pandas as pd
import json
from datetime import datetime
import io
import os
import pytz # Assicurati che pytz sia nel requirements.txt

# --- Inizializzazione Session State ---
if 'file_uploader_key_json' not in st.session_state:
    st.session_state.file_uploader_key_json = 0
if 'file_uploader_key_customers' not in st.session_state:
    st.session_state.file_uploader_key_customers = 0

# --- CSS personalizzato ---
st.markdown(
    """
    <style>
    .stApp {
        background-color: #FFFFCC; /* Giallo pastello */
        background-attachment: fixed;
    }
    h1, h2 {
        color: #1E90FF; /* Blu acceso */
    }
    p, label {
        color: #000080; /* Blu scuro */
    }
    .stButton>button {
        color: white;
        background-color: #1E90FF; /* Blu acceso */
        border: none;
        border-radius: 5px;
        padding: 10px 20px;
        font-size: 16px;
    }
    .stButton>button:hover {
        background-color: #4169E1; /* Blu medio al passaggio del mouse */
    }
    </style>
    """,
    unsafe_allow_html=True
)

# Definizione del fuso orario per l'Italia (per coerenza, anche se le date Outbound sembrano solo data)
ITALY_TIMEZONE = pytz.timezone("Europe/Rome")
UTC_TIMEZONE = pytz.utc

# Funzione di conversione per Outbound
def convert_json_to_excel_data_outbound(json_file_content: str, customers_df: pd.DataFrame = None):
    try:
        json_data = json.loads(json_file_content)

        # Campi a livello radice
        site_code_json = json_data.get('siteCode')
        company_code_json = json_data.get('companyCode')

        # Campi sotto 'outbound'
        outbound_info = json_data.get('outbound', {})
        
        order_number = outbound_info.get('orderNumber')
        order_type_json = outbound_info.get('orderType')
        delivery_type_fixed = "DELIVERY" # Valore fisso

        order_doc_date_str = outbound_info.get('orderDocDate')
        order_date_detail = outbound_info.get('orderDateDetail')
        due_date_str = outbound_info.get('orderDate') # Corrisponde a 'dueDate'

        reason_code = outbound_info.get('reasonCode')
        service_fixed = "PDB" # Valore fisso
        service_type_fixed = "Standard" # Valore fisso

        payment_info_value = outbound_info.get('paymentInfo', {}).get('value')
        
        delivery_customer = outbound_info.get('deliveryCustomer', {})
        customer_code = delivery_customer.get('code')
        customer_mail = delivery_customer.get('refMail') # customer.mail dal JSON

        # Consignee data (from JSON)
        consignee_name = delivery_customer.get('name')
        consignee_address = delivery_customer.get('address')
        consignee_city = delivery_customer.get('city')
        consignee_province = delivery_customer.get('province')
        consignee_zip_code = delivery_customer.get('zipCode')
        consignee_country = delivery_customer.get('country')

        # Customer data (from Excel lookup)
        customer_name = ""
        customer_address = ""
        customer_city = ""
        customer_province = ""
        customer_zip_code = ""
        customer_country = ""
        
        # Esegui la lookup solo se customers_df è fornito e non vuoto
        if customers_df is not None and not customers_df.empty and customer_code and company_code_json:
            # Assicurati che le colonne di join siano dello stesso tipo
            # customers_df['Code'] = customers_df['Code'].astype(str)
            # customers_df['company.code'] = customers_df['company.code'].astype(str)
            
            # Filtra per Code e company.code
            customer_data = customers_df[
                (customers_df['Code'].astype(str) == str(customer_code)) &
                (customers_df['company.code'].astype(str) == str(company_code_json))
            ]

            if not customer_data.empty:
                # Prendiamo la prima riga se ce ne sono più di una
                customer_row = customer_data.iloc[0]
                customer_name = customer_row.get('partner.name', '')
                customer_address = customer_row.get('partner.address', '')
                customer_city = customer_row.get('partner.address.city', '')
                customer_province = customer_row.get('partner.address.province', '')
                customer_zip_code = customer_row.get('partner.address.zipCode', '')
                customer_country = customer_row.get('partner.address.country', '')
            else:
                st.warning(f"Attenzione: Dettagli cliente non trovati nell'Excel per Code: '{customer_code}' e Company: '{company_code_json}'.")

        # Note fields
        note_ddt = outbound_info.get('noteDdt')
        note_warehouse = outbound_info.get('noteWarehouse')
        note_delivery = outbound_info.get('noteDelivery')
        note_shipment = outbound_info.get('noteShipment')
        reason_code_shp = outbound_info.get('reasonCodeShp')
        info_delivery = outbound_info.get('infoDelivery')
        booking_info = outbound_info.get('bookingInfo')

        # Formattazione delle date (Outbound sembra avere solo date, senza ora)
        order_doc_date_formatted = None
        if order_doc_date_str:
            try:
                # Rimuovi 'Z' se presente e converti a datetime senza info fuso orario, poi formatta
                dt_object = datetime.fromisoformat(order_doc_date_str.replace('Z', ''))
                order_doc_date_formatted = dt_object.strftime('%d/%m/%Y')
            except ValueError:
                order_doc_date_formatted = order_doc_date_str
                st.warning(f"Attenzione: Impossibile parsare 'orderDocDate' '{order_doc_date_str}'. Verrà usata la stringa originale.")
        
        due_date_formatted = None
        if due_date_str: # Questo è il campo 'orderDate' del JSON che diventa 'dueDate'
            try:
                dt_object = datetime.fromisoformat(due_date_str.replace('Z', ''))
                due_date_formatted = dt_object.strftime('%d/%m/%Y')
            except ValueError:
                due_date_formatted = due_date_str
                st.warning(f"Attenzione: Impossibile parsare 'orderDate' (dueDate) '{due_date_str}'. Verrà usata la stringa originale.")

        details = outbound_info.get('details', [])

        processed_data_for_this_file = []
        for item in details:
            row = {
                'siteCode': site_code_json,
                'companyCode': company_code_json,
                'orderNumber': order_number,
                'orderType': order_type_json, 
                'deliveryType': delivery_type_fixed, 
                'orderDocDate': order_doc_date_formatted,
                'orderDateDetail': order_date_detail,
                'dueDate': due_date_formatted,
                'reasonCode': reason_code,
                'service': service_fixed, 
                'serviceType': service_type_fixed, 
                'paymentInfo.value': payment_info_value,
                'customer.code': customer_code,
                'customer.name': customer_name, # Da Excel
                'customer.address': customer_address, # Da Excel
                'customer.city': customer_city, # Da Excel
                'customer.province': customer_province, # Da Excel
                'customer.zipCode': customer_zip_code, # Da Excel
                'customer.country': customer_country, # Da Excel
                'consignee.name': consignee_name, # Dal JSON
                'consignee.address': consignee_address, # Dal JSON
                'consignee.city': consignee_city, # Dal JSON
                'consignee.province': consignee_province, # Dal JSON
                'consignee.zipCode': consignee_zip_code, # Dal JSON
                'consignee.country': consignee_country, # Dal JSON
                'customer.mail': customer_mail, # Dal JSON
                'details.positionId': item.get('positionId'),
                'details.itemNumber': item.get('itemCode'), # Rinominato da itemCode
                'details.requestedQt': item.get('quantity'), # Rinominato da quantity
                'details.lotId': item.get('lotId'),
                'details.stockOwner': item.get('stockOwner'),
                'details.magCli': item.get('warehouse'), # Rinominato da warehouse
                'details.availability': item.get('availability'),
                'details.accountingStatus': item.get('accountingStatus'),
                'details.refOrder': item.get('refOrder'),
                'noteDdt': note_ddt,
                'noteWarehouse': note_warehouse,
                'noteDelivery': note_delivery,
                'noteShipment': note_shipment,
                'reasonCodeShp': reason_code_shp,
                'infoDelivery': info_delivery,
                'bookingInfo': booking_info
            }
            processed_data_for_this_file.append(row)

        if not processed_data_for_this_file:
            return pd.DataFrame() 

        return pd.DataFrame(processed_data_for_this_file)

    except json.JSONDecodeError as e:
        st.error(f"Errore nel file JSON: {e}. Assicurati che il contenuto sia un JSON valido.")
        return None
    except KeyError as ke:
        st.error(f"Errore: Chiave JSON mancante o struttura inattesa: {ke}. Assicurati che il JSON abbia la struttura attesa.")
        return None
    except Exception as e:
        st.error(f"Si è verificato un errore generico durante la conversione di un file JSON: {e}")
        return None

# --- Interfaccia Streamlit ---
st.title("Convertitore JSON Outbound a Template Excel Outbound")
st.write("Carica i tuoi file JSON degli ordini Outbound e un file Excel con i dati dei clienti.")

st.header("1. Carica il file Excel dei Clienti")
st.info("Questo file Excel deve contenere le colonne: `Code`, `company.code`, `partner.name`, `partner.address`, `partner.address.city`, `partner.address.province`, `partner.address.zipCode`, `partner.address.country`.")
uploaded_customers_file = st.file_uploader(
    "Scegli il file Excel con i dettagli dei Clienti",
    type=["xlsx", "xls"],
    key=st.session_state.file_uploader_key_customers
)

customers_df = pd.DataFrame()
if uploaded_customers_file:
    try:
        customers_df = pd.read_excel(uploaded_customers_file)
        # Rinomina le colonne per facilitare l'accesso, se necessario, o assicurati che i nomi corrispondano
        # Es: customers_df.rename(columns={'Codice': 'Code', 'Nome': 'partner.name'}, inplace=True)
        # Assicurati che le colonne per il merge siano stringhe per evitare problemi di tipo
        customers_df['Code'] = customers_df['Code'].astype(str)
        customers_df['company.code'] = customers_df['company.code'].astype(str)
        st.success("File Excel Clienti caricato con successo.")
        # Debug: st.dataframe(customers_df.head())
    except Exception as e:
        st.error(f"Errore nel caricamento del file Excel Clienti: {e}. Assicurati che sia un file Excel valido e che le colonne siano corrette.")
        customers_df = pd.DataFrame() # Reset in caso di errore

st.header("2. Carica i tuoi file JSON degli Ordini Outbound")
uploaded_json_files = st.file_uploader(
    "Scegli uno o più file JSON di tipo 'Ordine_Outbound'",
    type=["json"],
    accept_multiple_files=True,
    key=st.session_state.file_uploader_key_json
)

all_processed_data = []

if uploaded_json_files and not customers_df.empty:
    st.info(f"Caricati {len(uploaded_json_files)} file JSON. Elaborazione in corso...")
    
    conversion_successful = True
    
    for uploaded_file in uploaded_json_files:
        file_name = uploaded_file.name
        
        if not file_name.lower().startswith("ordine_outbound"):
            st.error(f"Errore: Il file '{file_name}' non inizia con 'Ordine_Outbound'. Verrà ignorato.")
            conversion_successful = False
            continue
        
        try:
            json_content_bytes = uploaded_file.read()
            json_content_string = json_content_bytes.decode('utf-8')

            # Passa customers_df alla funzione di conversione
            df_this_file = convert_json_to_excel_data_outbound(json_content_string, customers_df)
            
            if df_this_file is not None and not df_this_file.empty: 
                all_processed_data.append(df_this_file)
            elif df_this_file is None:
                conversion_successful = False
                st.error(f"Impossibile convertire il file '{file_name}'. Controlla i log per i dettagli.")
                    
        except Exception as e:
            st.error(f"Si è verificato un errore imprevisto durante la lettura o l'elaborazione del file '{file_name}': {e}")
            conversion_successful = False

    if all_processed_data:
        final_df = pd.concat(all_processed_data, ignore_index=True)
        st.success(f"Elaborazione di {len(all_processed_data)} file JSON completata! Pronto per il download.")
        
        # --- Post-processing per la formattazione Excel ---
        
        # Definizione delle colonne di testata
        header_columns = [
            'siteCode', 'companyCode', 'orderNumber', 'orderType', 'deliveryType',
            'orderDocDate', 'orderDateDetail', 'dueDate', 'reasonCode',
            'service', 'serviceType', 'paymentInfo.value', 
            'customer.code', 'customer.name', 'customer.address', 'customer.city',
            'customer.province', 'customer.zipCode', 'customer.country',
            'consignee.name', 'consignee.address', 'consignee.city', 'consignee.province',
            'consignee.zipCode', 'consignee.country', 'customer.mail',
            'noteDdt', 'noteWarehouse', 'noteDelivery', 'noteShipment',
            'reasonCodeShp', 'infoDelivery', 'bookingInfo'
        ]

        # Ordina il DataFrame per orderNumber e details.positionId per raggruppare gli ordini
        final_df = final_df.sort_values(by=['orderNumber', 'details.positionId']).reset_index(drop=True)

        # Crea una maschera booleana per identificare le righe duplicate per orderNumber
        rows_to_clear_headers = final_df.duplicated(subset=['orderNumber'], keep='first')
        
        # Azzerare le colonne di testata per le righe identificate
        final_df.loc[rows_to_clear_headers, header_columns] = ""

        # --- Creazione file Excel ---
        output = io.BytesIO()
        writer = pd.ExcelWriter(output, engine='xlsxwriter')
        
        sheet_name_excel = 'OutboundOrder'

        workbook = writer.book
        worksheet = writer.sheets.get(sheet_name_excel)
        if worksheet is None:
            worksheet = workbook.add_worksheet(sheet_name_excel)

        # Lista delle colonne finali nell'ordine desiderato
        final_columns_order = [
            'siteCode', 'companyCode', 'orderNumber', 'orderType', 'deliveryType',
            'orderDocDate', 'orderDateDetail', 'dueDate', 'reasonCode',
            'service', 'serviceType', 'paymentInfo.value', 
            'customer.code', 'customer.name', 'customer.address', 'customer.city',
            'customer.province', 'customer.zipCode', 'customer.country',
            'consignee.name', 'consignee.address', 'consignee.city', 'consignee.province',
            'consignee.zipCode', 'consignee.country', 'customer.mail',
            'details.positionId', 'details.itemNumber', 'details.requestedQt',
            'details.lotId', 'details.stockOwner', 'details.magCli', 'details.availability',
            'details.accountingStatus', 'details.refOrder',
            'noteDdt', 'noteWarehouse', 'noteDelivery', 'noteShipment',
            'reasonCodeShp', 'infoDelivery', 'bookingInfo'
        ]
        
        # Assicurati che il DataFrame abbia tutte le colonne nell'ordine desiderato
        for col in final_columns_order:
            if col not in final_df.columns:
                final_df[col] = "" 
        final_df = final_df[final_columns_order]

        # Larghezze delle colonne aggiornate (adattate per Outbound)
        col_widths = {
            'siteCode': 10, 'companyCode': 15, 'orderNumber': 15, 'orderType': 10, 'deliveryType': 12,
            'orderDocDate': 12, 'orderDateDetail': 15, 'dueDate': 12, 'reasonCode': 12, # Date solo gg/mm/aaaa
            'service': 10, 'serviceType': 12, 'paymentInfo.value': 15, 
            'customer.code': 15, 'customer.name': 30, 'customer.address': 30, 'customer.city': 15, 
            'customer.province': 10, 'customer.zipCode': 10, 'customer.country': 10,
            'consignee.name': 30, 'consignee.address': 30, 'consignee.city': 15, 'consignee.province': 10,
            'consignee.zipCode': 10, 'consignee.country': 10, 'customer.mail': 25,
            'details.positionId': 12, 'details.itemNumber': 20, 'details.requestedQt': 15,
            'details.lotId': 10, 'details.stockOwner': 15, 'details.magCli': 15, 'details.availability': 15,
            'details.accountingStatus': 18, 'details.refOrder': 15,
            'noteDdt': 20, 'noteWarehouse': 20, 'noteDelivery': 20, 'noteShipment': 20,
            'reasonCodeShp': 15, 'infoDelivery': 20, 'bookingInfo': 20
        }
        
        # Formato data per le colonne di data (solo data, senza ora)
        date_format = workbook.add_format({'num_format': 'dd/mm/yyyy'})

        # Scrivi i nomi delle colonne nella riga 2 (indice 1 in xlsxwriter)
        for col_num, col_name in enumerate(final_df.columns.values):
            worksheet.write(1, col_num, col_name) 

            # Imposta le larghezze delle colonne
            if col_name in col_widths:
                worksheet.set_column(col_num, col_num, col_widths[col_name])
            else:
                worksheet.set_column(col_num, col_num, 15) 
            
            # Applica il formato data alle colonne 'orderDocDate' e 'dueDate'
            if col_name in ['orderDocDate', 'dueDate']:
                worksheet.set_column(col_num, col_num, col_widths[col_name], date_format)

        # Scrivi i dati a partire dalla riga 3 (indice 2 in xlsxwriter)
        final_df.to_excel(writer, sheet_name=sheet_name_excel, index=False, header=False, startrow=2)

        writer.close()
        output.seek(0)

        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        output_file_name = f"OutboundOrders_{timestamp}.xlsx"

        st.download_button(
            label="Scarica file Excel Unificato",
            data=output.getvalue(),
            file_name=output_file_name,
            mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        )
    elif uploaded_json_files and customers_df.empty:
        st.warning("Per elaborare i file JSON, devi prima caricare un file Excel con i dettagli dei clienti.")
    elif not conversion_successful:
        st.error("Nessun file valido è stato elaborato con successo. Controlla i messaggi di errore sopra.")
    else:
        st.info("Carica i tuoi file JSON per iniziare.")

# --- Funzione per il reset ---
def clear_inputs():
    st.session_state.file_uploader_key_json += 1 
    st.session_state.file_uploader_key_customers += 1

st.markdown("---")
st.button("Pulisci Input (RESET)", on_click=clear_inputs)

st.markdown("Sviluppato con Streamlit e Python.")