File size: 12,000 Bytes
4f5cdba
3820315
 
488a502
91fc965
3820315
 
 
5dfcb54
 
b96bef5
488a502
5049b40
 
 
 
 
b96bef5
5049b40
 
 
b96bef5
5049b40
 
b96bef5
5049b40
 
 
b96bef5
5049b40
 
 
 
 
 
b96bef5
5049b40
 
57c8431
db3eb47
5049b40
 
91fc965
e6d8cdd
91fc965
3708ecb
3820315
db3eb47
3820315
 
 
 
db3eb47
b96bef5
3820315
b96bef5
 
3820315
 
5049b40
 
3820315
 
 
91fc965
e6d8cdd
3708ecb
 
91fc965
3708ecb
3820315
488a502
5049b40
91fc965
e6d8cdd
 
3820315
 
 
5049b40
3820315
488a502
a539f96
 
 
 
c76e26a
a539f96
488a502
3820315
db3eb47
b96bef5
5049b40
 
b96bef5
5049b40
3708ecb
 
5049b40
 
 
 
 
 
b96bef5
db3eb47
3708ecb
b96bef5
3820315
5049b40
3820315
5049b40
488a502
3820315
5049b40
3820315
5049b40
 
488a502
3820315
 
 
 
5049b40
3820315
 
 
5049b40
 
3820315
b96bef5
3820315
b96bef5
488a502
b96bef5
488a502
 
b96bef5
488a502
3820315
488a502
3820315
5049b40
b96bef5
 
 
 
 
 
5049b40
b96bef5
 
 
 
5049b40
b96bef5
 
 
 
 
5049b40
b96bef5
 
 
5049b40
b96bef5
 
 
 
5049b40
b96bef5
 
 
 
db3eb47
 
a539f96
db3eb47
 
 
 
 
a539f96
db3eb47
 
a539f96
db3eb47
 
a539f96
db3eb47
 
 
b96bef5
 
 
 
3820315
b96bef5
a539f96
db3eb47
 
 
b96bef5
 
 
 
 
 
 
 
 
3708ecb
b96bef5
 
 
 
 
 
 
db3eb47
3708ecb
b96bef5
 
 
db3eb47
 
 
91fc965
db3eb47
b96bef5
db3eb47
b96bef5
 
 
 
 
 
91fc965
b96bef5
3708ecb
b96bef5
 
 
 
 
 
 
 
a539f96
b96bef5
 
 
91fc965
 
 
 
db3eb47
b96bef5
3708ecb
db3eb47
 
b96bef5
 
 
3708ecb
91fc965
 
 
 
 
 
 
db3eb47
 
 
b96bef5
 
 
 
 
 
 
 
 
 
 
91fc965
b96bef5
 
 
 
 
5049b40
3708ecb
4f5cdba
b96bef5
488a502
a539f96
488a502
5dfcb54
488a502
 
3820315
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
import streamlit as st
import pandas as pd
import json
from datetime import datetime
import pytz 
import io
import os

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

# --- CSS personalizzato per sfondo giallo e testo blu ---
st.markdown(
    """
    <style>
    .stApp {
        background-color: #FFFFCC;
        background-attachment: fixed;
    }
    h1, h2 {
        color: #1E90FF;
    }
    p, label {
        color: #000080;
    }
    .stButton>button {
        color: white;
        background-color: #1E90FF;
        border: none;
        border-radius: 5px;
        padding: 10px 20px;
        font-size: 16px;
    }
    .stButton>button:hover {
        background-color: #4169E1;
    }
    </style>
    """,
    unsafe_allow_html=True
)

# Definizione del fuso orario per l'Italia
ITALY_TIMEZONE = pytz.timezone("Europe/Rome")
UTC_TIMEZONE = pytz.utc

