diff --git a/sage_connector.py b/sage_connector.py index b75dbff..b50950a 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -3120,12 +3120,14 @@ class SageConnector: 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. + Crée un devis dans Sage avec support de la référence et des dates. Args: devis_data: dict contenant: - client: {code: str} - date_devis: str ou date + - date_expedition: str ou date (optionnel) + - date_livraison: str ou date (optionnel) - reference: str (optionnel) - lignes: list[dict] forcer_brouillon: bool, force le statut brouillon @@ -3160,7 +3162,19 @@ class SageConnector: logger.info("📄 Document devis créé") + # ===== DATES ===== doc.DO_Date = pywintypes.Time(self.normaliser_date(devis_data.get("date_devis"))) + + if "date_expedition" in devis_data and devis_data["date_expedition"]: + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(devis_data["date_expedition"]) + ) + logger.info(f"📅 Date expédition: {devis_data['date_expedition']}") + + if "date_livraison" in devis_data and devis_data["date_livraison"]: + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(devis_data["date_livraison"]) + ) # ===== CLIENT ===== factory_client = self.cial.CptaApplication.FactoryClient @@ -3323,17 +3337,17 @@ class SageConnector: try: logger.info(f"🔖 Application de la référence: {devis_data['reference']}") - # RECHARGER le document par son numéro (comme dans modifier_devis) + # RECHARGER le document par son numéro 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 + doc_reload.Write() time.sleep(0.5) - doc_reload.Read() # Relire après sauvegarde + doc_reload.Read() logger.info(f"✅ Référence définie: {nouvelle_reference}") except Exception as e: @@ -3362,7 +3376,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.""" @@ -3406,7 +3420,7 @@ class SageConnector: logger.debug("ReadPiece échoué, recherche dans List()...") persist_reread = self._rechercher_devis_dans_liste(numero_devis, factory_doc) - # Extraction des totaux + # Extraction des informations if persist_reread: doc_final = win32com.client.CastTo( persist_reread, "IBODocumentVente3" @@ -3417,6 +3431,24 @@ class SageConnector: total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) statut_final = getattr(doc_final, "DO_Statut", 0) reference_final = getattr(doc_final, "DO_Ref", "") + + # Dates + date_expedition_final = None + date_livraison_final = None + + try: + date_exped = getattr(doc_final, "DO_DateExpedition", None) + if date_exped: + date_expedition_final = date_exped.strftime("%Y-%m-%d") + except: + pass + + try: + date_livr = getattr(doc_final, "DO_DateLivr", None) + if date_livr: + date_livraison_final = date_livr.strftime("%Y-%m-%d") + except: + pass else: # Fallback: calculer manuellement total_calcule = sum( @@ -3426,12 +3458,18 @@ class SageConnector: total_ttc = round(total_calcule * 1.20, 2) statut_final = 0 if forcer_brouillon else 2 reference_final = devis_data.get("reference", "") + date_expedition_final = devis_data.get("date_expedition") + date_livraison_final = devis_data.get("date_livraison") logger.info(f"💰 Total HT: {total_ht}€") logger.info(f"💰 Total TTC: {total_ttc}€") logger.info(f"📊 Statut final: {statut_final}") if reference_final: logger.info(f"🔖 Référence: {reference_final}") + if date_expedition_final: + logger.info(f"📅 Date expédition: {date_expedition_final}") + if date_livraison_final: + logger.info(f"📅 Date livraison: {date_livraison_final}") return { "numero_devis": numero_devis, @@ -3440,11 +3478,12 @@ class SageConnector: "nb_lignes": len(devis_data["lignes"]), "client_code": devis_data["client"]["code"], "date_devis": str(devis_data.get("date_devis", "")), + "date_expedition": date_expedition_final, + "date_livraison": date_livraison_final, "reference": reference_final, "statut": statut_final, } - def _rechercher_devis_dans_liste(self, numero_devis, factory_doc): """Recherche un devis dans les 100 premiers éléments de la liste.""" index = 1 @@ -3648,7 +3687,7 @@ class SageConnector: def _modifier_champs_simples(self, doc, devis_data: Dict) -> list: - """Modifie les champs simples du devis (date, référence, statut).""" + """Modifie les champs simples du devis (date, dates expédition/livraison, référence, statut).""" champs_modifies = [] # IMPORTANT: Relire le document pour s'assurer qu'il est à jour @@ -3657,18 +3696,53 @@ class SageConnector: except: pass - # DATE - Modifier et sauvegarder immédiatement + # DATE DEVIS - Modifier et sauvegarder immédiatement if "date_devis" in devis_data: try: - doc.DO_Date = pywintypes.Time(self.normaliser_date(devis_data.get("date_devis"))) - doc.Write() # Sauvegarder immédiatement - doc.Read() # Relire après sauvegarde + doc.Write() + doc.Read() champs_modifies.append("date") + logger.info(f"📅 Date devis modifiée: {devis_data['date_devis']}") except Exception as e: logger.warning(f"⚠️ Impossible de modifier la date: {e}") + # DATE EXPÉDITION - Modifier et sauvegarder immédiatement + if "date_expedition" in devis_data: + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(devis_data["date_expedition"]) + ) + logger.info(f"📅 Date expédition modifiée: {devis_data['date_expedition']}") + + doc.Write() + doc.Read() + + champs_modifies.append("date_expedition") + + # DATE LIVRAISON - Modifier et sauvegarder immédiatement + if "date_livraison" in devis_data: + try: + if devis_data["date_livraison"]: + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(devis_data["date_livraison"]) + ) + logger.info(f"📅 Date livraison modifiée: {devis_data['date_livraison']}") + else: + # Si None ou vide, effacer la date + try: + doc.DO_DateLivr = None + logger.info("📅 Date livraison effacée") + except: + pass + + doc.Write() + doc.Read() + + champs_modifies.append("date_livraison") + except Exception as e: + logger.warning(f"⚠️ Impossible de modifier la date de livraison: {e}") + # RÉFÉRENCE - Modifier et sauvegarder immédiatement if "reference" in devis_data: try: @@ -3676,8 +3750,8 @@ class SageConnector: ancienne_reference = getattr(doc, "DO_Ref", "") doc.DO_Ref = str(nouvelle_reference) if nouvelle_reference else "" - doc.Write() # Sauvegarder immédiatement - doc.Read() # Relire après sauvegarde + doc.Write() + doc.Read() champs_modifies.append("reference") logger.info(f"🔖 Référence: '{ancienne_reference}' → '{nouvelle_reference}'") @@ -3936,16 +4010,36 @@ class SageConnector: total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) statut = getattr(doc, "DO_Statut", 0) reference = getattr(doc, "DO_Ref", "") + + # Extraction des dates + date_expedition = None + date_livraison = None + + try: + date_exped = getattr(doc, "DO_DateExpedition", None) + if date_exped: + date_expedition = date_exped.strftime("%Y-%m-%d") + except: + pass + + try: + date_livr = getattr(doc, "DO_DateLivr", None) + if date_livr: + date_livraison = date_livr.strftime("%Y-%m-%d") + except: + pass return { "numero": numero, "total_ht": total_ht, "total_ttc": total_ttc, "reference": reference, + "date_expedition": date_expedition, + "date_livraison": date_livraison, "champs_modifies": champs_modifies, "statut": statut, } - + def lire_devis(self, numero_devis): try: @@ -5750,6 +5844,18 @@ class SageConnector: raise RuntimeError(f"Erreur technique Sage: {error_message}") def creer_commande_enrichi(self, commande_data: dict) -> Dict: + """ + Crée une commande dans Sage avec support des dates. + + Args: + commande_data: dict contenant: + - client: {code: str} + - date_commande: str ou date + - date_expedition: str ou date (optionnel) + - date_livraison: str ou date (optionnel) + - reference: str (optionnel) + - lignes: list[dict] + """ if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -5768,7 +5874,7 @@ class SageConnector: pass try: - # Création document COMMANDE (type 10) + # ===== CRÉATION DOCUMENT COMMANDE (type 10) ===== process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_BON_COMMANDE ) @@ -5781,9 +5887,24 @@ class SageConnector: logger.info("📄 Document commande créé") - doc.DO_Date = pywintypes.Time(self.normaliser_date(commande_data.get("date_commande"))) + # ===== DATES ===== + doc.DO_Date = pywintypes.Time( + self.normaliser_date(commande_data.get("date_commande")) + ) + + if "date_expedition" in commande_data and commande_data["date_expedition"]: + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(commande_data["date_expedition"]) + ) + logger.info(f"📅 Date expédition: {commande_data['date_expedition']}") + + if "date_livraison" in commande_data and commande_data["date_livraison"]: + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(commande_data["date_livraison"]) + ) + logger.info(f"📅 Date livraison: {commande_data['date_livraison']}") - # Client (CRITIQUE) + # ===== CLIENT (CRITIQUE) ===== factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( commande_data["client"]["code"] @@ -5802,7 +5923,7 @@ class SageConnector: doc.Write() logger.info(f"👤 Client {commande_data['client']['code']} associé") - # Référence externe (optionnelle) + # ===== RÉFÉRENCE EXTERNE (optionnelle) ===== if commande_data.get("reference"): try: doc.DO_Ref = commande_data["reference"] @@ -5810,7 +5931,7 @@ class SageConnector: except: pass - # Lignes + # ===== LIGNES ===== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -5947,14 +6068,14 @@ class SageConnector: except Exception as e: logger.warning(f"⚠️ Impossible de vérifier: {e}") - # Validation + # ===== VALIDATION ===== doc.Write() process.Process() if transaction_active: self.cial.CptaApplication.CommitTrans() - # Récupération numéro + # ===== RÉCUPÉRATION NUMÉRO ===== time.sleep(2) numero_commande = None @@ -5972,7 +6093,7 @@ class SageConnector: if not numero_commande: numero_commande = getattr(doc, "DO_Piece", "") - # Relecture + # ===== RELECTURE ===== factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece( settings.SAGE_TYPE_BON_COMMANDE, numero_commande @@ -5986,13 +6107,39 @@ class SageConnector: total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) + reference_finale = getattr(doc_final, "DO_Ref", "") + + # Dates + date_expedition_final = None + date_livraison_final = None + + try: + date_exped = getattr(doc_final, "DO_DateExpedition", None) + if date_exped: + date_expedition_final = date_exped.strftime("%Y-%m-%d") + except: + pass + + try: + date_livr = getattr(doc_final, "DO_DateLivr", None) + if date_livr: + date_livraison_final = date_livr.strftime("%Y-%m-%d") + except: + pass else: total_ht = 0.0 total_ttc = 0.0 + reference_finale = commande_data.get("reference", "") + date_expedition_final = commande_data.get("date_expedition") + date_livraison_final = commande_data.get("date_livraison") logger.info( f"✅✅✅ COMMANDE CRÉÉE: {numero_commande} - {total_ttc}€ TTC ✅✅✅" ) + if date_expedition_final: + logger.info(f"📅 Date expédition: {date_expedition_final}") + if date_livraison_final: + logger.info(f"📅 Date livraison: {date_livraison_final}") return { "numero_commande": numero_commande, @@ -6001,6 +6148,9 @@ class SageConnector: "nb_lignes": len(commande_data["lignes"]), "client_code": commande_data["client"]["code"], "date_commande": str(self.normaliser_date(commande_data.get("date_commande"))), + "date_expedition": date_expedition_final, + "date_livraison": date_livraison_final, + "reference": reference_finale, } except Exception as e: @@ -6017,6 +6167,19 @@ class SageConnector: def modifier_commande(self, numero: str, commande_data: Dict) -> Dict: + """ + Modifie une commande existante dans Sage. + + Args: + numero: Numéro de la commande + commande_data: dict contenant les champs à modifier: + - date_commande: str ou date (optionnel) + - date_expedition: str ou date (optionnel) + - date_livraison: str ou date (optionnel) + - reference: str (optionnel) + - statut: int (optionnel) + - lignes: list[dict] (optionnel) + """ if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -6099,6 +6262,8 @@ class SageConnector: champs_modifies = [] modif_date = "date_commande" in commande_data + modif_date_expedition = "date_expedition" in commande_data + modif_date_livraison = "date_livraison" in commande_data modif_statut = "statut" in commande_data modif_ref = "reference" in commande_data modif_lignes = ( @@ -6106,25 +6271,28 @@ class SageConnector: ) logger.info(f"📋 Modifications demandées:") - logger.info(f" Date: {modif_date}") + logger.info(f" Date commande: {modif_date}") + logger.info(f" Date expédition: {modif_date_expedition}") + logger.info(f" Date livraison: {modif_date_livraison}") 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 ===== + commande_data_temp = commande_data.copy() reference_a_modifier = None statut_a_modifier = None if modif_lignes: if modif_ref: - reference_a_modifier = commande_data.pop("reference") + reference_a_modifier = commande_data_temp.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 + modif_ref = False if modif_statut: - statut_a_modifier = commande_data.pop("statut") + statut_a_modifier = commande_data_temp.pop("statut") logger.info("📊 Modification du statut reportée après les lignes") - modif_statut = False # Ne pas traiter dans les modifications simples + modif_statut = False # ======================================== # ÉTAPE 4 : TEST WRITE() BASIQUE @@ -6160,18 +6328,34 @@ class SageConnector: # ======================================== # ÉTAPE 5 : MODIFICATIONS SIMPLES (pas de lignes) # ======================================== - if not modif_lignes and (modif_date or modif_statut or modif_ref): + if not modif_lignes and (modif_date or modif_date_expedition or modif_date_livraison 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(commande_data.get("date_commande"))) + logger.info(" 📅 Modification date commande...") + doc.DO_Date = pywintypes.Time( + self.normaliser_date(commande_data_temp.get("date_commande")) + ) champs_modifies.append("date") + if modif_date_expedition: + logger.info(" 📅 Modification date expédition...") + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(commande_data_temp["date_expedition"]) + ) + champs_modifies.append("date_expedition") + + if modif_date_livraison: + logger.info(" 📅 Modification date livraison...") + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(commande_data_temp["date_livraison"]) + ) + logger.info(f" ✅ Date livraison: {commande_data_temp['date_livraison']}") + champs_modifies.append("date_livraison") + if modif_statut: logger.info(" 📊 Modification statut...") - nouveau_statut = commande_data["statut"] + nouveau_statut = commande_data_temp["statut"] doc.DO_Statut = nouveau_statut champs_modifies.append("statut") logger.info(f" ✅ Statut défini: {nouveau_statut}") @@ -6179,10 +6363,10 @@ class SageConnector: if modif_ref: logger.info(" 📖 Modification référence...") try: - doc.DO_Ref = commande_data["reference"] + doc.DO_Ref = commande_data_temp["reference"] champs_modifies.append("reference") logger.info( - f" ✅ Référence définie: {commande_data['reference']}" + f" ✅ Référence définie: {commande_data_temp['reference']}" ) except Exception as e: logger.warning(f" ⚠️ Référence non définie: {e}") @@ -6225,6 +6409,28 @@ class SageConnector: elif modif_lignes: logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") + # D'abord modifier les dates si demandées + if modif_date: + doc.DO_Date = pywintypes.Time( + self.normaliser_date(commande_data_temp.get("date_commande")) + ) + champs_modifies.append("date") + logger.info(" 📅 Date commande modifiée") + + if modif_date_expedition: + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(commande_data_temp["date_expedition"]) + ) + logger.info(" 📅 Date expédition modifiée") + champs_modifies.append("date_expedition") + + if modif_date_livraison: + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(commande_data_temp["date_livraison"]) + ) + logger.info(" 📅 Date livraison modifiée") + champs_modifies.append("date_livraison") + nouvelles_lignes = commande_data["lignes"] nb_nouvelles = len(nouvelles_lignes) @@ -6362,7 +6568,7 @@ class SageConnector: champs_modifies.append("lignes") # ======================================== - # ÉTAPE 6.5 : MODIFIER RÉFÉRENCE (APRÈS les lignes - même logique que devis) + # ÉTAPE 6.5 : MODIFIER RÉFÉRENCE (APRÈS les lignes) # ======================================== if reference_a_modifier is not None: try: @@ -6383,7 +6589,7 @@ class SageConnector: logger.warning(f"⚠️ Impossible de modifier la référence: {e}") # ======================================== - # ÉTAPE 6.6 : MODIFIER STATUT (EN DERNIER - même logique que devis) + # ÉTAPE 6.6 : MODIFIER STATUT (EN DERNIER) # ======================================== if statut_a_modifier is not None: try: @@ -6425,11 +6631,33 @@ 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", "") + + # Extraire les dates + date_expedition_final = None + date_livraison_final = None + + try: + date_exped = getattr(doc, "DO_DateExpedition", None) + if date_exped: + date_expedition_final = date_exped.strftime("%Y-%m-%d") + except: + pass + + try: + date_livr = getattr(doc, "DO_DateLivr", None) + if date_livr: + date_livraison_final = date_livr.strftime("%Y-%m-%d") + except: + pass 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}") + if date_expedition_final: + logger.info(f" 📅 Date expédition: {date_expedition_final}") + if date_livraison_final: + logger.info(f" 📅 Date livraison: {date_livraison_final}") logger.info(f" 📝 Champs modifiés: {champs_modifies}") return { @@ -6437,6 +6665,8 @@ class SageConnector: "total_ht": total_ht, "total_ttc": total_ttc, "reference": reference_finale, + "date_expedition": date_expedition_final, + "date_livraison": date_livraison_final, "champs_modifies": champs_modifies, "statut": getattr(doc, "DO_Statut", 0), "client_code": client_final, @@ -6461,9 +6691,21 @@ class SageConnector: pass raise RuntimeError(f"Erreur Sage: {error_message}") - + def creer_livraison_enrichi(self, livraison_data: dict) -> Dict: + """ + Crée une livraison dans Sage avec support des dates. + + Args: + livraison_data: dict contenant: + - client: {code: str} + - date_livraison: str ou date (date du document) + - date_expedition: str ou date (optionnel - date prévue expédition) + - date_livraison_prevue: str ou date (optionnel - date prévue livraison client) + - reference: str (optionnel) + - lignes: list[dict] + """ if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -6482,7 +6724,7 @@ class SageConnector: pass try: - # Création document LIVRAISON (type 30) + # ===== CRÉATION DOCUMENT LIVRAISON (type 30) ===== process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_BON_LIVRAISON ) @@ -6495,9 +6737,27 @@ class SageConnector: logger.info("📄 Document livraison créé") - doc.DO_Date = pywintypes.Time(self.normaliser_date(livraison_data.get("date_livraison"))) + # ===== DATES ===== + # Date du document (DO_Date) + doc.DO_Date = pywintypes.Time( + self.normaliser_date(livraison_data.get("date_livraison")) + ) + + # Date d'expédition prévue (DO_DateExpedition) + if "date_expedition" in livraison_data and livraison_data["date_expedition"]: + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(livraison_data["date_expedition"]) + ) + logger.info(f"📅 Date expédition: {livraison_data['date_expedition']}") + + # Date de livraison prévue chez le client (DO_DateLivr) + if "date_livraison_prevue" in livraison_data and livraison_data["date_livraison_prevue"]: + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(livraison_data["date_livraison_prevue"]) + ) + logger.info(f"📅 Date livraison prévue: {livraison_data['date_livraison_prevue']}") - # Client (CRITIQUE) + # ===== CLIENT (CRITIQUE) ===== factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( livraison_data["client"]["code"] @@ -6516,7 +6776,7 @@ class SageConnector: doc.Write() logger.info(f"👤 Client {livraison_data['client']['code']} associé") - # Référence externe (optionnelle) + # ===== RÉFÉRENCE EXTERNE (optionnelle) ===== if livraison_data.get("reference"): try: doc.DO_Ref = livraison_data["reference"] @@ -6524,7 +6784,7 @@ class SageConnector: except: pass - # Lignes + # ===== LIGNES ===== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -6642,14 +6902,14 @@ class SageConnector: ligne_obj.Write() logger.info(f"✅ Ligne {idx} écrite") - # Validation + # ===== VALIDATION ===== doc.Write() process.Process() if transaction_active: self.cial.CptaApplication.CommitTrans() - # Récupération numéro + # ===== RÉCUPÉRATION NUMÉRO ===== time.sleep(2) numero_livraison = None @@ -6667,7 +6927,7 @@ class SageConnector: if not numero_livraison: numero_livraison = getattr(doc, "DO_Piece", "") - # Relecture + # ===== RELECTURE ===== factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece( settings.SAGE_TYPE_BON_LIVRAISON, numero_livraison @@ -6681,13 +6941,39 @@ class SageConnector: total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) + reference_finale = getattr(doc_final, "DO_Ref", "") + + # Dates + date_expedition_final = None + date_livraison_prevue_final = None + + try: + date_exped = getattr(doc_final, "DO_DateExpedition", None) + if date_exped: + date_expedition_final = date_exped.strftime("%Y-%m-%d") + except: + pass + + try: + date_livr = getattr(doc_final, "DO_DateLivr", None) + if date_livr: + date_livraison_prevue_final = date_livr.strftime("%Y-%m-%d") + except: + pass else: total_ht = 0.0 total_ttc = 0.0 + reference_finale = livraison_data.get("reference", "") + date_expedition_final = livraison_data.get("date_expedition") + date_livraison_prevue_final = livraison_data.get("date_livraison_prevue") logger.info( f"✅✅✅ LIVRAISON CRÉÉE: {numero_livraison} - {total_ttc}€ TTC ✅✅✅" ) + if date_expedition_final: + logger.info(f"📅 Date expédition: {date_expedition_final}") + if date_livraison_prevue_final: + logger.info(f"📅 Date livraison prévue: {date_livraison_prevue_final}") return { "numero_livraison": numero_livraison, @@ -6696,6 +6982,9 @@ class SageConnector: "nb_lignes": len(livraison_data["lignes"]), "client_code": livraison_data["client"]["code"], "date_livraison": str(self.normaliser_date(livraison_data.get("date_livraison"))), + "date_expedition": date_expedition_final, + "date_livraison_prevue": date_livraison_prevue_final, + "reference": reference_finale, } except Exception as e: @@ -6712,275 +7001,356 @@ class SageConnector: def modifier_livraison(self, numero: str, livraison_data: Dict) -> Dict: - if not self.cial: - raise RuntimeError("Connexion Sage non établie") + """ + Modifie une livraison existante dans Sage. + + Args: + numero: Numéro de la livraison + livraison_data: dict contenant les champs à modifier: + - date_livraison: str ou date (optionnel - date du document) + - date_expedition: str ou date (optionnel - date prévue expédition) + - date_livraison_prevue: str ou date (optionnel - date prévue livraison client) + - reference: str (optionnel) + - statut: int (optionnel) + - lignes: list[dict] (optionnel) + """ + 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 + # Chercher le document + for type_test in [30, settings.SAGE_TYPE_BON_LIVRAISON]: 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: + 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}") + 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 = [] + # ======================================== + # É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 + modif_date = "date_livraison" in livraison_data + modif_date_expedition = "date_expedition" in livraison_data + modif_date_livraison_prevue = "date_livraison_prevue" 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 livraison: {modif_date}") + logger.info(f" Date expédition: {modif_date_expedition}") + logger.info(f" Date livraison prévue: {modif_date_livraison_prevue}") + 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 ===== + livraison_data_temp = livraison_data.copy() + reference_a_modifier = None + statut_a_modifier = None + + if modif_lignes: + if modif_ref: + reference_a_modifier = livraison_data_temp.pop("reference") + logger.info("🔖 Modification de la référence reportée après les lignes") + modif_ref = False + + if modif_statut: + statut_a_modifier = livraison_data_temp.pop("statut") + logger.info("📊 Modification du statut reportée après les lignes") + modif_statut = False + + # ======================================== + # ÉTAPE 3 : MODIFICATIONS SIMPLES (pas de lignes) + # ======================================== + if not modif_lignes and (modif_date or modif_date_expedition or modif_date_livraison_prevue or modif_statut or modif_ref): + logger.info("🎯 Modifications simples (sans lignes)...") + + if modif_date: + logger.info(" 📅 Modification date livraison...") + doc.DO_Date = pywintypes.Time( + self.normaliser_date(livraison_data_temp.get("date_livraison")) + ) + champs_modifies.append("date") + + if modif_date_expedition: + logger.info(" 📅 Modification date expédition...") + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(livraison_data_temp["date_expedition"]) + ) + logger.info(f" ✅ Date expédition: {livraison_data_temp['date_expedition']}") + champs_modifies.append("date_expedition") + + if modif_date_livraison_prevue: + logger.info(" 📅 Modification date livraison prévue...") + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(livraison_data_temp["date_livraison_prevue"]) + ) + logger.info(f" ✅ Date livraison prévue: {livraison_data_temp['date_livraison_prevue']}") + champs_modifies.append("date_livraison_prevue") + + if modif_statut: + logger.info(" 📊 Modification statut...") + nouveau_statut = livraison_data_temp["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: + doc.DO_Ref = livraison_data_temp["reference"] + champs_modifies.append("reference") + logger.info(f" ✅ Référence définie: {livraison_data_temp['reference']}") + except Exception as e: + logger.warning(f" ⚠️ Référence non définie: {e}") + + logger.info(" 💾 Write()...") + doc.Write() + logger.info(" ✅ Write() réussi") + + # ======================================== + # ÉTAPE 4 : REMPLACEMENT COMPLET LIGNES + # ======================================== + elif modif_lignes: + logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") + + # D'abord modifier les dates si demandées + if modif_date: + doc.DO_Date = pywintypes.Time( + self.normaliser_date(livraison_data_temp.get("date_livraison")) + ) + champs_modifies.append("date") + logger.info(" 📅 Date livraison modifiée") + + if modif_date_expedition: + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(livraison_data_temp["date_expedition"]) + ) + logger.info(" 📅 Date expédition modifiée") + champs_modifies.append("date_expedition") + + if modif_date_livraison_prevue: + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(livraison_data_temp["date_livraison_prevue"]) + ) + logger.info(" 📅 Date livraison prévue modifiée") + champs_modifies.append("date_livraison_prevue") + + nouvelles_lignes = livraison_data["lignes"] + nb_nouvelles = len(nouvelles_lignes) + + logger.info( + f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles" ) - 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}") + try: + factory_lignes = doc.FactoryDocumentLigne + except: + factory_lignes = doc.FactoryDocumentVenteLigne - # ===== 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 + factory_article = self.cial.FactoryArticle - # ======================================== - # É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)...") + # ============================================ + # SOUS-ÉTAPE 1 : SUPPRESSION TOUTES LES LIGNES + # ============================================ + if nb_lignes_initial > 0: + logger.info(f" 🗑️ Suppression de {nb_lignes_initial} lignes...") - 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...") + # Supprimer depuis la fin pour éviter les problèmes d'index + for idx in range(nb_lignes_initial, 0, -1): try: - doc.DO_Ref = livraison_data["reference"] - champs_modifies.append("reference") - logger.info(f" ✅ Référence définie: {livraison_data['reference']}") + 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" ⚠️ Référence non définie: {e}") + logger.warning( + f" ⚠️ Erreur suppression ligne {idx}: {e}" + ) + # Continuer même si une suppression échoue - logger.info(" 💾 Write()...") - doc.Write() - logger.info(" ✅ Write() réussi") + logger.info(" ✅ Toutes les lignes supprimées") - # ======================================== - # ÉTAPE 4 : REMPLACEMENT COMPLET LIGNES - # ======================================== - elif modif_lignes: - logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") - - nouvelles_lignes = livraison_data["lignes"] - nb_nouvelles = len(nouvelles_lignes) + # ============================================ + # 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" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles" + 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: - factory_lignes = doc.FactoryDocumentLigne + ligne_obj = win32com.client.CastTo( + ligne_persist, "IBODocumentLigne3" + ) except: - 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']} ---" + ligne_obj = win32com.client.CastTo( + ligne_persist, "IBODocumentVenteLigne3" ) - # Charger l'article - persist_article = factory_article.ReadReference( - ligne_data["article_code"] + quantite = float(ligne_data["quantite"]) + + # Associer article + try: + ligne_obj.SetDefaultArticleReference( + ligne_data["article_code"], quantite ) - 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() - + except: try: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentLigne3" - ) + ligne_obj.SetDefaultArticle(article_obj, quantite) except: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentVenteLigne3" - ) + ligne_obj.DL_Design = ligne_data.get("designation", "") + ligne_obj.DL_Qte = quantite - quantite = float(ligne_data["quantite"]) + # Prix + if ligne_data.get("prix_unitaire_ht"): + ligne_obj.DL_PrixUnitaire = float( + ligne_data["prix_unitaire_ht"] + ) - # Associer article + # Remise + if ligne_data.get("remise_pourcentage", 0) > 0: try: - ligne_obj.SetDefaultArticleReference( - ligne_data["article_code"], quantite + ligne_obj.DL_Remise01REM_Valeur = float( + ligne_data["remise_pourcentage"] ) + ligne_obj.DL_Remise01REM_Type = 0 except: - try: - ligne_obj.SetDefaultArticle(article_obj, quantite) - except: - ligne_obj.DL_Design = ligne_data.get("designation", "") - ligne_obj.DL_Qte = quantite + pass - # Prix - if ligne_data.get("prix_unitaire_ht"): - ligne_obj.DL_PrixUnitaire = float( - ligne_data["prix_unitaire_ht"] - ) + # Écrire la ligne + ligne_obj.Write() + logger.info(f" ✅ Ligne {idx} ajoutée") - # 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 + logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") - # Écrire la ligne - ligne_obj.Write() - logger.info(f" ✅ Ligne {idx} ajoutée") + # Écrire le document + logger.info(" 💾 Write() document après remplacement lignes...") + doc.Write() + logger.info(" ✅ Document écrit") - logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées") + import time + time.sleep(0.5) - # Écrire le document - logger.info(" 💾 Write() document après remplacement lignes...") + doc.Read() + + 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() - logger.info(" ✅ Document écrit") - + 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}") - 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 "" + # ======================================== + # É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}") - logger.info(f"🔖 Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'") - - doc.DO_Ref = nouvelle_reference + doc.DO_Statut = nouveau_statut doc.Write() import time @@ -6988,87 +7358,99 @@ class SageConnector: 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}") + 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 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...") - # ======================================== - # ÉTAPE 5 : RELECTURE ET RETOUR - # ======================================== - logger.info("📊 Relecture finale...") + import time + time.sleep(0.5) - 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}") + 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) + # Extraire les dates + date_expedition_final = None + date_livraison_prevue_final = None + + try: + date_exped = getattr(doc, "DO_DateExpedition", None) + if date_exped: + date_expedition_final = date_exped.strftime("%Y-%m-%d") + except: + pass + + try: + date_livr = getattr(doc, "DO_DateLivr", None) + if date_livr: + date_livraison_prevue_final = date_livr.strftime("%Y-%m-%d") + except: + pass + + 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}") + if date_expedition_final: + logger.info(f" 📅 Date expédition: {date_expedition_final}") + if date_livraison_prevue_final: + logger.info(f" 📅 Date livraison prévue: {date_livraison_prevue_final}") + logger.info(f" 📝 Champs modifiés: {champs_modifies}") + + return { + "numero": numero, + "total_ht": total_ht, + "total_ttc": total_ttc, + "reference": reference_finale, + "date_expedition": date_expedition_final, + "date_livraison_prevue": date_livraison_prevue_final, + "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: + """ + Crée un avoir dans Sage avec support des dates. + + Args: + avoir_data: dict contenant: + - client: {code: str} + - date_avoir: str ou date + - date_expedition: str ou date (optionnel) + - date_livraison: str ou date (optionnel) + - reference: str (optionnel) + - lignes: list[dict] + """ if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -7087,7 +7469,7 @@ class SageConnector: pass try: - # Création document AVOIR (type 50) + # ===== CRÉATION DOCUMENT AVOIR (type 50) ===== process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_BON_AVOIR ) @@ -7100,9 +7482,24 @@ class SageConnector: logger.info("📄 Document avoir créé") - doc.DO_Date = pywintypes.Time(self.normaliser_date(avoir_data.get("date_avoir"))) + # ===== DATES ===== + doc.DO_Date = pywintypes.Time( + self.normaliser_date(avoir_data.get("date_avoir")) + ) + + if "date_expedition" in avoir_data and avoir_data["date_expedition"]: + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(avoir_data["date_expedition"]) + ) + logger.info(f"📅 Date expédition: {avoir_data['date_expedition']}") + + if "date_livraison" in avoir_data and avoir_data["date_livraison"]: + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(avoir_data["date_livraison"]) + ) + logger.info(f"📅 Date livraison: {avoir_data['date_livraison']}") - # Client (CRITIQUE) + # ===== CLIENT (CRITIQUE) ===== factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( avoir_data["client"]["code"] @@ -7121,7 +7518,7 @@ class SageConnector: doc.Write() logger.info(f"👤 Client {avoir_data['client']['code']} associé") - # Référence externe (optionnelle) + # ===== RÉFÉRENCE EXTERNE (optionnelle) ===== if avoir_data.get("reference"): try: doc.DO_Ref = avoir_data["reference"] @@ -7129,7 +7526,7 @@ class SageConnector: except: pass - # Lignes + # ===== LIGNES ===== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -7245,14 +7642,14 @@ class SageConnector: ligne_obj.Write() logger.info(f"✅ Ligne {idx} écrite") - # Validation + # ===== VALIDATION ===== doc.Write() process.Process() if transaction_active: self.cial.CptaApplication.CommitTrans() - # Récupération numéro + # ===== RÉCUPÉRATION NUMÉRO ===== time.sleep(2) numero_avoir = None @@ -7270,7 +7667,7 @@ class SageConnector: if not numero_avoir: numero_avoir = getattr(doc, "DO_Piece", "") - # Relecture + # ===== RELECTURE ===== factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece( settings.SAGE_TYPE_BON_AVOIR, numero_avoir @@ -7284,13 +7681,39 @@ class SageConnector: total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) + reference_finale = getattr(doc_final, "DO_Ref", "") + + # Dates + date_expedition_final = None + date_livraison_final = None + + try: + date_exped = getattr(doc_final, "DO_DateExpedition", None) + if date_exped: + date_expedition_final = date_exped.strftime("%Y-%m-%d") + except: + pass + + try: + date_livr = getattr(doc_final, "DO_DateLivr", None) + if date_livr: + date_livraison_final = date_livr.strftime("%Y-%m-%d") + except: + pass else: total_ht = 0.0 total_ttc = 0.0 + reference_finale = avoir_data.get("reference", "") + date_expedition_final = avoir_data.get("date_expedition") + date_livraison_final = avoir_data.get("date_livraison") logger.info( f"✅✅✅ AVOIR CRÉÉ: {numero_avoir} - {total_ttc}€ TTC ✅✅✅" ) + if date_expedition_final: + logger.info(f"📅 Date expédition: {date_expedition_final}") + if date_livraison_final: + logger.info(f"📅 Date livraison: {date_livraison_final}") return { "numero_avoir": numero_avoir, @@ -7299,6 +7722,9 @@ class SageConnector: "nb_lignes": len(avoir_data["lignes"]), "client_code": avoir_data["client"]["code"], "date_avoir": str(self.normaliser_date(avoir_data.get("date_avoir"))), + "date_expedition": date_expedition_final, + "date_livraison": date_livraison_final, + "reference": reference_finale, } except Exception as e: @@ -7315,343 +7741,223 @@ class SageConnector: def modifier_avoir(self, numero: str, avoir_data: Dict) -> Dict: - if not self.cial: - raise RuntimeError("Connexion Sage non établie") + """ + Modifie un avoir existant dans Sage. + + Args: + numero: Numéro de l'avoir + avoir_data: dict contenant les champs à modifier: + - date_avoir: str ou date (optionnel) + - date_expedition: str ou date (optionnel) + - date_livraison: str ou date (optionnel) + - reference: str (optionnel) + - statut: int (optionnel) + - lignes: list[dict] (optionnel) + """ + 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]: + # 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) + type_reel = getattr(doc, "DO_Type", -1) + + logger.info(f" 📊 Type={type_reel}, Statut={statut_actuel}") + + # 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 statut_actuel == 6: + raise ValueError(f"L'avoir {numero} est annulé") + + # ======================================== + # É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: + logger.error(" ❌ Objet Client NULL à l'état 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: try: - persist_test = factory.ReadPiece(type_test, numero) - if persist_test: - persist = persist_test - logger.info(f" ✅ Document trouvé (type={type_test})") + ligne_p = factory_lignes.List(index) + if ligne_p is None: break + nb_lignes_initial += 1 + index += 1 except: - continue + break - if not persist: - raise ValueError(f"❌ Avoir {numero} INTROUVABLE") + logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") + except Exception as e: + logger.warning(f" ⚠️ Erreur comptage lignes: {e}") - doc = win32com.client.CastTo(persist, "IBODocumentVente3") + # ======================================== + # ÉTAPE 3 : DÉTERMINER LES MODIFICATIONS + # ======================================== + champs_modifies = [] + + modif_date = "date_avoir" in avoir_data + modif_date_expedition = "date_expedition" in avoir_data + modif_date_livraison = "date_livraison" 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 avoir: {modif_date}") + logger.info(f" Date expédition: {modif_date_expedition}") + logger.info(f" Date livraison: {modif_date_livraison}") + 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 ===== + avoir_data_temp = avoir_data.copy() + reference_a_modifier = None + statut_a_modifier = None + + if modif_lignes: + if modif_ref: + reference_a_modifier = avoir_data_temp.pop("reference") + logger.info("🔖 Modification de la référence reportée après les lignes") + modif_ref = False + + if modif_statut: + statut_a_modifier = avoir_data_temp.pop("statut") + logger.info("📊 Modification du statut reportée après les lignes") + modif_statut = False + + # ======================================== + # ÉTAPE 4 : TEST WRITE() BASIQUE + # ======================================== + logger.info("🧪 Test Write() basique (sans modification)...") + + try: + doc.Write() + logger.info(" ✅ Write() basique OK") doc.Read() - statut_actuel = getattr(doc, "DO_Statut", 0) - type_reel = getattr(doc, "DO_Type", -1) - - logger.info(f" 📊 Type={type_reel}, Statut={statut_actuel}") - - # 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 statut_actuel == 6: - raise ValueError(f"L'avoir {numero} est annulé") - - # ======================================== - # É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}") + # 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(" ❌ Objet Client NULL à l'état initial !") - except Exception as e: - logger.error(f" ❌ Erreur lecture client initial: {e}") + logger.error( + f" ❌ Client a changé: {client_code_initial} → {client_apres}" + ) + else: + logger.error(" ❌ Client devenu NULL après Write() basique") - 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: - 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 3 : DÉTERMINER LES MODIFICATIONS - # ======================================== - champs_modifies = [] - - 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 + 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}" ) - 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 5 : MODIFICATIONS SIMPLES (pas de lignes) + # ======================================== + if not modif_lignes and (modif_date or modif_date_expedition or modif_date_livraison or modif_statut or modif_ref): + logger.info("🎯 Modifications simples (sans 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 + if modif_date: + logger.info(" 📅 Modification date avoir...") + doc.DO_Date = pywintypes.Time( + self.normaliser_date(avoir_data_temp.get("date_avoir")) + ) + champs_modifies.append("date") - # ======================================== - # ÉTAPE 4 : TEST WRITE() BASIQUE - # ======================================== - logger.info("🧪 Test Write() basique (sans modification)...") + if modif_date_expedition: + logger.info(" 📅 Modification date expédition...") + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(avoir_data_temp["date_expedition"]) + ) + logger.info(f" ✅ Date expédition: {avoir_data_temp['date_expedition']}") + champs_modifies.append("date_expedition") + if modif_date_livraison: + logger.info(" 📅 Modification date livraison...") + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(avoir_data_temp["date_livraison"]) + ) + logger.info(f" ✅ Date livraison: {avoir_data_temp['date_livraison']}") + champs_modifies.append("date_livraison") + + if modif_statut: + logger.info(" 📊 Modification statut...") + nouveau_statut = avoir_data_temp["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: + doc.DO_Ref = avoir_data_temp["reference"] + champs_modifies.append("reference") + logger.info( + f" ✅ Référence définie: {avoir_data_temp['reference']}" + ) + except Exception as e: + logger.warning(f" ⚠️ Référence non définie: {e}") + + # Écrire sans réassocier le client + logger.info(" 💾 Write() sans réassociation client...") try: doc.Write() - logger.info(" ✅ Write() basique OK") - doc.Read() - - # 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") - - 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 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: - 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" ⚠️ Référence non définie: {e}") - - # Écrire sans réassocier le client - logger.info(" 💾 Write() sans réassociation client...") - try: - doc.Write() - logger.info(" ✅ Write() réussi") - - doc.Read() - - # 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: - sage_error = self.cial.CptaApplication.LastError - if sage_error: - error_msg = f"{sage_error.Description} (Code: {sage_error.Number})" - except: - pass - - logger.error(f" ❌ Write() échoue: {error_msg}") - raise ValueError(f"Sage refuse: {error_msg}") - - # ======================================== - # ÉTAPE 6 : REMPLACEMENT COMPLET DES 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 lignes" - ) - - try: - factory_lignes = doc.FactoryDocumentLigne - except: - factory_lignes = doc.FactoryDocumentVenteLigne - - factory_article = self.cial.FactoryArticle - - # ============================================ - # SOUS-ÉTAPE 1 : SUPPRIMER TOUTES LES LIGNES EXISTANTES - # ============================================ - if nb_lignes_initial > 0: - logger.info( - f" 🗑️ Suppression de {nb_lignes_initial} lignes existantes..." - ) - - # 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() - - # ✅ 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) + logger.info(" ✅ Write() réussi") doc.Read() @@ -7660,23 +7966,224 @@ class SageConnector: 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") + 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}" + ) - champs_modifies.append("lignes") - - # ======================================== - # ÉTAPE 6.5 : MODIFIER RÉFÉRENCE (APRÈS les lignes) - # ======================================== - if reference_a_modifier is not None: + except Exception as e: + error_msg = str(e) try: - ancienne_reference = getattr(doc, "DO_Ref", "") - nouvelle_reference = str(reference_a_modifier) if reference_a_modifier else "" + sage_error = self.cial.CptaApplication.LastError + if sage_error: + error_msg = f"{sage_error.Description} (Code: {sage_error.Number})" + except: + pass + + logger.error(f" ❌ Write() échoue: {error_msg}") + raise ValueError(f"Sage refuse: {error_msg}") + + # ======================================== + # ÉTAPE 6 : REMPLACEMENT COMPLET DES LIGNES + # ======================================== + elif modif_lignes: + logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") + + # D'abord modifier les dates si demandées + if modif_date: + doc.DO_Date = pywintypes.Time( + self.normaliser_date(avoir_data_temp.get("date_avoir")) + ) + champs_modifies.append("date") + logger.info(" 📅 Date avoir modifiée") + + if modif_date_expedition: + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(avoir_data_temp["date_expedition"]) + ) + logger.info(" 📅 Date expédition modifiée") + champs_modifies.append("date_expedition") + + if modif_date_livraison: + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(avoir_data_temp["date_livraison"]) + ) + logger.info(" 📅 Date livraison modifiée") + champs_modifies.append("date_livraison") + + nouvelles_lignes = avoir_data["lignes"] + nb_nouvelles = len(nouvelles_lignes) + + logger.info( + f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" + ) + + try: + factory_lignes = doc.FactoryDocumentLigne + except: + factory_lignes = doc.FactoryDocumentVenteLigne + + factory_article = self.cial.FactoryArticle + + # ============================================ + # SOUS-ÉTAPE 1 : SUPPRIMER TOUTES LES LIGNES EXISTANTES + # ============================================ + if nb_lignes_initial > 0: + logger.info( + f" 🗑️ Suppression de {nb_lignes_initial} lignes existantes..." + ) + + # 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() + + # ✅ 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}") - logger.info(f"🔖 Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'") - - doc.DO_Ref = nouvelle_reference + doc.DO_Statut = nouveau_statut doc.Write() import time @@ -7684,97 +8191,109 @@ class SageConnector: 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}") + 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 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...") - # ======================================== - # ÉTAPE 7 : RELECTURE ET RETOUR - # ======================================== - logger.info("📊 Relecture finale...") + import time + time.sleep(0.5) - import time - time.sleep(0.5) + doc.Read() - 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 = "" - # 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) + + # Extraire les dates + date_expedition_final = None + date_livraison_final = None + + try: + date_exped = getattr(doc, "DO_DateExpedition", None) + if date_exped: + date_expedition_final = date_exped.strftime("%Y-%m-%d") + except: + pass + + try: + date_livr = getattr(doc, "DO_DateLivr", None) + if date_livr: + date_livraison_final = date_livr.strftime("%Y-%m-%d") + except: + pass - 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}") + if date_expedition_final: + logger.info(f" 📅 Date expédition: {date_expedition_final}") + if date_livraison_final: + logger.info(f" 📅 Date livraison: {date_livraison_final}") + logger.info(f" 📝 Champs modifiés: {champs_modifies}") - 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, + "date_expedition": date_expedition_final, + "date_livraison": date_livraison_final, + "champs_modifies": champs_modifies, + "statut": statut_final, + "client_code": client_final, + } - 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 ValueError as e: - logger.error(f"❌ ERREUR MÉTIER: {e}") - raise + except Exception as e: + logger.error(f"❌ ERREUR TECHNIQUE: {e}", exc_info=True) - 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}") + 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: + """ + Crée une facture dans Sage avec support des dates. + + Args: + facture_data: dict contenant: + - client: {code: str} + - date_facture: str ou date + - date_expedition: str ou date (optionnel) + - date_livraison: str ou date (optionnel) + - reference: str (optionnel) + - lignes: list[dict] + """ if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -7793,7 +8312,7 @@ class SageConnector: pass try: - # Création document FACTURE (type 60) + # ===== CRÉATION DOCUMENT FACTURE (type 60) ===== process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_FACTURE ) @@ -7806,9 +8325,24 @@ class SageConnector: logger.info("📄 Document facture créé") - doc.DO_Date = pywintypes.Time(self.normaliser_date(facture_data.get("date_facture"))) + # ===== DATES ===== + doc.DO_Date = pywintypes.Time( + self.normaliser_date(facture_data.get("date_facture")) + ) + + if "date_expedition" in facture_data and facture_data["date_expedition"]: + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(facture_data["date_expedition"]) + ) + logger.info(f"📅 Date expédition: {facture_data['date_expedition']}") + + if "date_livraison" in facture_data and facture_data["date_livraison"]: + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(facture_data["date_livraison"]) + ) + logger.info(f"📅 Date livraison: {facture_data['date_livraison']}") - # Client (CRITIQUE) + # ===== CLIENT (CRITIQUE) ===== factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( facture_data["client"]["code"] @@ -7827,7 +8361,7 @@ class SageConnector: doc.Write() logger.info(f"👤 Client {facture_data['client']['code']} associé") - # Référence externe (optionnelle) + # ===== RÉFÉRENCE EXTERNE (optionnelle) ===== if facture_data.get("reference"): try: doc.DO_Ref = facture_data["reference"] @@ -7835,9 +8369,7 @@ class SageConnector: except: pass - # ============================================ - # CHAMPS SPÉCIFIQUES FACTURES - # ============================================ + # ===== CHAMPS SPÉCIFIQUES FACTURES ===== logger.info("⚙️ Configuration champs spécifiques factures...") # Code journal (si disponible) @@ -7875,7 +8407,7 @@ class SageConnector: except: pass - # Lignes + # ===== LIGNES ===== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -7991,9 +8523,7 @@ class SageConnector: ligne_obj.Write() logger.info(f"✅ Ligne {idx} écrite") - # ============================================ - # VALIDATION FINALE - # ============================================ + # ===== VALIDATION FINALE ===== logger.info("💾 Validation facture...") # Réassocier le client avant validation (critique pour factures) @@ -8015,7 +8545,7 @@ class SageConnector: self.cial.CptaApplication.CommitTrans() logger.info("✅ Transaction committée") - # Récupération numéro + # ===== RÉCUPÉRATION NUMÉRO ===== time.sleep(2) numero_facture = None @@ -8038,7 +8568,7 @@ class SageConnector: logger.info(f"📄 Numéro facture: {numero_facture}") - # Relecture + # ===== RELECTURE ===== factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece( settings.SAGE_TYPE_FACTURE, numero_facture @@ -8052,13 +8582,39 @@ class SageConnector: total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) + reference_finale = getattr(doc_final, "DO_Ref", "") + + # Dates + date_expedition_final = None + date_livraison_final = None + + try: + date_exped = getattr(doc_final, "DO_DateExpedition", None) + if date_exped: + date_expedition_final = date_exped.strftime("%Y-%m-%d") + except: + pass + + try: + date_livr = getattr(doc_final, "DO_DateLivr", None) + if date_livr: + date_livraison_final = date_livr.strftime("%Y-%m-%d") + except: + pass else: total_ht = 0.0 total_ttc = 0.0 + reference_finale = facture_data.get("reference", "") + date_expedition_final = facture_data.get("date_expedition") + date_livraison_final = facture_data.get("date_livraison") logger.info( f"✅✅✅ FACTURE CRÉÉE: {numero_facture} - {total_ttc}€ TTC ✅✅✅" ) + if date_expedition_final: + logger.info(f"📅 Date expédition: {date_expedition_final}") + if date_livraison_final: + logger.info(f"📅 Date livraison: {date_livraison_final}") return { "numero_facture": numero_facture, @@ -8067,6 +8623,9 @@ class SageConnector: "nb_lignes": len(facture_data["lignes"]), "client_code": facture_data["client"]["code"], "date_facture": str(self.normaliser_date(facture_data.get("date_facture"))), + "date_expedition": date_expedition_final, + "date_livraison": date_livraison_final, + "reference": reference_finale, } except Exception as e: @@ -8084,343 +8643,223 @@ class SageConnector: def modifier_facture(self, numero: str, facture_data: Dict) -> Dict: - if not self.cial: - raise RuntimeError("Connexion Sage non établie") + """ + Modifie une facture existante dans Sage. + + Args: + numero: Numéro de la facture + facture_data: dict contenant les champs à modifier: + - date_facture: str ou date (optionnel) + - date_expedition: str ou date (optionnel) + - date_livraison: str ou date (optionnel) + - reference: str (optionnel) + - statut: int (optionnel) + - lignes: list[dict] (optionnel) + """ + 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]: + # 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) + type_reel = getattr(doc, "DO_Type", -1) + + logger.info(f" 📊 Type={type_reel}, 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") + + # ======================================== + # É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: + logger.error(" ❌ Objet Client NULL à l'état 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: try: - persist_test = factory.ReadPiece(type_test, numero) - if persist_test: - persist = persist_test - logger.info(f" ✅ Document trouvé (type={type_test})") + ligne_p = factory_lignes.List(index) + if ligne_p is None: break + nb_lignes_initial += 1 + index += 1 except: - continue + break - if not persist: - raise ValueError(f"❌ Facture {numero} INTROUVABLE") + logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}") + except Exception as e: + logger.warning(f" ⚠️ Erreur comptage lignes: {e}") - doc = win32com.client.CastTo(persist, "IBODocumentVente3") + # ======================================== + # ÉTAPE 3 : DÉTERMINER LES MODIFICATIONS + # ======================================== + champs_modifies = [] + + modif_date = "date_facture" in facture_data + modif_date_expedition = "date_expedition" in facture_data + modif_date_livraison = "date_livraison" 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 facture: {modif_date}") + logger.info(f" Date expédition: {modif_date_expedition}") + logger.info(f" Date livraison: {modif_date_livraison}") + 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 ===== + facture_data_temp = facture_data.copy() + reference_a_modifier = None + statut_a_modifier = None + + if modif_lignes: + if modif_ref: + reference_a_modifier = facture_data_temp.pop("reference") + logger.info("🔖 Modification de la référence reportée après les lignes") + modif_ref = False + + if modif_statut: + statut_a_modifier = facture_data_temp.pop("statut") + logger.info("📊 Modification du statut reportée après les lignes") + modif_statut = False + + # ======================================== + # ÉTAPE 4 : TEST WRITE() BASIQUE + # ======================================== + logger.info("🧪 Test Write() basique (sans modification)...") + + try: + doc.Write() + logger.info(" ✅ Write() basique OK") doc.Read() - statut_actuel = getattr(doc, "DO_Statut", 0) - type_reel = getattr(doc, "DO_Type", -1) - - logger.info(f" 📊 Type={type_reel}, 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") - - # ======================================== - # É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}") + # 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(" ❌ Objet Client NULL à l'état initial !") - except Exception as e: - logger.error(f" ❌ Erreur lecture client initial: {e}") + logger.error( + f" ❌ Client a changé: {client_code_initial} → {client_apres}" + ) + else: + logger.error(" ❌ Client devenu NULL après Write() basique") - 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: - 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 3 : 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 + 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}" ) - 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 5 : MODIFICATIONS SIMPLES (pas de lignes) + # ======================================== + if not modif_lignes and (modif_date or modif_date_expedition or modif_date_livraison or modif_statut or modif_ref): + logger.info("🎯 Modifications simples (sans 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 + if modif_date: + logger.info(" 📅 Modification date facture...") + doc.DO_Date = pywintypes.Time( + self.normaliser_date(facture_data_temp.get("date_facture")) + ) + champs_modifies.append("date") - # ======================================== - # ÉTAPE 4 : TEST WRITE() BASIQUE - # ======================================== - logger.info("🧪 Test Write() basique (sans modification)...") + if modif_date_expedition: + logger.info(" 📅 Modification date expédition...") + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(facture_data_temp["date_expedition"]) + ) + logger.info(f" ✅ Date expédition: {facture_data_temp['date_expedition']}") + champs_modifies.append("date_expedition") + if modif_date_livraison: + logger.info(" 📅 Modification date livraison...") + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(facture_data_temp["date_livraison"]) + ) + logger.info(f" ✅ Date livraison: {facture_data_temp['date_livraison']}") + champs_modifies.append("date_livraison") + + if modif_statut: + logger.info(" 📊 Modification statut...") + nouveau_statut = facture_data_temp["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: + doc.DO_Ref = facture_data_temp["reference"] + champs_modifies.append("reference") + logger.info( + f" ✅ Référence définie: {facture_data_temp['reference']}" + ) + except Exception as e: + logger.warning(f" ⚠️ Référence non définie: {e}") + + # Écrire sans réassocier le client + logger.info(" 💾 Write() sans réassociation client...") try: doc.Write() - logger.info(" ✅ Write() basique OK") - doc.Read() - - # 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") - - 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 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: - 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" ⚠️ Référence non définie: {e}") - - # Écrire sans réassocier le client - logger.info(" 💾 Write() sans réassociation client...") - try: - doc.Write() - logger.info(" ✅ Write() réussi") - - doc.Read() - - # 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: - sage_error = self.cial.CptaApplication.LastError - if sage_error: - error_msg = f"{sage_error.Description} (Code: {sage_error.Number})" - except: - pass - - logger.error(f" ❌ Write() échoue: {error_msg}") - raise ValueError(f"Sage refuse: {error_msg}") - - # ======================================== - # ÉTAPE 6 : REMPLACEMENT COMPLET DES 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 lignes" - ) - - try: - factory_lignes = doc.FactoryDocumentLigne - except: - factory_lignes = doc.FactoryDocumentVenteLigne - - factory_article = self.cial.FactoryArticle - - # ============================================ - # SOUS-ÉTAPE 1 : SUPPRIMER TOUTES LES LIGNES EXISTANTES - # ============================================ - if nb_lignes_initial > 0: - logger.info( - f" 🗑️ Suppression de {nb_lignes_initial} lignes existantes..." - ) - - # 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() - - # ✅ 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) + logger.info(" ✅ Write() réussi") doc.Read() @@ -8429,23 +8868,224 @@ class SageConnector: 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") + 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}" + ) - champs_modifies.append("lignes") - - # ======================================== - # ÉTAPE 6.5 : MODIFIER RÉFÉRENCE (APRÈS les lignes) - # ======================================== - if reference_a_modifier is not None: + except Exception as e: + error_msg = str(e) try: - ancienne_reference = getattr(doc, "DO_Ref", "") - nouvelle_reference = str(reference_a_modifier) if reference_a_modifier else "" + sage_error = self.cial.CptaApplication.LastError + if sage_error: + error_msg = f"{sage_error.Description} (Code: {sage_error.Number})" + except: + pass + + logger.error(f" ❌ Write() échoue: {error_msg}") + raise ValueError(f"Sage refuse: {error_msg}") + + # ======================================== + # ÉTAPE 6 : REMPLACEMENT COMPLET DES LIGNES + # ======================================== + elif modif_lignes: + logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...") + + # D'abord modifier les dates si demandées + if modif_date: + doc.DO_Date = pywintypes.Time( + self.normaliser_date(facture_data_temp.get("date_facture")) + ) + champs_modifies.append("date") + logger.info(" 📅 Date facture modifiée") + + if modif_date_expedition: + doc.DO_DateExpedition = pywintypes.Time( + self.normaliser_date(facture_data_temp["date_expedition"]) + ) + logger.info(" 📅 Date expédition modifiée") + champs_modifies.append("date_expedition") + + if modif_date_livraison: + doc.DO_DateLivr = pywintypes.Time( + self.normaliser_date(facture_data_temp["date_livraison"]) + ) + logger.info(" 📅 Date livraison modifiée") + champs_modifies.append("date_livraison") + + nouvelles_lignes = facture_data["lignes"] + nb_nouvelles = len(nouvelles_lignes) + + logger.info( + f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" + ) + + try: + factory_lignes = doc.FactoryDocumentLigne + except: + factory_lignes = doc.FactoryDocumentVenteLigne + + factory_article = self.cial.FactoryArticle + + # ============================================ + # SOUS-ÉTAPE 1 : SUPPRIMER TOUTES LES LIGNES EXISTANTES + # ============================================ + if nb_lignes_initial > 0: + logger.info( + f" 🗑️ Suppression de {nb_lignes_initial} lignes existantes..." + ) + + # 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() + + # ✅ 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}") - logger.info(f"🔖 Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'") - - doc.DO_Ref = nouvelle_reference + doc.DO_Statut = nouveau_statut doc.Write() import time @@ -8453,95 +9093,95 @@ class SageConnector: 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}") + 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 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...") - # ======================================== - # ÉTAPE 7 : RELECTURE ET RETOUR - # ======================================== - logger.info("📊 Relecture finale...") + import time + time.sleep(0.5) - import time - time.sleep(0.5) + doc.Read() - 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 = "" - # 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) + + # Extraire les dates + date_expedition_final = None + date_livraison_final = None + + try: + date_exped = getattr(doc, "DO_DateExpedition", None) + if date_exped: + date_expedition_final = date_exped.strftime("%Y-%m-%d") + except: + pass + + try: + date_livr = getattr(doc, "DO_DateLivr", None) + if date_livr: + date_livraison_final = date_livr.strftime("%Y-%m-%d") + except: + pass - 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}") + if date_expedition_final: + logger.info(f" 📅 Date expédition: {date_expedition_final}") + if date_livraison_final: + logger.info(f" 📅 Date livraison: {date_livraison_final}") + logger.info(f" 📝 Champs modifiés: {champs_modifies}") - 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, + "date_expedition": date_expedition_final, + "date_livraison": date_livraison_final, + "champs_modifies": champs_modifies, + "statut": statut_final, + "client_code": client_final, + } - 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 ValueError as e: - logger.error(f"❌ ERREUR MÉTIER: {e}") - raise + except Exception as e: + logger.error(f"❌ ERREUR TECHNIQUE: {e}", exc_info=True) - 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}") + 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_article(self, article_data: dict) -> dict: with self._com_context(), self._lock_com: