OdaStdConversion / src /streamlit_app.py
pasquale991's picture
Update src/streamlit_app.py
91fc965 verified
Raw History Blame Contribute Delete
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.")