# Funzione di conversione
def convert_json_to_excel_data(json_file_content: str):
    try:
        json_data = json.loads(json_file_content)

        company_code = json_data.get('companyCode')
        site_code_json = json_data.get('siteCode')

        order_info = json_data.get('order', {})
        
        doc_number = order_info.get('orderNumber') 
        partner_code = order_info.get('partnerCode')
        
        order_date_str = order_info.get('orderDate')
        
        order_date_formatted = None
        if order_date_str:
            try:
                dt_object_naive = datetime.fromisoformat(order_date_str.replace('Z', ''))
                dt_object_utc = UTC_TIMEZONE.localize(dt_object_naive)
                dt_object_italy = dt_object_utc.astimezone(ITALY_TIMEZONE)
                
                # La stringa di formattazione rimane corretta per Python
                order_date_formatted = dt_object_italy.strftime('%d/%m/%Y %H:%M:%S') 
            except ValueError:
                order_date_formatted = order_date_str
                st.warning(f"Attenzione: Impossibile parsare la data '{order_date_str}' per un file. Verrà usata la stringa originale.")
            except Exception as tz_error:
                st.warning(f"Attenzione: Errore nella conversione del fuso orario per '{order_date_str}': {tz_error}. Verrà usata la stringa originale.")
                order_date_formatted = order_date_str

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

        processed_data_for_this_file = []
        for item in details:
            cancel_pos_oda_val = item.get('cancelPosOdA')
            if cancel_pos_oda_val is None or \
               (isinstance(cancel_pos_oda_val, bool) and not cancel_pos_oda_val) or \
               (isinstance(cancel_pos_oda_val, str) and cancel_pos_oda_val.lower() == 'false'):
                positions_status_value = 'CREATED'
            else:
                positions_status_value = 'CANCELLED'

            row = {
                'site.code': site_code_json,
                'docNumber': doc_number,
                'company.code': company_code,
                'supplier.partnerRef.code': partner_code,
                'supplier.partnerRef.company.code': company_code,
                'docDate': order_date_formatted,
                'status': 'CREATED', 
                'positions.status': positions_status_value, 
                'positions.positionId': item.get('positionId'),
                'positions.stock.item.itemNumber': item.get('itemCode'),
                'positions.stock.quantity': item.get('quantity'),
                'positions.stock.accountingInfo.stockOwner': item.get('stockOwner'),
                'positions.stock.accountingInfo.costCenter': item.get('warehouse'),
                'positions.stock.accountingInfo.availability': item.get('availability'),
                'positions.stock.item.company.code': company_code,
                'positions.spare.string1': item.get('refOrder'),
                'positions.reason.code': item.get('reasonCode'),
                'positions.reason.company.code': company_code,
            }
            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 (es. 'order.details').")
        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 Oda a Template Excel Oda")
st.write("Carica i tuoi file JSON degli ordini ODA e scarica il foglio Excel formattato.")

st.markdown("**(Il Site Code verrà letto direttamente dal JSON)**")

st.header("1. Carica i tuoi file JSON")
uploaded_files = st.file_uploader(
    "Scegli uno o più file JSON di tipo 'Ordine_Acquisto'",
    type=["json"],
    accept_multiple_files=True,
    key=st.session_state.file_uploader_key
)

all_processed_data = []

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

            df_this_file = convert_json_to_excel_data(json_content_string)
            
            if df_this_file is not None:
                all_processed_data.append(df_this_file)
            else:
                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 = [
            'site.code', 'docNumber', 'company.code', 'supplier.partnerRef.code',
            'supplier.partnerRef.company.code', 'docDate', 'status'
        ]

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

        # Crea una maschera booleana per identificare le righe duplicate per docNumber
        rows_to_clear_headers = final_df.duplicated(subset=['docNumber'], 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 = 'PurchaseOrder'

        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 = [
            'site.code',
            'docNumber',
            'company.code',
            'supplier.partnerRef.code',
            'supplier.partnerRef.company.code',
            'docDate',
            'status',
            'positions.status',
            'positions.positionId',
            'positions.stock.item.itemNumber',
            'positions.stock.quantity',
            'positions.stock.accountingInfo.stockOwner',
            'positions.stock.accountingInfo.costCenter',
            'positions.stock.accountingInfo.availability',
            'positions.stock.item.company.code',
            'positions.spare.string1',
            'positions.reason.code',
            'positions.reason.company.code',
        ]
        
        # 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
        col_widths = {
            'site.code': 10,
            'docNumber': 15,
            'company.code': 15,
            'supplier.partnerRef.code': 18,
            'supplier.partnerRef.company.code': 25,
            'docDate': 20, # Lascia 20, è abbastanza larga
            'status': 10,
            'positions.status': 12,
            'positions.positionId': 12,
            'positions.stock.item.itemNumber': 25,
            'positions.stock.quantity': 10,
            'positions.stock.accountingInfo.stockOwner': 18,
            'positions.stock.accountingInfo.costCenter': 18,
            'positions.stock.accountingInfo.availability': 15,
            'positions.stock.item.company.code': 25,
            'positions.spare.string1': 15,
            'positions.reason.code': 15,
            'positions.reason.company.code': 25,
        }
        
        # *** NUOVA PARTE: Formattazione della colonna docDate in Excel ***
        # Crea un formato numerico personalizzato per data e ora completa
        date_time_format = workbook.add_format({'num_format': 'dd/mm/yyyy hh:mm:ss'})

        # 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/ora alla colonna 'docDate'
            if col_name == 'docDate':
                # +1 perché stiamo scrivendo da startrow=2 (quindi la colonna dati inizia da riga 2)
                # Il +1 per l'indice della colonna non serve, è già l'indice corretto
                worksheet.set_column(col_num, col_num, col_widths[col_name], date_time_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"PurchaseOrder_{timestamp}.xlsx"

        st.download_button(
            label="Scarica file Excel Unificato",
            data=output.getvalue(),
            file_name=output_file_name,
            mime="application/vnd.openxmlformats-officedocument.spreadsheet1.sheet"
        )
    elif not conversion_successful:
        st.error("Nessun file valido è stato elaborato con successo. Controlla i messaggi di errore sopra.")
    else:
        st.info("Nessun dato valido estratto dai file selezionati.")
else:
    st.info("Carica i tuoi file JSON per iniziare.")

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

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

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