From 5ccf470167327f615efafba2d49ab75d2b374242 Mon Sep 17 00:00:00 2001 From: mickael Date: Sat, 20 Dec 2025 12:55:05 +0100 Subject: [PATCH] Added better handling for "reference" row data --- main.py | 6 +- sage_connector.py | 2407 ++++++++++++++++++++++++--------------------- 2 files changed, 1307 insertions(+), 1106 deletions(-) diff --git a/main.py b/main.py index 1c7b57a..acaba56 100644 --- a/main.py +++ b/main.py @@ -63,9 +63,8 @@ class ChampLibreRequest(BaseModel): class DevisRequest(BaseModel): client_id: str date_devis: Optional[date] = None - lignes: List[ - Dict - ] # Format: {article_code, quantite, prix_unitaire_ht, remise_pourcentage} + reference: Optional[str] = None + lignes: List[Dict] class TransformationRequest(BaseModel): @@ -514,6 +513,7 @@ def creer_devis(req: DevisRequest): devis_data = { "client": {"code": req.client_id, "intitule": ""}, "date_devis": req.date_devis or date.today(), + "reference": req.reference, "lignes": req.lignes, } diff --git a/sage_connector.py b/sage_connector.py index b57e079..b75dbff 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -8,6 +8,7 @@ import logging from config import settings, validate_settings import pyodbc from contextlib import contextmanager +import pywintypes logger = logging.getLogger(__name__) @@ -1169,220 +1170,6 @@ class SageConnector: mapping = {0: 0, 1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 6: 60} return mapping.get(type_sql, type_sql) - def _construire_liaisons_recursives( - self, - numero: str, - type_doc: int, - profondeur: int = 0, - max_profondeur: int = 5, - deja_visites: set = None, - ): - """ - Construit récursivement la structure des liaisons d'un document. - - Args: - numero: Numéro du document - type_doc: Type du document - profondeur: Profondeur actuelle de récursion - max_profondeur: Profondeur maximale pour éviter les boucles infinies - deja_visites: Set des documents déjà visités pour éviter les boucles - - Returns: - dict: Structure des liaisons avec origine et descendants - """ - if deja_visites is None: - deja_visites = set() - - # Éviter les boucles infinies - cle_doc = f"{numero}_{type_doc}" - if cle_doc in deja_visites or profondeur >= max_profondeur: - return None - - deja_visites.add(cle_doc) - - try: - with self._get_sql_connection() as conn: - cursor = conn.cursor() - - # ======================================== - # 1. CHERCHER LE DOCUMENT ORIGINE (ascendant) - # ======================================== - origine = None - - cursor.execute( - """ - SELECT DISTINCT - DL_PieceDE, DL_PieceBC, DL_PieceBL - FROM F_DOCLIGNE - WHERE DO_Piece = ? AND DO_Type = ? - """, - (numero, type_doc), - ) - lignes = cursor.fetchall() - - piece_origine = None - type_origine = None - - for ligne in lignes: - if ligne.DL_PieceDE and ligne.DL_PieceDE.strip(): - piece_origine = ligne.DL_PieceDE.strip() - type_origine = 0 # Devis - break - elif ligne.DL_PieceBC and ligne.DL_PieceBC.strip(): - piece_origine = ligne.DL_PieceBC.strip() - type_origine = 10 # Commande - break - elif ligne.DL_PieceBL and ligne.DL_PieceBL.strip(): - piece_origine = ligne.DL_PieceBL.strip() - type_origine = 30 # BL - break - - if piece_origine and type_origine is not None: - # Récupérer les infos de base du document origine - cursor.execute( - """ - SELECT DO_Piece, DO_Type, DO_Ref, DO_Date, DO_TotalHT, DO_Statut - FROM F_DOCENTETE - WHERE DO_Piece = ? AND DO_Type = ? - """, - (piece_origine, type_origine), - ) - origine_row = cursor.fetchone() - - if origine_row: - origine = { - "numero": origine_row.DO_Piece.strip(), - "type": self._normaliser_type_document( - int(origine_row.DO_Type) - ), - "type_libelle": self._get_type_libelle( - int(origine_row.DO_Type) - ), - "ref": self._safe_strip(origine_row.DO_Ref), - "date": ( - str(origine_row.DO_Date) if origine_row.DO_Date else "" - ), - "total_ht": ( - float(origine_row.DO_TotalHT) - if origine_row.DO_TotalHT - else 0.0 - ), - "statut": ( - int(origine_row.DO_Statut) - if origine_row.DO_Statut - else 0 - ), - # Récursion sur l'origine - "liaisons": self._construire_liaisons_recursives( - origine_row.DO_Piece.strip(), - int(origine_row.DO_Type), - profondeur + 1, - max_profondeur, - deja_visites, - ), - } - - # ======================================== - # 2. CHERCHER LES DOCUMENTS DESCENDANTS - # ======================================== - descendants = [] - - # Utiliser la fonction existante pour trouver les transformations - verif = self.verifier_si_deja_transforme_sql(numero, type_doc) - - for doc_cible in verif["documents_cibles"]: - # Récupérer les infos complètes - cursor.execute( - """ - SELECT DO_Piece, DO_Type, DO_Ref, DO_Date, DO_TotalHT, DO_Statut - FROM F_DOCENTETE - WHERE DO_Piece = ? AND DO_Type = ? - """, - (doc_cible["numero"], doc_cible["type"]), - ) - desc_row = cursor.fetchone() - - if desc_row: - descendant = { - "numero": desc_row.DO_Piece.strip(), - "type": self._normaliser_type_document( - int(desc_row.DO_Type) - ), - "type_libelle": self._get_type_libelle( - int(desc_row.DO_Type) - ), - "ref": self._safe_strip(desc_row.DO_Ref), - "date": str(desc_row.DO_Date) if desc_row.DO_Date else "", - "total_ht": ( - float(desc_row.DO_TotalHT) - if desc_row.DO_TotalHT - else 0.0 - ), - "statut": ( - int(desc_row.DO_Statut) if desc_row.DO_Statut else 0 - ), - "nb_lignes": doc_cible.get("nb_lignes", 0), - # Récursion sur le descendant - "liaisons": self._construire_liaisons_recursives( - desc_row.DO_Piece.strip(), - int(desc_row.DO_Type), - profondeur + 1, - max_profondeur, - deja_visites, - ), - } - descendants.append(descendant) - - return {"origine": origine, "descendants": descendants} - - except Exception as e: - logger.error(f"Erreur construction liaisons pour {numero}: {e}") - return {"origine": None, "descendants": []} - - def _calculer_transformations_possibles(self, numero: str, type_doc: int): - """ - Calcule toutes les transformations possibles pour un document donné. - - Returns: - tuple: (peut_etre_transforme, transformations_possibles) - """ - type_doc = self._normaliser_type_document(type_doc) - - # Mapping des transformations autorisées - transformations_autorisees = { - 0: [10, 30, 60], # Devis → Commande, BL, Facture - 10: [30, 60], # Commande → BL, Facture - 30: [60], # BL → Facture - 50: [], # Avoir → rien - 60: [], # Facture → rien - } - - types_possibles = transformations_autorisees.get(type_doc, []) - transformations_possibles = [] - peut_etre_transforme = False - - for type_cible in types_possibles: - verif = self.peut_etre_transforme(numero, type_doc, type_cible) - - transformation = { - "type_cible": type_cible, - "type_libelle": self._get_type_libelle(type_cible), - "possible": verif["possible"], - } - - if not verif["possible"]: - transformation["raison"] = verif["raison"] - if verif.get("documents_existants"): - transformation["documents_existants"] = [ - d["numero"] for d in verif["documents_existants"] - ] - - transformations_possibles.append(transformation) - - if verif["possible"]: - peut_etre_transforme = True - - return peut_etre_transforme, transformations_possibles def _lire_document_sql(self, numero: str, type_doc: int): """ @@ -1444,10 +1231,10 @@ class SageConnector: "reference": self._safe_strip(row[2]), # DO_Ref "date": str(row[1]) if row[1] else "", # DO_Date "date_livraison": ( - str(row[7]) if row[7] else "1753-01-01 00:00:00" + str(row[7]) if row[7] else "" ), # DO_DateLivr "date_expedition": ( - str(row[8]) if row[8] else "1753-01-01 00:00:00" + str(row[8]) if row[8] else "" ), # DO_DateExpedition # Client (indices 6 et 39-44) "client_code": self._safe_strip(row[6]), # DO_Tiers @@ -1707,25 +1494,6 @@ class SageConnector: doc["total_ttc_calcule"] = total_ttc_calcule doc["total_taxes_calcule"] = total_taxes_calcule - # ======================================== - # AJOUTER LES DOCUMENTS LIÉS (RÉCURSIF) - # ======================================== - logger.info(f"Construction liaisons récursives pour {numero}...") - doc["documents_lies"] = self._construire_liaisons_recursives( - numero, type_doc - ) - - # ======================================== - # AJOUTER LES TRANSFORMATIONS POSSIBLES - # ======================================== - logger.info(f"Calcul transformations possibles pour {numero}...") - peut_etre_transforme, transformations_possibles = ( - self._calculer_transformations_possibles(numero, type_doc) - ) - - doc["peut_etre_transforme"] = peut_etre_transforme - doc["transformations_possibles"] = transformations_possibles - return doc except Exception as e: @@ -2226,73 +1994,6 @@ class SageConnector: stats["erreur_lignes"] += 1 # Continuer quand même avec 0 lignes - # ======================================== - # ÉTAPE 4 : TRANSFORMATIONS - # ======================================== - if calculer_transformations: - try: - logger.debug( - f"[SQL LIST] 🔄 {numero} : calcul transformations..." - ) - - peut_etre_transforme, transformations_possibles = ( - self._calculer_transformations_possibles( - doc["numero"], type_doc # Type COM - ) - ) - doc["peut_etre_transforme"] = peut_etre_transforme - doc["transformations_possibles"] = ( - transformations_possibles - ) - - logger.info( - f"[SQL LIST] ✅ {numero} : peut_etre_transforme={peut_etre_transforme}, " - f"{len(transformations_possibles)} transformations" - ) - - except Exception as e: - logger.error( - f"[SQL LIST] ❌ {numero} : ERREUR TRANSFORMATIONS: {e}", - exc_info=True, - ) - stats["erreur_transformations"] += 1 - doc["peut_etre_transforme"] = False - doc["transformations_possibles"] = [] - doc["erreur_transformations"] = str(e) - else: - doc["peut_etre_transforme"] = False - doc["transformations_possibles"] = [] - - # ======================================== - # ÉTAPE 5 : LIAISONS - # ======================================== - if inclure_liaisons: - try: - logger.debug( - f"[SQL LIST] 🔄 {numero} : construction liaisons..." - ) - - doc["documents_lies"] = ( - self._construire_liaisons_recursives( - doc["numero"], type_doc - ) - ) - - logger.debug(f"[SQL LIST] ✅ {numero} : liaisons OK") - - except Exception as e: - logger.error( - f"[SQL LIST] ⚠️ {numero} : ERREUR liaisons: {e}", - exc_info=True, - ) - stats["erreur_liaisons"] += 1 - doc["documents_lies"] = { - "origine": None, - "descendants": [], - } - else: - doc["documents_lies"] = {"origine": None, "descendants": []} - # ======================================== # ÉTAPE 6 : AJOUT DU DOCUMENT # ======================================== @@ -3399,6 +3100,24 @@ class SageConnector: "date_modification": "", } + + def normaliser_date(self, valeur): + if isinstance(valeur, str): + try: + return datetime.fromisoformat(valeur) + except ValueError: + return datetime.now() + + elif isinstance(valeur, date): + return datetime.combine(valeur, datetime.min.time()) + + elif isinstance(valeur, datetime): + return valeur + + else: + return datetime.now() + + def creer_devis_enrichi(self, devis_data: dict, forcer_brouillon: bool = False): """ Crée un devis dans Sage avec support de la référence. @@ -3441,23 +3160,7 @@ class SageConnector: logger.info("📄 Document devis créé") - # ===== DATE ===== - import pywintypes - - if isinstance(devis_data["date_devis"], str): - try: - date_obj = datetime.fromisoformat(devis_data["date_devis"]) - except: - date_obj = datetime.now() - elif isinstance(devis_data["date_devis"], date): - date_obj = datetime.combine( - devis_data["date_devis"], datetime.min.time() - ) - else: - date_obj = datetime.now() - - doc.DO_Date = pywintypes.Time(date_obj) - logger.info(f"📅 Date définie: {date_obj.date()}") + doc.DO_Date = pywintypes.Time(self.normaliser_date(devis_data.get("date_devis"))) # ===== CLIENT ===== factory_client = self.cial.CptaApplication.FactoryClient @@ -3479,18 +3182,10 @@ class SageConnector: doc.SetDefaultClient(client_obj) logger.info(f"👤 Client {devis_data['client']['code']} associé") - # ===== RÉFÉRENCE ===== - if "reference" in devis_data and devis_data["reference"]: - try: - doc.DO_Ref = str(devis_data["reference"]) - logger.info(f"🔖 Référence définie: {devis_data['reference']}") - except Exception as e: - logger.warning(f"⚠️ Impossible de définir la référence: {e}") - # ===== STATUT ===== if forcer_brouillon: doc.DO_Statut = 0 - logger.info("📊 Statut forcé: 0 (Brouillon)") + logger.info("📊 Statut défini: 0 (Brouillon)") else: doc.DO_Statut = 2 logger.info("📊 Statut défini: 2 (Accepté)") @@ -3619,10 +3314,34 @@ class SageConnector: except: pass - # ===== RELECTURE ===== + # ===== ATTENTE POUR STABILISATION ===== import time time.sleep(0.5) + # ===== RÉFÉRENCE (RECHARGER D'ABORD LE DOCUMENT) ===== + if "reference" in devis_data and devis_data["reference"]: + try: + logger.info(f"🔖 Application de la référence: {devis_data['reference']}") + + # RECHARGER le document par son numéro (comme dans modifier_devis) + doc_reload = self._charger_devis(numero_devis) + + # Appliquer la référence + nouvelle_reference = devis_data["reference"] + doc_reload.DO_Ref = str(nouvelle_reference) if nouvelle_reference else "" + doc_reload.Write() # Sauvegarder immédiatement + + time.sleep(0.5) + + doc_reload.Read() # Relire après sauvegarde + + logger.info(f"✅ Référence définie: {nouvelle_reference}") + except Exception as e: + logger.warning(f"⚠️ Impossible de définir la référence: {e}", exc_info=True) + + # ===== RELECTURE FINALE ===== + time.sleep(0.5) + doc_final_data = self._relire_devis(numero_devis, devis_data, forcer_brouillon) logger.info( @@ -3643,7 +3362,7 @@ class SageConnector: except Exception as e: logger.error(f"❌ ERREUR CRÉATION DEVIS: {e}", exc_info=True) raise RuntimeError(f"Échec création devis: {str(e)}") - + def _recuperer_numero_devis(self, process, doc): """Récupère le numéro du devis créé via plusieurs méthodes.""" @@ -3755,54 +3474,42 @@ class SageConnector: def modifier_devis(self, numero: str, devis_data: Dict) -> Dict: - """ - Modifie un devis existant dans Sage. - - Args: - numero: Numéro du devis à modifier - devis_data: dict contenant les champs à modifier: - - date_devis: str ou date (optionnel) - - reference: str (optionnel) - - statut: int (optionnel) - - lignes: list[dict] (optionnel) - - Returns: - dict contenant les informations du devis modifié - """ + """Modifie un devis existant dans Sage.""" if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: - # ===== ÉTAPE 1 : CHARGER LE DEVIS ===== logger.info(f"🔍 Recherche devis {numero}...") doc = self._charger_devis(numero) logger.info(f"✅ Devis {numero} trouvé") - # ===== VÉRIFICATION ===== self._verifier_devis_non_transforme(numero, doc) champs_modifies = [] - # ===== ÉTAPE 2 : MODIFIER LES CHAMPS SIMPLES (sauf statut si lignes à modifier) ===== - # Créer une copie des données sans le statut si on modifie les lignes - devis_data_champs = devis_data.copy() + # ===== EXTRAIRE référence et statut pour les traiter à la fin ===== + devis_data_temp = devis_data.copy() + reference_a_modifier = None statut_a_modifier = None + if "reference" in devis_data_temp: + reference_a_modifier = devis_data_temp.pop("reference") + logger.info("🔖 Modification de la référence reportée après les lignes") + if "lignes" in devis_data and devis_data["lignes"] is not None: - # Si on modifie les lignes, on garde le statut pour la fin - if "statut" in devis_data_champs: - statut_a_modifier = devis_data_champs.pop("statut") + if "statut" in devis_data_temp: + statut_a_modifier = devis_data_temp.pop("statut") logger.info("📊 Modification du statut reportée après les lignes") - champs_modifies = self._modifier_champs_simples(doc, devis_data_champs) + # ===== MODIFIER CHAMPS SIMPLES (sauf référence et statut) ===== + champs_modifies = self._modifier_champs_simples(doc, devis_data_temp) - # ===== ÉTAPE 3 : MODIFICATION DES LIGNES ===== + # ===== MODIFICATION DES LIGNES ===== if "lignes" in devis_data and devis_data["lignes"] is not None: self._modifier_lignes_devis(doc, devis_data["lignes"]) champs_modifies.append("lignes") - # Validation après modification des lignes logger.info("💾 Sauvegarde après modification des lignes...") doc.Write() @@ -3811,7 +3518,26 @@ class SageConnector: doc.Read() - # ===== ÉTAPE 4 : MODIFIER LE STATUT (si nécessaire et après les lignes) ===== + # ===== MODIFIER LA RÉFÉRENCE (APRÈS les lignes) ===== + if reference_a_modifier is not None: + try: + ancienne_reference = getattr(doc, "DO_Ref", "") + nouvelle_reference = str(reference_a_modifier) if reference_a_modifier else "" + + doc.DO_Ref = nouvelle_reference + doc.Write() + + import time + time.sleep(0.5) + + doc.Read() + + champs_modifies.append("reference") + logger.info(f"🔖 Référence modifiée: '{ancienne_reference}' → '{nouvelle_reference}'") + except Exception as e: + logger.warning(f"⚠️ Impossible de modifier la référence: {e}") + + # ===== MODIFIER LE STATUT (EN DERNIER) ===== if statut_a_modifier is not None: try: statut_actuel = getattr(doc, "DO_Statut", 0) @@ -3836,7 +3562,7 @@ class SageConnector: try: doc.Write() except: - pass # Peut échouer si déjà sauvegardé + pass import time time.sleep(0.5) @@ -3858,7 +3584,7 @@ class SageConnector: except Exception as e: logger.error(f"❌ Erreur technique: {e}", exc_info=True) raise RuntimeError(f"Erreur technique Sage: {str(e)}") - + def _charger_devis(self, numero: str): """Charge un devis depuis Sage.""" @@ -3934,20 +3660,12 @@ class SageConnector: # DATE - Modifier et sauvegarder immédiatement if "date_devis" in devis_data: try: - import pywintypes - date_str = devis_data["date_devis"] - date_obj = ( - datetime.fromisoformat(date_str) - if isinstance(date_str, str) - else date_str - ) - doc.DO_Date = pywintypes.Time(date_obj) + doc.DO_Date = pywintypes.Time(self.normaliser_date(devis_data.get("date_devis"))) doc.Write() # Sauvegarder immédiatement doc.Read() # Relire après sauvegarde champs_modifies.append("date") - logger.info(f"📅 Date modifiée: {date_obj.date()}") except Exception as e: logger.warning(f"⚠️ Impossible de modifier la date: {e}") @@ -3990,6 +3708,7 @@ class SageConnector: return champs_modifies + def _modifier_lignes_devis(self, doc, nouvelles_lignes: list): """Modifie intelligemment les lignes du devis.""" logger.info(f"🔄 Modification intelligente des lignes...") @@ -4242,6 +3961,7 @@ class SageConnector: logger.error(f"❌ Erreur SQL lecture devis {numero_devis}: {e}") return None + def lire_document(self, numero, type_doc): return self._lire_document_sql(numero, type_doc) @@ -4751,20 +4471,6 @@ class SageConnector: conserver_document_source=True, verifier_doublons=True, ): - """ - Transforme un document Sage en utilisant UNIQUEMENT l'API COM/BOI officielle. - - Args: - numero_source: Numéro du document source (ex: "DE00119") - type_source: Type COM du document source (0, 10, 30...) - type_cible: Type COM du document cible (10, 30, 60...) - ignorer_controle_stock: Non utilisé (géré par Sage) - conserver_document_source: Si True, tente de conserver le document source - verifier_doublons: Si True, vérifie les doublons avant transformation - - Returns: - dict: Informations sur la transformation réussie - """ if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -6075,22 +5781,7 @@ class SageConnector: logger.info("📄 Document commande créé") - # Date - import pywintypes - - if isinstance(commande_data["date_commande"], str): - date_obj = datetime.fromisoformat( - commande_data["date_commande"] - ) - elif isinstance(commande_data["date_commande"], date): - date_obj = datetime.combine( - commande_data["date_commande"], datetime.min.time() - ) - else: - date_obj = datetime.now() - - doc.DO_Date = pywintypes.Time(date_obj) - logger.info(f"📅 Date définie: {date_obj.date()}") + doc.DO_Date = pywintypes.Time(self.normaliser_date(commande_data.get("date_commande"))) # Client (CRITIQUE) factory_client = self.cial.CptaApplication.FactoryClient @@ -6309,7 +6000,7 @@ class SageConnector: "total_ttc": total_ttc, "nb_lignes": len(commande_data["lignes"]), "client_code": commande_data["client"]["code"], - "date_commande": str(date_obj.date()), + "date_commande": str(self.normaliser_date(commande_data.get("date_commande"))), } except Exception as e: @@ -6324,6 +6015,7 @@ class SageConnector: logger.error(f"❌ Erreur création commande: {e}", exc_info=True) raise RuntimeError(f"Échec création commande: {str(e)}") + def modifier_commande(self, numero: str, commande_data: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -6419,6 +6111,21 @@ class SageConnector: logger.info(f" Référence: {modif_ref}") logger.info(f" Lignes: {modif_lignes}") + # ===== EXTRAIRE référence et statut pour les traiter à la fin si lignes modifiées ===== + reference_a_modifier = None + statut_a_modifier = None + + if modif_lignes: + if modif_ref: + reference_a_modifier = commande_data.pop("reference") + logger.info("🔖 Modification de la référence reportée après les lignes") + modif_ref = False # Ne pas traiter dans les modifications simples + + if modif_statut: + statut_a_modifier = commande_data.pop("statut") + logger.info("📊 Modification du statut reportée après les lignes") + modif_statut = False # Ne pas traiter dans les modifications simples + # ======================================== # ÉTAPE 4 : TEST WRITE() BASIQUE # ======================================== @@ -6458,20 +6165,9 @@ class SageConnector: if modif_date: logger.info(" 📅 Modification date...") - import pywintypes - date_str = commande_data["date_commande"] - - 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) + doc.DO_Date = pywintypes.Time(self.normaliser_date(commande_data.get("date_commande"))) champs_modifies.append("date") - logger.info(f" ✅ Date définie: {date_obj.date()}") if modif_statut: logger.info(" 📊 Modification statut...") @@ -6561,7 +6257,7 @@ class SageConnector: ) ligne.Read() - # ✅ Utiliser .Remove() comme indiqué + # ✅ Utiliser .Remove() ligne.Remove() logger.debug(f" ✅ Ligne {idx} supprimée") except Exception as e: @@ -6649,6 +6345,9 @@ class SageConnector: doc.Write() logger.info(" ✅ Document écrit") + import time + time.sleep(0.5) + doc.Read() # Vérifier client @@ -6662,14 +6361,56 @@ class SageConnector: champs_modifies.append("lignes") + # ======================================== + # ÉTAPE 6.5 : MODIFIER RÉFÉRENCE (APRÈS les lignes - même logique que devis) + # ======================================== + if reference_a_modifier is not None: + try: + ancienne_reference = getattr(doc, "DO_Ref", "") + nouvelle_reference = str(reference_a_modifier) if reference_a_modifier else "" + + doc.DO_Ref = nouvelle_reference + doc.Write() + + import time + time.sleep(0.5) + + doc.Read() + + champs_modifies.append("reference") + logger.info(f"🔖 Référence modifiée: '{ancienne_reference}' → '{nouvelle_reference}'") + except Exception as e: + logger.warning(f"⚠️ Impossible de modifier la référence: {e}") + + # ======================================== + # ÉTAPE 6.6 : MODIFIER STATUT (EN DERNIER - même logique que devis) + # ======================================== + if statut_a_modifier is not None: + try: + statut_actuel = getattr(doc, "DO_Statut", 0) + nouveau_statut = int(statut_a_modifier) + + if nouveau_statut != statut_actuel: + doc.DO_Statut = nouveau_statut + doc.Write() + + import time + time.sleep(0.5) + + doc.Read() + + champs_modifies.append("statut") + logger.info(f"📊 Statut modifié: {statut_actuel} → {nouveau_statut}") + except Exception as e: + logger.warning(f"⚠️ Impossible de modifier le statut: {e}") + # ======================================== # ÉTAPE 7 : RELECTURE ET RETOUR # ======================================== logger.info("📊 Relecture finale...") import time - - time.sleep(1) + time.sleep(0.5) doc.Read() @@ -6683,16 +6424,19 @@ class SageConnector: total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) + reference_finale = getattr(doc, "DO_Ref", "") logger.info(f"✅ ✅ ✅ SUCCÈS: {numero} modifiée ✅ ✅ ✅") logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC") logger.info(f" 👤 Client final: {client_final}") + logger.info(f" 🔖 Référence: {reference_finale}") logger.info(f" 📝 Champs modifiés: {champs_modifies}") return { "numero": numero, "total_ht": total_ht, "total_ttc": total_ttc, + "reference": reference_finale, "champs_modifies": champs_modifies, "statut": getattr(doc, "DO_Statut", 0), "client_code": client_final, @@ -6717,7 +6461,8 @@ class SageConnector: pass raise RuntimeError(f"Erreur Sage: {error_message}") - + + def creer_livraison_enrichi(self, livraison_data: dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -6750,22 +6495,7 @@ class SageConnector: 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()}") + doc.DO_Date = pywintypes.Time(self.normaliser_date(livraison_data.get("date_livraison"))) # Client (CRITIQUE) factory_client = self.cial.CptaApplication.FactoryClient @@ -6965,7 +6695,7 @@ class SageConnector: "total_ttc": total_ttc, "nb_lignes": len(livraison_data["lignes"]), "client_code": livraison_data["client"]["code"], - "date_livraison": str(date_obj.date()), + "date_livraison": str(self.normaliser_date(livraison_data.get("date_livraison"))), } except Exception as e: @@ -6980,262 +6710,364 @@ class SageConnector: 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: - if not self.cial: - raise RuntimeError("Connexion Sage non établie") + if not self.cial: + raise RuntimeError("Connexion Sage non établie") - try: - with self._com_context(), self._lock_com: - logger.info(f"🔬 === MODIFICATION LIVRAISON {numero} ===") + try: + with self._com_context(), self._lock_com: + logger.info(f"🔬 === MODIFICATION LIVRAISON {numero} ===") - # ======================================== - # ÉTAPE 1 : CHARGER LE DOCUMENT - # ======================================== - logger.info("📂 Chargement document...") + # ======================================== + # ÉTAPE 1 : CHARGER LE DOCUMENT + # ======================================== + logger.info("📂 Chargement document...") - factory = self.cial.FactoryDocumentVente - persist = None + 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: + # Chercher le document + for type_test in [30, settings.SAGE_TYPE_BON_LIVRAISON]: try: - ligne_p = factory_lignes.List(index) - if ligne_p is None: + persist_test = factory.ReadPiece(type_test, numero) + if persist_test: + persist = persist_test + logger.info(f" ✅ Document trouvé (type={type_test})") break - nb_lignes_initial += 1 - index += 1 except: - break + continue - logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") - except Exception as e: - logger.warning(f" ⚠️ Erreur comptage lignes: {e}") + if not persist: + raise ValueError(f"❌ Livraison {numero} INTROUVABLE") - # ======================================== - # ÉTAPE 2 : DÉTERMINER LES MODIFICATIONS - # ======================================== - champs_modifies = [] + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() - 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 - ) + statut_actuel = getattr(doc, "DO_Statut", 0) - 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}") + logger.info(f" 📊 Statut={statut_actuel}") - # ======================================== - # ÉTAPE 3 : MODIFICATIONS SIMPLES - # ======================================== - if not modif_lignes and (modif_date or modif_statut or modif_ref): - logger.info("🎯 Modifications simples (sans lignes)...") + # 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 modif_date: - import pywintypes + if statut_actuel == 6: + raise ValueError(f"La livraison {numero} est annulée") - date_str = livraison_data["date_livraison"] + # 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 - 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 + logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") + except Exception as e: + logger.warning(f" ⚠️ Erreur comptage lignes: {e}") - doc.DO_Date = pywintypes.Time(date_obj) - champs_modifies.append("date") - logger.info(f" ✅ Date définie: {date_obj.date()}") + # ======================================== + # ÉTAPE 2 : DÉTERMINER LES MODIFICATIONS + # ======================================== + champs_modifies = [] - 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" + 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 ) - try: - factory_lignes = doc.FactoryDocumentLigne - except: - factory_lignes = doc.FactoryDocumentVenteLigne + 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}") - factory_article = self.cial.FactoryArticle + # ===== EXTRAIRE référence et statut pour les traiter à la fin si lignes modifiées ===== + reference_a_modifier = None + statut_a_modifier = None + + if modif_lignes: + if modif_ref: + reference_a_modifier = livraison_data.pop("reference") + logger.info("🔖 Modification de la référence reportée après les lignes") + modif_ref = False # Ne pas traiter dans les modifications simples + + if modif_statut: + statut_a_modifier = livraison_data.pop("statut") + logger.info("📊 Modification du statut reportée après les lignes") + modif_statut = False # Ne pas traiter dans les modifications simples - # SUPPRESSION TOUTES LES LIGNES - if nb_lignes_initial > 0: - logger.info(f" 🗑️ Suppression de {nb_lignes_initial} lignes...") + # ======================================== + # ÉTAPE 3 : MODIFICATIONS SIMPLES (pas de lignes) + # ======================================== + if not modif_lignes and (modif_date or modif_statut or modif_ref): + logger.info("🎯 Modifications simples (sans lignes)...") - for idx in range(nb_lignes_initial, 0, -1): + if modif_date: + logger.info(" 📅 Modification date...") + + doc.DO_Date = pywintypes.Time(self.normaliser_date(livraison_data.get("date_livraison"))) + champs_modifies.append("date") + + if modif_statut: + logger.info(" 📊 Modification 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: + logger.info(" 📖 Modification référence...") 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") + doc.DO_Ref = livraison_data["reference"] + champs_modifies.append("reference") + logger.info(f" ✅ Référence définie: {livraison_data['reference']}") except Exception as e: - logger.warning( - f" ⚠️ Erreur suppression ligne {idx}: {e}" - ) + logger.warning(f" ⚠️ Référence non définie: {e}") - logger.info(" ✅ Toutes les lignes supprimées") + logger.info(" 💾 Write()...") + doc.Write() + logger.info(" ✅ Write() réussi") - # AJOUT NOUVELLES LIGNES - logger.info(f" ➕ Ajout de {nb_nouvelles} nouvelles lignes...") + # ======================================== + # ÉTAPE 4 : REMPLACEMENT COMPLET LIGNES + # ======================================== + elif modif_lignes: + logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") - for idx, ligne_data in enumerate(nouvelles_lignes, 1): - persist_article = factory_article.ReadReference( - ligne_data["article_code"] + nouvelles_lignes = livraison_data["lignes"] + nb_nouvelles = len(nouvelles_lignes) + + logger.info( + f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles" ) - 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" - ) + factory_lignes = doc.FactoryDocumentLigne except: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentVenteLigne3" + factory_lignes = doc.FactoryDocumentVenteLigne + + factory_article = self.cial.FactoryArticle + + # ============================================ + # SOUS-ÉTAPE 1 : SUPPRESSION TOUTES LES LIGNES + # ============================================ + if nb_lignes_initial > 0: + logger.info(f" 🗑️ Suppression de {nb_lignes_initial} lignes...") + + # Supprimer depuis la fin pour éviter les problèmes d'index + 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}" + ) + # Continuer même si une suppression échoue + + logger.info(" ✅ Toutes les lignes supprimées") + + # ============================================ + # SOUS-ÉTAPE 2 : AJOUT NOUVELLES LIGNES + # ============================================ + logger.info(f" ➕ Ajout de {nb_nouvelles} nouvelles lignes...") + + for idx, ligne_data in enumerate(nouvelles_lignes, 1): + logger.info( + f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---" ) - quantite = float(ligne_data["quantite"]) - - try: - ligne_obj.SetDefaultArticleReference( - ligne_data["article_code"], quantite + # Charger l'article + persist_article = factory_article.ReadReference( + ligne_data["article_code"] ) - 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"] + if not persist_article: + raise ValueError( + f"Article {ligne_data['article_code']} introuvable" + ) + + article_obj = win32com.client.CastTo( + persist_article, "IBOArticle3" + ) + article_obj.Read() + + # Créer nouvelle ligne + ligne_persist = factory_lignes.Create() + + try: + ligne_obj = win32com.client.CastTo( + ligne_persist, "IBODocumentLigne3" ) - ligne_obj.DL_Remise01REM_Type = 0 except: - pass + ligne_obj = win32com.client.CastTo( + ligne_persist, "IBODocumentVenteLigne3" + ) - ligne_obj.Write() - logger.debug(f" ✅ Ligne {idx} ajoutée") + quantite = float(ligne_data["quantite"]) - logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") + # Associer article + 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 - doc.Write() - champs_modifies.append("lignes") + # Prix + if ligne_data.get("prix_unitaire_ht"): + ligne_obj.DL_PrixUnitaire = float( + ligne_data["prix_unitaire_ht"] + ) - # ======================================== - # ÉTAPE 5 : RELECTURE ET RETOUR - # ======================================== - import time + # Remise + 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 - time.sleep(1) + # Écrire la ligne + ligne_obj.Write() + logger.info(f" ✅ Ligne {idx} ajoutée") - doc.Read() + logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") - total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) - total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) + # Écrire le document + logger.info(" 💾 Write() document après remplacement lignes...") + doc.Write() + logger.info(" ✅ Document écrit") - logger.info(f"✅✅✅ LIVRAISON MODIFIÉE: {numero} ✅✅✅") - logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC") + import time + time.sleep(0.5) - return { - "numero": numero, - "total_ht": total_ht, - "total_ttc": total_ttc, - "champs_modifies": champs_modifies, - "statut": getattr(doc, "DO_Statut", 0), - } + doc.Read() - 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)}") + champs_modifies.append("lignes") + # ======================================== + # ÉTAPE 4.5 : MODIFIER RÉFÉRENCE (APRÈS les lignes) + # ======================================== + if reference_a_modifier is not None: + try: + ancienne_reference = getattr(doc, "DO_Ref", "") + nouvelle_reference = str(reference_a_modifier) if reference_a_modifier else "" + + logger.info(f"🔖 Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'") + + doc.DO_Ref = nouvelle_reference + doc.Write() + + import time + time.sleep(0.5) + + doc.Read() + + champs_modifies.append("reference") + logger.info(f" ✅ Référence modifiée avec succès") + except Exception as e: + logger.warning(f"⚠️ Impossible de modifier la référence: {e}") + + # ======================================== + # ÉTAPE 4.6 : MODIFIER STATUT (EN DERNIER) + # ======================================== + if statut_a_modifier is not None: + try: + statut_actuel = getattr(doc, "DO_Statut", 0) + nouveau_statut = int(statut_a_modifier) + + if nouveau_statut != statut_actuel: + logger.info(f"📊 Modification statut: {statut_actuel} → {nouveau_statut}") + + doc.DO_Statut = nouveau_statut + doc.Write() + + import time + time.sleep(0.5) + + doc.Read() + + champs_modifies.append("statut") + logger.info(f" ✅ Statut modifié avec succès") + except Exception as e: + logger.warning(f"⚠️ Impossible de modifier le statut: {e}") + + # ======================================== + # ÉTAPE 5 : RELECTURE ET RETOUR + # ======================================== + logger.info("📊 Relecture finale...") + + import time + time.sleep(0.5) + + doc.Read() + + total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) + total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) + reference_finale = getattr(doc, "DO_Ref", "") + statut_final = getattr(doc, "DO_Statut", 0) + + logger.info(f"✅ ✅ ✅ SUCCÈS: {numero} modifiée ✅ ✅ ✅") + logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC") + logger.info(f" 🔖 Référence: {reference_finale}") + logger.info(f" 📊 Statut: {statut_final}") + logger.info(f" 📝 Champs modifiés: {champs_modifies}") + + return { + "numero": numero, + "total_ht": total_ht, + "total_ttc": total_ttc, + "reference": reference_finale, + "champs_modifies": champs_modifies, + "statut": statut_final, + } + + 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) + + error_message = str(e) + if self.cial: + try: + err = self.cial.CptaApplication.LastError + if err: + error_message = ( + f"Erreur Sage: {err.Description} (Code: {err.Number})" + ) + except: + pass + + raise RuntimeError(f"Erreur Sage: {error_message}") + + def creer_avoir_enrichi(self, avoir_data: dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -7268,20 +7100,7 @@ class SageConnector: logger.info("📄 Document avoir créé") - # Date - import pywintypes - - if isinstance(avoir_data["date_avoir"], str): - date_obj = datetime.fromisoformat(avoir_data["date_avoir"]) - elif isinstance(avoir_data["date_avoir"], date): - date_obj = datetime.combine( - avoir_data["date_avoir"], datetime.min.time() - ) - else: - date_obj = datetime.now() - - doc.DO_Date = pywintypes.Time(date_obj) - logger.info(f"📅 Date définie: {date_obj.date()}") + doc.DO_Date = pywintypes.Time(self.normaliser_date(avoir_data.get("date_avoir"))) # Client (CRITIQUE) factory_client = self.cial.CptaApplication.FactoryClient @@ -7479,7 +7298,7 @@ class SageConnector: "total_ttc": total_ttc, "nb_lignes": len(avoir_data["lignes"]), "client_code": avoir_data["client"]["code"], - "date_avoir": str(date_obj.date()), + "date_avoir": str(self.normaliser_date(avoir_data.get("date_avoir"))), } except Exception as e: @@ -7494,259 +7313,467 @@ class SageConnector: logger.error(f"❌ Erreur création avoir: {e}", exc_info=True) raise RuntimeError(f"Échec création avoir: {str(e)}") + def modifier_avoir(self, numero: str, avoir_data: Dict) -> Dict: - if not self.cial: - raise RuntimeError("Connexion Sage non établie") + if not self.cial: + raise RuntimeError("Connexion Sage non établie") - try: - with self._com_context(), self._lock_com: - logger.info(f"🔬 === MODIFICATION AVOIR {numero} ===") + try: + with self._com_context(), self._lock_com: + logger.info(f"🔬 === MODIFICATION AVOIR {numero} ===") - # ÉTAPE 1 : CHARGER LE DOCUMENT - logger.info("📂 Chargement document...") + # ======================================== + # ÉTAPE 1 : CHARGER LE DOCUMENT + # ======================================== + logger.info("📂 Chargement document...") - factory = self.cial.FactoryDocumentVente - persist = None + factory = self.cial.FactoryDocumentVente + persist = None - # Chercher le document - for type_test in [50, settings.SAGE_TYPE_BON_AVOIR]: - 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"❌ Avoir {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'il n'est pas transformé - if statut_actuel == 5: - raise ValueError(f"L'avoir {numero} a déjà été transformé") - - if statut_actuel == 6: - raise ValueError(f"L'avoir {numero} est annulé") - - # 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: + # Chercher le document + for type_test in [50, settings.SAGE_TYPE_BON_AVOIR]: try: - ligne_p = factory_lignes.List(index) - if ligne_p is None: + persist_test = factory.ReadPiece(type_test, numero) + if persist_test: + persist = persist_test + logger.info(f" ✅ Document trouvé (type={type_test})") break - nb_lignes_initial += 1 - index += 1 except: - break + continue - logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") - except Exception as e: - logger.warning(f" ⚠️ Erreur comptage lignes: {e}") + if not persist: + raise ValueError(f"❌ Avoir {numero} INTROUVABLE") - # ÉTAPE 2 : DÉTERMINER LES MODIFICATIONS - champs_modifies = [] + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() - modif_date = "date_avoir" in avoir_data - modif_statut = "statut" in avoir_data - modif_ref = "reference" in avoir_data - modif_lignes = ( - "lignes" in avoir_data and avoir_data["lignes"] is not None - ) + statut_actuel = getattr(doc, "DO_Statut", 0) + type_reel = getattr(doc, "DO_Type", -1) - 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}") + logger.info(f" 📊 Type={type_reel}, Statut={statut_actuel}") - # ÉTAPE 3 : MODIFICATIONS SIMPLES - if not modif_lignes and (modif_date or modif_statut or modif_ref): - logger.info("🎯 Modifications simples (sans lignes)...") + # Vérifier qu'il n'est pas transformé ou annulé + if statut_actuel == 5: + raise ValueError(f"L'avoir {numero} a déjà été transformé") - if modif_date: - import pywintypes + if statut_actuel == 6: + raise ValueError(f"L'avoir {numero} est annulé") - date_str = avoir_data["date_avoir"] - - 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()) + # ======================================== + # ÉTAPE 2 : VÉRIFIER CLIENT INITIAL + # ======================================== + client_code_initial = "" + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_code_initial = getattr(client_obj, "CT_Num", "").strip() + logger.info(f" 👤 Client initial: {client_code_initial}") else: - date_obj = date_str + logger.error(" ❌ Objet Client NULL à l'état initial !") + except Exception as e: + logger.error(f" ❌ Erreur lecture client initial: {e}") - doc.DO_Date = pywintypes.Time(date_obj) - champs_modifies.append("date") - logger.info(f" ✅ Date définie: {date_obj.date()}") + if not client_code_initial: + raise ValueError("❌ Client introuvable dans le document") - if modif_statut: - nouveau_statut = avoir_data["statut"] - doc.DO_Statut = nouveau_statut - champs_modifies.append("statut") - logger.info(f" ✅ Statut défini: {nouveau_statut}") + # 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 - if modif_ref: - try: - doc.DO_Ref = avoir_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}") + logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") + except Exception as e: + logger.warning(f" ⚠️ Erreur comptage lignes: {e}") - doc.Write() - logger.info(" ✅ Write() réussi") + # ======================================== + # ÉTAPE 3 : DÉTERMINER LES MODIFICATIONS + # ======================================== + champs_modifies = [] - # ÉTAPE 4 : REMPLACEMENT COMPLET LIGNES - elif modif_lignes: - logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") - - nouvelles_lignes = avoir_data["lignes"] - nb_nouvelles = len(nouvelles_lignes) - - logger.info( - f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles" + modif_date = "date_avoir" in avoir_data + modif_statut = "statut" in avoir_data + modif_ref = "reference" in avoir_data + modif_lignes = ( + "lignes" in avoir_data and avoir_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}") + + # ===== EXTRAIRE référence et statut pour les traiter à la fin si lignes modifiées ===== + reference_a_modifier = None + statut_a_modifier = None + + if modif_lignes: + if modif_ref: + reference_a_modifier = avoir_data.pop("reference") + logger.info("🔖 Modification de la référence reportée après les lignes") + modif_ref = False # Ne pas traiter dans les modifications simples + + if modif_statut: + statut_a_modifier = avoir_data.pop("statut") + logger.info("📊 Modification du statut reportée après les lignes") + modif_statut = False # Ne pas traiter dans les modifications simples + + # ======================================== + # ÉTAPE 4 : TEST WRITE() BASIQUE + # ======================================== + logger.info("🧪 Test Write() basique (sans modification)...") + try: - factory_lignes = doc.FactoryDocumentLigne - except: - factory_lignes = doc.FactoryDocumentVenteLigne + doc.Write() + logger.info(" ✅ Write() basique OK") + doc.Read() - factory_article = self.cial.FactoryArticle + # Vérifier que le client est toujours là + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_apres = getattr(client_obj, "CT_Num", "") + if client_apres == client_code_initial: + logger.info(f" ✅ Client préservé: {client_apres}") + else: + logger.error( + f" ❌ Client a changé: {client_code_initial} → {client_apres}" + ) + else: + logger.error(" ❌ Client devenu NULL après Write() basique") - # SUPPRESSION TOUTES LES LIGNES - if nb_lignes_initial > 0: - logger.info(f" 🗑️ Suppression de {nb_lignes_initial} lignes...") + except Exception as e: + logger.error(f" ❌ Write() basique ÉCHOUE: {e}") + logger.error(f" ❌ ABANDON: Le document est VERROUILLÉ") + raise ValueError( + f"Document verrouillé, impossible de modifier: {e}" + ) - for idx in range(nb_lignes_initial, 0, -1): + # ======================================== + # ÉTAPE 5 : MODIFICATIONS SIMPLES (pas de lignes) + # ======================================== + if not modif_lignes and (modif_date or modif_statut or modif_ref): + logger.info("🎯 Modifications simples (sans lignes)...") + + if modif_date: + logger.info(" 📅 Modification date...") + + doc.DO_Date = pywintypes.Time(self.normaliser_date(avoir_data.get("date_avoir"))) + champs_modifies.append("date") + + if modif_statut: + logger.info(" 📊 Modification statut...") + nouveau_statut = avoir_data["statut"] + doc.DO_Statut = nouveau_statut + champs_modifies.append("statut") + logger.info(f" ✅ Statut défini: {nouveau_statut}") + + if modif_ref: + logger.info(" 📖 Modification référence...") 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") + doc.DO_Ref = avoir_data["reference"] + champs_modifies.append("reference") + logger.info( + f" ✅ Référence définie: {avoir_data['reference']}" + ) 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() + logger.warning(f" ⚠️ Référence non définie: {e}") + # Écrire sans réassocier le client + logger.info(" 💾 Write() sans réassociation client...") try: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentLigne3" - ) - except: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentVenteLigne3" - ) + doc.Write() + logger.info(" ✅ Write() réussi") - quantite = float(ligne_data["quantite"]) + doc.Read() - try: - ligne_obj.SetDefaultArticleReference( - ligne_data["article_code"], quantite - ) - except: + # Vérifier client + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_apres = getattr(client_obj, "CT_Num", "") + if client_apres == client_code_initial: + logger.info(f" ✅ Client préservé: {client_apres}") + else: + logger.error( + f" ❌ Client perdu: {client_code_initial} → {client_apres}" + ) + + except Exception as e: + error_msg = str(e) 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 + sage_error = self.cial.CptaApplication.LastError + if sage_error: + error_msg = f"{sage_error.Description} (Code: {sage_error.Number})" except: pass - ligne_obj.Write() - logger.debug(f" ✅ Ligne {idx} ajoutée") + logger.error(f" ❌ Write() échoue: {error_msg}") + raise ValueError(f"Sage refuse: {error_msg}") - logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") + # ======================================== + # ÉTAPE 6 : REMPLACEMENT COMPLET DES LIGNES + # ======================================== + elif modif_lignes: + logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") - doc.Write() - champs_modifies.append("lignes") + nouvelles_lignes = avoir_data["lignes"] + nb_nouvelles = len(nouvelles_lignes) - # ÉTAPE 5 : RELECTURE ET RETOUR - import time + logger.info( + f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" + ) - time.sleep(1) + try: + factory_lignes = doc.FactoryDocumentLigne + except: + factory_lignes = doc.FactoryDocumentVenteLigne - doc.Read() + factory_article = self.cial.FactoryArticle - total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) - total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) + # ============================================ + # SOUS-ÉTAPE 1 : SUPPRIMER TOUTES LES LIGNES EXISTANTES + # ============================================ + if nb_lignes_initial > 0: + logger.info( + f" 🗑️ Suppression de {nb_lignes_initial} lignes existantes..." + ) - logger.info(f"✅✅✅ AVOIR MODIFIÉ: {numero} ✅✅✅") - logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC") + # Supprimer depuis la fin pour éviter les problèmes d'index + 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() - return { - "numero": numero, - "total_ht": total_ht, - "total_ttc": total_ttc, - "champs_modifies": champs_modifies, - "statut": getattr(doc, "DO_Statut", 0), - } + # ✅ Utiliser .Remove() + ligne.Remove() + logger.debug(f" ✅ Ligne {idx} supprimée") + except Exception as e: + logger.warning( + f" ⚠️ Impossible de supprimer ligne {idx}: {e}" + ) + # Continuer même si une suppression échoue - 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)}") + logger.info(" ✅ Toutes les lignes existantes supprimées") - 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)}") + # ============================================ + # SOUS-ÉTAPE 2 : AJOUTER LES NOUVELLES LIGNES + # ============================================ + logger.info(f" ➕ Ajout de {nb_nouvelles} nouvelles lignes...") + for idx, ligne_data in enumerate(nouvelles_lignes, 1): + logger.info( + f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---" + ) + + # Charger l'article + 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() + + # Créer nouvelle 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"]) + + # Associer article + 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 + + # Prix + if ligne_data.get("prix_unitaire_ht"): + ligne_obj.DL_PrixUnitaire = float( + ligne_data["prix_unitaire_ht"] + ) + + # Remise + 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 + + # Écrire la ligne + ligne_obj.Write() + logger.info(f" ✅ Ligne {idx} ajoutée") + + logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") + + # Écrire le document + logger.info(" 💾 Write() document après remplacement lignes...") + doc.Write() + logger.info(" ✅ Document écrit") + + import time + time.sleep(0.5) + + doc.Read() + + # Vérifier client + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_apres = getattr(client_obj, "CT_Num", "") + logger.info(f" 👤 Client après remplacement: {client_apres}") + else: + logger.error(" ❌ Client NULL après remplacement") + + champs_modifies.append("lignes") + + # ======================================== + # ÉTAPE 6.5 : MODIFIER RÉFÉRENCE (APRÈS les lignes) + # ======================================== + if reference_a_modifier is not None: + try: + ancienne_reference = getattr(doc, "DO_Ref", "") + nouvelle_reference = str(reference_a_modifier) if reference_a_modifier else "" + + logger.info(f"🔖 Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'") + + doc.DO_Ref = nouvelle_reference + doc.Write() + + import time + time.sleep(0.5) + + doc.Read() + + champs_modifies.append("reference") + logger.info(f" ✅ Référence modifiée avec succès") + except Exception as e: + logger.warning(f"⚠️ Impossible de modifier la référence: {e}") + + # ======================================== + # ÉTAPE 6.6 : MODIFIER STATUT (EN DERNIER) + # ======================================== + if statut_a_modifier is not None: + try: + statut_actuel = getattr(doc, "DO_Statut", 0) + nouveau_statut = int(statut_a_modifier) + + if nouveau_statut != statut_actuel: + logger.info(f"📊 Modification statut: {statut_actuel} → {nouveau_statut}") + + doc.DO_Statut = nouveau_statut + doc.Write() + + import time + time.sleep(0.5) + + doc.Read() + + champs_modifies.append("statut") + logger.info(f" ✅ Statut modifié avec succès") + except Exception as e: + logger.warning(f"⚠️ Impossible de modifier le statut: {e}") + + # ======================================== + # ÉTAPE 7 : RELECTURE ET RETOUR + # ======================================== + logger.info("📊 Relecture finale...") + + import time + time.sleep(0.5) + + doc.Read() + + # Vérifier client final + client_obj_final = getattr(doc, "Client", None) + if client_obj_final: + client_obj_final.Read() + client_final = getattr(client_obj_final, "CT_Num", "") + else: + client_final = "" + + total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) + total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) + reference_finale = getattr(doc, "DO_Ref", "") + statut_final = getattr(doc, "DO_Statut", 0) + + logger.info(f"✅ ✅ ✅ SUCCÈS: {numero} modifié ✅ ✅ ✅") + logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC") + logger.info(f" 👤 Client final: {client_final}") + logger.info(f" 🔖 Référence: {reference_finale}") + logger.info(f" 📊 Statut: {statut_final}") + logger.info(f" 📝 Champs modifiés: {champs_modifies}") + + return { + "numero": numero, + "total_ht": total_ht, + "total_ttc": total_ttc, + "reference": reference_finale, + "champs_modifies": champs_modifies, + "statut": statut_final, + "client_code": client_final, + } + + 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) + + error_message = str(e) + if self.cial: + try: + err = self.cial.CptaApplication.LastError + if err: + error_message = ( + f"Erreur Sage: {err.Description} (Code: {err.Number})" + ) + except: + pass + + raise RuntimeError(f"Erreur Sage: {error_message}") + + def creer_facture_enrichi(self, facture_data: dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -7779,20 +7806,7 @@ class SageConnector: logger.info("📄 Document facture créé") - # Date - import pywintypes - - if isinstance(facture_data["date_facture"], str): - date_obj = datetime.fromisoformat(facture_data["date_facture"]) - elif isinstance(facture_data["date_facture"], date): - date_obj = datetime.combine( - facture_data["date_facture"], datetime.min.time() - ) - else: - date_obj = datetime.now() - - doc.DO_Date = pywintypes.Time(date_obj) - logger.info(f"📅 Date définie: {date_obj.date()}") + doc.DO_Date = pywintypes.Time(self.normaliser_date(facture_data.get("date_facture"))) # Client (CRITIQUE) factory_client = self.cial.CptaApplication.FactoryClient @@ -8052,7 +8066,7 @@ class SageConnector: "total_ttc": total_ttc, "nb_lignes": len(facture_data["lignes"]), "client_code": facture_data["client"]["code"], - "date_facture": str(date_obj.date()), + "date_facture": str(self.normaliser_date(facture_data.get("date_facture"))), } except Exception as e: @@ -8068,279 +8082,466 @@ class SageConnector: logger.error(f"❌ Erreur création facture: {e}", exc_info=True) raise RuntimeError(f"Échec création facture: {str(e)}") + def modifier_facture(self, numero: str, facture_data: Dict) -> Dict: - if not self.cial: - raise RuntimeError("Connexion Sage non établie") + if not self.cial: + raise RuntimeError("Connexion Sage non établie") - try: - with self._com_context(), self._lock_com: - logger.info(f"🔬 === MODIFICATION FACTURE {numero} ===") + try: + with self._com_context(), self._lock_com: + logger.info(f"🔬 === MODIFICATION FACTURE {numero} ===") - # ÉTAPE 1 : CHARGER LE DOCUMENT - logger.info("📂 Chargement document...") + # ======================================== + # ÉTAPE 1 : CHARGER LE DOCUMENT + # ======================================== + logger.info("📂 Chargement document...") - factory = self.cial.FactoryDocumentVente - persist = None + factory = self.cial.FactoryDocumentVente + persist = None - # Chercher le document - for type_test in [60, 5, settings.SAGE_TYPE_FACTURE]: - 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"❌ Facture {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 ou annulée - if statut_actuel == 5: - raise ValueError(f"La facture {numero} a déjà été transformée") - - if statut_actuel == 6: - raise ValueError(f"La facture {numero} est annulée") - - # Vérifier client initial - client_code_initial = "" - try: - client_obj = getattr(doc, "Client", None) - if client_obj: - client_obj.Read() - client_code_initial = getattr(client_obj, "CT_Num", "").strip() - logger.info(f" 👤 Client initial: {client_code_initial}") - except Exception as e: - logger.error(f" ❌ Erreur lecture client initial: {e}") - - if not client_code_initial: - raise ValueError("❌ Client introuvable dans le document") - - # 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: + # Chercher le document + for type_test in [60, 5, settings.SAGE_TYPE_FACTURE]: try: - ligne_p = factory_lignes.List(index) - if ligne_p is None: + persist_test = factory.ReadPiece(type_test, numero) + if persist_test: + persist = persist_test + logger.info(f" ✅ Document trouvé (type={type_test})") break - nb_lignes_initial += 1 - index += 1 except: - break + continue - logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") - except Exception as e: - logger.warning(f" ⚠️ Erreur comptage lignes: {e}") + if not persist: + raise ValueError(f"❌ Facture {numero} INTROUVABLE") - # ÉTAPE 2 : DÉTERMINER LES MODIFICATIONS - champs_modifies = [] - - modif_date = "date_facture" in facture_data - modif_statut = "statut" in facture_data - modif_ref = "reference" in facture_data - modif_lignes = ( - "lignes" in facture_data and facture_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 : TEST WRITE() BASIQUE - logger.info("🧪 Test Write() basique (sans modification)...") - - try: - doc.Write() - logger.info(" ✅ Write() basique OK") + doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() - except Exception as e: - logger.error(f" ❌ Write() basique ÉCHOUE: {e}") - logger.error(f" ❌ ABANDON: Le document est VERROUILLÉ") - raise ValueError( - f"Document verrouillé, impossible de modifier: {e}" - ) - # ÉTAPE 4 : MODIFICATIONS SIMPLES - if not modif_lignes and (modif_date or modif_statut or modif_ref): - logger.info("🎯 Modifications simples (sans lignes)...") + statut_actuel = getattr(doc, "DO_Statut", 0) + type_reel = getattr(doc, "DO_Type", -1) - if modif_date: - import pywintypes + logger.info(f" 📊 Type={type_reel}, Statut={statut_actuel}") - date_str = facture_data["date_facture"] + # Vérifier qu'elle n'est pas transformée ou annulée + if statut_actuel == 5: + raise ValueError(f"La facture {numero} a déjà été transformée") - 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()) + if statut_actuel == 6: + raise ValueError(f"La facture {numero} est annulée") + + # ======================================== + # ÉTAPE 2 : VÉRIFIER CLIENT INITIAL + # ======================================== + client_code_initial = "" + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_code_initial = getattr(client_obj, "CT_Num", "").strip() + logger.info(f" 👤 Client initial: {client_code_initial}") else: - date_obj = date_str + logger.error(" ❌ Objet Client NULL à l'état initial !") + except Exception as e: + logger.error(f" ❌ Erreur lecture client initial: {e}") - doc.DO_Date = pywintypes.Time(date_obj) - champs_modifies.append("date") - logger.info(f" ✅ Date définie: {date_obj.date()}") + if not client_code_initial: + raise ValueError("❌ Client introuvable dans le document") - if modif_statut: - nouveau_statut = facture_data["statut"] - doc.DO_Statut = nouveau_statut - champs_modifies.append("statut") - logger.info(f" ✅ Statut défini: {nouveau_statut}") + # 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 - if modif_ref: - try: - doc.DO_Ref = facture_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}") + logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") + except Exception as e: + logger.warning(f" ⚠️ Erreur comptage lignes: {e}") - doc.Write() - logger.info(" ✅ Write() réussi") + # ======================================== + # ÉTAPE 3 : DÉTERMINER LES MODIFICATIONS + # ======================================== + champs_modifies = [] - # ÉTAPE 5 : REMPLACEMENT COMPLET LIGNES - elif modif_lignes: - logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") - - nouvelles_lignes = facture_data["lignes"] - nb_nouvelles = len(nouvelles_lignes) - - logger.info( - f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles" + modif_date = "date_facture" in facture_data + modif_statut = "statut" in facture_data + modif_ref = "reference" in facture_data + modif_lignes = ( + "lignes" in facture_data and facture_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}") + + # ===== EXTRAIRE référence et statut pour les traiter à la fin si lignes modifiées ===== + reference_a_modifier = None + statut_a_modifier = None + + if modif_lignes: + if modif_ref: + reference_a_modifier = facture_data.pop("reference") + logger.info("🔖 Modification de la référence reportée après les lignes") + modif_ref = False # Ne pas traiter dans les modifications simples + + if modif_statut: + statut_a_modifier = facture_data.pop("statut") + logger.info("📊 Modification du statut reportée après les lignes") + modif_statut = False # Ne pas traiter dans les modifications simples + + # ======================================== + # ÉTAPE 4 : TEST WRITE() BASIQUE + # ======================================== + logger.info("🧪 Test Write() basique (sans modification)...") + try: - factory_lignes = doc.FactoryDocumentLigne - except: - factory_lignes = doc.FactoryDocumentVenteLigne + doc.Write() + logger.info(" ✅ Write() basique OK") + doc.Read() - factory_article = self.cial.FactoryArticle + # Vérifier que le client est toujours là + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_apres = getattr(client_obj, "CT_Num", "") + if client_apres == client_code_initial: + logger.info(f" ✅ Client préservé: {client_apres}") + else: + logger.error( + f" ❌ Client a changé: {client_code_initial} → {client_apres}" + ) + else: + logger.error(" ❌ Client devenu NULL après Write() basique") - # SUPPRESSION TOUTES LES LIGNES - if nb_lignes_initial > 0: - logger.info(f" 🗑️ Suppression de {nb_lignes_initial} lignes...") + except Exception as e: + logger.error(f" ❌ Write() basique ÉCHOUE: {e}") + logger.error(f" ❌ ABANDON: Le document est VERROUILLÉ") + raise ValueError( + f"Document verrouillé, impossible de modifier: {e}" + ) - for idx in range(nb_lignes_initial, 0, -1): + # ======================================== + # ÉTAPE 5 : MODIFICATIONS SIMPLES (pas de lignes) + # ======================================== + if not modif_lignes and (modif_date or modif_statut or modif_ref): + logger.info("🎯 Modifications simples (sans lignes)...") + + if modif_date: + logger.info(" 📅 Modification date...") + + doc.DO_Date = pywintypes.Time(self.normaliser_date(facture_data.get("date_facture"))) + champs_modifies.append("date") + + if modif_statut: + logger.info(" 📊 Modification statut...") + nouveau_statut = facture_data["statut"] + doc.DO_Statut = nouveau_statut + champs_modifies.append("statut") + logger.info(f" ✅ Statut défini: {nouveau_statut}") + + if modif_ref: + logger.info(" 📖 Modification référence...") 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") + doc.DO_Ref = facture_data["reference"] + champs_modifies.append("reference") + logger.info( + f" ✅ Référence définie: {facture_data['reference']}" + ) 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() + logger.warning(f" ⚠️ Référence non définie: {e}") + # Écrire sans réassocier le client + logger.info(" 💾 Write() sans réassociation client...") try: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentLigne3" - ) - except: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentVenteLigne3" - ) + doc.Write() + logger.info(" ✅ Write() réussi") - quantite = float(ligne_data["quantite"]) + doc.Read() - try: - ligne_obj.SetDefaultArticleReference( - ligne_data["article_code"], quantite - ) - except: + # Vérifier client + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_apres = getattr(client_obj, "CT_Num", "") + if client_apres == client_code_initial: + logger.info(f" ✅ Client préservé: {client_apres}") + else: + logger.error( + f" ❌ Client perdu: {client_code_initial} → {client_apres}" + ) + + except Exception as e: + error_msg = str(e) 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 + sage_error = self.cial.CptaApplication.LastError + if sage_error: + error_msg = f"{sage_error.Description} (Code: {sage_error.Number})" except: pass - ligne_obj.Write() - logger.debug(f" ✅ Ligne {idx} ajoutée") + logger.error(f" ❌ Write() échoue: {error_msg}") + raise ValueError(f"Sage refuse: {error_msg}") - logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") + # ======================================== + # ÉTAPE 6 : REMPLACEMENT COMPLET DES LIGNES + # ======================================== + elif modif_lignes: + logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") - doc.Write() - champs_modifies.append("lignes") + nouvelles_lignes = facture_data["lignes"] + nb_nouvelles = len(nouvelles_lignes) - # ÉTAPE 6 : RELECTURE ET RETOUR - import time + logger.info( + f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" + ) - time.sleep(1) + try: + factory_lignes = doc.FactoryDocumentLigne + except: + factory_lignes = doc.FactoryDocumentVenteLigne - doc.Read() + factory_article = self.cial.FactoryArticle - total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) - total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) + # ============================================ + # SOUS-ÉTAPE 1 : SUPPRIMER TOUTES LES LIGNES EXISTANTES + # ============================================ + if nb_lignes_initial > 0: + logger.info( + f" 🗑️ Suppression de {nb_lignes_initial} lignes existantes..." + ) - logger.info(f"✅✅✅ FACTURE MODIFIÉE: {numero} ✅✅✅") - logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC") + # Supprimer depuis la fin pour éviter les problèmes d'index + 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() - return { - "numero": numero, - "total_ht": total_ht, - "total_ttc": total_ttc, - "champs_modifies": champs_modifies, - "statut": getattr(doc, "DO_Statut", 0), - } + # ✅ Utiliser .Remove() + ligne.Remove() + logger.debug(f" ✅ Ligne {idx} supprimée") + except Exception as e: + logger.warning( + f" ⚠️ Impossible de supprimer ligne {idx}: {e}" + ) + # Continuer même si une suppression échoue + + logger.info(" ✅ Toutes les lignes existantes supprimées") + + # ============================================ + # SOUS-ÉTAPE 2 : AJOUTER LES NOUVELLES LIGNES + # ============================================ + logger.info(f" ➕ Ajout de {nb_nouvelles} nouvelles lignes...") + + for idx, ligne_data in enumerate(nouvelles_lignes, 1): + logger.info( + f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---" + ) + + # Charger l'article + 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() + + # Créer nouvelle 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"]) + + # Associer article + 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 + + # Prix + if ligne_data.get("prix_unitaire_ht"): + ligne_obj.DL_PrixUnitaire = float( + ligne_data["prix_unitaire_ht"] + ) + + # Remise + 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 + + # Écrire la ligne + ligne_obj.Write() + logger.info(f" ✅ Ligne {idx} ajoutée") + + logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") + + # Écrire le document + logger.info(" 💾 Write() document après remplacement lignes...") + doc.Write() + logger.info(" ✅ Document écrit") + + import time + time.sleep(0.5) + + doc.Read() + + # Vérifier client + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_apres = getattr(client_obj, "CT_Num", "") + logger.info(f" 👤 Client après remplacement: {client_apres}") + else: + logger.error(" ❌ Client NULL après remplacement") + + champs_modifies.append("lignes") + + # ======================================== + # ÉTAPE 6.5 : MODIFIER RÉFÉRENCE (APRÈS les lignes) + # ======================================== + if reference_a_modifier is not None: + try: + ancienne_reference = getattr(doc, "DO_Ref", "") + nouvelle_reference = str(reference_a_modifier) if reference_a_modifier else "" + + logger.info(f"🔖 Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'") + + doc.DO_Ref = nouvelle_reference + doc.Write() + + import time + time.sleep(0.5) + + doc.Read() + + champs_modifies.append("reference") + logger.info(f" ✅ Référence modifiée avec succès") + except Exception as e: + logger.warning(f"⚠️ Impossible de modifier la référence: {e}") + + # ======================================== + # ÉTAPE 6.6 : MODIFIER STATUT (EN DERNIER) + # ======================================== + if statut_a_modifier is not None: + try: + statut_actuel = getattr(doc, "DO_Statut", 0) + nouveau_statut = int(statut_a_modifier) + + if nouveau_statut != statut_actuel: + logger.info(f"📊 Modification statut: {statut_actuel} → {nouveau_statut}") + + doc.DO_Statut = nouveau_statut + doc.Write() + + import time + time.sleep(0.5) + + doc.Read() + + champs_modifies.append("statut") + logger.info(f" ✅ Statut modifié avec succès") + except Exception as e: + logger.warning(f"⚠️ Impossible de modifier le statut: {e}") + + # ======================================== + # ÉTAPE 7 : RELECTURE ET RETOUR + # ======================================== + logger.info("📊 Relecture finale...") + + import time + time.sleep(0.5) + + doc.Read() + + # Vérifier client final + client_obj_final = getattr(doc, "Client", None) + if client_obj_final: + client_obj_final.Read() + client_final = getattr(client_obj_final, "CT_Num", "") + else: + client_final = "" + + total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) + total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) + reference_finale = getattr(doc, "DO_Ref", "") + statut_final = getattr(doc, "DO_Statut", 0) + + logger.info(f"✅ ✅ ✅ SUCCÈS: {numero} modifiée ✅ ✅ ✅") + logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC") + logger.info(f" 👤 Client final: {client_final}") + logger.info(f" 🔖 Référence: {reference_finale}") + logger.info(f" 📊 Statut: {statut_final}") + logger.info(f" 📝 Champs modifiés: {champs_modifies}") + + return { + "numero": numero, + "total_ht": total_ht, + "total_ttc": total_ttc, + "reference": reference_finale, + "champs_modifies": champs_modifies, + "statut": statut_final, + "client_code": client_final, + } + + 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) + + error_message = str(e) + if self.cial: + try: + err = self.cial.CptaApplication.LastError + if err: + error_message = ( + f"Erreur Sage: {err.Description} (Code: {err.Number})" + ) + except: + pass + + raise RuntimeError(f"Erreur Sage: {error_message}") - 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)}") def creer_article(self, article_data: dict) -> dict: with self._com_context(), self._lock_com: @@ -10089,7 +10290,7 @@ class SageConnector: doc = win32com.client.CastTo(persist_doc, "IBODocumentStock3") doc.SetDefault() - import pywintypes + date_mouv = entree_data.get("date_mouvement") if isinstance(date_mouv, date): @@ -11286,7 +11487,7 @@ class SageConnector: doc.SetDefault() # Date - import pywintypes + date_mouv = sortie_data.get("date_mouvement") if isinstance(date_mouv, date):