From cec838930275252403e2123e2788914035a549d4 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Fri, 28 Nov 2025 22:50:06 +0300 Subject: [PATCH] Document transformation process back to normal Further diagnotic with new routes --- main.py | 1039 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 805 insertions(+), 234 deletions(-) diff --git a/main.py b/main.py index 04c2c51..67b07f3 100644 --- a/main.py +++ b/main.py @@ -489,255 +489,74 @@ def lire_document(numero: str, type_doc: int): @app.post("/sage/documents/transform", dependencies=[Depends(verify_token)]) def transformer_document( - numero: str, type_source: int = Query(...), type_cible: int = Query(...) + numero_source: str = Query(..., description="NumĂ©ro du document source"), + type_source: int = Query(..., description="Type document source"), + type_cible: int = Query(..., description="Type document cible"), ): """ - 🔍 DIAGNOSTIC AVANCÉ: Analyse pourquoi une transformation Ă©choue + 🔧 Transformation de document - VĂ©rifie: - - Statut du document source - - Statuts autorisĂ©s - - Lignes du document - - Client associĂ© - - Champs obligatoires manquants + ✅ CORRECTION : Utilise les VRAIS types Sage Dataven + + Types valides : + - 0: Devis + - 10: Bon de commande + - 20: PrĂ©paration + - 30: Bon de livraison + - 40: Bon de retour + - 50: Bon d'avoir + - 60: Facture + + Transformations autorisĂ©es : + - Devis (0) → Commande (10) + - Commande (10) → Bon livraison (30) + - Commande (10) → Facture (60) + - Bon livraison (30) → Facture (60) """ try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") + logger.info( + f"🔄 Transformation demandĂ©e: {numero_source} " + f"(type {type_source}) → type {type_cible}" + ) - with sage._com_context(), sage._lock_com: - factory = sage.cial.FactoryDocumentVente + # ✅ Matrice des transformations valides pour VOTRE Sage + transformations_valides = { + (0, 10), # Devis → Commande + (10, 30), # Commande → Bon de livraison + (10, 60), # Commande → Facture + (30, 60), # Bon de livraison → Facture + (0, 60), # Devis → Facture (si autorisĂ©) + } - # Lire le document source - persist = factory.ReadPiece(type_source, numero) - - if not persist: - persist = sage._find_document_in_list(numero, type_source) - - if not persist: - raise HTTPException( - 404, f"Document {numero} (type {type_source}) introuvable" - ) - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - diagnostic = { - "numero": numero, - "type_source": type_source, - "type_cible": type_cible, - "problemes_detectes": [], - "avertissements": [], - "suggestions": [], - } - - # 1. VĂ©rifier le statut - statut_actuel = getattr(doc, "DO_Statut", -1) - diagnostic["statut_actuel"] = statut_actuel - - if statut_actuel == 5: - diagnostic["problemes_detectes"].append( - { - "severite": "BLOQUANT", - "champ": "DO_Statut", - "valeur": 5, - "message": "Document dĂ©jĂ  transformĂ© (statut=5)", - } - ) - - elif statut_actuel == 6: - diagnostic["problemes_detectes"].append( - { - "severite": "BLOQUANT", - "champ": "DO_Statut", - "valeur": 6, - "message": "Document annulĂ© (statut=6)", - } - ) - - elif statut_actuel in [3, 4]: - diagnostic["avertissements"].append( - { - "severite": "ATTENTION", - "champ": "DO_Statut", - "valeur": statut_actuel, - "message": f"Document dĂ©jĂ  rĂ©alisĂ© (statut={statut_actuel}). " - f"Un document cible existe peut-ĂȘtre dĂ©jĂ .", - } - ) - - elif statut_actuel == 0: - diagnostic["suggestions"].append( - "Le document est en 'Brouillon' (statut=0). " - "Le systĂšme le passera automatiquement Ă  'AcceptĂ©' (statut=2) avant transformation." - ) - - # 2. VĂ©rifier le client - client_code = "" - try: - client_obj = getattr(doc, "Client", None) - if client_obj: - client_obj.Read() - client_code = getattr(client_obj, "CT_Num", "").strip() - except: - pass - - if not client_code: - diagnostic["problemes_detectes"].append( - { - "severite": "BLOQUANT", - "champ": "CT_Num", - "valeur": None, - "message": "Aucun client associĂ© au document", - } - ) - else: - diagnostic["client_code"] = client_code - - # 3. VĂ©rifier les lignes - try: - factory_lignes = getattr(doc, "FactoryDocumentLigne", None) or getattr( - doc, "FactoryDocumentVenteLigne", None - ) - - nb_lignes = 0 - lignes_problemes = [] - - if factory_lignes: - index = 1 - while index <= 100: - try: - ligne_p = factory_lignes.List(index) - if ligne_p is None: - break - - ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") - ligne.Read() - - nb_lignes += 1 - - # VĂ©rifier article - article_ref = "" - try: - article_ref = getattr(ligne, "AR_Ref", "").strip() - if not article_ref: - article_obj = getattr(ligne, "Article", None) - if article_obj: - article_obj.Read() - article_ref = getattr( - article_obj, "AR_Ref", "" - ).strip() - except: - pass - - if not article_ref: - lignes_problemes.append( - { - "ligne": index, - "probleme": "Aucune rĂ©fĂ©rence article", - } - ) - - # VĂ©rifier prix - prix = float(getattr(ligne, "DL_PrixUnitaire", 0.0)) - if prix == 0: - lignes_problemes.append( - {"ligne": index, "probleme": "Prix unitaire = 0"} - ) - - index += 1 - except: - break - - diagnostic["nb_lignes"] = nb_lignes - - if nb_lignes == 0: - diagnostic["problemes_detectes"].append( - { - "severite": "BLOQUANT", - "champ": "Lignes", - "valeur": 0, - "message": "Document vide (aucune ligne)", - } - ) - - if lignes_problemes: - diagnostic["avertissements"].append( - { - "severite": "ATTENTION", - "champ": "Lignes", - "message": f"{len(lignes_problemes)} ligne(s) avec des problĂšmes", - "details": lignes_problemes, - } - ) - - except Exception as e: - diagnostic["avertissements"].append( - { - "severite": "ERREUR", - "champ": "Lignes", - "message": f"Impossible de lire les lignes: {e}", - } - ) - - # 4. VĂ©rifier les totaux - total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) - total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) - - diagnostic["totaux"] = {"total_ht": total_ht, "total_ttc": total_ttc} - - if total_ht == 0 and total_ttc == 0: - diagnostic["avertissements"].append( - { - "severite": "ATTENTION", - "champ": "Totaux", - "message": "Tous les totaux sont Ă  0", - } - ) - - # 5. VĂ©rifier si la transformation est autorisĂ©e - transformations_valides = {(0, 10), (10, 30), (10, 60), (30, 60), (0, 60)} - - if (type_source, type_cible) not in transformations_valides: - diagnostic["problemes_detectes"].append( - { - "severite": "BLOQUANT", - "champ": "Transformation", - "message": f"Transformation {type_source} → {type_cible} non autorisĂ©e. " - f"Transformations valides: {transformations_valides}", - } - ) - - # RĂ©sumĂ© - nb_bloquants = sum( - 1 - for p in diagnostic["problemes_detectes"] - if p.get("severite") == "BLOQUANT" + if (type_source, type_cible) not in transformations_valides: + logger.error( + f"❌ Transformation non autorisĂ©e: {type_source} → {type_cible}" + ) + raise HTTPException( + 400, + f"Transformation non autorisĂ©e: type {type_source} → type {type_cible}. " + f"Transformations valides: {transformations_valides}", ) - nb_avertissements = len(diagnostic["avertissements"]) - diagnostic["resume"] = { - "peut_transformer": nb_bloquants == 0, - "nb_problemes_bloquants": nb_bloquants, - "nb_avertissements": nb_avertissements, - } + # Appel au connecteur Sage + resultat = sage.transformer_document(numero_source, type_source, type_cible) - if nb_bloquants == 0: - diagnostic["suggestions"].append( - "✅ Aucun problĂšme bloquant dĂ©tectĂ©. La transformation devrait fonctionner." - ) - else: - diagnostic["suggestions"].append( - f"❌ {nb_bloquants} problĂšme(s) bloquant(s) doivent ĂȘtre rĂ©solus avant la transformation." - ) + logger.info( + f"✅ Transformation rĂ©ussie: {numero_source} → " + f"{resultat.get('document_cible', '?')} " + f"({resultat.get('nb_lignes', 0)} lignes)" + ) - return {"success": True, "diagnostic": diagnostic} + return {"success": True, "data": resultat} except HTTPException: raise + except ValueError as e: + logger.error(f"❌ Erreur mĂ©tier transformation: {e}") + raise HTTPException(400, str(e)) except Exception as e: - logger.error(f"[DIAG] Erreur diagnostic transformation: {e}", exc_info=True) - raise HTTPException(500, str(e)) + logger.error(f"❌ Erreur technique transformation: {e}", exc_info=True) + raise HTTPException(500, f"Erreur transformation: {str(e)}") @app.post("/sage/documents/champ-libre", dependencies=[Depends(verify_token)]) @@ -1873,6 +1692,758 @@ def verifier_parametres_facture(): raise HTTPException(500, str(e)) +@app.get("/sage/diagnostic/statuts-globaux", dependencies=[Depends(verify_token)]) +def diagnostiquer_statuts_globaux(): + """ + 📊 MATRICE COMPLÈTE DES STATUTS SAGE + + Retourne pour CHAQUE type de document : + - Tous les statuts possibles avec leurs descriptions + - Les statuts requis pour transformation + - Les changements de statuts aprĂšs transformation + - Les restrictions de changement de statut + + Cette route analyse la base Sage pour dĂ©couvrir les rĂšgles rĂ©elles + """ + try: + if not sage or not sage.cial: + raise HTTPException(503, "Service Sage indisponible") + + with sage._com_context(), sage._lock_com: + factory = sage.cial.FactoryDocumentVente + + # DĂ©finition des types de documents + types_documents = { + 0: "Devis", + 10: "Bon de commande", + 20: "PrĂ©paration", + 30: "Bon de livraison", + 40: "Bon de retour", + 50: "Bon d'avoir", + 60: "Facture", + } + + # Descriptions standard des statuts Sage + descriptions_statuts = { + 0: "Brouillon", + 1: "Soumis/En attente", + 2: "AcceptĂ©/ValidĂ©", + 3: "RĂ©alisĂ© partiellement", + 4: "RĂ©alisĂ© totalement", + 5: "TransformĂ©", + 6: "AnnulĂ©", + } + + matrice_complete = {} + + logger.info( + "[DIAG] 🔍 Analyse des statuts pour tous les types de documents..." + ) + + # Pour chaque type de document + for type_doc, libelle_type in types_documents.items(): + logger.info(f"[DIAG] Analyse type {type_doc} ({libelle_type})...") + + analyse_type = { + "type": type_doc, + "libelle": libelle_type, + "statuts_observes": {}, + "exemples_par_statut": {}, + "nb_documents_total": 0, + } + + # Scanner tous les documents de ce type + index = 1 + max_scan = 1000 + + while index < max_scan: + try: + persist = factory.List(index) + if persist is None: + break + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + doc_type = getattr(doc, "DO_Type", -1) + + # Filtrer sur le type qu'on analyse + if doc_type != type_doc: + index += 1 + continue + + analyse_type["nb_documents_total"] += 1 + + # RĂ©cupĂ©rer le statut + statut = getattr(doc, "DO_Statut", -1) + + # Compter les statuts observĂ©s + if statut not in analyse_type["statuts_observes"]: + analyse_type["statuts_observes"][statut] = { + "count": 0, + "description": descriptions_statuts.get( + statut, f"Statut {statut}" + ), + "exemples": [], + } + + analyse_type["statuts_observes"][statut]["count"] += 1 + + # Garder quelques exemples + if ( + len(analyse_type["statuts_observes"][statut]["exemples"]) + < 3 + ): + numero = getattr(doc, "DO_Piece", "") + date = str(getattr(doc, "DO_Date", "")) + + analyse_type["statuts_observes"][statut]["exemples"].append( + { + "numero": numero, + "date": date, + "total_ttc": float( + getattr(doc, "DO_TotalTTC", 0.0) + ), + } + ) + + index += 1 + + except Exception as e: + index += 1 + continue + + # Trier les statuts par nombre d'occurrences + analyse_type["statuts_par_frequence"] = sorted( + [ + { + "statut": s, + "description": info["description"], + "count": info["count"], + "pourcentage": ( + round( + info["count"] + / analyse_type["nb_documents_total"] + * 100, + 1, + ) + if analyse_type["nb_documents_total"] > 0 + else 0 + ), + } + for s, info in analyse_type["statuts_observes"].items() + ], + key=lambda x: x["count"], + reverse=True, + ) + + matrice_complete[type_doc] = analyse_type + + logger.info( + f"[DIAG] ✅ Type {type_doc}: {analyse_type['nb_documents_total']} docs, " + f"{len(analyse_type['statuts_observes'])} statuts diffĂ©rents" + ) + + # RÈGLES DE TRANSFORMATION + regles_transformation = { + "transformations_valides": [ + { + "source_type": 0, + "source_libelle": "Devis", + "cible_type": 10, + "cible_libelle": "Bon de commande", + "statut_source_requis": [2], + "statut_source_requis_description": ["AcceptĂ©/ValidĂ©"], + "statut_source_apres": 5, + "statut_source_apres_description": "TransformĂ©", + "statut_cible_initial": 2, + "statut_cible_initial_description": "AcceptĂ©/ValidĂ©", + }, + { + "source_type": 10, + "source_libelle": "Bon de commande", + "cible_type": 30, + "cible_libelle": "Bon de livraison", + "statut_source_requis": [2], + "statut_source_requis_description": ["AcceptĂ©/ValidĂ©"], + "statut_source_apres": 5, + "statut_source_apres_description": "TransformĂ©", + "statut_cible_initial": 2, + "statut_cible_initial_description": "AcceptĂ©/ValidĂ©", + }, + { + "source_type": 10, + "source_libelle": "Bon de commande", + "cible_type": 60, + "cible_libelle": "Facture", + "statut_source_requis": [2], + "statut_source_requis_description": ["AcceptĂ©/ValidĂ©"], + "statut_source_apres": 5, + "statut_source_apres_description": "TransformĂ©", + "statut_cible_initial": 2, + "statut_cible_initial_description": "AcceptĂ©/ValidĂ©", + }, + { + "source_type": 30, + "source_libelle": "Bon de livraison", + "cible_type": 60, + "cible_libelle": "Facture", + "statut_source_requis": [2], + "statut_source_requis_description": ["AcceptĂ©/ValidĂ©"], + "statut_source_apres": 5, + "statut_source_apres_description": "TransformĂ©", + "statut_cible_initial": 2, + "statut_cible_initial_description": "AcceptĂ©/ValidĂ©", + }, + { + "source_type": 0, + "source_libelle": "Devis", + "cible_type": 60, + "cible_libelle": "Facture", + "statut_source_requis": [2], + "statut_source_requis_description": ["AcceptĂ©/ValidĂ©"], + "statut_source_apres": 5, + "statut_source_apres_description": "TransformĂ©", + "statut_cible_initial": 2, + "statut_cible_initial_description": "AcceptĂ©/ValidĂ©", + }, + ], + "statuts_bloquants_pour_transformation": [ + { + "statut": 5, + "description": "TransformĂ©", + "raison": "Le document a dĂ©jĂ  Ă©tĂ© transformĂ©", + }, + { + "statut": 6, + "description": "AnnulĂ©", + "raison": "Le document est annulĂ©", + }, + { + "statut": 3, + "description": "RĂ©alisĂ© partiellement", + "raison": "Un document cible existe probablement dĂ©jĂ  (transformation partielle effectuĂ©e)", + }, + { + "statut": 4, + "description": "RĂ©alisĂ© totalement", + "raison": "Le document a Ă©tĂ© entiĂšrement rĂ©alisĂ© (transformation dĂ©jĂ  effectuĂ©e)", + }, + ], + "changements_statut_autorises": { + "0_Brouillon": { + "vers": [2, 6], + "descriptions": ["AcceptĂ©/ValidĂ©", "AnnulĂ©"], + "note": "Un brouillon peut ĂȘtre acceptĂ© ou annulĂ©", + }, + "2_Accepte": { + "vers": [5, 6], + "descriptions": ["TransformĂ©", "AnnulĂ©"], + "note": "Un document acceptĂ© peut ĂȘtre transformĂ© ou annulĂ©", + }, + "5_Transforme": { + "vers": [], + "descriptions": [], + "note": "Un document transformĂ© ne peut plus changer de statut", + }, + "6_Annule": { + "vers": [], + "descriptions": [], + "note": "Un document annulĂ© ne peut plus changer de statut", + }, + }, + } + + return { + "success": True, + "matrice_statuts_par_type": matrice_complete, + "regles_transformation": regles_transformation, + "legende_statuts": descriptions_statuts, + "types_documents": types_documents, + "date_analyse": datetime.now().isoformat(), + "note": "Cette matrice est construite Ă  partir des documents rĂ©els dans votre base Sage", + } + + except Exception as e: + logger.error(f"[DIAG] Erreur diagnostic global: {e}", exc_info=True) + raise HTTPException(500, str(e)) + + +@app.get( + "/sage/diagnostic/statuts-permis/{numero}", dependencies=[Depends(verify_token)] +) +def diagnostiquer_statuts_permis(numero: str): + """ + 🔍 DIAGNOSTIC CRITIQUE: DĂ©couvre TOUS les statuts possibles pour un document + + Teste tous les statuts de 0 Ă  10 pour identifier lesquels sont valides + """ + try: + if not sage or not sage.cial: + raise HTTPException(503, "Service Sage indisponible") + + with sage._com_context(), sage._lock_com: + factory = sage.cial.FactoryDocumentVente + + # Chercher le document (tous types confondus) + persist = None + type_doc_trouve = None + + # Essayer ReadPiece pour diffĂ©rents types + for type_test in range(7): # 0-6 + try: + persist_test = factory.ReadPiece(type_test, numero) + if persist_test: + persist = persist_test + type_doc_trouve = type_test + logger.info( + f"[DIAG] Document {numero} trouvĂ© avec ReadPiece(type={type_test})" + ) + break + except: + continue + + # Si pas trouvĂ©, chercher dans List() + if not persist: + index = 1 + while index < 10000: + try: + persist_test = factory.List(index) + if persist_test is None: + break + + doc_test = win32com.client.CastTo( + persist_test, "IBODocumentVente3" + ) + doc_test.Read() + + if getattr(doc_test, "DO_Piece", "") == numero: + persist = persist_test + type_doc_trouve = getattr(doc_test, "DO_Type", -1) + logger.info( + f"[DIAG] Document {numero} trouvĂ© dans List() Ă  l'index {index}" + ) + break + + index += 1 + except: + index += 1 + + if not persist: + raise HTTPException(404, f"Document {numero} introuvable") + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + # Infos du document + statut_actuel = getattr(doc, "DO_Statut", -1) + type_actuel = getattr(doc, "DO_Type", -1) + + diagnostic = { + "numero": numero, + "type_document": type_actuel, + "type_libelle": { + 0: "Devis", + 10: "Bon de commande", + 20: "PrĂ©paration", + 30: "Bon de livraison", + 40: "Bon de retour", + 50: "Bon d'avoir", + 60: "Facture", + }.get(type_actuel, f"Type {type_actuel}"), + "statut_actuel": statut_actuel, + "statut_actuel_libelle": { + 0: "Brouillon", + 1: "Soumis/En attente", + 2: "AcceptĂ©/ValidĂ©", + 3: "RĂ©alisĂ© partiellement", + 4: "RĂ©alisĂ© totalement", + 5: "TransformĂ©", + 6: "AnnulĂ©", + }.get(statut_actuel, f"Statut {statut_actuel}"), + "tests_statuts": [], + } + + # Tester tous les statuts de 0 Ă  10 + logger.info(f"[DIAG] Test des statuts pour {numero}...") + + for statut_test in range(11): + resultat_test = { + "statut": statut_test, + "libelle": { + 0: "Brouillon", + 1: "Soumis/En attente", + 2: "AcceptĂ©/ValidĂ©", + 3: "RĂ©alisĂ© partiellement", + 4: "RĂ©alisĂ© totalement", + 5: "TransformĂ©", + 6: "AnnulĂ©", + 7: "Statut 7", + 8: "Statut 8", + 9: "Statut 9", + 10: "Statut 10", + }.get(statut_test, f"Statut {statut_test}"), + "autorise": False, + "erreur": None, + "est_statut_actuel": (statut_test == statut_actuel), + } + + # Si c'est le statut actuel, on sait qu'il est valide + if statut_test == statut_actuel: + resultat_test["autorise"] = True + resultat_test["note"] = "Statut actuel du document" + else: + # Tester le changement de statut + try: + # Relire le document + doc.Read() + + # Essayer de changer le statut + doc.DO_Statut = statut_test + + # Essayer d'Ă©crire + doc.Write() + + # Si on arrive ici, le statut est valide ! + resultat_test["autorise"] = True + resultat_test["note"] = "Changement de statut rĂ©ussi" + + logger.info(f"[DIAG] ✅ Statut {statut_test} AUTORISÉ") + + # Restaurer le statut d'origine immĂ©diatement + doc.Read() + doc.DO_Statut = statut_actuel + doc.Write() + + except Exception as e: + erreur_str = str(e) + resultat_test["autorise"] = False + resultat_test["erreur"] = erreur_str + + logger.debug( + f"[DIAG] ❌ Statut {statut_test} REFUSÉ: {erreur_str[:100]}" + ) + + # Restaurer en cas d'erreur + try: + doc.Read() + except: + pass + + diagnostic["tests_statuts"].append(resultat_test) + + # RĂ©sumĂ© + statuts_autorises = [ + t["statut"] for t in diagnostic["tests_statuts"] if t["autorise"] + ] + statuts_refuses = [ + t["statut"] for t in diagnostic["tests_statuts"] if not t["autorise"] + ] + + diagnostic["resume"] = { + "nb_statuts_autorises": len(statuts_autorises), + "statuts_autorises": statuts_autorises, + "statuts_autorises_libelles": [ + t["libelle"] for t in diagnostic["tests_statuts"] if t["autorise"] + ], + "nb_statuts_refuses": len(statuts_refuses), + "statuts_refuses": statuts_refuses, + } + + # Recommandations + recommendations = [] + + if 2 in statuts_autorises and statut_actuel == 0: + recommendations.append( + "✅ Vous pouvez passer ce document de 'Brouillon' (0) Ă  'AcceptĂ©' (2)" + ) + + if 5 in statuts_autorises: + recommendations.append( + "✅ Le statut 'TransformĂ©' (5) est disponible - utilisĂ© aprĂšs transformation" + ) + + if 6 in statuts_autorises: + recommendations.append("✅ Vous pouvez annuler ce document (statut 6)") + + if not any(s in statuts_autorises for s in [2, 3, 4]): + recommendations.append( + "⚠ Aucun statut de validation (2/3/4) n'est disponible - " + "le document a peut-ĂȘtre dĂ©jĂ  Ă©tĂ© traitĂ©" + ) + + diagnostic["recommendations"] = recommendations + + logger.info( + f"[DIAG] Statuts autorisĂ©s pour {numero}: " + f"{statuts_autorises} / RefusĂ©s: {statuts_refuses}" + ) + + return {"success": True, "diagnostic": diagnostic} + + except HTTPException: + raise + except Exception as e: + logger.error(f"[DIAG] Erreur diagnostic statuts: {e}", exc_info=True) + raise HTTPException(500, str(e)) + + +@app.get( + "/sage/diagnostic/erreur-transformation/{numero}", + dependencies=[Depends(verify_token)], +) +def diagnostiquer_erreur_transformation( + numero: str, type_source: int = Query(...), type_cible: int = Query(...) +): + """ + 🔍 DIAGNOSTIC AVANCÉ: Analyse pourquoi une transformation Ă©choue + + VĂ©rifie: + - Statut du document source + - Statuts autorisĂ©s + - Lignes du document + - Client associĂ© + - Champs obligatoires manquants + """ + try: + if not sage or not sage.cial: + raise HTTPException(503, "Service Sage indisponible") + + with sage._com_context(), sage._lock_com: + factory = sage.cial.FactoryDocumentVente + + # Lire le document source + persist = factory.ReadPiece(type_source, numero) + + if not persist: + persist = sage._find_document_in_list(numero, type_source) + + if not persist: + raise HTTPException( + 404, f"Document {numero} (type {type_source}) introuvable" + ) + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + diagnostic = { + "numero": numero, + "type_source": type_source, + "type_cible": type_cible, + "problemes_detectes": [], + "avertissements": [], + "suggestions": [], + } + + # 1. VĂ©rifier le statut + statut_actuel = getattr(doc, "DO_Statut", -1) + diagnostic["statut_actuel"] = statut_actuel + + if statut_actuel == 5: + diagnostic["problemes_detectes"].append( + { + "severite": "BLOQUANT", + "champ": "DO_Statut", + "valeur": 5, + "message": "Document dĂ©jĂ  transformĂ© (statut=5)", + } + ) + + elif statut_actuel == 6: + diagnostic["problemes_detectes"].append( + { + "severite": "BLOQUANT", + "champ": "DO_Statut", + "valeur": 6, + "message": "Document annulĂ© (statut=6)", + } + ) + + elif statut_actuel in [3, 4]: + diagnostic["avertissements"].append( + { + "severite": "ATTENTION", + "champ": "DO_Statut", + "valeur": statut_actuel, + "message": f"Document dĂ©jĂ  rĂ©alisĂ© (statut={statut_actuel}). " + f"Un document cible existe peut-ĂȘtre dĂ©jĂ .", + } + ) + + elif statut_actuel == 0: + diagnostic["suggestions"].append( + "Le document est en 'Brouillon' (statut=0). " + "Le systĂšme le passera automatiquement Ă  'AcceptĂ©' (statut=2) avant transformation." + ) + + # 2. VĂ©rifier le client + client_code = "" + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_code = getattr(client_obj, "CT_Num", "").strip() + except: + pass + + if not client_code: + diagnostic["problemes_detectes"].append( + { + "severite": "BLOQUANT", + "champ": "CT_Num", + "valeur": None, + "message": "Aucun client associĂ© au document", + } + ) + else: + diagnostic["client_code"] = client_code + + # 3. VĂ©rifier les lignes + try: + factory_lignes = getattr(doc, "FactoryDocumentLigne", None) or getattr( + doc, "FactoryDocumentVenteLigne", None + ) + + nb_lignes = 0 + lignes_problemes = [] + + if factory_lignes: + index = 1 + while index <= 100: + try: + ligne_p = factory_lignes.List(index) + if ligne_p is None: + break + + ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") + ligne.Read() + + nb_lignes += 1 + + # VĂ©rifier article + article_ref = "" + try: + article_ref = getattr(ligne, "AR_Ref", "").strip() + if not article_ref: + article_obj = getattr(ligne, "Article", None) + if article_obj: + article_obj.Read() + article_ref = getattr( + article_obj, "AR_Ref", "" + ).strip() + except: + pass + + if not article_ref: + lignes_problemes.append( + { + "ligne": index, + "probleme": "Aucune rĂ©fĂ©rence article", + } + ) + + # VĂ©rifier prix + prix = float(getattr(ligne, "DL_PrixUnitaire", 0.0)) + if prix == 0: + lignes_problemes.append( + {"ligne": index, "probleme": "Prix unitaire = 0"} + ) + + index += 1 + except: + break + + diagnostic["nb_lignes"] = nb_lignes + + if nb_lignes == 0: + diagnostic["problemes_detectes"].append( + { + "severite": "BLOQUANT", + "champ": "Lignes", + "valeur": 0, + "message": "Document vide (aucune ligne)", + } + ) + + if lignes_problemes: + diagnostic["avertissements"].append( + { + "severite": "ATTENTION", + "champ": "Lignes", + "message": f"{len(lignes_problemes)} ligne(s) avec des problĂšmes", + "details": lignes_problemes, + } + ) + + except Exception as e: + diagnostic["avertissements"].append( + { + "severite": "ERREUR", + "champ": "Lignes", + "message": f"Impossible de lire les lignes: {e}", + } + ) + + # 4. VĂ©rifier les totaux + total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) + total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) + + diagnostic["totaux"] = {"total_ht": total_ht, "total_ttc": total_ttc} + + if total_ht == 0 and total_ttc == 0: + diagnostic["avertissements"].append( + { + "severite": "ATTENTION", + "champ": "Totaux", + "message": "Tous les totaux sont Ă  0", + } + ) + + # 5. VĂ©rifier si la transformation est autorisĂ©e + transformations_valides = {(0, 10), (10, 30), (10, 60), (30, 60), (0, 60)} + + if (type_source, type_cible) not in transformations_valides: + diagnostic["problemes_detectes"].append( + { + "severite": "BLOQUANT", + "champ": "Transformation", + "message": f"Transformation {type_source} → {type_cible} non autorisĂ©e. " + f"Transformations valides: {transformations_valides}", + } + ) + + # RĂ©sumĂ© + nb_bloquants = sum( + 1 + for p in diagnostic["problemes_detectes"] + if p.get("severite") == "BLOQUANT" + ) + nb_avertissements = len(diagnostic["avertissements"]) + + diagnostic["resume"] = { + "peut_transformer": nb_bloquants == 0, + "nb_problemes_bloquants": nb_bloquants, + "nb_avertissements": nb_avertissements, + } + + if nb_bloquants == 0: + diagnostic["suggestions"].append( + "✅ Aucun problĂšme bloquant dĂ©tectĂ©. La transformation devrait fonctionner." + ) + else: + diagnostic["suggestions"].append( + f"❌ {nb_bloquants} problĂšme(s) bloquant(s) doivent ĂȘtre rĂ©solus avant la transformation." + ) + + return {"success": True, "diagnostic": diagnostic} + + except HTTPException: + raise + except Exception as e: + logger.error(f"[DIAG] Erreur diagnostic transformation: {e}", exc_info=True) + raise HTTPException(500, str(e)) + + # ===================================================== # LANCEMENT # =====================================================