import json import pandas as pd import streamlit as st from io import BytesIO import openpyxl from datetime import datetime # Configuration Streamlit st.set_page_config( page_title="IFS NEO Data Extractor", layout="wide", initial_sidebar_state="expanded" ) # CSS personnalisé pour améliorer l'apparence def apply_custom_css(): st.markdown(""" """, unsafe_allow_html=True) def flatten_json_safe(nested_json, parent_key='', sep='_'): """Aplatit une structure JSON imbriquée de manière sécurisée.""" items = [] if isinstance(nested_json, dict): for k, v in nested_json.items(): new_key = f'{parent_key}{sep}{k}' if parent_key else k if isinstance(v, dict): items.extend(flatten_json_safe(v, new_key, sep=sep).items()) elif isinstance(v, list): for i, item in enumerate(v): items.extend(flatten_json_safe(item, f'{new_key}{sep}{i}', sep=sep).items()) else: items.append((new_key, v)) else: items.append((parent_key, nested_json)) return dict(items) def extract_from_flattened(flattened_data, mapping, selected_fields): """Extrait les données du JSON aplati selon le mapping fourni.""" extracted_data = {} for label, flat_path in mapping.items(): if label in selected_fields: extracted_data[label] = flattened_data.get(flat_path, 'N/A') return extracted_data def get_user_comments(): """Récupère tous les commentaires utilisateur depuis la session state.""" comments = {} for key, value in st.session_state.items(): if key.startswith(('profile_comment_', 'checklist_comment_', 'non_conformity_comment_')): comments[key] = value return comments def initialize_session_state(): """Initialise les variables de session state nécessaires.""" if 'json_data' not in st.session_state: st.session_state.json_data = None if 'profile_data' not in st.session_state: st.session_state.profile_data = {} if 'checklist_data' not in st.session_state: st.session_state.checklist_data = [] if 'non_conformities' not in st.session_state: st.session_state.non_conformities = [] # Mapping complet des champs FLATTENED_FIELD_MAPPING = { "Nom du site à auditer": "data_modules_food_8_questions_companyName_answer", "N° COID du portail": "data_modules_food_8_questions_companyCoid_answer", "Code GLN": "data_modules_food_8_questions_companyGln_answer_0_rootQuestions_companyGlnNumber_answer", "Rue": "data_modules_food_8_questions_companyStreetNo_answer", "Code postal": "data_modules_food_8_questions_companyZip_answer", "Nom de la ville": "data_modules_food_8_questions_companyCity_answer", "Pays": "data_modules_food_8_questions_companyCountry_answer", "Téléphone": "data_modules_food_8_questions_companyTelephone_answer", "Latitude": "data_modules_food_8_questions_companyGpsLatitude_answer", "Longitude": "data_modules_food_8_questions_companyGpsLongitude_answer", "Email": "data_modules_food_8_questions_companyEmail_answer", "Nom du siège social": "data_modules_food_8_questions_headquartersName_answer", "Rue (siège social)": "data_modules_food_8_questions_headquartersStreetNo_answer", "Nom de la ville (siège social)": "data_modules_food_8_questions_headquartersCity_answer", "Code postal (siège social)": "data_modules_food_8_questions_headquartersZip_answer", "Pays (siège social)": "data_modules_food_8_questions_headquartersCountry_answer", "Téléphone (siège social)": "data_modules_food_8_questions_headquartersTelephone_answer", "Surface couverte de l'entreprise (m²)": "data_modules_food_8_questions_productionAreaSize_answer", "Nombre de bâtiments": "data_modules_food_8_questions_numberOfBuildings_answer", "Nombre de lignes de production": "data_modules_food_8_questions_numberOfProductionLines_answer", "Nombre d'étages": "data_modules_food_8_questions_numberOfFloors_answer", "Nombre maximum d'employés dans l'année, au pic de production": "data_modules_food_8_questions_numberOfEmployeesForTimeCalculation_answer", "Langue parlée et écrite sur le site": "data_modules_food_8_questions_workingLanguage_answer", "Périmètre de l'audit": "data_modules_food_8_questions_scopeCertificateScopeDescription_en_answer", "Process et activités": "data_modules_food_8_questions_scopeProductGroupsDescription_answer", "Activité saisonnière ? (O/N)": "data_modules_food_8_questions_seasonalProduction_answer", "Une partie du procédé de fabrication est-elle sous traitée? (OUI/NON)": "data_modules_food_8_questions_partlyOutsourcedProcesses_answer", "Si oui lister les procédés sous-traités": "data_modules_food_8_questions_partlyOutsourcedProcessesDescription_answer", "Avez-vous des produits totalement sous-traités? (OUI/NON)": "data_modules_food_8_questions_fullyOutsourcedProducts_answer", "Si oui, lister les produits totalement sous-traités": "data_modules_food_8_questions_fullyOutsourcedProductsDescription_answer", "Avez-vous des produits de négoce? (OUI/NON)": "data_modules_food_8_questions_tradedProductsBrokerActivity_answer", "Si oui, lister les produits de négoce": "data_modules_food_8_questions_tradedProductsBrokerActivityDescription_answer", "Produits à exclure du champ d'audit (OUI/NON)": "data_modules_food_8_questions_exclusions_answer", "Préciser les produits à exclure": "data_modules_food_8_questions_exclusionsDescription_answer" } def process_json_file(uploaded_file): """Traite le fichier JSON uploadé et extrait les données.""" try: json_data = json.load(uploaded_file) st.session_state.json_data = json_data # Aplatir les données JSON flattened_json_data = flatten_json_safe(json_data) # Extraire les données de profil profile_data = extract_from_flattened( flattened_json_data, FLATTENED_FIELD_MAPPING, list(FLATTENED_FIELD_MAPPING.keys()) ) st.session_state.profile_data = profile_data # Extraire les données de checklist checklist_data = [] if 'data' in json_data and 'modules' in json_data['data']: modules = json_data['data']['modules'] if 'food_8' in modules and 'checklists' in modules['food_8']: checklists = modules['food_8']['checklists'] if 'checklistFood8' in checklists and 'resultScorings' in checklists['checklistFood8']: for uuid, scoring in checklists['checklistFood8']['resultScorings'].items(): checklist_data.append({ "Num": uuid, "Explanation": scoring['answers'].get('englishExplanationText', 'N/A'), "Detailed Explanation": scoring['answers'].get('explanationText', 'N/A'), "Score": scoring['score']['label'], "Response": scoring['answers'].get('fieldAnswers', 'N/A') }) st.session_state.checklist_data = checklist_data # Extraire les non-conformités non_conformities = [item for item in checklist_data if item['Score'] != 'A'] st.session_state.non_conformities = non_conformities return True, "Fichier traité avec succès!" except json.JSONDecodeError as e: return False, f"Erreur lors du décodage JSON: {str(e)}" except Exception as e: return False, f"Erreur lors du traitement du fichier: {str(e)}" def display_profile_section(): """Affiche la section du profil avec possibilité d'ajout de commentaires.""" st.markdown('
📋 Profil de l\'entreprise
', unsafe_allow_html=True) if not st.session_state.profile_data: st.warning("Aucune donnée de profil disponible. Veuillez d'abord charger un fichier IFS.") return # Organiser les données en colonnes pour une meilleure présentation col1, col2 = st.columns(2) profile_items = list(st.session_state.profile_data.items()) mid_point = len(profile_items) // 2 with col1: for field, value in profile_items[:mid_point]: st.text_input(f"**{field}**", value=str(value), key=f"profile_field_{field}", disabled=True) # Zone de commentaire pour chaque champ st.text_area( f"Commentaire - {field}", key=f"profile_comment_{field}", height=60, placeholder="Ajoutez vos commentaires ici..." ) with col2: for field, value in profile_items[mid_point:]: st.text_input(f"**{field}**", value=str(value), key=f"profile_field_{field}", disabled=True) # Zone de commentaire pour chaque champ st.text_area( f"Commentaire - {field}", key=f"profile_comment_{field}", height=60, placeholder="Ajoutez vos commentaires ici..." ) def display_checklist_section(): """Affiche la section de la checklist complète.""" st.markdown('
✅ Checklist complète
', unsafe_allow_html=True) if not st.session_state.checklist_data: st.warning("Aucune donnée de checklist disponible. Veuillez d'abord charger un fichier IFS.") return # Filtre par score score_filter = st.selectbox( "Filtrer par score:", ["Tous", "A", "B", "C", "D", "Non applicable"] ) # Appliquer le filtre filtered_data = st.session_state.checklist_data if score_filter != "Tous": filtered_data = [item for item in st.session_state.checklist_data if item['Score'] == score_filter] st.info(f"Affichage de {len(filtered_data)} éléments sur {len(st.session_state.checklist_data)} au total") # Afficher les éléments de la checklist for i, item in enumerate(filtered_data): with st.expander(f"Exigence {item['Num']} - Score: {item['Score']}", expanded=False): col1, col2 = st.columns([3, 1]) with col1: st.write(f"**Explication:** {item['Explanation']}") st.write(f"**Explication détaillée:** {item['Detailed Explanation']}") st.write(f"**Réponse:** {item['Response']}") # Zone de commentaire pour chaque élément st.text_area( "Commentaire de l'auditeur:", key=f"checklist_comment_{item['Num']}", height=100, placeholder="Ajoutez vos observations, commentaires ou actions à prendre..." ) with col2: # Affichage du score avec couleur score_color = { 'A': '#28a745', 'B': '#ffc107', 'C': '#fd7e14', 'D': '#dc3545', 'Non applicable': '#6c757d' }.get(item['Score'], '#6c757d') st.markdown(f"""
{item['Score']}
""", unsafe_allow_html=True) def display_non_conformities_section(): """Affiche la section des non-conformités.""" st.markdown('
⚠️ Non-conformités
', unsafe_allow_html=True) if not st.session_state.non_conformities: st.success("Aucune non-conformité détectée ! Toutes les exigences sont notées A.") return st.warning(f"Nombre de non-conformités détectées: {len(st.session_state.non_conformities)}") # Statistiques des non-conformités scores_count = {} for item in st.session_state.non_conformities: score = item['Score'] scores_count[score] = scores_count.get(score, 0) + 1 col1, col2, col3, col4 = st.columns(4) for i, (score, count) in enumerate(scores_count.items()): with [col1, col2, col3, col4][i % 4]: st.metric(f"Score {score}", count) # Afficher chaque non-conformité for item in st.session_state.non_conformities: with st.container(): st.markdown(f"""

