Spaces:
Sleeping
Sleeping
Download src/streamlit_app.py from supportDTO/OdaStdConversion: direct link, hf CLI and curl.
- Browser
- Download file 12 kB
-
https://huggingface.co/spaces/supportDTO/OdaStdConversion/resolve/main/src/streamlit_app.py
- Command line
-
hf download hf://spaces/supportDTO/OdaStdConversion/src/streamlit_app.py
-
curl -L -o streamlit_app.py https://huggingface.co/spaces/supportDTO/OdaStdConversion/resolve/main/src/streamlit_app.py
12 kB
| 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.") |