diff --git a/main.py b/main.py index 6ea21ae..6cff5fc 100644 --- a/main.py +++ b/main.py @@ -146,6 +146,19 @@ class CommandeUpdateGatewayRequest(BaseModel): numero: str commande_data: Dict +class LivraisonCreateGatewayRequest(BaseModel): + """Création d'une livraison côté gateway""" + client_id: str + date_livraison: Optional[date] = None + lignes: List[Dict] + reference: Optional[str] = None + + +class LivraisonUpdateGatewayRequest(BaseModel): + """Modèle pour modification livraison côté gateway""" + numero: str + livraison_data: Dict + # ===================================================== # SÉCURITÉ @@ -2840,1470 +2853,52 @@ def modifier_commande_endpoint(req: CommandeUpdateGatewayRequest): raise HTTPException(500, str(e)) -# À ajouter dans main.py (Windows Gateway) - -@app.get("/sage/devis/{numero}/diagnostic-brouillon", dependencies=[Depends(verify_token)]) -def diagnostiquer_devis_brouillon(numero: str): +@app.post("/sage/livraisons/create", dependencies=[Depends(verify_token)]) +def creer_livraison_endpoint(req: LivraisonCreateGatewayRequest): """ - 🔍 DIAGNOSTIC: Cherche un devis brouillon partout dans Sage - - Teste toutes les méthodes de lecture possibles + ➕ Création d'une livraison (Bon de livraison) dans Sage """ try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") + # Vérifier que le client existe + client = sage.lire_client(req.client_id) + if not client: + raise HTTPException(404, f"Client {req.client_id} introuvable") - with sage._com_context(), sage._lock_com: - factory = sage.cial.FactoryDocumentVente - - diagnostic = { - "numero_recherche": numero, - "methodes_testees": [], - "document_trouve": False, - "details": None - } - - # ===== TEST 1: ReadPiece(0, numero) ===== - try: - persist_rp = factory.ReadPiece(0, numero) - if persist_rp: - doc_rp = win32com.client.CastTo(persist_rp, "IBODocumentVente3") - doc_rp.Read() - - diagnostic["methodes_testees"].append({ - "methode": "ReadPiece(0, numero)", - "succes": True, - "statut": getattr(doc_rp, "DO_Statut", -1), - "type": getattr(doc_rp, "DO_Type", -1) - }) - diagnostic["document_trouve"] = True - diagnostic["details"] = { - "numero": getattr(doc_rp, "DO_Piece", ""), - "type": getattr(doc_rp, "DO_Type", -1), - "statut": getattr(doc_rp, "DO_Statut", -1), - "total_ht": float(getattr(doc_rp, "DO_TotalHT", 0.0)), - "total_ttc": float(getattr(doc_rp, "DO_TotalTTC", 0.0)), - "client_code": getattr(doc_rp, "CT_Num", "") - } - else: - diagnostic["methodes_testees"].append({ - "methode": "ReadPiece(0, numero)", - "succes": False, - "erreur": "Retourne None" - }) - except Exception as e: - diagnostic["methodes_testees"].append({ - "methode": "ReadPiece(0, numero)", - "succes": False, - "erreur": str(e) - }) - - # ===== TEST 2: Recherche dans List() ===== - if not diagnostic["document_trouve"]: - try: - logger.info(f"🔍 Scan List() pour {numero}...") - index = 1 - trouve_index = None - - while index < 1000: - try: - persist_list = factory.List(index) - if persist_list is None: - break - - doc_list = win32com.client.CastTo(persist_list, "IBODocumentVente3") - doc_list.Read() - - piece = getattr(doc_list, "DO_Piece", "") - type_doc = getattr(doc_list, "DO_Type", -1) - - if type_doc == 0 and piece == numero: - trouve_index = index - diagnostic["document_trouve"] = True - diagnostic["details"] = { - "numero": piece, - "type": type_doc, - "statut": getattr(doc_list, "DO_Statut", -1), - "total_ht": float(getattr(doc_list, "DO_TotalHT", 0.0)), - "total_ttc": float(getattr(doc_list, "DO_TotalTTC", 0.0)), - "client_code": getattr(doc_list, "CT_Num", ""), - "trouve_a_index": index - } - break - - index += 1 - except: - index += 1 - continue - - diagnostic["methodes_testees"].append({ - "methode": f"List() scan (0-{index})", - "succes": diagnostic["document_trouve"], - "trouve_a_index": trouve_index if diagnostic["document_trouve"] else None - }) - - except Exception as e: - diagnostic["methodes_testees"].append({ - "methode": "List() scan", - "succes": False, - "erreur": str(e) - }) - - # ===== TEST 3: Compter tous les devis brouillons récents ===== - try: - nb_brouillons_recents = 0 - index = 1 - - while index < 100: - try: - persist = factory.List(index) - if persist is None: - break - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - if getattr(doc, "DO_Type", -1) == 0 and getattr(doc, "DO_Statut", -1) == 0: - nb_brouillons_recents += 1 - - index += 1 - except: - index += 1 - continue - - diagnostic["statistiques"] = { - "nb_brouillons_dans_100_premiers": nb_brouillons_recents - } - - except Exception as e: - diagnostic["statistiques"] = { - "erreur": str(e) - } - - return { - "success": True, - "diagnostic": diagnostic - } - - except HTTPException: - raise - except Exception as e: - logger.error(f"[DIAG] Erreur: {e}", exc_info=True) - raise HTTPException(500, str(e)) - -# À ajouter dans main.py (Windows Gateway) - -@app.get("/sage/diagnostic/commande/{numero}", dependencies=[Depends(verify_token)]) -def diagnostiquer_commande_complete(numero: str): - """ - 🔍 DIAGNOSTIC ULTRA-COMPLET d'une commande - - Analyse TOUS les aspects qui peuvent bloquer une modification: - - Statut et type réels - - Champs verrouillés - - Liens avec d'autres documents - - Permissions - - Propriétés COM accessibles - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") + # Préparer les données pour le connecteur + livraison_data = { + "client": {"code": req.client_id, "intitule": ""}, + "date_livraison": req.date_livraison or date.today(), + "reference": req.reference, + "lignes": req.lignes, + } - with sage._com_context(), sage._lock_com: - factory = sage.cial.FactoryDocumentVente - - diagnostic = { - "numero": numero, - "trouve": False, - "methode_lecture": None, - "proprietes_document": {}, - "tests_modification": {}, - "liens_documents": {}, - "diagnostic_lignes": {}, - "conclusions": [] - } - - # ===== ÉTAPE 1: TROUVER LA COMMANDE ===== - persist = None - - # Test 1: ReadPiece avec différents types - for type_test in [10, 3, settings.SAGE_TYPE_BON_COMMANDE]: - try: - persist_test = factory.ReadPiece(type_test, numero) - if persist_test: - persist = persist_test - diagnostic["methode_lecture"] = f"ReadPiece(type={type_test})" - diagnostic["trouve"] = True - break - except: - continue - - # Test 2: Scan List() - if not persist: - logger.info(f"[DIAG] Scan List() pour {numero}...") - 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 - diagnostic["methode_lecture"] = f"List() à index {index}" - diagnostic["trouve"] = True - break - - index += 1 - except: - index += 1 - - if not persist: - diagnostic["conclusions"].append("❌ DOCUMENT INTROUVABLE") - return {"success": False, "diagnostic": diagnostic} - - # ===== ÉTAPE 2: CAST ET LECTURE ===== - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - # ===== ÉTAPE 3: PROPRIÉTÉS CRITIQUES ===== - proprietes_critiques = [ - "DO_Piece", "DO_Type", "DO_Statut", "DO_Date", - "DO_TotalHT", "DO_TotalTTC", "DO_Ref", - "CT_Num", "DO_Domaine", "DO_Souche", - "DO_Transfere", "DO_Valide", "DO_Cloture" - ] - - for prop in proprietes_critiques: - try: - valeur = getattr(doc, prop, "N/A") - diagnostic["proprietes_document"][prop] = { - "valeur": str(valeur), - "type": type(valeur).__name__ - } - except Exception as e: - diagnostic["proprietes_document"][prop] = { - "erreur": str(e) - } - - # ===== ÉTAPE 4: VÉRIFICATION TRANSFORMATION ===== - numero_doc = diagnostic["proprietes_document"].get("DO_Piece", {}).get("valeur", numero) - type_doc = diagnostic["proprietes_document"].get("DO_Type", {}).get("valeur", "10") - - try: - type_doc_int = int(type_doc) - verification = sage.verifier_si_deja_transforme(numero_doc, type_doc_int) - - diagnostic["liens_documents"] = { - "deja_transforme": verification.get("deja_transforme", False), - "nb_transformations": verification.get("nb_transformations", 0), - "documents_cibles": verification.get("documents_cibles", []) - } - - if verification.get("deja_transforme"): - diagnostic["conclusions"].append( - f"⚠️ TRANSFORMÉE {verification['nb_transformations']} fois - " - f"Docs cibles: {[d['numero'] for d in verification['documents_cibles']]}" - ) - except Exception as e: - diagnostic["liens_documents"]["erreur"] = str(e) - - # ===== ÉTAPE 5: ANALYSE DES LIGNES ===== - try: - factory_lignes = getattr(doc, "FactoryDocumentLigne", None) - if not factory_lignes: - factory_lignes = getattr(doc, "FactoryDocumentVenteLigne", None) - - nb_lignes = 0 - lignes_detail = [] - - 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 - - # Propriétés de la ligne - ligne_info = { - "index": index, - "AR_Ref": getattr(ligne, "AR_Ref", ""), - "DL_Design": getattr(ligne, "DL_Design", ""), - "DL_Qte": float(getattr(ligne, "DL_Qte", 0.0)), - "DL_PrixUnitaire": float(getattr(ligne, "DL_PrixUnitaire", 0.0)), - } - - # ✅ TEST CRITIQUE: Peut-on ÉCRIRE sur cette ligne ? - try: - # Sauvegarder la valeur originale - qte_originale = ligne_info["DL_Qte"] - - # Tenter de modifier - ligne.DL_Qte = qte_originale - ligne.Write() - - ligne_info["peut_ecrire"] = True - - # Restaurer - ligne.Read() - except Exception as e: - ligne_info["peut_ecrire"] = False - ligne_info["erreur_ecriture"] = str(e)[:100] - - lignes_detail.append(ligne_info) - index += 1 - - except Exception as e: - logger.debug(f"Erreur ligne {index}: {e}") - break - - diagnostic["diagnostic_lignes"] = { - "nb_lignes": nb_lignes, - "lignes": lignes_detail, - "nb_lignes_modifiables": sum(1 for l in lignes_detail if l.get("peut_ecrire")) - } - - except Exception as e: - diagnostic["diagnostic_lignes"]["erreur"] = str(e) - - # ===== ÉTAPE 6: TESTS DE MODIFICATION ===== - - # Test 1: Peut-on écrire le document ? - try: - doc.Write() - diagnostic["tests_modification"]["write_basique"] = { - "succes": True, - "note": "Write() sans modification fonctionne" - } - except Exception as e: - diagnostic["tests_modification"]["write_basique"] = { - "succes": False, - "erreur": str(e) - } - diagnostic["conclusions"].append(f"❌ Write() basique ÉCHOUE: {str(e)[:100]}") - - # Test 2: Peut-on modifier DO_Ref ? - try: - ref_originale = getattr(doc, "DO_Ref", "") - doc.DO_Ref = ref_originale - doc.Write() - doc.Read() - - diagnostic["tests_modification"]["modification_do_ref"] = { - "succes": True - } - except Exception as e: - diagnostic["tests_modification"]["modification_do_ref"] = { - "succes": False, - "erreur": str(e) - } - - # Test 3: Peut-on modifier le statut ? - statut_actuel = int(diagnostic["proprietes_document"].get("DO_Statut", {}).get("valeur", "0")) - - try: - doc.DO_Statut = statut_actuel - doc.Write() - doc.Read() - - diagnostic["tests_modification"]["modification_statut"] = { - "succes": True - } - except Exception as e: - diagnostic["tests_modification"]["modification_statut"] = { - "succes": False, - "erreur": str(e) - } - diagnostic["conclusions"].append(f"❌ Modification statut BLOQUÉE: {str(e)[:100]}") - - # ===== ÉTAPE 7: COMPARAISON AVEC BC00067 (qui fonctionne) ===== - if numero != "BC00067": - try: - # Lire BC00067 pour comparaison - persist_ref = None - - for type_test in [10, 3, settings.SAGE_TYPE_BON_COMMANDE]: - try: - persist_ref = factory.ReadPiece(type_test, "BC00067") - if persist_ref: - break - except: - continue - - if persist_ref: - doc_ref = win32com.client.CastTo(persist_ref, "IBODocumentVente3") - doc_ref.Read() - - comparaison = {} - - for prop in proprietes_critiques: - try: - val_actuelle = getattr(doc, prop, None) - val_ref = getattr(doc_ref, prop, None) - - if val_actuelle != val_ref: - comparaison[prop] = { - "actuelle": str(val_actuelle), - "bc00067": str(val_ref), - "different": True - } - except: - pass - - diagnostic["comparaison_bc00067"] = comparaison - - if comparaison: - diagnostic["conclusions"].append( - f"🔍 DIFFÉRENCES avec BC00067: {list(comparaison.keys())}" - ) - - except Exception as e: - diagnostic["comparaison_bc00067"] = {"erreur": str(e)} - - # ===== ÉTAPE 8: CONCLUSIONS FINALES ===== - - # Statut - if statut_actuel == 5: - diagnostic["conclusions"].append("❌ STATUT = 5 (Transformé) - MODIFICATION IMPOSSIBLE") - elif statut_actuel == 6: - diagnostic["conclusions"].append("❌ STATUT = 6 (Annulé) - MODIFICATION IMPOSSIBLE") - - # Transformation - if diagnostic["liens_documents"].get("deja_transforme"): - diagnostic["conclusions"].append("⚠️ Document déjà transformé - Risque de blocage") - - # Write basique - if not diagnostic["tests_modification"].get("write_basique", {}).get("succes"): - diagnostic["conclusions"].append("❌ CRITIQUE: Write() basique échoue - Document verrouillé") - - # Lignes - nb_lignes_modif = diagnostic["diagnostic_lignes"].get("nb_lignes_modifiables", 0) - nb_lignes_total = diagnostic["diagnostic_lignes"].get("nb_lignes", 0) - - if nb_lignes_total > 0 and nb_lignes_modif == 0: - diagnostic["conclusions"].append("❌ AUCUNE ligne modifiable - Document figé") - elif nb_lignes_modif < nb_lignes_total: - diagnostic["conclusions"].append(f"⚠️ Seulement {nb_lignes_modif}/{nb_lignes_total} lignes modifiables") - - # Si aucun problème détecté - if not diagnostic["conclusions"]: - diagnostic["conclusions"].append("✅ Aucun blocage évident détecté") - - return { - "success": True, - "diagnostic": diagnostic - } - - except HTTPException: - raise + resultat = sage.creer_livraison_enrichi(livraison_data) + return {"success": True, "data": resultat} + + except ValueError as e: + logger.warning(f"Erreur métier création livraison: {e}") + raise HTTPException(400, str(e)) except Exception as e: - logger.error(f"[DIAG] Erreur diagnostic complet: {e}", exc_info=True) + logger.error(f"Erreur technique création livraison: {e}") raise HTTPException(500, str(e)) -@app.get("/sage/diagnostic/comparer-commandes", dependencies=[Depends(verify_token)]) -def comparer_commandes_modifiables(): +@app.post("/sage/livraisons/update", dependencies=[Depends(verify_token)]) +def modifier_livraison_endpoint(req: LivraisonUpdateGatewayRequest): """ - 🔍 COMPARAISON GLOBALE: BC00067 (fonctionne) vs autres (bloquées) - - Scanne les 20 premières commandes et compare leurs propriétés - pour identifier LE facteur qui bloque les modifications + ✏️ Modification d'une livraison dans Sage """ try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") + resultat = sage.modifier_livraison(req.numero, req.livraison_data) + return {"success": True, "data": resultat} - with sage._com_context(), sage._lock_com: - factory = sage.cial.FactoryDocumentVente - - commandes_analysees = [] - bc00067_ref = None - - index = 1 - max_scan = 500 # Scanner plus large - - logger.info("[DIAG] Scan de toutes les commandes...") - - while index < max_scan and len(commandes_analysees) < 50: - try: - persist = factory.List(index) - if persist is None: - break - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - # Filtrer sur commandes (type 10 ou 3) - doc_type = getattr(doc, "DO_Type", -1) - if doc_type not in [3, 10, settings.SAGE_TYPE_BON_COMMANDE]: - index += 1 - continue - - numero = getattr(doc, "DO_Piece", "") - - analyse = { - "numero": numero, - "type": doc_type, - "statut": getattr(doc, "DO_Statut", -1), - "date": str(getattr(doc, "DO_Date", "")), - "total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)), - "reference": getattr(doc, "DO_Ref", ""), - "client": getattr(doc, "CT_Num", ""), - } - - # Propriétés supplémentaires - for prop in ["DO_Transfere", "DO_Valide", "DO_Cloture", "DO_Souche", "DO_Domaine"]: - try: - analyse[prop] = str(getattr(doc, prop, "N/A")) - except: - analyse[prop] = "N/A" - - # ✅ TEST: Peut-on écrire ? - try: - doc.Write() - analyse["peut_ecrire"] = True - except Exception as e: - analyse["peut_ecrire"] = False - analyse["erreur_ecriture"] = str(e)[:100] - - # Vérifier transformation - try: - verif = sage.verifier_si_deja_transforme(numero, doc_type) - analyse["deja_transforme"] = verif.get("deja_transforme", False) - analyse["nb_transformations"] = verif.get("nb_transformations", 0) - except: - analyse["deja_transforme"] = None - - commandes_analysees.append(analyse) - - # Sauvegarder BC00067 comme référence - if numero == "BC00067": - bc00067_ref = analyse - - index += 1 - - except Exception as e: - logger.debug(f"Erreur index {index}: {e}") - index += 1 - - # ===== ANALYSE COMPARATIVE ===== - - if not bc00067_ref: - return { - "success": False, - "erreur": "BC00067 introuvable pour comparaison" - } - - # Séparer modifiables vs bloquées - modifiables = [c for c in commandes_analysees if c.get("peut_ecrire")] - bloquees = [c for c in commandes_analysees if not c.get("peut_ecrire")] - - # Identifier les propriétés COMMUNES aux modifiables - # mais DIFFÉRENTES pour les bloquées - - proprietes_a_comparer = [ - "type", "statut", "DO_Transfere", "DO_Valide", - "DO_Cloture", "DO_Souche", "DO_Domaine", - "deja_transforme" - ] - - facteurs_distinctifs = {} - - for prop in proprietes_a_comparer: - # Valeur chez BC00067 - valeur_ref = bc00067_ref.get(prop) - - # Compter combien de modifiables ont la même valeur - modif_meme_valeur = sum( - 1 for c in modifiables - if c.get(prop) == valeur_ref - ) - - # Compter combien de bloquées ont une valeur différente - bloq_valeur_diff = sum( - 1 for c in bloquees - if c.get(prop) != valeur_ref - ) - - if bloq_valeur_diff > len(bloquees) * 0.5: # Plus de 50% - facteurs_distinctifs[prop] = { - "valeur_bc00067": str(valeur_ref), - "pct_modifiables_meme_valeur": round(modif_meme_valeur / len(modifiables) * 100, 1) if modifiables else 0, - "pct_bloquees_valeur_differente": round(bloq_valeur_diff / len(bloquees) * 100, 1) if bloquees else 0, - "correlation": "FORTE" if bloq_valeur_diff > len(bloquees) * 0.8 else "MOYENNE" - } - - # Statistiques - statistiques = { - "total_commandes": len(commandes_analysees), - "nb_modifiables": len(modifiables), - "nb_bloquees": len(bloquees), - "taux_modification": round(len(modifiables) / len(commandes_analysees) * 100, 1) if commandes_analysees else 0 - } - - # Distribution des statuts - statuts_modif = {} - statuts_bloq = {} - - for c in modifiables: - s = c.get("statut", -1) - statuts_modif[s] = statuts_modif.get(s, 0) + 1 - - for c in bloquees: - s = c.get("statut", -1) - statuts_bloq[s] = statuts_bloq.get(s, 0) + 1 - - return { - "success": True, - "bc00067_reference": bc00067_ref, - "statistiques": statistiques, - "facteurs_distinctifs": facteurs_distinctifs, - "distribution_statuts": { - "modifiables": statuts_modif, - "bloquees": statuts_bloq - }, - "exemples_modifiables": modifiables[:5], - "exemples_bloquees": bloquees[:5], - "conclusion": ( - "✅ Facteurs identifiés - Voir 'facteurs_distinctifs'" - if facteurs_distinctifs - else "⚠️ Aucun facteur distinctif évident - Nécessite analyse manuelle" - ) - } - + except ValueError as e: + logger.warning(f"Erreur métier modification livraison: {e}") + raise HTTPException(404, str(e)) except Exception as e: - logger.error(f"[DIAG] Erreur comparaison: {e}", exc_info=True) + logger.error(f"Erreur technique modification livraison: {e}") raise HTTPException(500, str(e)) - -@app.post("/sage/diagnostic/test-suppression-ligne/{numero}", dependencies=[Depends(verify_token)]) -def tester_suppression_ligne(numero: str): - """ - 🧪 TEST CRITIQUE: Peut-on supprimer les lignes de cette commande ? - - Teste la méthode WriteDefault() + Delete() sur la première ligne - """ - 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 - - # Trouver la commande - persist = None - for type_test in [10, 3, settings.SAGE_TYPE_BON_COMMANDE]: - try: - persist = factory.ReadPiece(type_test, numero) - if persist: - break - except: - continue - - if not persist: - raise HTTPException(404, f"Commande {numero} introuvable") - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - # Obtenir factory lignes - try: - factory_lignes = doc.FactoryDocumentLigne - except: - factory_lignes = doc.FactoryDocumentVenteLigne - - resultats = { - "numero": numero, - "tests": [] - } - - # Test 1: Lire la première ligne - try: - ligne_p = factory_lignes.List(1) - if ligne_p is None: - return { - "success": False, - "erreur": "Aucune ligne trouvée" - } - - ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") - ligne.Read() - - resultats["tests"].append({ - "test": "Lecture ligne 1", - "succes": True, - "article": getattr(ligne, "AR_Ref", ""), - "quantite": float(getattr(ligne, "DL_Qte", 0.0)) - }) - - except Exception as e: - resultats["tests"].append({ - "test": "Lecture ligne 1", - "succes": False, - "erreur": str(e) - }) - return {"success": False, "resultats": resultats} - - # Test 2: WriteDefault() - try: - ligne.WriteDefault() - resultats["tests"].append({ - "test": "WriteDefault()", - "succes": True - }) - except Exception as e: - resultats["tests"].append({ - "test": "WriteDefault()", - "succes": False, - "erreur": str(e) - }) - return {"success": False, "resultats": resultats} - - # Test 3: Delete() - try: - ligne.Delete() - resultats["tests"].append({ - "test": "Delete()", - "succes": True - }) - except Exception as e: - resultats["tests"].append({ - "test": "Delete()", - "succes": False, - "erreur": str(e) - }) - return {"success": False, "resultats": resultats} - - # Test 4: Write() sur le document - try: - doc.Write() - resultats["tests"].append({ - "test": "Write() document après suppression", - "succes": True - }) - except Exception as e: - resultats["tests"].append({ - "test": "Write() document après suppression", - "succes": False, - "erreur": str(e) - }) - - # Test 5: Vérifier que la ligne a bien été supprimée - try: - doc.Read() - - ligne_test = factory_lignes.List(1) - if ligne_test: - ligne_test_obj = win32com.client.CastTo(ligne_test, "IBODocumentLigne3") - ligne_test_obj.Read() - - article_apres = getattr(ligne_test_obj, "AR_Ref", "") - article_avant = resultats["tests"][0].get("article", "") - - if article_apres != article_avant: - resultats["tests"].append({ - "test": "Vérification suppression", - "succes": True, - "note": "La ligne a bien été supprimée (ligne suivante est devenue ligne 1)" - }) - else: - resultats["tests"].append({ - "test": "Vérification suppression", - "succes": False, - "note": "La même ligne est toujours en position 1 - Suppression échouée" - }) - else: - resultats["tests"].append({ - "test": "Vérification suppression", - "succes": True, - "note": "Aucune ligne trouvée - Document maintenant vide" - }) - - except Exception as e: - resultats["tests"].append({ - "test": "Vérification suppression", - "succes": False, - "erreur": str(e) - }) - - # Conclusion - tous_succes = all(t.get("succes") for t in resultats["tests"]) - - resultats["conclusion"] = ( - "✅ TOUTES les opérations de suppression fonctionnent" - if tous_succes - else "❌ Certaines opérations échouent - Voir détails" - ) - - return { - "success": True, - "resultats": resultats - } - - except HTTPException: - raise - except Exception as e: - logger.error(f"[TEST] Erreur test suppression: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - - # 🔍 SCRIPT DE DIAGNOSTIC ULTRA-DÉTAILLÉ -# À ajouter dans main.py (Windows Gateway) - -@app.post("/sage/diagnostic/modification-commande/{numero}", dependencies=[Depends(verify_token)]) -def diagnostiquer_modification_commande( - numero: str, - test_date: bool = Query(False, description="Tester modification date"), - test_statut: bool = Query(False, description="Tester modification statut"), - test_lignes: bool = Query(False, description="Tester modification lignes") -): - """ - 🔬 DIAGNOSTIC ULTRA-DÉTAILLÉ : Modification Commande - - Ce script va : - 1. Capturer TOUS les champs avant modification - 2. Tenter chaque type de modification séparément - 3. Identifier EXACTEMENT quel champ pose problème - 4. Vérifier si le client reste attaché - """ - 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 - - diagnostic = { - "numero": numero, - "etapes": [], - "champs_avant": {}, - "champs_apres": {}, - "tests_modifications": [], - "erreurs_detectees": [], - "recommandations": [] - } - - # ======================================== - # ÉTAPE 1 : CHARGER LE DOCUMENT - # ======================================== - diagnostic["etapes"].append("🔍 Chargement document...") - - persist = None - for type_test in [10, 3, settings.SAGE_TYPE_BON_COMMANDE]: - try: - persist = factory.ReadPiece(type_test, numero) - if persist: - diagnostic["type_utilise"] = type_test - break - except: - continue - - if not persist: - raise HTTPException(404, f"Commande {numero} introuvable") - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - diagnostic["etapes"].append("✅ Document chargé") - - # ======================================== - # ÉTAPE 2 : CAPTURER L'ÉTAT INITIAL COMPLET - # ======================================== - diagnostic["etapes"].append("📸 Capture état initial...") - - champs_critiques = [ - # Identification - "DO_Piece", "DO_Type", "DO_Domaine", "DO_Souche", - - # Client (CRITIQUE) - "CT_Num", - - # Dates - "DO_Date", "DO_DateLivr", - - # Statut - "DO_Statut", - - # Montants - "DO_TotalHT", "DO_TotalTTC", - - # Références - "DO_Ref", - - # Champs métier - "DO_Expedit", "DO_NbFacture", "DO_BLFact", - "DO_TxEscompte", "DO_Reliquat", "DO_Imprim", - - # Champs comptables - "DO_Condition", "DO_Tarif", "DO_Colisage", - "DO_TypeColis", "DO_Transaction", "DO_Langue", - "DO_Ecart", "DO_Regime", - - # Flags - "DO_Valide", "DO_Transfere", "DO_Cloture", - "DO_NoWeb", "DO_Attente", "DO_Provenance" - ] - - for champ in champs_critiques: - try: - valeur = getattr(doc, champ, None) - - # Conversion en string pour éviter les problèmes de sérialisation - if valeur is None: - valeur_str = "NULL" - elif isinstance(valeur, bool): - valeur_str = "True" if valeur else "False" - elif isinstance(valeur, (int, float)): - valeur_str = str(valeur) - else: - valeur_str = str(valeur) - - diagnostic["champs_avant"][champ] = { - "valeur": valeur_str, - "type": type(valeur).__name__ if valeur is not None else "NoneType" - } - - except Exception as e: - diagnostic["champs_avant"][champ] = { - "erreur": str(e)[:100] - } - - # ======================================== - # ÉTAPE 3 : VÉRIFIER LE CLIENT (ULTRA-CRITIQUE) - # ======================================== - diagnostic["etapes"].append("👤 Vérification client...") - - client_info = { - "avant_modification": {} - } - - try: - # Méthode 1 : Via CT_Num - client_code_direct = getattr(doc, "CT_Num", "") - client_info["avant_modification"]["CT_Num_direct"] = client_code_direct - - # Méthode 2 : Via objet Client - client_obj = getattr(doc, "Client", None) - if client_obj: - client_obj.Read() - client_info["avant_modification"]["Client_obj_exists"] = True - client_info["avant_modification"]["Client_CT_Num"] = getattr(client_obj, "CT_Num", "").strip() - client_info["avant_modification"]["Client_CT_Intitule"] = getattr(client_obj, "CT_Intitule", "").strip() - else: - client_info["avant_modification"]["Client_obj_exists"] = False - diagnostic["erreurs_detectees"].append({ - "severite": "CRITIQUE", - "message": "Objet Client est NULL avant toute modification" - }) - - except Exception as e: - client_info["avant_modification"]["erreur"] = str(e) - diagnostic["erreurs_detectees"].append({ - "severite": "CRITIQUE", - "message": f"Impossible de lire le client: {e}" - }) - - diagnostic["client"] = client_info - - # ======================================== - # ÉTAPE 4 : TEST WRITE() BASIQUE (SANS MODIFICATION) - # ======================================== - diagnostic["etapes"].append("🧪 Test Write() sans modification...") - - test_write_basique = { - "description": "Write() sans aucune modification", - "succes": False - } - - try: - doc.Write() - test_write_basique["succes"] = True - test_write_basique["note"] = "✅ Write() basique fonctionne" - - except Exception as e: - test_write_basique["succes"] = False - test_write_basique["erreur"] = str(e) - test_write_basique["note"] = "❌ CRITIQUE: Write() échoue même sans modification" - - diagnostic["erreurs_detectees"].append({ - "severite": "BLOQUANT", - "message": f"Write() basique échoue: {str(e)[:200]}" - }) - - diagnostic["tests_modifications"].append(test_write_basique) - - # Si Write() basique échoue, inutile de continuer - if not test_write_basique["succes"]: - diagnostic["recommandations"].append( - "❌ Le document est VERROUILLÉ - Impossible de le modifier même sans changement" - ) - return {"success": False, "diagnostic": diagnostic} - - # ======================================== - # ÉTAPE 5 : TEST MODIFICATION DATE - # ======================================== - if test_date: - diagnostic["etapes"].append("📅 Test modification date...") - - test_date_modif = { - "description": "Modification de DO_Date", - "succes": False - } - - try: - # Sauvegarder la date originale - date_originale = getattr(doc, "DO_Date", None) - - # Réaffecter la même date - doc.DO_Date = date_originale - - # ✅ CRITIQUE : Vérifier si le client est toujours là - client_obj_test = getattr(doc, "Client", None) - if client_obj_test: - client_obj_test.Read() - test_date_modif["client_apres_date"] = getattr(client_obj_test, "CT_Num", "") - else: - test_date_modif["client_apres_date"] = "NULL" - diagnostic["erreurs_detectees"].append({ - "severite": "CRITIQUE", - "etape": "Après modification date", - "message": "Le client a DISPARU après modification de DO_Date" - }) - - # Écrire - doc.Write() - - test_date_modif["succes"] = True - test_date_modif["note"] = "✅ Modification date OK" - - except Exception as e: - test_date_modif["succes"] = False - test_date_modif["erreur"] = str(e) - test_date_modif["note"] = f"❌ Échec: {str(e)[:100]}" - - diagnostic["erreurs_detectees"].append({ - "severite": "BLOQUANT", - "etape": "Modification date", - "message": str(e) - }) - - diagnostic["tests_modifications"].append(test_date_modif) - - # Relire le document - doc.Read() - - # ======================================== - # ÉTAPE 6 : TEST MODIFICATION STATUT - # ======================================== - if test_statut: - diagnostic["etapes"].append("📊 Test modification statut...") - - test_statut_modif = { - "description": "Modification de DO_Statut", - "succes": False - } - - try: - # Sauvegarder le statut original - statut_original = getattr(doc, "DO_Statut", 0) - - # Réaffecter le même statut - doc.DO_Statut = statut_original - - # Vérifier client - client_obj_test = getattr(doc, "Client", None) - if client_obj_test: - client_obj_test.Read() - test_statut_modif["client_apres_statut"] = getattr(client_obj_test, "CT_Num", "") - else: - test_statut_modif["client_apres_statut"] = "NULL" - diagnostic["erreurs_detectees"].append({ - "severite": "CRITIQUE", - "etape": "Après modification statut", - "message": "Le client a DISPARU après modification de DO_Statut" - }) - - # Écrire - doc.Write() - - test_statut_modif["succes"] = True - test_statut_modif["note"] = "✅ Modification statut OK" - - except Exception as e: - test_statut_modif["succes"] = False - test_statut_modif["erreur"] = str(e) - test_statut_modif["note"] = f"❌ Échec: {str(e)[:100]}" - - diagnostic["erreurs_detectees"].append({ - "severite": "BLOQUANT", - "etape": "Modification statut", - "message": str(e) - }) - - diagnostic["tests_modifications"].append(test_statut_modif) - - # Relire - doc.Read() - - # ======================================== - # ÉTAPE 7 : TEST MODIFICATION LIGNES - # ======================================== - if test_lignes: - diagnostic["etapes"].append("📦 Test modification lignes...") - - test_lignes_modif = { - "description": "Modification lignes", - "succes": False, - "details": [] - } - - try: - # Obtenir factory lignes - try: - factory_lignes = doc.FactoryDocumentLigne - except: - factory_lignes = doc.FactoryDocumentVenteLigne - - # Lire la première ligne - ligne_p = factory_lignes.List(1) - if ligne_p is None: - test_lignes_modif["note"] = "⚠️ Aucune ligne trouvée" - else: - ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") - ligne.Read() - - # Sauvegarder quantité originale - qte_originale = float(getattr(ligne, "DL_Qte", 0.0)) - - test_lignes_modif["details"].append({ - "action": "Lecture ligne 1", - "quantite_originale": qte_originale - }) - - # Réaffecter la même quantité - ligne.DL_Qte = qte_originale - ligne.Write() - - test_lignes_modif["details"].append({ - "action": "Write() ligne", - "succes": True - }) - - # ✅ CRITIQUE : Vérifier si le client du document est toujours là - doc.Read() - client_obj_test = getattr(doc, "Client", None) - if client_obj_test: - client_obj_test.Read() - test_lignes_modif["client_apres_lignes"] = getattr(client_obj_test, "CT_Num", "") - else: - test_lignes_modif["client_apres_lignes"] = "NULL" - diagnostic["erreurs_detectees"].append({ - "severite": "CRITIQUE", - "etape": "Après modification lignes", - "message": "Le client a DISPARU après modification des lignes" - }) - - # Écrire le document - doc.Write() - - test_lignes_modif["succes"] = True - test_lignes_modif["note"] = "✅ Modification lignes OK" - - except Exception as e: - test_lignes_modif["succes"] = False - test_lignes_modif["erreur"] = str(e) - test_lignes_modif["note"] = f"❌ Échec: {str(e)[:100]}" - - diagnostic["erreurs_detectees"].append({ - "severite": "BLOQUANT", - "etape": "Modification lignes", - "message": str(e) - }) - - diagnostic["tests_modifications"].append(test_lignes_modif) - - # ======================================== - # ÉTAPE 8 : CAPTURER L'ÉTAT FINAL - # ======================================== - diagnostic["etapes"].append("📸 Capture état final...") - - doc.Read() - - for champ in champs_critiques: - try: - valeur = getattr(doc, champ, None) - - if valeur is None: - valeur_str = "NULL" - elif isinstance(valeur, bool): - valeur_str = "True" if valeur else "False" - elif isinstance(valeur, (int, float)): - valeur_str = str(valeur) - else: - valeur_str = str(valeur) - - diagnostic["champs_apres"][champ] = { - "valeur": valeur_str, - "type": type(valeur).__name__ if valeur is not None else "NoneType" - } - - except Exception as e: - diagnostic["champs_apres"][champ] = { - "erreur": str(e)[:100] - } - - # ======================================== - # ÉTAPE 9 : COMPARAISON AVANT/APRÈS - # ======================================== - diagnostic["etapes"].append("🔍 Comparaison avant/après...") - - champs_modifies = [] - champs_vides = [] - - for champ in champs_critiques: - avant = diagnostic["champs_avant"].get(champ, {}).get("valeur", "N/A") - apres = diagnostic["champs_apres"].get(champ, {}).get("valeur", "N/A") - - if avant != apres: - champs_modifies.append({ - "champ": champ, - "avant": avant, - "apres": apres - }) - - # Vérifier si un champ critique est devenu vide - if apres in ["NULL", "", "0"] and avant not in ["NULL", "", "0"]: - if champ in ["CT_Num", "DO_Date", "DO_Type", "DO_Souche"]: - champs_vides.append({ - "champ": champ, - "avant": avant, - "apres": apres, - "severite": "CRITIQUE" - }) - - diagnostic["comparaison"] = { - "champs_modifies": champs_modifies, - "champs_vides": champs_vides - } - - # ======================================== - # ÉTAPE 10 : RECOMMANDATIONS - # ======================================== - - if champs_vides: - diagnostic["recommandations"].append( - f"❌ CRITIQUE: {len(champs_vides)} champ(s) critique(s) sont devenus vides: " - f"{[c['champ'] for c in champs_vides]}" - ) - - if any(e["severite"] == "CRITIQUE" for e in diagnostic["erreurs_detectees"] if "client" in e.get("message", "").lower()): - diagnostic["recommandations"].append( - "❌ PROBLÈME CLIENT DÉTECTÉ: Le client se perd pendant les modifications. " - "Vous devez réassocier le client APRÈS chaque modification." - ) - - if not diagnostic["erreurs_detectees"]: - diagnostic["recommandations"].append( - "✅ Aucun problème détecté dans les tests. Le document semble modifiable." - ) - - return { - "success": True, - "diagnostic": diagnostic - } - - except HTTPException: - raise - except Exception as e: - logger.error(f"[DIAG] Erreur diagnostic modification: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -# À ajouter dans main.py (Windows Gateway) - -@app.get("/sage/diagnostic/comparer-bc00067-bc00068") -def comparer_bc00067_bc00068(): - """ - 🔬 DIAGNOSTIC ULTRA-DÉTAILLÉ : BC00067 (échoue) vs BC00068 (marche) - - Compare TOUS les champs pour identifier LA différence qui fait que BC00067 ne peut pas être modifié - """ - 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 - - comparaison = { - "bc00067": {}, - "bc00068": {}, - "differences": [], - "tests": [] - } - - # ======================================== - # CHARGER LES DEUX DOCUMENTS - # ======================================== - docs = {} - - for numero in ["BC00067", "BC00068"]: - persist = None - - for type_test in [10, 3, settings.SAGE_TYPE_BON_COMMANDE]: - try: - persist = factory.ReadPiece(type_test, numero) - if persist: - break - except: - continue - - if not persist: - return { - "success": False, - "error": f"{numero} introuvable" - } - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - docs[numero] = doc - - # ======================================== - # COMPARER TOUS LES CHAMPS CRITIQUES - # ======================================== - champs_a_comparer = [ - # Identification - "DO_Piece", "DO_Type", "DO_Domaine", "DO_Souche", - - # Client - "CT_Num", - - # Dates - "DO_Date", "DO_DateLivr", - - # Statut et état - "DO_Statut", "DO_Valide", "DO_Transfere", "DO_Cloture", - - # Montants - "DO_TotalHT", "DO_TotalTTC", - - # Références - "DO_Ref", - - # Champs métier - "DO_Expedit", "DO_NbFacture", "DO_BLFact", - "DO_TxEscompte", "DO_Reliquat", "DO_Imprim", - - # Champs comptables - "DO_Condition", "DO_Tarif", "DO_Colisage", - "DO_TypeColis", "DO_Transaction", "DO_Langue", - "DO_Ecart", "DO_Regime", - - # Flags - "DO_NoWeb", "DO_Attente", "DO_Provenance" - ] - - for champ in champs_a_comparer: - try: - val_67 = getattr(docs["BC00067"], champ, None) - val_68 = getattr(docs["BC00068"], champ, None) - - # Convertir en string pour comparaison - str_67 = str(val_67) if val_67 is not None else "NULL" - str_68 = str(val_68) if val_68 is not None else "NULL" - - comparaison["bc00067"][champ] = str_67 - comparaison["bc00068"][champ] = str_68 - - if str_67 != str_68: - comparaison["differences"].append({ - "champ": champ, - "bc00067": str_67, - "bc00068": str_68, - "critique": champ in [ - "DO_Type", "DO_Domaine", "DO_Souche", - "DO_Statut", "DO_Transfere", "DO_Cloture", - "CT_Num" - ] - }) - - except Exception as e: - comparaison["differences"].append({ - "champ": champ, - "erreur": str(e) - }) - - # ======================================== - # TEST WRITE() SUR CHAQUE DOCUMENT - # ======================================== - for numero in ["BC00067", "BC00068"]: - test_result = { - "numero": numero, - "write_basique": False, - "client_avant": "", - "client_apres": "", - "erreur": None - } - - doc = docs[numero] - - try: - # Client avant - client_obj = getattr(doc, "Client", None) - if client_obj: - client_obj.Read() - test_result["client_avant"] = getattr(client_obj, "CT_Num", "") - - # Test Write() - doc.Write() - test_result["write_basique"] = True - - # Client après - doc.Read() - client_obj = getattr(doc, "Client", None) - if client_obj: - client_obj.Read() - test_result["client_apres"] = getattr(client_obj, "CT_Num", "") - - except Exception as e: - test_result["write_basique"] = False - test_result["erreur"] = str(e) - - comparaison["tests"].append(test_result) - - # ======================================== - # RÉSUMÉ - # ======================================== - nb_diff = len(comparaison["differences"]) - nb_diff_critiques = sum(1 for d in comparaison["differences"] if d.get("critique", False)) - - comparaison["resume"] = { - "nb_differences": nb_diff, - "nb_differences_critiques": nb_diff_critiques, - "bc00067_write_ok": any(t["numero"] == "BC00067" and t["write_basique"] for t in comparaison["tests"]), - "bc00068_write_ok": any(t["numero"] == "BC00068" and t["write_basique"] for t in comparaison["tests"]), - } - - return { - "success": True, - "comparaison": comparaison - } - - except Exception as e: - logger.error(f"❌ Erreur comparaison: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - # ===================================================== # LANCEMENT # ===================================================== diff --git a/sage_connector.py b/sage_connector.py index ad6d1da..a92262a 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -4480,8 +4480,459 @@ class SageConnector: raise RuntimeError(f"Erreur Sage: {error_message}") + def creer_livraison_enrichi(self, livraison_data: dict) -> Dict: + """ + ➕ Création d'une livraison (type 30 = Bon de livraison) + + ✅ Gestion identique aux commandes/devis + """ + if not self.cial: + raise RuntimeError("Connexion Sage non établie") + + logger.info(f"🚀 Début création livraison pour client {livraison_data['client']['code']}") + + try: + with self._com_context(), self._lock_com: + transaction_active = False + try: + self.cial.CptaApplication.BeginTrans() + transaction_active = True + logger.debug("✅ Transaction Sage démarrée") + except: + pass + + try: + # Création document LIVRAISON (type 30) + process = self.cial.CreateProcess_Document(settings.SAGE_TYPE_BON_LIVRAISON) + doc = process.Document + + try: + doc = win32com.client.CastTo(doc, "IBODocumentVente3") + except: + pass + + logger.info("📄 Document livraison créé") + + # Date + import pywintypes + + if isinstance(livraison_data["date_livraison"], str): + date_obj = datetime.fromisoformat(livraison_data["date_livraison"]) + elif isinstance(livraison_data["date_livraison"], date): + date_obj = datetime.combine(livraison_data["date_livraison"], datetime.min.time()) + else: + date_obj = datetime.now() + + doc.DO_Date = pywintypes.Time(date_obj) + logger.info(f"📅 Date définie: {date_obj.date()}") + + # Client (CRITIQUE) + factory_client = self.cial.CptaApplication.FactoryClient + persist_client = factory_client.ReadNumero(livraison_data["client"]["code"]) + + if not persist_client: + raise ValueError(f"Client {livraison_data['client']['code']} introuvable") + + client_obj = self._cast_client(persist_client) + if not client_obj: + raise ValueError(f"Impossible de charger le client") + + doc.SetDefaultClient(client_obj) + doc.Write() + logger.info(f"👤 Client {livraison_data['client']['code']} associé") + + # Référence externe (optionnelle) + if livraison_data.get("reference"): + try: + doc.DO_Ref = livraison_data["reference"] + logger.info(f"📖 Référence: {livraison_data['reference']}") + except: + pass + + # Lignes + try: + factory_lignes = doc.FactoryDocumentLigne + except: + factory_lignes = doc.FactoryDocumentVenteLigne + + factory_article = self.cial.FactoryArticle + + logger.info(f"📦 Ajout de {len(livraison_data['lignes'])} lignes...") + + for idx, ligne_data in enumerate(livraison_data["lignes"], 1): + logger.info(f"--- Ligne {idx}: {ligne_data['article_code']} ---") + + # Charger l'article RÉEL depuis Sage + persist_article = factory_article.ReadReference(ligne_data["article_code"]) + + if not persist_article: + raise ValueError(f"❌ Article {ligne_data['article_code']} introuvable dans Sage") + + article_obj = win32com.client.CastTo(persist_article, "IBOArticle3") + article_obj.Read() + + # Récupérer le prix de vente RÉEL + prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) + designation_sage = getattr(article_obj, "AR_Design", "") + logger.info(f"💰 Prix Sage: {prix_sage}€") + + if prix_sage == 0: + logger.warning(f"⚠️ Article {ligne_data['article_code']} a un prix = 0€ (toléré)") + + # Créer la ligne + ligne_persist = factory_lignes.Create() + + try: + ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3") + except: + ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3") + + quantite = float(ligne_data["quantite"]) + + try: + ligne_obj.SetDefaultArticleReference(ligne_data["article_code"], quantite) + logger.info(f"✅ Article associé via SetDefaultArticleReference") + except Exception as e: + logger.warning(f"⚠️ SetDefaultArticleReference échoué: {e}, tentative avec objet") + try: + ligne_obj.SetDefaultArticle(article_obj, quantite) + logger.info(f"✅ Article associé via SetDefaultArticle") + except Exception as e2: + logger.error(f"❌ Toutes les méthodes ont échoué") + ligne_obj.DL_Design = designation_sage or ligne_data.get("designation", "") + ligne_obj.DL_Qte = quantite + logger.warning("⚠️ Configuration manuelle appliquée") + + # Vérifier le prix automatique + prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) + logger.info(f"💰 Prix auto chargé: {prix_auto}€") + + # Ajuster le prix si nécessaire + prix_a_utiliser = ligne_data.get("prix_unitaire_ht") + + if prix_a_utiliser is not None and prix_a_utiliser > 0: + ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) + logger.info(f"💰 Prix personnalisé: {prix_a_utiliser}€") + elif prix_auto == 0 and prix_sage > 0: + ligne_obj.DL_PrixUnitaire = float(prix_sage) + logger.info(f"💰 Prix Sage forcé: {prix_sage}€") + elif prix_auto > 0: + logger.info(f"💰 Prix auto conservé: {prix_auto}€") + + prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) + montant_ligne = quantite * prix_final + logger.info(f"✅ {quantite} x {prix_final}€ = {montant_ligne}€") + + # Remise + remise = ligne_data.get("remise_pourcentage", 0) + if remise > 0: + try: + ligne_obj.DL_Remise01REM_Valeur = float(remise) + ligne_obj.DL_Remise01REM_Type = 0 + montant_apres_remise = montant_ligne * (1 - remise / 100) + logger.info(f"🎁 Remise {remise}% → {montant_apres_remise}€") + except Exception as e: + logger.warning(f"⚠️ Remise non appliquée: {e}") + + # Écrire la ligne + ligne_obj.Write() + logger.info(f"✅ Ligne {idx} écrite") + + # Validation + doc.Write() + process.Process() + + if transaction_active: + self.cial.CptaApplication.CommitTrans() + + # Récupération numéro + time.sleep(2) + + numero_livraison = None + try: + doc_result = process.DocumentResult + if doc_result: + doc_result = win32com.client.CastTo(doc_result, "IBODocumentVente3") + doc_result.Read() + numero_livraison = getattr(doc_result, "DO_Piece", "") + except: + pass + + if not numero_livraison: + numero_livraison = getattr(doc, "DO_Piece", "") + + # Relecture + factory_doc = self.cial.FactoryDocumentVente + persist_reread = factory_doc.ReadPiece(settings.SAGE_TYPE_BON_LIVRAISON, numero_livraison) + + if persist_reread: + doc_final = win32com.client.CastTo(persist_reread, "IBODocumentVente3") + doc_final.Read() + + total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) + total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) + else: + total_ht = 0.0 + total_ttc = 0.0 + + logger.info(f"✅✅✅ LIVRAISON CRÉÉE: {numero_livraison} - {total_ttc}€ TTC ✅✅✅") + + return { + "numero_livraison": numero_livraison, + "total_ht": total_ht, + "total_ttc": total_ttc, + "nb_lignes": len(livraison_data["lignes"]), + "client_code": livraison_data["client"]["code"], + "date_livraison": str(date_obj.date()), + } + + except Exception as e: + if transaction_active: + try: + self.cial.CptaApplication.RollbackTrans() + except: + pass + raise + + except Exception as e: + logger.error(f"❌ Erreur création livraison: {e}", exc_info=True) + raise RuntimeError(f"Échec création livraison: {str(e)}") + def modifier_livraison(self, numero: str, livraison_data: Dict) -> Dict: + """ + ✏️ Modification d'une livraison existante + + 🔧 STRATÉGIE REMPLACEMENT LIGNES: + - Si nouvelles lignes fournies → Supprime TOUTES les anciennes puis ajoute les nouvelles + - Utilise .Remove() pour la suppression + """ + if not self.cial: + raise RuntimeError("Connexion Sage non établie") + + try: + with self._com_context(), self._lock_com: + logger.info(f"🔬 === MODIFICATION LIVRAISON {numero} ===") + + # ======================================== + # ÉTAPE 1 : CHARGER LE DOCUMENT + # ======================================== + logger.info("📂 Chargement document...") + + factory = self.cial.FactoryDocumentVente + persist = None + + # Chercher le document + for type_test in [30, settings.SAGE_TYPE_BON_LIVRAISON]: + try: + persist_test = factory.ReadPiece(type_test, numero) + if persist_test: + persist = persist_test + logger.info(f" ✅ Document trouvé (type={type_test})") + break + except: + continue + + if not persist: + raise ValueError(f"❌ Livraison {numero} INTROUVABLE") + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + statut_actuel = getattr(doc, "DO_Statut", 0) + + logger.info(f" 📊 Statut={statut_actuel}") + + # Vérifier qu'elle n'est pas transformée + if statut_actuel == 5: + raise ValueError(f"La livraison {numero} a déjà été transformée") + + if statut_actuel == 6: + raise ValueError(f"La livraison {numero} est annulée") + + # Compter les lignes initiales + nb_lignes_initial = 0 + try: + factory_lignes = getattr(doc, "FactoryDocumentLigne", None) or getattr(doc, "FactoryDocumentVenteLigne", None) + index = 1 + while index <= 100: + try: + ligne_p = factory_lignes.List(index) + if ligne_p is None: + break + nb_lignes_initial += 1 + index += 1 + except: + break + + logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") + except Exception as e: + logger.warning(f" ⚠️ Erreur comptage lignes: {e}") + + # ======================================== + # ÉTAPE 2 : DÉTERMINER LES MODIFICATIONS + # ======================================== + champs_modifies = [] + + modif_date = "date_livraison" in livraison_data + modif_statut = "statut" in livraison_data + modif_ref = "reference" in livraison_data + modif_lignes = "lignes" in livraison_data and livraison_data["lignes"] is not None + + logger.info(f"📋 Modifications demandées:") + logger.info(f" Date: {modif_date}") + logger.info(f" Statut: {modif_statut}") + logger.info(f" Référence: {modif_ref}") + logger.info(f" Lignes: {modif_lignes}") + + # ======================================== + # ÉTAPE 3 : MODIFICATIONS SIMPLES + # ======================================== + if not modif_lignes and (modif_date or modif_statut or modif_ref): + logger.info("🎯 Modifications simples (sans lignes)...") + + if modif_date: + import pywintypes + date_str = livraison_data["date_livraison"] + + if isinstance(date_str, str): + date_obj = datetime.fromisoformat(date_str) + elif isinstance(date_str, date): + date_obj = datetime.combine(date_str, datetime.min.time()) + else: + date_obj = date_str + + doc.DO_Date = pywintypes.Time(date_obj) + champs_modifies.append("date") + logger.info(f" ✅ Date définie: {date_obj.date()}") + + if modif_statut: + nouveau_statut = livraison_data["statut"] + doc.DO_Statut = nouveau_statut + champs_modifies.append("statut") + logger.info(f" ✅ Statut défini: {nouveau_statut}") + + if modif_ref: + try: + doc.DO_Ref = livraison_data["reference"] + champs_modifies.append("reference") + logger.info(f" ✅ Référence définie") + except Exception as e: + logger.warning(f" ⚠️ Référence non définie: {e}") + + doc.Write() + logger.info(" ✅ Write() réussi") + + # ======================================== + # ÉTAPE 4 : REMPLACEMENT COMPLET LIGNES + # ======================================== + elif modif_lignes: + logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") + + nouvelles_lignes = livraison_data["lignes"] + nb_nouvelles = len(nouvelles_lignes) + + logger.info(f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles") + + try: + factory_lignes = doc.FactoryDocumentLigne + except: + factory_lignes = doc.FactoryDocumentVenteLigne + + factory_article = self.cial.FactoryArticle + + # SUPPRESSION TOUTES LES LIGNES + if nb_lignes_initial > 0: + logger.info(f" 🗑️ Suppression de {nb_lignes_initial} lignes...") + + for idx in range(nb_lignes_initial, 0, -1): + try: + ligne_p = factory_lignes.List(idx) + if ligne_p: + ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") + ligne.Read() + ligne.Remove() + logger.debug(f" ✅ Ligne {idx} supprimée") + except Exception as e: + logger.warning(f" ⚠️ Erreur suppression ligne {idx}: {e}") + + logger.info(" ✅ Toutes les lignes supprimées") + + # AJOUT NOUVELLES LIGNES + logger.info(f" ➕ Ajout de {nb_nouvelles} nouvelles lignes...") + + for idx, ligne_data in enumerate(nouvelles_lignes, 1): + persist_article = factory_article.ReadReference(ligne_data["article_code"]) + if not persist_article: + raise ValueError(f"Article {ligne_data['article_code']} introuvable") + + article_obj = win32com.client.CastTo(persist_article, "IBOArticle3") + article_obj.Read() + + ligne_persist = factory_lignes.Create() + + try: + ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3") + except: + ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3") + + quantite = float(ligne_data["quantite"]) + + try: + ligne_obj.SetDefaultArticleReference(ligne_data["article_code"], quantite) + except: + try: + ligne_obj.SetDefaultArticle(article_obj, quantite) + except: + ligne_obj.DL_Design = ligne_data.get("designation", "") + ligne_obj.DL_Qte = quantite + + if ligne_data.get("prix_unitaire_ht"): + ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"]) + + if ligne_data.get("remise_pourcentage", 0) > 0: + try: + ligne_obj.DL_Remise01REM_Valeur = float(ligne_data["remise_pourcentage"]) + ligne_obj.DL_Remise01REM_Type = 0 + except: + pass + + ligne_obj.Write() + logger.debug(f" ✅ Ligne {idx} ajoutée") + + logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") + + doc.Write() + champs_modifies.append("lignes") + + # ======================================== + # ÉTAPE 5 : RELECTURE ET RETOUR + # ======================================== + import time + time.sleep(1) + + doc.Read() + + total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) + total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) + + logger.info(f"✅✅✅ LIVRAISON MODIFIÉE: {numero} ✅✅✅") + logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC") + + return { + "numero": numero, + "total_ht": total_ht, + "total_ttc": total_ttc, + "champs_modifies": champs_modifies, + "statut": getattr(doc, "DO_Statut", 0) + } + + except ValueError as e: + logger.error(f"❌ Erreur métier: {e}") + raise + except Exception as e: + logger.error(f"❌ Erreur technique: {e}", exc_info=True) + raise RuntimeError(f"Erreur Sage: {str(e)}")