🔍 Exigence {item['Num']} - Score: {item['Score']}

""", unsafe_allow_html=True) col1, col2 = st.columns([3, 1]) with col1: st.write(f"**Explication:** {item['Explanation']}") st.write(f"**Explication détaillée:** {item['Detailed Explanation']}") st.write(f"**Réponse:** {item['Response']}") # Plan d'action st.text_area( "Plan d'action corrective:", key=f"non_conformity_action_{item['Num']}", height=100, placeholder="Décrivez les actions correctives à mettre en place..." ) # Commentaire de l'auditeur st.text_area( "Commentaire de l'auditeur:", key=f"non_conformity_comment_{item['Num']}", height=80, placeholder="Observations de l'auditeur..." ) with col2: # Sélection de la priorité priority = st.selectbox( "Priorité:", ["Haute", "Moyenne", "Basse"], key=f"priority_{item['Num']}" ) # Date limite deadline = st.date_input( "Date limite:", key=f"deadline_{item['Num']}" ) # Responsable responsible = st.text_input( "Responsable:", key=f"responsible_{item['Num']}", placeholder="Nom du responsable" ) def create_enhanced_excel_export(): """Crée un fichier Excel enrichi avec toutes les données et commentaires.""" if not st.session_state.profile_data: st.error("Aucune donnée à exporter. Veuillez d'abord charger un fichier IFS.") return None # Récupérer tous les commentaires comments = get_user_comments() # Créer le fichier Excel en mémoire output = BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: # Onglet Profil profile_rows = [] for field, value in st.session_state.profile_data.items(): comment_key = f"profile_comment_{field}" comment = comments.get(comment_key, "") profile_rows.append({ "Champ": field, "Valeur": value, "Commentaire": comment, "Réponse auditeur": "" }) df_profile = pd.DataFrame(profile_rows) df_profile.to_excel(writer, index=False, sheet_name="Profil") # Onglet Checklist complète checklist_rows = [] for item in st.session_state.checklist_data: comment_key = f"checklist_comment_{item['Num']}" comment = comments.get(comment_key, "") checklist_rows.append({ "Numéro": item['Num'], "Explication": item['Explanation'], "Explication détaillée": item['Detailed Explanation'], "Score": item['Score'], "Réponse": item['Response'], "Commentaire auditeur": comment, "Action requise": "" }) df_checklist = pd.DataFrame(checklist_rows) df_checklist.to_excel(writer, index=False, sheet_name="Checklist") # Onglet Non-conformités avec plan d'action nc_rows = [] for item in st.session_state.non_conformities: comment_key = f"non_conformity_comment_{item['Num']}" action_key = f"non_conformity_action_{item['Num']}" priority_key = f"priority_{item['Num']}" deadline_key = f"deadline_{item['Num']}" responsible_key = f"responsible_{item['Num']}" comment = comments.get(comment_key, "") action = st.session_state.get(action_key, "") priority = st.session_state.get(priority_key, "") deadline = st.session_state.get(deadline_key, "") responsible = st.session_state.get(responsible_key, "") nc_rows.append({ "Numéro": item['Num'], "Score": item['Score'], "Explication": item['Explanation'], "Explication détaillée": item['Detailed Explanation'], "Réponse": item['Response'], "Commentaire auditeur": comment, "Plan d'action": action, "Priorité": priority, "Date limite": deadline, "Responsable": responsible, "Statut": "En attente" }) df_nc = pd.DataFrame(nc_rows) df_nc.to_excel(writer, index=False, sheet_name="Non-conformités") # Onglet Résumé summary_data = { "Indicateur": [ "Nombre total d'exigences", "Exigences conformes (A)", "Non-conformités mineures (B)", "Non-conformités majeures (C)", "Non-conformités critiques (D)", "Taux de conformité (%)" ], "Valeur": [ len(st.session_state.checklist_data), len([x for x in st.session_state.checklist_data if x['Score'] == 'A']), len([x for x in st.session_state.checklist_data if x['Score'] == 'B']), len([x for x in st.session_state.checklist_data if x['Score'] == 'C']), len([x for x in st.session_state.checklist_data if x['Score'] == 'D']), round((len([x for x in st.session_state.checklist_data if x['Score'] == 'A']) / len(st.session_state.checklist_data)) * 100, 2) if st.session_state.checklist_data else 0 ] } df_summary = pd.DataFrame(summary_data) df_summary.to_excel(writer, index=False, sheet_name="Résumé") # Ajuster la largeur des colonnes for sheet_name in writer.sheets: worksheet = writer.sheets[sheet_name] for column in worksheet.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = min(max_length + 2, 50) worksheet.column_dimensions[column_letter].width = adjusted_width output.seek(0) return output def main(): """Fonction principale de l'application.""" # Initialiser la session state initialize_session_state() # Appliquer le CSS personnalisé apply_custom_css() # En-tête principal st.markdown('
🔍 IFS NEO Data Extractor
', unsafe_allow_html=True) st.markdown('
Application d\'extraction et d\'analyse des données d\'audit IFS
', unsafe_allow_html=True) # Navigation dans la sidebar st.sidebar.title("📋 Navigation") # Upload du fichier IFS st.sidebar.markdown("### 📁 Chargement des fichiers") uploaded_json_file = st.sidebar.file_uploader( "Charger le fichier IFS (.ifs)", type="ifs", help="Sélectionnez le fichier d'audit IFS exporté depuis NEO" ) # Traitement du fichier JSON if uploaded_json_file and st.session_state.json_data is None: with st.spinner("Traitement du fichier IFS en cours..."): success, message = process_json_file(uploaded_json_file) if success: st.sidebar.success(message) else: st.sidebar.error(message) return # Menu de navigation principal if st.session_state.json_data: st.sidebar.markdown("### 🎯 Sections disponibles") page = st.sidebar.radio( "Choisissez une section:", ["📋 Profil de l'entreprise", "✅ Checklist complète", "⚠️ Non-conformités", "📊 Tableau de bord", "📄 Export Excel"] ) # Affichage des sections selon la navigation if page == "📋 Profil de l'entreprise": display_profile_section() elif page == "✅ Checklist complète": display_checklist_section() elif page == "⚠️ Non-conformités": display_non_conformities_section() elif page == "📊 Tableau de bord": st.markdown('
📊 Tableau de bord de l\'audit
', unsafe_allow_html=True) # Métriques principales col1, col2, col3, col4 = st.columns(4) total_items = len(st.session_state.checklist_data) conformes = len([x for x in st.session_state.checklist_data if x['Score'] == 'A']) non_conformites = len(st.session_state.non_conformities) taux_conformite = (conformes / total_items * 100) if total_items > 0 else 0 with col1: st.metric("Total exigences", total_items) with col2: st.metric("Conformes (A)", conformes) with col3: st.metric("Non-conformités", non_conformites) with col4: st.metric("Taux conformité", f"{taux_conformite:.1f}%") # Répartition des scores if st.session_state.checklist_data: scores_count = {} for item in st.session_state.checklist_data: score = item['Score'] scores_count[score] = scores_count.get(score, 0) + 1 # Graphique de répartition st.subheader("Répartition des scores") chart_data = pd.DataFrame(list(scores_count.items()), columns=['Score', 'Nombre']) st.bar_chart(chart_data.set_index('Score')) elif page == "📄 Export Excel": st.markdown('
📄 Export des données
', unsafe_allow_html=True) st.info("Exportez toutes les données collectées avec vos commentaires dans un fichier Excel structuré.") if st.button("🔄 Générer le fichier Excel", type="primary"): with st.spinner("Génération du fichier Excel..."): excel_file = create_enhanced_excel_export() if excel_file: # Nom du fichier avec COID et date coid = st.session_state.profile_data.get("N° COID du portail", "inconnu") date_str = datetime.now().strftime("%Y%m%d_%H%M") filename = f"audit_IFS_{coid}_{date_str}.xlsx" st.download_button( label="📥 Télécharger le rapport Excel", data=excel_file, file_name=filename, mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) st.success("Fichier Excel généré avec succès!") # Informations sur le contenu du fichier st.markdown("### 📋 Contenu du fichier Excel:") st.markdown(""" - **Profil**: Informations sur l'entreprise avec commentaires - **Checklist**: Liste complète des exigences avec scores et commentaires - **Non-conformités**: Plan d'action détaillé pour chaque non-conformité - **Résumé**: Statistiques et indicateurs de performance """) else: # Page d'accueil si aucun fichier n'est chargé st.markdown(""" ### 🚀 Bienvenue dans l'extracteur de données IFS NEO Cette application vous permet de: - 📊 Extraire et analyser les données d'audit IFS - 💬 Ajouter vos commentaires et observations - 📋 Créer des plans d'action pour les non-conformités - 📄 Exporter tout dans un rapport Excel structuré **Pour commencer:** 1. Chargez votre fichier d'audit IFS (.ifs) dans la barre latérale 2. Naviguez entre les différentes sections 3. Ajoutez vos commentaires et plans d'action 4. Exportez le rapport final """) st.markdown('
⚠️ Veuillez charger un fichier IFS pour commencer l\'analyse.
', unsafe_allow_html=True) if __name__ == "__main__": main()