From e8ea774c418ce61ab689c63a6081b5ce56f7bb2c Mon Sep 17 00:00:00 2001 From: fanilo Date: Fri, 26 Dec 2025 15:38:10 +0100 Subject: [PATCH] Deleted all comments --- .gitignore | 3 + sage_connector.py | 1100 --------------------------------------------- 2 files changed, 3 insertions(+), 1100 deletions(-) diff --git a/.gitignore b/.gitignore index 5407a98..7f792c9 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ htmlcov/ *~ .build/ dist/ + + +cleaner.py \ No newline at end of file diff --git a/sage_connector.py b/sage_connector.py index 90726a3..e4c919d 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -39,16 +39,11 @@ class SageConnector: self._lock_com = threading.RLock() - # Thread-local storage pour COM self._thread_local = threading.local() - # ========================================================================= - # GESTION COM THREAD-SAFE - # ========================================================================= @contextmanager def _com_context(self): - # Vérifier si COM est déjà initialisé pour ce thread if not hasattr(self._thread_local, "com_initialized"): try: pythoncom.CoInitialize() @@ -63,7 +58,6 @@ class SageConnector: try: yield finally: - # Ne pas désinitialiser COM ici car le thread peut être réutilisé pass @contextmanager @@ -100,16 +94,10 @@ class SageConnector: except: pass - # ========================================================================= - # CONNEXION - # ========================================================================= def connecter(self): """Connexion initiale à Sage - VERSION HYBRIDE""" try: - # ======================================== - # CONNEXION COM (pour écritures) - # ======================================== with self._com_context(): self.cial = win32com.client.gencache.EnsureDispatch( "Objets100c.Cial.Stream" @@ -121,9 +109,6 @@ class SageConnector: logger.info(f" Connexion COM Sage réussie: {self.chemin_base}") - # ======================================== - # TEST CONNEXION SQL (pour lectures) - # ======================================== try: with self._get_sql_connection() as conn: cursor = conn.cursor() @@ -261,15 +246,11 @@ class SageConnector: try: with self._com_context(), self._lock_com: - # ======================================== - # ÉTAPE 0 : VALIDATION & NETTOYAGE - # ======================================== logger.info(" === VALIDATION DONNÉES FOURNISSEUR ===") if not fournisseur_data.get("intitule"): raise ValueError("Le champ 'intitule' est obligatoire") - # Nettoyage et troncature (longueurs max Sage) intitule = str(fournisseur_data["intitule"])[:69].strip() num_prop = ( str(fournisseur_data.get("num", "")).upper()[:17].strip() @@ -295,46 +276,32 @@ class SageConnector: logger.info(f" num: '{num_prop or 'AUTO'}' (len={len(num_prop)})") logger.info(f" compte: '{compte}' (len={len(compte)})") - # ======================================== - # ÉTAPE 1 : CRÉATION OBJET FOURNISSEUR - # ======================================== - # 🔑 CRITIQUE: Utiliser FactoryFournisseur, PAS FactoryClient ! factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur persist = factory_fournisseur.Create() fournisseur = win32com.client.CastTo(persist, "IBOFournisseur3") - # 🔑 CRITIQUE : Initialiser l'objet fournisseur.SetDefault() logger.info(" Objet fournisseur créé et initialisé") - # ======================================== - # ÉTAPE 2 : CHAMPS OBLIGATOIRES - # ======================================== logger.info(" Définition des champs obligatoires...") - # 1. Intitulé (OBLIGATOIRE) fournisseur.CT_Intitule = intitule logger.debug(f" CT_Intitule: '{intitule}'") - # 2. Type = Fournisseur (1) - # NOTE: Sur certaines versions Sage, CT_Type n'existe pas - # et le type est automatiquement défini par la factory utilisée try: fournisseur.CT_Type = 1 # 1 = Fournisseur logger.debug(" CT_Type: 1 (Fournisseur)") except: logger.debug(" CT_Type non défini (géré par FactoryFournisseur)") - # 3. Qualité (pour versions récentes Sage) try: fournisseur.CT_Qualite = "FOU" logger.debug(" CT_Qualite: 'FOU'") except: logger.debug(" CT_Qualite non défini (pas critique)") - # 4. Compte général principal (OBLIGATOIRE) try: factory_compte = self.cial.CptaApplication.FactoryCompteG persist_compte = factory_compte.ReadNumero(compte) @@ -345,7 +312,6 @@ class SageConnector: ) compte_obj.Read() - # Assigner l'objet CompteG fournisseur.CompteGPrinc = compte_obj logger.debug(f" CompteGPrinc: objet '{compte}' assigné") else: @@ -355,20 +321,16 @@ class SageConnector: except Exception as e: logger.warning(f" Erreur CompteGPrinc: {e}") - # 5. Numéro fournisseur (OBLIGATOIRE - générer si vide) if num_prop: fournisseur.CT_Num = num_prop logger.debug(f" CT_Num fourni: '{num_prop}'") else: - # 🔑 CRITIQUE : Générer le numéro automatiquement try: - # Méthode 1 : SetDefaultNumPiece (si disponible) if hasattr(fournisseur, "SetDefaultNumPiece"): fournisseur.SetDefaultNumPiece() num_genere = getattr(fournisseur, "CT_Num", "") logger.debug(f" CT_Num auto-généré: '{num_genere}'") else: - # Méthode 2 : GetNextNumero depuis la factory num_genere = factory_fournisseur.GetNextNumero() if num_genere: fournisseur.CT_Num = num_genere @@ -376,7 +338,6 @@ class SageConnector: f" CT_Num auto (GetNextNumero): '{num_genere}'" ) else: - # Méthode 3 : Fallback - timestamp import time num_genere = f"FOUR{int(time.time()) % 1000000}" @@ -388,7 +349,6 @@ class SageConnector: "Impossible de générer le numéro fournisseur automatiquement" ) - # 6. Catégories (valeurs par défaut) try: if hasattr(fournisseur, "N_CatTarif"): fournisseur.N_CatTarif = 1 @@ -400,12 +360,8 @@ class SageConnector: except Exception as e: logger.warning(f" Catégories: {e}") - # ======================================== - # ÉTAPE 3 : CHAMPS OPTIONNELS - # ======================================== logger.info(" Définition champs optionnels...") - # Adresse (objet IAdresse) if any([adresse, code_postal, ville, pays]): try: adresse_obj = fournisseur.Adresse @@ -423,7 +379,6 @@ class SageConnector: except Exception as e: logger.warning(f" Adresse: {e}") - # Télécom (objet ITelecom) if telephone or email: try: telecom_obj = fournisseur.Telecom @@ -437,7 +392,6 @@ class SageConnector: except Exception as e: logger.warning(f" Télécom: {e}") - # Identifiants fiscaux if siret: try: fournisseur.CT_Siret = siret @@ -452,7 +406,6 @@ class SageConnector: except Exception as e: logger.warning(f" TVA: {e}") - # Options par défaut try: if hasattr(fournisseur, "CT_Lettrage"): fournisseur.CT_Lettrage = True @@ -462,9 +415,6 @@ class SageConnector: except Exception as e: logger.debug(f" Options: {e}") - # ======================================== - # ÉTAPE 4 : VÉRIFICATION PRÉ-WRITE - # ======================================== logger.info(" === DIAGNOSTIC PRÉ-WRITE ===") num_avant_write = getattr(fournisseur, "CT_Num", "") @@ -474,9 +424,6 @@ class SageConnector: logger.info(f" CT_Num confirmé: '{num_avant_write}'") - # ======================================== - # ÉTAPE 5 : ÉCRITURE EN BASE - # ======================================== logger.info(" Écriture du fournisseur dans Sage...") try: @@ -486,7 +433,6 @@ class SageConnector: except Exception as e: error_detail = str(e) - # Récupérer l'erreur Sage détaillée try: sage_error = self.cial.CptaApplication.LastError if sage_error: @@ -497,7 +443,6 @@ class SageConnector: except: pass - # Analyser l'erreur if ( "doublon" in error_detail.lower() or "existe" in error_detail.lower() @@ -506,9 +451,6 @@ class SageConnector: raise RuntimeError(f"Échec Write(): {error_detail}") - # ======================================== - # ÉTAPE 6 : RELECTURE & FINALISATION - # ======================================== try: fournisseur.Read() except Exception as e: @@ -521,9 +463,6 @@ class SageConnector: logger.info(f" FOURNISSEUR CRÉÉ: {num_final} - {intitule} ") - # ======================================== - # ÉTAPE 7 : CONSTRUCTION RÉPONSE - # ======================================== resultat = { "numero": num_final, "intitule": intitule, @@ -540,9 +479,6 @@ class SageConnector: "tva_intra": tva_intra or None, } - # PAS DE REFRESH CACHE ICI - # Car lister_tous_fournisseurs() utilise FactoryFournisseur.List() - # qui lit directement depuis Sage (pas de cache) return resultat @@ -570,9 +506,6 @@ class SageConnector: try: with self._com_context(), self._lock_com: - # ======================================== - # ÉTAPE 1 : CHARGER LE FOURNISSEUR EXISTANT - # ======================================== logger.info(f" Recherche fournisseur {code}...") factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur @@ -589,20 +522,15 @@ class SageConnector: f" Fournisseur {code} trouvé: {getattr(fournisseur, 'CT_Intitule', '')}" ) - # ======================================== - # ÉTAPE 2 : METTRE À JOUR LES CHAMPS FOURNIS - # ======================================== logger.info(" Mise à jour des champs...") champs_modifies = [] - # Intitulé if "intitule" in fournisseur_data: intitule = str(fournisseur_data["intitule"])[:69].strip() fournisseur.CT_Intitule = intitule champs_modifies.append(f"intitule='{intitule}'") - # Adresse if any( k in fournisseur_data for k in ["adresse", "code_postal", "ville", "pays"] @@ -633,7 +561,6 @@ class SageConnector: except Exception as e: logger.warning(f"Erreur mise à jour adresse: {e}") - # Télécom if "email" in fournisseur_data or "telephone" in fournisseur_data: try: telecom_obj = fournisseur.Telecom @@ -651,7 +578,6 @@ class SageConnector: except Exception as e: logger.warning(f"Erreur mise à jour télécom: {e}") - # SIRET if "siret" in fournisseur_data: try: siret = str(fournisseur_data["siret"])[:14].strip() @@ -660,7 +586,6 @@ class SageConnector: except Exception as e: logger.warning(f"Erreur mise à jour SIRET: {e}") - # TVA Intracommunautaire if "tva_intra" in fournisseur_data: try: tva = str(fournisseur_data["tva_intra"])[:25].strip() @@ -671,7 +596,6 @@ class SageConnector: if not champs_modifies: logger.warning("Aucun champ à modifier") - # Retourner les données actuelles via extraction directe return { "numero": getattr(fournisseur, "CT_Num", "").strip(), "intitule": getattr(fournisseur, "CT_Intitule", "").strip(), @@ -681,9 +605,6 @@ class SageConnector: logger.info(f" Champs à modifier: {', '.join(champs_modifies)}") - # ======================================== - # ÉTAPE 3 : ÉCRIRE LES MODIFICATIONS - # ======================================== logger.info(" Écriture des modifications...") try: @@ -705,16 +626,12 @@ class SageConnector: logger.error(f" Erreur Write(): {error_detail}") raise RuntimeError(f"Échec modification: {error_detail}") - # ======================================== - # ÉTAPE 4 : RELIRE ET RETOURNER - # ======================================== fournisseur.Read() logger.info( f" FOURNISSEUR MODIFIÉ: {code} ({len(champs_modifies)} champs) " ) - # Extraction directe (comme lire_fournisseur) numero = getattr(fournisseur, "CT_Num", "").strip() intitule = getattr(fournisseur, "CT_Intitule", "").strip() @@ -725,7 +642,6 @@ class SageConnector: "est_fournisseur": True, } - # Adresse try: adresse_obj = getattr(fournisseur, "Adresse", None) if adresse_obj: @@ -739,7 +655,6 @@ class SageConnector: data["code_postal"] = "" data["ville"] = "" - # Télécom try: telecom_obj = getattr(fournisseur, "Telecom", None) if telecom_obj: @@ -848,7 +763,6 @@ class SageConnector: clients = [] for row in rows: client = { - # IDENTIFICATION "numero": self._safe_strip(row.CT_Num), "intitule": self._safe_strip(row.CT_Intitule), "type_tiers": row.CT_Type, @@ -859,7 +773,6 @@ class SageConnector: "tva_intra": self._safe_strip(row.CT_Identifiant), "code_naf": self._safe_strip(row.CT_Ape), - # ADRESSE "contact": self._safe_strip(row.CT_Contact), "adresse": self._safe_strip(row.CT_Adresse), "complement": self._safe_strip(row.CT_Complement), @@ -868,7 +781,6 @@ class SageConnector: "region": self._safe_strip(row.CT_CodeRegion), "pays": self._safe_strip(row.CT_Pays), - # TELECOM "telephone": self._safe_strip(row.CT_Telephone), "telecopie": self._safe_strip(row.CT_Telecopie), "email": self._safe_strip(row.CT_EMail), @@ -876,13 +788,11 @@ class SageConnector: "facebook": self._safe_strip(row.CT_Facebook), "linkedin": self._safe_strip(row.CT_LinkedIn), - # TAUX "taux01": row.CT_Taux01, "taux02": row.CT_Taux02, "taux03": row.CT_Taux03, "taux04": row.CT_Taux04, - # STATISTIQUES "statistique01": self._safe_strip(row.CT_Statistique01), "statistique02": self._safe_strip(row.CT_Statistique02), "statistique03": self._safe_strip(row.CT_Statistique03), @@ -894,13 +804,11 @@ class SageConnector: "statistique09": self._safe_strip(row.CT_Statistique09), "statistique10": self._safe_strip(row.CT_Statistique10), - # COMMERCIAL "encours_autorise": row.CT_Encours, "assurance_credit": row.CT_Assurance, "langue": row.CT_Langue, "commercial_code": row.CO_No, - # FACTURATION "lettrage_auto": (row.CT_Lettrage == 1), "est_actif": (row.CT_Sommeil == 0), "type_facture": row.CT_Facture, @@ -913,19 +821,15 @@ class SageConnector: "exclure_penalites": (row.CT_NotPenal == 1), "bon_a_payer": row.CT_BonAPayer, - # LOGISTIQUE "priorite_livraison": row.CT_PrioriteLivr, "livraison_partielle": row.CT_LivrPartielle, "delai_transport": row.CT_DelaiTransport, "delai_appro": row.CT_DelaiAppro, - # COMMENTAIRE "commentaire": self._safe_strip(row.CT_Commentaire), - # ANALYTIQUE "section_analytique": self._safe_strip(row.CA_Num), - # ORGANISATION / SURVEILLANCE "mode_reglement_code": row.MR_No, "surveillance_active": (row.CT_Surveillance == 1), "coface": self._safe_strip(row.CT_Coface), @@ -937,7 +841,6 @@ class SageConnector: "sv_chiffre_affaires": row.CT_SvCA, "sv_resultat": row.CT_SvResultat, - # COMPTE GENERAL ET CATEGORIES "compte_general": self._safe_strip(row.CG_NumPrinc), "categorie_tarif": row.N_CatTarif, "categorie_compta": row.N_CatCompta, @@ -958,7 +861,6 @@ class SageConnector: with self._get_sql_connection() as conn: cursor = conn.cursor() - # MÊME REQUÊTE que lister_tous_clients (colonnes existantes uniquement) cursor.execute( """ SELECT @@ -1004,9 +906,6 @@ class SageConnector: with self._get_sql_connection() as conn: cursor = conn.cursor() - # ======================================== - # ÉTAPE 1 : LIRE LES ARTICLES DE BASE - # ======================================== query = """ SELECT AR_Ref, AR_Design, AR_PrixVen, AR_PrixAch, @@ -1042,7 +941,6 @@ class SageConnector: "est_actif": (row[6] == 0), "code_ean": self._safe_strip(row[7]), "type_article": row[8] if row[8] is not None else 0, - # CORRECTION : Pas de AR_Stock dans ta base ! "stock_reel": 0.0, "stock_mini": 0.0, "stock_maxi": 0.0, @@ -1053,16 +951,12 @@ class SageConnector: article["code_barre"] = article["code_ean"] articles.append(article) - # ======================================== - # ÉTAPE 2 : ENRICHIR AVEC STOCK DEPUIS F_ARTSTOCK (OBLIGATOIRE) - # ======================================== if avec_stock and articles: logger.info( f" Enrichissement stock depuis F_ARTSTOCK pour {len(articles)} articles..." ) try: - # Créer un mapping des références references = [ a["reference"] for a in articles if a["reference"] ] @@ -1070,7 +964,6 @@ class SageConnector: if not references: return articles - # Requête pour récupérer TOUS les stocks en une fois placeholders = ",".join(["?"] * len(references)) stock_query = f""" SELECT @@ -1114,7 +1007,6 @@ class SageConnector: f" Stocks chargés pour {len(stock_map)} articles depuis F_ARTSTOCK" ) - # Enrichir les articles for article in articles: if article["reference"] in stock_map: stock_data = stock_map[article["reference"]] @@ -1123,7 +1015,6 @@ class SageConnector: article["stock_reel"] - article["stock_reserve"] ) else: - # Article sans stock enregistré article["stock_reel"] = 0.0 article["stock_mini"] = 0.0 article["stock_maxi"] = 0.0 @@ -1135,7 +1026,6 @@ class SageConnector: logger.error( f" Erreur lecture F_ARTSTOCK: {e}", exc_info=True ) - # Ne pas lever d'exception, retourner les articles sans stock logger.info( f" SQL: {len(articles)} articles (stock={'oui' if avec_stock else 'non'})" @@ -1184,7 +1074,6 @@ class SageConnector: 1: "Prestation", 2: "Divers", }.get(row[8] if row[8] is not None else 0, "Article"), - # Champs optionnels (initialisés à vide/valeur par défaut) "description": "", "designation_complementaire": "", "poids": 0.0, @@ -1192,7 +1081,6 @@ class SageConnector: "tva_code": "", "date_creation": "", "date_modification": "", - # Stock initialisé à 0 - sera mis à jour depuis F_ARTSTOCK "stock_reel": 0.0, "stock_mini": 0.0, "stock_maxi": 0.0, @@ -1201,12 +1089,8 @@ class SageConnector: "stock_disponible": 0.0, } - # TVA taux (par défaut 20%) article["tva_taux"] = 20.0 - # ======================================== - # ÉTAPE 2 : LIRE LE STOCK DEPUIS F_ARTSTOCK (OBLIGATOIRE) - # ======================================== logger.info(f" Lecture stock depuis F_ARTSTOCK pour {reference}...") try: @@ -1228,7 +1112,6 @@ class SageConnector: stock_row = cursor.fetchone() if stock_row: - # STOCK DEPUIS F_ARTSTOCK article["stock_reel"] = ( float(stock_row[0]) if stock_row[0] else 0.0 ) @@ -1239,7 +1122,6 @@ class SageConnector: float(stock_row[2]) if stock_row[2] else 0.0 ) - # Priorité aux réserves/commandes de F_ARTSTOCK si disponibles stock_reserve_artstock = ( float(stock_row[3]) if stock_row[3] else 0.0 ) @@ -1267,9 +1149,6 @@ class SageConnector: except Exception as e: logger.error(f" Erreur lecture F_ARTSTOCK pour {reference}: {e}") - # ======================================== - # ÉTAPE 3 : ENRICHIR AVEC LIBELLÉ FAMILLE - # ======================================== if article["famille_code"]: try: cursor.execute( @@ -1313,9 +1192,6 @@ class SageConnector: with self._get_sql_connection() as conn: cursor = conn.cursor() - # ======================================== - # LIRE L'ENTÊTE (recherche directe, sans filtres restrictifs) - # ======================================== query = """ SELECT d.DO_Piece, d.DO_Date, d.DO_Ref, d.DO_TotalHT, d.DO_TotalTTC, @@ -1350,16 +1226,8 @@ class SageConnector: numero_piece = self._safe_strip(row[0]) logger.info(f"[SQL READ] Document trouvé: {numero_piece}") - # ======================================== - # PAS DE FILTRE PAR PRÉFIXE ICI ! - # On retourne le document demandé, qu'il soit transformé ou non - # ======================================== - # ======================================== - # CONSTRUIRE L'OBJET DOCUMENT - # ======================================== doc = { - # Informations de base (indices 0-9) "numero": numero_piece, "reference": self._safe_strip(row[2]), # DO_Ref "date": str(row[1]) if row[1] else "", # DO_Date @@ -1367,7 +1235,6 @@ class SageConnector: "date_expedition": ( str(row[8]) if row[8] else "" ), # DO_DateExpedition - # Client (indices 6 et 39-44) "client_code": self._safe_strip(row[6]), # DO_Tiers "client_intitule": self._safe_strip(row[39]), # CT_Intitule "client_adresse": self._safe_strip(row[40]), # CT_Adresse @@ -1376,7 +1243,6 @@ class SageConnector: "client_telephone": self._safe_strip(row[43]), # CT_Telephone "client_email": self._safe_strip(row[44]), # CT_EMail "contact": self._safe_strip(row[9]), # DO_Contact - # Totaux (indices 3-4, 10-13) "total_ht": float(row[3]) if row[3] else 0.0, # DO_TotalHT "total_ht_net": float(row[10]) if row[10] else 0.0, # DO_TotalHTNet "total_ttc": float(row[4]) if row[4] else 0.0, # DO_TotalTTC @@ -1385,7 +1251,6 @@ class SageConnector: float(row[12]) if row[12] else 0.0 ), # DO_MontantRegle "reliquat": float(row[13]) if row[13] else 0.0, # DO_Reliquat - # Taxes (indices 14-21) "taux_escompte": ( float(row[14]) if row[14] else 0.0 ), # DO_TxEscompte @@ -1396,7 +1261,6 @@ class SageConnector: "code_taxe1": self._safe_strip(row[19]), # DO_CodeTaxe1 "code_taxe2": self._safe_strip(row[20]), # DO_CodeTaxe2 "code_taxe3": self._safe_strip(row[21]), # DO_CodeTaxe3 - # Statuts (indices 5, 22-26) "statut": int(row[5]) if row[5] is not None else 0, # DO_Statut "statut_estatut": ( int(row[22]) if row[22] is not None else 0 @@ -1407,7 +1271,6 @@ class SageConnector: "transfere": ( int(row[26]) if row[26] is not None else 0 ), # DO_Transfere - # Autres (indices 27-38) "souche": int(row[27]) if row[27] is not None else 0, # DO_Souche "piece_origine": self._safe_strip(row[28]), # DO_PieceOrig "guid": self._safe_strip(row[29]), # DO_GUID @@ -1430,9 +1293,6 @@ class SageConnector: "valeur_franco": float(row[38]) if row[38] else 0.0, # DO_ValFranco } - # ======================================== - # CHARGER LES LIGNES - # ======================================== cursor.execute( """ SELECT @@ -1616,7 +1476,6 @@ class SageConnector: doc["lignes"] = lignes doc["nb_lignes"] = len(lignes) - # Totaux calculés total_ht_calcule = sum(l.get("montant_ligne_ht", 0) for l in lignes) total_ttc_calcule = sum(l.get("montant_ligne_ttc", 0) for l in lignes) total_taxes_calcule = sum(l.get("total_taxes", 0) for l in lignes) @@ -1641,7 +1500,6 @@ class SageConnector: ): """Liste les documents avec leurs lignes.""" try: - # Convertir le type pour SQL type_doc_sql = self._convertir_type_pour_sql(type_doc) logger.info(f"[SQL LIST] ═══ Type COM {type_doc} → SQL {type_doc_sql} ═══") @@ -1701,9 +1559,6 @@ class SageConnector: ) try: - # ======================================== - # ÉTAPE 1 : FILTRE PRÉFIXE - # ======================================== prefixes_vente = { 0: ["DE"], 10: ["BC"], @@ -1727,9 +1582,6 @@ class SageConnector: logger.debug(f"[SQL LIST] {numero} : préfixe OK") - # ======================================== - # ÉTAPE 2 : CONSTRUIRE DOCUMENT DE BASE - # ======================================== try: type_doc_depuis_sql = self._convertir_type_depuis_sql( int(entete.DO_Type) @@ -1887,9 +1739,6 @@ class SageConnector: stats["erreur_construction"] += 1 continue - # ======================================== - # ÉTAPE 3 : CHARGER LES LIGNES - # ======================================== try: cursor.execute( """ @@ -2123,11 +1972,7 @@ class SageConnector: exc_info=True, ) stats["erreur_lignes"] += 1 - # Continuer quand même avec 0 lignes - # ======================================== - # ÉTAPE 6 : AJOUT DU DOCUMENT - # ======================================== documents.append(doc) stats["succes"] += 1 logger.info( @@ -2141,9 +1986,6 @@ class SageConnector: ) continue - # ======================================== - # RÉSUMÉ FINAL - # ======================================== logger.info(f"[SQL LIST] ═══════════════════════════") logger.info(f"[SQL LIST] STATISTIQUES FINALES:") logger.info(f"[SQL LIST] Total SQL: {stats['total']}") @@ -2202,9 +2044,6 @@ class SageConnector: def lire_avoir_cache(self, numero): return self._lire_document_sql(numero, type_doc=5) - # ========================================================================= - # CAST HELPERS - # ========================================================================= def _cast_client(self, persist_obj): try: @@ -2225,7 +2064,6 @@ class SageConnector: def _extraire_client(self, client_obj): try: - # === 1. CHAMPS OBLIGATOIRES === try: numero = getattr(client_obj, "CT_Num", "").strip() if not numero: @@ -2243,18 +2081,14 @@ class SageConnector: logger.debug(f"Erreur CT_Intitule sur {numero}: {e}") intitule = "" - # === 2. CONSTRUCTION OBJET DE BASE === data = { "numero": numero, "intitule": intitule, } - # === 3. TYPE DE TIERS (CLIENT/PROSPECT/FOURNISSEUR) === - # CT_Qualite : 0=Client, 1=Fournisseur, 2=Client+Fournisseur, 3=Salarié, 4=Prospect try: qualite_code = getattr(client_obj, "CT_Type", None) - # Mapper les codes vers des libellés qualite_map = { 0: "CLI", # Client 1: "FOU", # Fournisseur @@ -2270,13 +2104,11 @@ class SageConnector: data["qualite"] = "CLI" data["est_fournisseur"] = False - # CT_Prospect : 0=Non, 1=Oui try: data["est_prospect"] = getattr(client_obj, "CT_Prospect", 0) == 1 except: data["est_prospect"] = False - # Déterminer le type_tiers principal if data["est_prospect"]: data["type_tiers"] = "prospect" elif data["est_fournisseur"] and data["qualite"] != "CLIFOU": @@ -2286,7 +2118,6 @@ class SageConnector: else: data["type_tiers"] = "client" - # === 4. STATUT (ACTIF / SOMMEIL) === try: sommeil = getattr(client_obj, "CT_Sommeil", 0) data["est_actif"] = sommeil == 0 @@ -2295,7 +2126,6 @@ class SageConnector: data["est_actif"] = True data["est_en_sommeil"] = False - # === 5. TYPE DE PERSONNE (ENTREPRISE VS PARTICULIER) === try: forme_juridique = getattr(client_obj, "CT_FormeJuridique", "").strip() data["forme_juridique"] = forme_juridique @@ -2306,7 +2136,6 @@ class SageConnector: data["est_entreprise"] = False data["est_particulier"] = True - # === 6. IDENTITÉ PERSONNE PHYSIQUE (SI PARTICULIER) === try: data["civilite"] = getattr(client_obj, "CT_Civilite", "").strip() except: @@ -2322,7 +2151,6 @@ class SageConnector: except: data["prenom"] = "" - # Nom complet formaté (pour particuliers) if data.get("nom") or data.get("prenom"): parts = [] if data.get("civilite"): @@ -2335,13 +2163,11 @@ class SageConnector: else: data["nom_complet"] = "" - # === 7. CONTACT PRINCIPAL === try: data["contact"] = getattr(client_obj, "CT_Contact", "").strip() except: data["contact"] = "" - # === 8. ADRESSE COMPLÈTE === try: adresse_obj = getattr(client_obj, "Adresse", None) if adresse_obj: @@ -2394,35 +2220,29 @@ class SageConnector: data["region"] = "" data["pays"] = "" - # === 9. TÉLÉCOMMUNICATIONS (DISTINCTION FIXE/MOBILE) === try: telecom = getattr(client_obj, "Telecom", None) if telecom: - # Téléphone FIXE try: data["telephone"] = getattr(telecom, "Telephone", "").strip() except: data["telephone"] = "" - # Téléphone MOBILE try: data["portable"] = getattr(telecom, "Portable", "").strip() except: data["portable"] = "" - # FAX try: data["telecopie"] = getattr(telecom, "Telecopie", "").strip() except: data["telecopie"] = "" - # EMAIL try: data["email"] = getattr(telecom, "EMail", "").strip() except: data["email"] = "" - # SITE WEB try: site = ( getattr(telecom, "Site", None) @@ -2446,7 +2266,6 @@ class SageConnector: data["email"] = "" data["site_web"] = "" - # === 10. INFORMATIONS JURIDIQUES (ENTREPRISES) === try: data["siret"] = getattr(client_obj, "CT_Siret", "").strip() except: @@ -2470,7 +2289,6 @@ class SageConnector: except: data["code_naf"] = "" - # === 11. INFORMATIONS COMMERCIALES === try: data["secteur"] = getattr(client_obj, "CT_Secteur", "").strip() except: @@ -2488,7 +2306,6 @@ class SageConnector: except: data["ca_annuel"] = None - # Commercial rattaché try: data["commercial_code"] = getattr(client_obj, "CO_No", "").strip() except: @@ -2514,7 +2331,6 @@ class SageConnector: else: data["commercial_nom"] = "" - # === 12. CATÉGORIES === try: data["categorie_tarifaire"] = getattr(client_obj, "N_CatTarif", None) except: @@ -2525,7 +2341,6 @@ class SageConnector: except: data["categorie_comptable"] = None - # === 13. INFORMATIONS FINANCIÈRES === try: data["encours_autorise"] = float(getattr(client_obj, "CT_Encours", 0.0)) except: @@ -2543,7 +2358,6 @@ class SageConnector: except: data["compte_general"] = "" - # === 14. DATES === try: date_creation = getattr(client_obj, "CT_DateCreate", None) data["date_creation"] = str(date_creation) if date_creation else "" @@ -2565,23 +2379,19 @@ class SageConnector: def _extraire_article(self, article_obj): try: data = { - # === IDENTIFICATION === "reference": getattr(article_obj, "AR_Ref", "").strip(), "designation": getattr(article_obj, "AR_Design", "").strip(), } - # === CODE EAN / CODE-BARRES === data["code_ean"] = "" data["code_barre"] = "" try: - # Essayer AR_CodeBarre (champ principal) code_barre = getattr(article_obj, "AR_CodeBarre", "").strip() if code_barre: data["code_ean"] = code_barre data["code_barre"] = code_barre - # Sinon essayer AR_CodeBarre1 if not data["code_ean"]: code_barre1 = getattr(article_obj, "AR_CodeBarre1", "").strip() if code_barre1: @@ -2590,7 +2400,6 @@ class SageConnector: except: pass - # === PRIX === try: data["prix_vente"] = float(getattr(article_obj, "AR_PrixVen", 0.0)) except: @@ -2608,7 +2417,6 @@ class SageConnector: except: data["prix_revient"] = 0.0 - # === STOCK === try: data["stock_reel"] = float(getattr(article_obj, "AR_Stock", 0.0)) except: @@ -2624,13 +2432,11 @@ class SageConnector: except: data["stock_maxi"] = 0.0 - # Stock réservé (en commande client) try: data["stock_reserve"] = float(getattr(article_obj, "AR_QteCom", 0.0)) except: data["stock_reserve"] = 0.0 - # Stock en commande fournisseur try: data["stock_commande"] = float( getattr(article_obj, "AR_QteComFou", 0.0) @@ -2638,29 +2444,23 @@ class SageConnector: except: data["stock_commande"] = 0.0 - # Stock disponible (réel - réservé) try: data["stock_disponible"] = data["stock_reel"] - data["stock_reserve"] except: data["stock_disponible"] = data["stock_reel"] - # === DESCRIPTIONS === - # Commentaire / Description détaillée try: commentaire = getattr(article_obj, "AR_Commentaire", "").strip() data["description"] = commentaire except: data["description"] = "" - # Désignation complémentaire try: design2 = getattr(article_obj, "AR_Design2", "").strip() data["designation_complementaire"] = design2 except: data["designation_complementaire"] = "" - # === CLASSIFICATION === - # Type d'article (0=Article, 1=Prestation, 2=Divers) try: type_art = getattr(article_obj, "AR_Type", 0) data["type_article"] = type_art @@ -2673,12 +2473,10 @@ class SageConnector: data["type_article"] = 0 data["type_article_libelle"] = "Article" - # Famille try: famille_code = getattr(article_obj, "FA_CodeFamille", "").strip() data["famille_code"] = famille_code - # Charger le libellé de la famille si disponible if famille_code: try: famille_obj = getattr(article_obj, "Famille", None) @@ -2697,12 +2495,10 @@ class SageConnector: data["famille_code"] = "" data["famille_libelle"] = "" - # === FOURNISSEUR PRINCIPAL === try: fournisseur_code = getattr(article_obj, "CT_Num", "").strip() data["fournisseur_principal"] = fournisseur_code - # Charger le nom du fournisseur si disponible if fournisseur_code: try: fourn_obj = getattr(article_obj, "Fournisseur", None) @@ -2721,7 +2517,6 @@ class SageConnector: data["fournisseur_principal"] = "" data["fournisseur_nom"] = "" - # === UNITÉS === try: data["unite_vente"] = getattr(article_obj, "AR_UniteVen", "").strip() except: @@ -2732,7 +2527,6 @@ class SageConnector: except: data["unite_achat"] = "" - # === CARACTÉRISTIQUES PHYSIQUES === try: data["poids"] = float(getattr(article_obj, "AR_Poids", 0.0)) except: @@ -2743,7 +2537,6 @@ class SageConnector: except: data["volume"] = 0.0 - # === STATUT === try: sommeil = getattr(article_obj, "AR_Sommeil", 0) data["est_actif"] = sommeil == 0 @@ -2752,12 +2545,10 @@ class SageConnector: data["est_actif"] = True data["en_sommeil"] = False - # === TVA === try: tva_code = getattr(article_obj, "TA_Code", "").strip() data["tva_code"] = tva_code - # Essayer de charger le taux try: tva_obj = getattr(article_obj, "Taxe1", None) if tva_obj: @@ -2771,7 +2562,6 @@ class SageConnector: data["tva_code"] = "" data["tva_taux"] = 20.0 - # === DATES === try: date_creation = getattr(article_obj, "AR_DateCreate", None) data["date_creation"] = str(date_creation) if date_creation else "" @@ -2788,7 +2578,6 @@ class SageConnector: except Exception as e: logger.error(f" Erreur extraction article: {e}", exc_info=True) - # Retourner structure minimale en cas d'erreur return { "reference": getattr(article_obj, "AR_Ref", "").strip(), "designation": getattr(article_obj, "AR_Design", "").strip(), @@ -2825,7 +2614,6 @@ class SageConnector: def _extraire_fournisseur_enrichi(self, fourn_obj): try: - # === IDENTIFICATION === numero = getattr(fourn_obj, "CT_Num", "").strip() if not numero: return None @@ -2839,7 +2627,6 @@ class SageConnector: "est_fournisseur": True, } - # === STATUT === try: sommeil = getattr(fourn_obj, "CT_Sommeil", 0) data["est_actif"] = sommeil == 0 @@ -2848,7 +2635,6 @@ class SageConnector: data["est_actif"] = True data["en_sommeil"] = False - # === ADRESSE PRINCIPALE === try: adresse_obj = getattr(fourn_obj, "Adresse", None) if adresse_obj: @@ -2859,7 +2645,6 @@ class SageConnector: data["region"] = getattr(adresse_obj, "Region", "").strip() data["pays"] = getattr(adresse_obj, "Pays", "").strip() - # Adresse formatée complète parties_adresse = [] if data["adresse"]: parties_adresse.append(data["adresse"]) @@ -2891,7 +2676,6 @@ class SageConnector: data["pays"] = "" data["adresse_complete"] = "" - # === TÉLÉCOMMUNICATIONS === try: telecom_obj = getattr(fourn_obj, "Telecom", None) if telecom_obj: @@ -2900,7 +2684,6 @@ class SageConnector: data["telecopie"] = getattr(telecom_obj, "Telecopie", "").strip() data["email"] = getattr(telecom_obj, "EMail", "").strip() - # Site web try: site = ( getattr(telecom_obj, "Site", None) @@ -2924,14 +2707,11 @@ class SageConnector: data["email"] = "" data["site_web"] = "" - # === INFORMATIONS FISCALES === - # SIRET try: data["siret"] = getattr(fourn_obj, "CT_Siret", "").strip() except: data["siret"] = "" - # SIREN (extrait du SIRET si disponible) try: if data["siret"] and len(data["siret"]) >= 9: data["siren"] = data["siret"][:9] @@ -2940,13 +2720,11 @@ class SageConnector: except: data["siren"] = "" - # TVA Intracommunautaire try: data["tva_intra"] = getattr(fourn_obj, "CT_Identifiant", "").strip() except: data["tva_intra"] = "" - # Code NAF/APE try: data["code_naf"] = ( getattr(fourn_obj, "CT_CodeNAF", "").strip() @@ -2955,7 +2733,6 @@ class SageConnector: except: data["code_naf"] = "" - # Forme juridique try: data["forme_juridique"] = getattr( fourn_obj, "CT_FormeJuridique", "" @@ -2963,8 +2740,6 @@ class SageConnector: except: data["forme_juridique"] = "" - # === CATÉGORIES === - # Catégorie tarifaire try: cat_tarif = getattr(fourn_obj, "N_CatTarif", None) data["categorie_tarifaire"] = ( @@ -2973,7 +2748,6 @@ class SageConnector: except: data["categorie_tarifaire"] = None - # Catégorie comptable try: cat_compta = getattr(fourn_obj, "N_CatCompta", None) data["categorie_comptable"] = ( @@ -2982,15 +2756,12 @@ class SageConnector: except: data["categorie_comptable"] = None - # === CONDITIONS DE RÈGLEMENT === try: cond_regl = getattr(fourn_obj, "CT_CondRegl", "").strip() data["conditions_reglement_code"] = cond_regl - # Charger le libellé si disponible if cond_regl: try: - # Essayer de charger l'objet ConditionReglement cond_obj = getattr(fourn_obj, "ConditionReglement", None) if cond_obj: cond_obj.Read() @@ -3007,12 +2778,10 @@ class SageConnector: data["conditions_reglement_code"] = "" data["conditions_reglement_libelle"] = "" - # Mode de règlement try: mode_regl = getattr(fourn_obj, "CT_ModeRegl", "").strip() data["mode_reglement_code"] = mode_regl - # Libellé du mode de règlement if mode_regl: try: mode_obj = getattr(fourn_obj, "ModeReglement", None) @@ -3031,11 +2800,9 @@ class SageConnector: data["mode_reglement_code"] = "" data["mode_reglement_libelle"] = "" - # === COORDONNÉES BANCAIRES (IBAN) === data["coordonnees_bancaires"] = [] try: - # Sage peut avoir plusieurs comptes bancaires factory_banque = getattr(fourn_obj, "FactoryBanque", None) if factory_banque: @@ -3069,7 +2836,6 @@ class SageConnector: "cle_rib": getattr(banque, "RIB_Cle", "").strip(), } - # Ne garder que si IBAN ou RIB complet if ( compte_bancaire["iban"] or compte_bancaire["numero_compte"] @@ -3084,7 +2850,6 @@ class SageConnector: f"Erreur coordonnées bancaires fournisseur {numero}: {e}" ) - # IBAN principal (premier de la liste) if data["coordonnees_bancaires"]: data["iban_principal"] = data["coordonnees_bancaires"][0].get( "iban", "" @@ -3094,7 +2859,6 @@ class SageConnector: data["iban_principal"] = "" data["bic_principal"] = "" - # === CONTACTS === data["contacts"] = [] try: @@ -3125,14 +2889,12 @@ class SageConnector: "email": getattr(contact, "CO_EMail", "").strip(), } - # Nom complet formaté nom_complet = f"{contact_data['prenom']} {contact_data['nom']}".strip() if nom_complet: contact_data["nom_complet"] = nom_complet else: contact_data["nom_complet"] = contact_data["nom"] - # Ne garder que si nom existe if contact_data["nom"]: data["contacts"].append(contact_data) @@ -3142,35 +2904,28 @@ class SageConnector: except Exception as e: logger.debug(f"Erreur contacts fournisseur {numero}: {e}") - # Nombre de contacts data["nb_contacts"] = len(data["contacts"]) - # Contact principal (premier de la liste) if data["contacts"]: data["contact_principal"] = data["contacts"][0] else: data["contact_principal"] = None - # === STATISTIQUES & INFORMATIONS COMMERCIALES === - # Encours autorisé try: data["encours_autorise"] = float(getattr(fourn_obj, "CT_Encours", 0.0)) except: data["encours_autorise"] = 0.0 - # Chiffre d'affaires annuel try: data["ca_annuel"] = float(getattr(fourn_obj, "CT_ChiffreAffaire", 0.0)) except: data["ca_annuel"] = 0.0 - # Compte général try: data["compte_general"] = getattr(fourn_obj, "CG_Num", "").strip() except: data["compte_general"] = "" - # === DATES === try: date_creation = getattr(fourn_obj, "CT_DateCreate", None) data["date_creation"] = str(date_creation) if date_creation else "" @@ -3187,7 +2942,6 @@ class SageConnector: except Exception as e: logger.error(f" Erreur extraction fournisseur: {e}", exc_info=True) - # Retourner structure minimale en cas d'erreur return { "numero": getattr(fourn_obj, "CT_Num", "").strip(), "intitule": getattr(fourn_obj, "CT_Intitule", "").strip(), @@ -3267,7 +3021,6 @@ class SageConnector: logger.warning(f"BeginTrans échoué: {e}") try: - # ===== CRÉATION DOCUMENT ===== process = self.cial.CreateProcess_Document(0) doc = process.Document @@ -3278,7 +3031,6 @@ class SageConnector: logger.info(" Document devis créé") - # ===== DATES ===== doc.DO_Date = pywintypes.Time( self.normaliser_date(devis_data.get("date_devis")) ) @@ -3288,7 +3040,6 @@ class SageConnector: self.normaliser_date(devis_data["date_livraison"]) ) - # ===== CLIENT ===== factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( devis_data["client"]["code"] @@ -3308,7 +3059,6 @@ class SageConnector: doc.SetDefaultClient(client_obj) logger.info(f" Client {devis_data['client']['code']} associé") - # ===== STATUT ===== if forcer_brouillon: doc.DO_Statut = 0 logger.info(" Statut défini: 0 (Brouillon)") @@ -3318,7 +3068,6 @@ class SageConnector: doc.Write() - # ===== LIGNES ===== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -3333,7 +3082,6 @@ class SageConnector: f"--- Ligne {idx}: {ligne_data['article_code']} ---" ) - # Charger l'article persist_article = factory_article.ReadReference( ligne_data["article_code"] ) @@ -3356,7 +3104,6 @@ class SageConnector: f"Article {ligne_data['article_code']} a un prix = 0€" ) - # Créer la ligne ligne_persist = factory_lignes.Create() try: @@ -3384,7 +3131,6 @@ class SageConnector: ) ligne_obj.DL_Qte = quantite - # Prix prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) prix_a_utiliser = ligne_data.get("prix_unitaire_ht") @@ -3397,7 +3143,6 @@ class SageConnector: ) ligne_obj.DL_PrixUnitaire = float(prix_sage) - # Remise remise = ligne_data.get("remise_pourcentage", 0) if remise > 0: try: @@ -3410,10 +3155,8 @@ class SageConnector: logger.info(f" {len(devis_data['lignes'])} lignes écrites") - # ===== VALIDATION ===== doc.Write() - # ===== PROCESS ===== if not forcer_brouillon: logger.info(" Lancement Process()...") process.Process() @@ -3424,7 +3167,6 @@ class SageConnector: except: logger.debug("Process() ignoré pour brouillon") - # ===== RÉCUPÉRATION NUMÉRO ===== numero_devis = self._recuperer_numero_devis(process, doc) if not numero_devis: @@ -3432,7 +3174,6 @@ class SageConnector: logger.info(f" Numéro: {numero_devis}") - # ===== COMMIT ===== if transaction_active: try: self.cial.CptaApplication.CommitTrans() @@ -3440,22 +3181,18 @@ class SageConnector: except: pass - # ===== ATTENTE POUR STABILISATION ===== import time time.sleep(0.5) - # ===== RÉFÉRENCE (RECHARGER D'ABORD LE DOCUMENT) ===== if "reference" in devis_data and devis_data["reference"]: try: logger.info( f" Application de la référence: {devis_data['reference']}" ) - # RECHARGER le document par son numéro 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 "" @@ -3473,7 +3210,6 @@ class SageConnector: exc_info=True, ) - # ===== RELECTURE FINALE ===== time.sleep(0.5) doc_final_data = self._relire_devis( @@ -3503,7 +3239,6 @@ class SageConnector: """Récupère le numéro du devis créé via plusieurs méthodes.""" numero_devis = None - # Méthode 1: DocumentResult try: doc_result = process.DocumentResult if doc_result: @@ -3513,11 +3248,9 @@ class SageConnector: except: pass - # Méthode 2: Document direct if not numero_devis: numero_devis = getattr(doc, "DO_Piece", "") - # Méthode 3: SetDefaultNumPiece if not numero_devis: try: doc.SetDefaultNumPiece() @@ -3540,7 +3273,6 @@ class SageConnector: numero_devis, factory_doc ) - # Extraction des informations if persist_reread: doc_final = win32com.client.CastTo(persist_reread, "IBODocumentVente3") doc_final.Read() @@ -3550,7 +3282,6 @@ class SageConnector: statut_final = getattr(doc_final, "DO_Statut", 0) reference_final = getattr(doc_final, "DO_Ref", "") - # Dates date_livraison_final = None try: @@ -3560,7 +3291,6 @@ class SageConnector: except: pass else: - # Fallback: calculer manuellement total_calcule = sum( l.get("montant_ligne_ht", 0) for l in devis_data["lignes"] ) @@ -3628,9 +3358,6 @@ class SageConnector: - statut: int (optionnel) - lignes: list[dict] (optionnel) """ - # ======================================== - # LOG INITIAL CRITIQUE - POUR VÉRIFIER QUE LA MÉTHODE EST APPELÉE - # ======================================== logger.info("=" * 100) logger.info("=" * 100) logger.info(f" NOUVELLE MÉTHODE modifier_devis() APPELÉE POUR {numero} ") @@ -3648,28 +3375,21 @@ class SageConnector: logger.info(f" [ÉTAPE 1] CHARGEMENT DU DEVIS {numero}") logger.info("=" * 80) - # Charger le devis doc = self._charger_devis(numero) logger.info(f" Devis {numero} chargé avec succès") - # Afficher l'état INITIAL logger.info("") self._afficher_etat_document(doc, "📸 ÉTAT INITIAL") - # Vérifier qu'il n'est pas transformé logger.info(" Vérification statut transformation...") self._verifier_devis_non_transforme(numero, doc) logger.info(" Devis non transformé - modification autorisée") - # ======================================== - # ÉTAPE 2 : INFORMATIONS CLIENT ET LIGNES - # ======================================== logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 2] ANALYSE DOCUMENT ACTUEL") logger.info("=" * 80) - # Client client_code_initial = "" try: client_obj = getattr(doc, "Client", None) @@ -3682,13 +3402,9 @@ class SageConnector: except Exception as e: logger.warning(f" Impossible de lire le client: {e}") - # Compter lignes nb_lignes_initial = self._compter_lignes_document(doc) logger.info(f" Nombre de lignes actuelles: {nb_lignes_initial}") - # ======================================== - # ÉTAPE 3 : ANALYSE DES MODIFICATIONS DEMANDÉES - # ======================================== logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 3] ANALYSE MODIFICATIONS DEMANDÉES") @@ -3722,7 +3438,6 @@ class SageConnector: for i, ligne in enumerate(devis_data['lignes'], 1): logger.info(f" → Ligne {i}: {ligne.get('article_code')} (Qté: {ligne.get('quantite')})") - # Reporter référence et statut après lignes devis_data_temp = devis_data.copy() reference_a_modifier = None statut_a_modifier = None @@ -3741,9 +3456,6 @@ class SageConnector: logger.info(f" Statut {statut_a_modifier} reporté") modif_statut = False - # ======================================== - # ÉTAPE 4 : TEST WRITE() BASIQUE - # ======================================== logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 4] TEST WRITE() BASIQUE") @@ -3764,16 +3476,12 @@ class SageConnector: champs_modifies = [] - # ======================================== - # ÉTAPE 5 : MODIFICATIONS SIMPLES (sans lignes) - # ======================================== if not modif_lignes and (modif_date or modif_date_livraison or modif_statut or modif_ref): logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 5A] MODIFICATIONS SIMPLES (sans lignes)") logger.info("=" * 80) - # Date devis if modif_date: logger.info("") logger.info(" Modification DATE_DEVIS...") @@ -3794,7 +3502,6 @@ class SageConnector: except Exception as e: logger.error(f" Erreur date devis: {e}", exc_info=True) - # Date livraison if modif_date_livraison: logger.info("") logger.info(" Modification DATE_LIVRAISON...") @@ -3820,7 +3527,6 @@ class SageConnector: except Exception as e: logger.error(f" Erreur date livraison: {e}", exc_info=True) - # Référence if modif_ref: logger.info("") logger.info(" Modification RÉFÉRENCE...") @@ -3839,7 +3545,6 @@ class SageConnector: except Exception as e: logger.error(f" Erreur référence: {e}", exc_info=True) - # Statut if modif_statut: logger.info("") logger.info(" Modification STATUT...") @@ -3861,7 +3566,6 @@ class SageConnector: except Exception as e: logger.error(f" Erreur statut: {e}", exc_info=True) - # WRITE FINAL pour modifications simples logger.info("") logger.info(" Write() modifications simples...") try: @@ -3875,16 +3579,12 @@ class SageConnector: logger.error(f" Write() a échoué: {e}", exc_info=True) raise - # ======================================== - # ÉTAPE 6 : REMPLACEMENT COMPLET DES LIGNES - # ======================================== elif modif_lignes: logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 5B] REMPLACEMENT COMPLET DES LIGNES") logger.info("=" * 80) - # Modifier dates AVANT les lignes if modif_date: logger.info(" Modification date devis (avant lignes)...") try: @@ -3915,7 +3615,6 @@ class SageConnector: logger.info("") logger.info(f" Remplacement: {nb_lignes_initial} lignes → {nb_nouvelles} lignes") - # Récupérer factories try: factory_lignes = doc.FactoryDocumentLigne except: @@ -3923,9 +3622,6 @@ class SageConnector: factory_article = self.cial.FactoryArticle - # ============================================ - # SUPPRESSION DE TOUTES LES LIGNES - # ============================================ if nb_lignes_initial > 0: logger.info("") logger.info(f" Suppression de {nb_lignes_initial} lignes existantes...") @@ -3947,9 +3643,6 @@ class SageConnector: logger.info(f" {nb_lignes_initial} lignes supprimées") - # ============================================ - # CRÉATION DES NOUVELLES LIGNES - # ============================================ logger.info("") logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...") @@ -3966,7 +3659,6 @@ class SageConnector: logger.info(f" Remise: {ligne_data['remise_pourcentage']}%") try: - # Charger article persist_article = factory_article.ReadReference(article_code) if not persist_article: raise ValueError(f"Article {article_code} INTROUVABLE") @@ -3975,7 +3667,6 @@ class SageConnector: article_obj.Read() logger.info(f" ✓ Article chargé") - # Créer ligne ligne_persist = factory_lignes.Create() try: @@ -3983,7 +3674,6 @@ class SageConnector: except: ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3") - # Associer article try: ligne_obj.SetDefaultArticleReference(article_code, quantite) logger.info(f" ✓ Article associé via SetDefaultArticleReference") @@ -3996,12 +3686,10 @@ class SageConnector: ligne_obj.DL_Qte = quantite logger.info(f" ✓ Article associé manuellement") - # Prix if ligne_data.get("prix_unitaire_ht"): ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"]) logger.info(f" ✓ Prix unitaire défini") - # Remise if ligne_data.get("remise_pourcentage", 0) > 0: try: ligne_obj.DL_Remise01REM_Valeur = float(ligne_data["remise_pourcentage"]) @@ -4010,7 +3698,6 @@ class SageConnector: except: logger.debug(f" Remise non supportée") - # Écrire ligne ligne_obj.Write() logger.info(f" Ligne {idx} créée avec succès") @@ -4021,7 +3708,6 @@ class SageConnector: logger.info("") logger.info(f" {nb_nouvelles} lignes créées") - # WRITE après lignes logger.info("") logger.info(" Write() après remplacement lignes...") try: @@ -4037,9 +3723,6 @@ class SageConnector: champs_modifies.append("lignes") - # ======================================== - # ÉTAPE 7 : RÉFÉRENCE APRÈS LIGNES - # ======================================== if reference_a_modifier is not None: logger.info("") logger.info("=" * 80) @@ -4068,9 +3751,6 @@ class SageConnector: except Exception as e: logger.error(f" Erreur référence: {e}", exc_info=True) - # ======================================== - # ÉTAPE 8 : STATUT EN DERNIER - # ======================================== if statut_a_modifier is not None: logger.info("") logger.info("=" * 80) @@ -4102,9 +3782,6 @@ class SageConnector: except Exception as e: logger.error(f" Erreur statut: {e}", exc_info=True) - # ======================================== - # ÉTAPE 9 : VALIDATION FINALE - # ======================================== logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 8] VALIDATION FINALE") @@ -4120,13 +3797,9 @@ class SageConnector: doc.Read() logger.info(" Read() final") - # Afficher état final logger.info("") self._afficher_etat_document(doc, "📸 ÉTAT FINAL") - # ======================================== - # ÉTAPE 10 : EXTRACTION RÉSULTAT - # ======================================== logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 9] EXTRACTION RÉSULTAT") @@ -4215,7 +3888,6 @@ class SageConnector: factory = self.cial.FactoryDocumentVente - # Tentative 1: ReadPiece logger.info(" Tentative ReadPiece(0, numero)...") persist = factory.ReadPiece(0, numero) @@ -4285,7 +3957,6 @@ class SageConnector: statut = getattr(doc, "DO_Statut", 0) reference = getattr(doc, "DO_Ref", "") - # Date devis date_devis = None try: date_doc = getattr(doc, "DO_Date", None) @@ -4294,7 +3965,6 @@ class SageConnector: except: pass - # Date livraison date_livraison = None try: date_livr = getattr(doc, "DO_DateLivr", None) @@ -4303,7 +3973,6 @@ class SageConnector: except: pass - # Client client_code = "" try: client_obj = getattr(doc, "Client", None) @@ -4328,7 +3997,6 @@ class SageConnector: def lire_devis(self, numero_devis): try: - # Lire le devis via SQL devis = self._lire_document_sql(numero_devis, type_doc=0) if not devis: @@ -4353,7 +4021,6 @@ class SageConnector: f"[VERIF] Vérification transformations de {numero_source} (type {type_source})" ) - # DEBUG COMPLET logger.info(f"[DEBUG] Type source brut: {type_source}") logger.info( f"[DEBUG] Type source après normalisation: {self._normaliser_type_document(type_source)}" @@ -4362,7 +4029,6 @@ class SageConnector: f"[DEBUG] Type source après normalisation SQL: {self._convertir_type_pour_sql(type_source)}" ) - # NORMALISER le type source type_source = self._convertir_type_pour_sql(type_source) champ_liaison_mapping = { @@ -4439,7 +4105,6 @@ class SageConnector: def peut_etre_transforme(self, numero_source, type_source, type_cible): """Version corrigée avec normalisation""" - # NORMALISER les types type_source = self._normaliser_type_document(type_source) type_cible = self._normaliser_type_document(type_cible) @@ -4450,7 +4115,6 @@ class SageConnector: verif = self.verifier_si_deja_transforme_sql(numero_source, type_source) - # Comparer avec le type NORMALISÉ docs_meme_type = [ d for d in verif["documents_cibles"] if d["type"] == type_cible ] @@ -4475,7 +4139,6 @@ class SageConnector: Retourne le libellé d'un type de document. Gère les 2 formats : valeur réelle Sage (0,10,20...) ET valeur affichée (0,1,2...) """ - # Mapping principal (valeurs Sage officielles) types_officiels = { 0: "Devis", 10: "Bon de commande", @@ -4486,7 +4149,6 @@ class SageConnector: 60: "Facture", } - # Mapping alternatif (parfois Sage stocke 1 au lieu de 10, 2 au lieu de 20, etc.) types_alternatifs = { 1: "Bon de commande", 2: "Préparation", @@ -4496,11 +4158,9 @@ class SageConnector: 6: "Facture", } - # Essayer d'abord le mapping officiel if type_doc in types_officiels: return types_officiels[type_doc] - # Puis le mapping alternatif if type_doc in types_alternatifs: return types_alternatifs[type_doc] @@ -4511,14 +4171,12 @@ class SageConnector: Normalise le type de document vers la valeur officielle Sage. Convertit 1→10, 2→20, etc. si nécessaire """ - # Si c'est déjà un type officiel, le retourner tel quel logger.info(f"[INFO] TYPE RECU{type_doc}") if type_doc in [0, 10, 20, 30, 40, 50, 60]: return type_doc - # Sinon, essayer de convertir mapping_normalisation = { 1: 10, # Commande 2: 20, # Préparation @@ -4549,9 +4207,6 @@ class SageConnector: f"[TRANSFORM] Transformation: {numero_source} ({type_source}) → type {type_cible}" ) - # ======================================== - # VALIDATION DES TYPES - # ======================================== transformations_valides = { (0, 10): ("Vente", "CreateProcess_Commander"), (0, 60): ("Vente", "CreateProcess_Facturer"), @@ -4569,9 +4224,6 @@ class SageConnector: module, methode = transformations_valides[(type_source, type_cible)] logger.info(f"[TRANSFORM] Méthode: Transformation.{module}.{methode}()") - # ======================================== - # VÉRIFICATION OPTIONNELLE DES DOUBLONS - # ======================================== if verifier_doublons: logger.info("[TRANSFORM] Vérification des doublons...") verif = self.peut_etre_transforme(numero_source, type_source, type_cible) @@ -4588,9 +4240,6 @@ class SageConnector: with self._com_context(), self._lock_com: factory = self.cial.FactoryDocumentVente - # ======================================== - # ÉTAPE 1 : LIRE LE DOCUMENT SOURCE - # ======================================== logger.info(f"[TRANSFORM] Lecture de {numero_source}...") if not factory.ExistPiece(type_source, numero_source): @@ -4603,7 +4252,6 @@ class SageConnector: doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3") doc_source.Read() - # Informations du document source statut_source = getattr(doc_source, "DO_Statut", 0) nb_lignes_source = 0 @@ -4622,9 +4270,6 @@ class SageConnector: if nb_lignes_source == 0: raise ValueError(f"Document {numero_source} vide (0 lignes)") - # ======================================== - # ÉTAPE 2 : CRÉER LE TRANSFORMER - # ======================================== logger.info("[TRANSFORM] 🔧 Création du transformer...") transformation = getattr(self.cial, "Transformation", None) @@ -4645,12 +4290,8 @@ class SageConnector: logger.info("[TRANSFORM] Transformer créé") - # ======================================== - # ÉTAPE 3 : CONFIGURATION - # ======================================== logger.info("[TRANSFORM] Configuration...") - # Tenter de définir ConserveDocuments if hasattr(transformer, "ConserveDocuments"): try: transformer.ConserveDocuments = conserver_document_source @@ -4662,9 +4303,6 @@ class SageConnector: f"[TRANSFORM] ConserveDocuments non modifiable: {e}" ) - # ======================================== - # ÉTAPE 4 : AJOUTER LE DOCUMENT - # ======================================== logger.info("[TRANSFORM] Ajout du document...") try: @@ -4673,9 +4311,6 @@ class SageConnector: except Exception as e: raise RuntimeError(f"Impossible d'ajouter le document: {e}") - # ======================================== - # ÉTAPE 5 : VÉRIFIER CANPROCESS - # ======================================== try: can_process = getattr(transformer, "CanProcess", False) logger.info(f"[TRANSFORM] CanProcess: {can_process}") @@ -4691,9 +4326,6 @@ class SageConnector: ) raise RuntimeError("Transformation impossible (CanProcess=False)") - # ======================================== - # ÉTAPE 6 : TRANSACTION (optionnelle) - # ======================================== transaction_active = False try: self.cial.CptaApplication.BeginTrans() @@ -4703,9 +4335,6 @@ class SageConnector: pass try: - # ======================================== - # ÉTAPE 7 : PROCESS (TRANSFORMATION) - # ======================================== logger.info("[TRANSFORM] Process()...") try: @@ -4722,9 +4351,6 @@ class SageConnector: raise RuntimeError(f"Échec: {' | '.join(msgs)}") raise RuntimeError(f"Échec transformation: {e}") - # ======================================== - # ÉTAPE 8 : RÉCUPÉRER LES RÉSULTATS - # ======================================== logger.info("[TRANSFORM] Récupération des résultats...") list_results = getattr(transformer, "ListDocumentsResult", None) @@ -4749,7 +4375,6 @@ class SageConnector: total_ht = float(getattr(doc_result, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc_result, "DO_TotalTTC", 0.0)) - # Compter les lignes via COM nb_lignes = 0 try: factory_lignes_result = getattr( @@ -4784,9 +4409,6 @@ class SageConnector: if not documents_crees: raise RuntimeError("Aucun document créé après Process()") - # ======================================== - # ÉTAPE 9 : COMMIT - # ======================================== if transaction_active: try: self.cial.CptaApplication.CommitTrans() @@ -4794,12 +4416,8 @@ class SageConnector: except: pass - # Pause de sécurité time.sleep(1.5) - # ======================================== - # RÉSULTAT - # ======================================== doc_principal = documents_crees[0] logger.info( @@ -4828,7 +4446,6 @@ class SageConnector: } except Exception as e: - # Rollback en cas d'erreur if transaction_active: try: self.cial.CptaApplication.RollbackTrans() @@ -4838,17 +4455,14 @@ class SageConnector: raise except ValueError as e: - # Erreur métier (validation, doublon, etc.) logger.error(f"[TRANSFORM] Erreur métier: {e}") raise except RuntimeError as e: - # Erreur technique Sage logger.error(f"[TRANSFORM] Erreur technique: {e}") raise except Exception as e: - # Erreur inattendue logger.error(f"[TRANSFORM] Erreur inattendue: {e}", exc_info=True) raise RuntimeError(f"Échec transformation: {str(e)}") @@ -4874,7 +4488,6 @@ class SageConnector: for i in range(1, nb_erreurs + 1): try: err = None - # Plusieurs façons d'accéder aux erreurs selon la version Sage try: err = obj.Errors.Item(i) except: @@ -4891,7 +4504,6 @@ class SageConnector: field = "" number = "" - # Description for attr in ["Description", "Descr", "Message", "Text"]: try: val = getattr(err, attr, None) @@ -4901,7 +4513,6 @@ class SageConnector: except: pass - # Champ concerné for attr in ["Field", "FieldName", "Champ", "Property"]: try: val = getattr(err, attr, None) @@ -4911,7 +4522,6 @@ class SageConnector: except: pass - # Numéro d'erreur for attr in ["Number", "Code", "ErrorCode", "Numero"]: try: val = getattr(err, attr, None) @@ -5029,7 +4639,6 @@ class SageConnector: if not client: return None - # Récupérer infos contact principal contact_info = { "client_code": code_client, "client_intitule": getattr(client, "CT_Intitule", ""), @@ -5038,7 +4647,6 @@ class SageConnector: "telephone": None, } - # Email principal depuis Telecom try: telecom = getattr(client, "Telecom", None) if telecom: @@ -5047,7 +4655,6 @@ class SageConnector: except: pass - # Nom du contact try: contact_info["nom"] = ( getattr(client, "CT_Contact", "") @@ -5287,9 +4894,6 @@ class SageConnector: logger.info("[CREATION CLIENT SAGE - DIAGNOSTIC COMPLET]") logger.info("=" * 80) - # ============================================================ - # UTILITAIRES - # ============================================================ def clean_str(value, max_len: int) -> str: if value is None or str(value).lower() in ('none', 'null', ''): return "" @@ -5329,9 +4933,6 @@ class SageConnector: return False - # ============================================================ - # VALIDATION - # ============================================================ if not client_data.get("intitule"): raise ValueError("intitule obligatoire") @@ -5342,9 +4943,6 @@ class SageConnector: numero = clean_str(client_data["numero"], 17).upper() type_tiers = safe_int(client_data.get("type_tiers"), 0) - # ============================================================ - # ETAPE 1 : CREATION OBJET - # ============================================================ logger.info("[ETAPE 1] CREATION OBJET") factory_map = { @@ -5361,12 +4959,8 @@ class SageConnector: logger.info(f" Objet cree: {interface_name}") - # ============================================================ - # ETAPE 2 : CONFIGURATION OBLIGATOIRE MINIMALE - # ============================================================ logger.info("[ETAPE 2] CONFIGURATION OBLIGATOIRE") - # 1. DEFINIR LES CHAMPS MINIMUMS D'ABORD client.CT_Intitule = intitule client.CT_Num = numero logger.info(f" CT_Num = {numero}") @@ -5377,14 +4971,12 @@ class SageConnector: client.CT_Qualite = qualite logger.info(f" CT_Qualite = {qualite}") - # 2. APPLIQUER SETDEFAULT client.SetDefault() logger.info(" SetDefault() applique") if client_data.get("raccourci"): raccourci = clean_str(client_data["raccourci"], 7).upper().strip() - # Vérifier unicité dans Sage try: factory_client = self.cial.CptaApplication.FactoryClient exist_client = factory_client.ReadRaccourci(raccourci) @@ -5395,14 +4987,12 @@ class SageConnector: client.CT_Raccourci = raccourci logger.info(f" CT_Raccourci = {raccourci} [OK]") except Exception as e: - # Si ReadRaccourci n'existe pas, essayer direct try: client.CT_Raccourci = raccourci logger.info(f" CT_Raccourci = {raccourci} [OK]") except Exception as e2: logger.warning(f" CT_Raccourci = {raccourci} [ECHEC: {e2}]") - # 3. FORCER CT_Type si nécessaire try: if not hasattr(client, 'CT_Type') or client.CT_Type is None: client.CT_Type = type_tiers @@ -5410,7 +5000,6 @@ class SageConnector: except: pass - # 4. COMPTE GENERAL (OBLIGATOIRE) COMPTES_DEFAUT = {0: "4110000", 1: "4010000", 2: "421", 3: "471"} compte = clean_str( client_data.get("compte_general") or COMPTES_DEFAUT.get(type_tiers, "4110000"), @@ -5434,7 +5023,6 @@ class SageConnector: compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3") compte_obj.Read() - # Vérifier que c'est un compte de détail (Type 0) type_compte = getattr(compte_obj, 'CG_Type', None) if type_compte == 0: @@ -5451,13 +5039,10 @@ class SageConnector: if not compte_trouve: raise RuntimeError("Aucun compte general valide trouve") - # 5. CATEGORIES VIA OBJETS (si nécessaire) logger.info(" Configuration categories:") - # Catégorie Tarifaire try: factory_cat_tarif = self.cial.CptaApplication.FactoryCategorieTarif - # Essayer d'abord l'ID 0, sinon 1 for cat_id in ["0", "1"]: try: persist_cat = factory_cat_tarif.ReadIntitule(cat_id) @@ -5472,7 +5057,6 @@ class SageConnector: except Exception as e: logger.warning(f" CatTarif erreur: {e}") - # Catégorie Comptable try: factory_cat_compta = self.cial.CptaApplication.FactoryCategorieCompta for cat_id in ["0", "1"]: @@ -5489,9 +5073,6 @@ class SageConnector: except Exception as e: logger.warning(f" CatCompta erreur: {e}") - # ============================================================ - # ETAPE 3 : IDENTIFICATION - # ============================================================ logger.info("[ETAPE 3] IDENTIFICATION") if client_data.get("classement"): @@ -5510,23 +5091,17 @@ class SageConnector: if client_data.get("code_naf"): try_set_attribute(client, "CT_Ape", clean_str(client_data["code_naf"], 7)) - # ============================================================ - # ETAPE 4 : ADRESSE - # ============================================================ logger.info("[ETAPE 4] ADRESSE") - # CT_Contact - DOUBLE AFFECTATION (client ET adresse) if client_data.get("contact"): contact_nom = clean_str(client_data["contact"], 35) - # Variante 1 : Sur l'objet client directement try: client.CT_Contact = contact_nom logger.info(f" CT_Contact (client) = {contact_nom} [OK]") except Exception as e: logger.warning(f" CT_Contact (client) [ECHEC: {e}]") - # Variante 2 : Via l'objet Adresse try: adresse_obj = client.Adresse adresse_obj.Contact = contact_nom @@ -5534,7 +5109,6 @@ class SageConnector: except Exception as e: logger.warning(f" Contact (adresse) [ECHEC: {e}]") - # Puis le reste de l'adresse try: adresse_obj = client.Adresse logger.info(" Objet Adresse OK") @@ -5560,9 +5134,6 @@ class SageConnector: except Exception as e: logger.error(f" Adresse erreur: {e}") - # ============================================================ - # ETAPE 5 : TELECOM - # ============================================================ logger.info("[ETAPE 5] TELECOM") try: @@ -5586,18 +5157,14 @@ class SageConnector: try_set_attribute(telecom_obj, "Portable", portable) logger.info(f" Portable = {portable}") - # CT_Facebook if client_data.get("facebook"): facebook = clean_str(client_data["facebook"], 69) # URL ou @username - # Essayer plusieurs variantes if not try_set_attribute(telecom_obj, "Facebook", facebook): try_set_attribute(client, "CT_Facebook", facebook) logger.info(f" Facebook = {facebook}") - # CT_LinkedIn if client_data.get("linkedin"): linkedin = clean_str(client_data["linkedin"], 69) # URL ou profil - # Essayer plusieurs variantes if not try_set_attribute(telecom_obj, "LinkedIn", linkedin): try_set_attribute(client, "CT_LinkedIn", linkedin) logger.info(f" LinkedIn = {linkedin}") @@ -5605,9 +5172,6 @@ class SageConnector: except Exception as e: logger.error(f" Telecom erreur: {e}") - # ============================================================ - # ETAPE 6 : TAUX - # ============================================================ logger.info("[ETAPE 6] TAUX") for i in range(1, 5): @@ -5615,9 +5179,6 @@ class SageConnector: if val is not None: try_set_attribute(client, f"CT_Taux{i:02d}", safe_float(val)) - # ============================================================ - # ETAPE 7 : STATISTIQUES - # ============================================================ logger.info("[ETAPE 7] STATISTIQUES") stat01 = client_data.get("statistique01") or client_data.get("secteur") @@ -5629,9 +5190,6 @@ class SageConnector: if val: try_set_attribute(client, f"CT_Statistique{i:02d}", clean_str(val, 21)) - # ============================================================ - # ETAPE 8 : COMMERCIAL - # ============================================================ logger.info("[ETAPE 8] COMMERCIAL") if client_data.get("encours_autorise"): @@ -5643,7 +5201,6 @@ class SageConnector: if client_data.get("langue") is not None: try_set_attribute(client, "CT_Langue", safe_int(client_data["langue"])) - # Collaborateur if client_data.get("commercial_code") is not None: co_no = safe_int(client_data["commercial_code"]) if not try_set_attribute(client, "CO_No", co_no): @@ -5658,9 +5215,6 @@ class SageConnector: except Exception as e: logger.debug(f" Collaborateur echec: {e}") - # ============================================================ - # ETAPE 9 : FACTURATION - # ============================================================ logger.info("[ETAPE 9] FACTURATION") try_set_attribute(client, "CT_Lettrage", 1 if client_data.get("lettrage_auto", True) else 0) @@ -5683,9 +5237,6 @@ class SageConnector: if client_data.get(key) is not None: try_set_attribute(client, attr, safe_int(client_data[key])) - # ============================================================ - # ETAPE 10 : LOGISTIQUE - # ============================================================ logger.info("[ETAPE 10] LOGISTIQUE") logistique_map = { @@ -5698,26 +5249,16 @@ class SageConnector: if client_data.get(key) is not None: try_set_attribute(client, attr, safe_int(client_data[key])) - # ============================================================ - # ETAPE 11 : COMMENTAIRE - # ============================================================ if client_data.get("commentaire"): try_set_attribute(client, "CT_Commentaire", clean_str(client_data["commentaire"], 35)) - # ============================================================ - # ETAPE 12 : ANALYTIQUE - # ============================================================ logger.info("[ETAPE 12] ANALYTIQUE") if client_data.get("section_analytique"): try_set_attribute(client, "CA_Num", clean_str(client_data["section_analytique"], 13)) - # ============================================================ - # ETAPE 13 : ORGANISATION - # ============================================================ logger.info("[ETAPE 13] ORGANISATION") - # Mode reglement if client_data.get("mode_reglement_code") is not None: mr_no = safe_int(client_data["mode_reglement_code"]) if not try_set_attribute(client, "MR_No", mr_no): @@ -5732,7 +5273,6 @@ class SageConnector: except Exception as e: logger.debug(f" ModeRegl echec: {e}") - # CT_Surveillance - DOIT être défini en PREMIER (0 ou 1) if client_data.get("surveillance_active") is not None: surveillance = 1 if client_data["surveillance_active"] else 0 try: @@ -5741,7 +5281,6 @@ class SageConnector: except Exception as e: logger.warning(f" CT_Surveillance [ECHEC: {e}]") - # CT_Coface - Code Coface (25 caractères max) if client_data.get("coface"): coface = clean_str(client_data["coface"], 25) try: @@ -5750,7 +5289,6 @@ class SageConnector: except Exception as e: logger.warning(f" CT_Coface [ECHEC: {e}]") - # Autres champs de surveillance if client_data.get("forme_juridique"): try_set_attribute(client, "CT_SvFormeJuri", clean_str(client_data["forme_juridique"], 33)) @@ -5773,9 +5311,6 @@ class SageConnector: if client_data.get("sv_resultat"): try_set_attribute(client, "CT_SvResultat", safe_float(client_data["sv_resultat"])) - # ============================================================ - # DIAGNOSTIC PRE-WRITE - # ============================================================ logger.info("=" * 80) logger.info("[DIAGNOSTIC PRE-WRITE]") @@ -5803,9 +5338,6 @@ class SageConnector: logger.info("=" * 80) - # ============================================================ - # WRITE - # ============================================================ logger.info("[WRITE]") try: @@ -5859,9 +5391,6 @@ class SageConnector: logger.info(f"[MODIFICATION CLIENT SAGE - {code}]") logger.info("=" * 80) - # ============================================================ - # UTILITAIRES (identiques à creer_client) - # ============================================================ def clean_str(value, max_len: int) -> str: if value is None or str(value).lower() in ('none', 'null', ''): return "" @@ -5903,9 +5432,6 @@ class SageConnector: champs_modifies = [] - # ============================================================ - # ÉTAPE 1 : CHARGER LE CLIENT EXISTANT - # ============================================================ logger.info("[ETAPE 1] CHARGEMENT CLIENT") factory_client = self.cial.CptaApplication.FactoryClient @@ -5919,9 +5445,6 @@ class SageConnector: logger.info(f" Client chargé: {getattr(client, 'CT_Intitule', '')}") - # ============================================================ - # ÉTAPE 2 : IDENTIFICATION - # ============================================================ logger.info("[ETAPE 2] IDENTIFICATION") if "intitule" in client_data: @@ -5942,7 +5465,6 @@ class SageConnector: if "raccourci" in client_data: raccourci = clean_str(client_data["raccourci"], 7).upper() - # Vérifier unicité try: exist_client = factory_client.ReadRaccourci(raccourci) if exist_client and exist_client.CT_Num != code: @@ -5966,20 +5488,15 @@ class SageConnector: if try_set_attribute(client, "CT_Ape", clean_str(client_data["code_naf"], 7)): champs_modifies.append("code_naf") - # ============================================================ - # ÉTAPE 3 : ADRESSE - # ============================================================ adresse_keys = ["contact", "adresse", "complement", "code_postal", "ville", "region", "pays"] if any(k in client_data for k in adresse_keys): logger.info("[ETAPE 3] ADRESSE") try: - # CT_Contact - DOUBLE AFFECTATION if "contact" in client_data: contact_nom = clean_str(client_data["contact"], 35) - # Sur l'objet client try: client.CT_Contact = contact_nom champs_modifies.append("contact (client)") @@ -5987,7 +5504,6 @@ class SageConnector: except Exception as e: logger.warning(f" CT_Contact (client) [ECHEC: {e}]") - # Via l'objet Adresse try: adresse_obj = client.Adresse adresse_obj.Contact = contact_nom @@ -5996,7 +5512,6 @@ class SageConnector: except Exception as e: logger.warning(f" Contact (adresse) [ECHEC: {e}]") - # Autres champs adresse adresse_obj = client.Adresse if "adresse" in client_data: @@ -6028,9 +5543,6 @@ class SageConnector: except Exception as e: logger.error(f" Adresse erreur: {e}") - # ============================================================ - # ÉTAPE 4 : TELECOM - # ============================================================ telecom_keys = ["telephone", "telecopie", "email", "site_web", "portable", "facebook", "linkedin"] if any(k in client_data for k in telecom_keys): @@ -6077,9 +5589,6 @@ class SageConnector: except Exception as e: logger.error(f" Telecom erreur: {e}") - # ============================================================ - # ÉTAPE 5 : COMPTE GENERAL - # ============================================================ if "compte_general" in client_data: logger.info("[ETAPE 5] COMPTE GENERAL") @@ -6102,9 +5611,6 @@ class SageConnector: except Exception as e: logger.warning(f" CompteGPrinc erreur: {e}") - # ============================================================ - # ÉTAPE 6 : CATEGORIES - # ============================================================ if "categorie_tarifaire" in client_data or "categorie_comptable" in client_data: logger.info("[ETAPE 6] CATEGORIES") @@ -6138,9 +5644,6 @@ class SageConnector: except Exception as e: logger.warning(f" CatCompta erreur: {e}") - # ============================================================ - # ÉTAPE 7 : TAUX - # ============================================================ taux_modifies = False for i in range(1, 5): key = f"taux{i:02d}" @@ -6153,9 +5656,6 @@ class SageConnector: if try_set_attribute(client, f"CT_Taux{i:02d}", val): champs_modifies.append(key) - # ============================================================ - # ÉTAPE 8 : STATISTIQUES - # ============================================================ stat_keys = ["statistique01", "secteur"] + [f"statistique{i:02d}" for i in range(2, 11)] stat_modifies = False @@ -6178,9 +5678,6 @@ class SageConnector: if try_set_attribute(client, f"CT_Statistique{i:02d}", clean_str(client_data[key], 21)): champs_modifies.append(key) - # ============================================================ - # ÉTAPE 9 : COMMERCIAL - # ============================================================ commercial_keys = ["encours_autorise", "assurance_credit", "langue", "commercial_code"] if any(k in client_data for k in commercial_keys): @@ -6213,9 +5710,6 @@ class SageConnector: except Exception as e: logger.warning(f" Collaborateur erreur: {e}") - # ============================================================ - # ÉTAPE 10 : FACTURATION - # ============================================================ facturation_keys = [ "lettrage_auto", "est_actif", "type_facture", "est_prospect", "bl_en_facture", "saut_page", "validation_echeance", "controle_encours", @@ -6256,9 +5750,6 @@ class SageConnector: if try_set_attribute(client, attr, safe_int(client_data[key])): champs_modifies.append(key) - # ============================================================ - # ÉTAPE 11 : LOGISTIQUE - # ============================================================ logistique_keys = ["priorite_livraison", "livraison_partielle", "delai_transport", "delai_appro"] if any(k in client_data for k in logistique_keys): @@ -6276,25 +5767,16 @@ class SageConnector: if try_set_attribute(client, attr, safe_int(client_data[key])): champs_modifies.append(key) - # ============================================================ - # ÉTAPE 12 : COMMENTAIRE - # ============================================================ if "commentaire" in client_data: logger.info("[ETAPE 12] COMMENTAIRE") if try_set_attribute(client, "CT_Commentaire", clean_str(client_data["commentaire"], 35)): champs_modifies.append("commentaire") - # ============================================================ - # ÉTAPE 13 : ANALYTIQUE - # ============================================================ if "section_analytique" in client_data: logger.info("[ETAPE 13] ANALYTIQUE") if try_set_attribute(client, "CA_Num", clean_str(client_data["section_analytique"], 13)): champs_modifies.append("section_analytique") - # ============================================================ - # ÉTAPE 14 : ORGANISATION & SURVEILLANCE - # ============================================================ organisation_keys = [ "mode_reglement_code", "surveillance_active", "coface", "forme_juridique", "effectif", "sv_regularite", "sv_cotation", @@ -6304,7 +5786,6 @@ class SageConnector: if any(k in client_data for k in organisation_keys): logger.info("[ETAPE 14] ORGANISATION & SURVEILLANCE") - # Mode règlement if "mode_reglement_code" in client_data: mr_no = safe_int(client_data["mode_reglement_code"]) if not try_set_attribute(client, "MR_No", mr_no): @@ -6320,7 +5801,6 @@ class SageConnector: except Exception as e: logger.warning(f" ModeRegl erreur: {e}") - # Surveillance - DOIT être défini AVANT Coface if "surveillance_active" in client_data: surveillance = 1 if client_data["surveillance_active"] else 0 try: @@ -6330,7 +5810,6 @@ class SageConnector: except Exception as e: logger.warning(f" CT_Surveillance [ECHEC: {e}]") - # Coface if "coface" in client_data: coface = clean_str(client_data["coface"], 25) try: @@ -6340,7 +5819,6 @@ class SageConnector: except Exception as e: logger.warning(f" CT_Coface [ECHEC: {e}]") - # Autres champs surveillance if "forme_juridique" in client_data: if try_set_attribute(client, "CT_SvFormeJuri", clean_str(client_data["forme_juridique"], 33)): champs_modifies.append("forme_juridique") @@ -6370,9 +5848,6 @@ class SageConnector: if try_set_attribute(client, "CT_SvResultat", safe_float(client_data["sv_resultat"])): champs_modifies.append("sv_resultat") - # ============================================================ - # VALIDATION ET WRITE - # ============================================================ if not champs_modifies: logger.warning("Aucun champ à modifier") return self._extraire_client(client) @@ -6442,7 +5917,6 @@ class SageConnector: pass try: - # ===== CRÉATION DOCUMENT COMMANDE (type 10) ===== process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_BON_COMMANDE ) @@ -6455,7 +5929,6 @@ class SageConnector: logger.info(" Document commande créé") - # ===== DATES ===== doc.DO_Date = pywintypes.Time( self.normaliser_date(commande_data.get("date_commande")) ) @@ -6468,7 +5941,6 @@ class SageConnector: f" Date livraison: {commande_data['date_livraison']}" ) - # ===== CLIENT (CRITIQUE) ===== factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( commande_data["client"]["code"] @@ -6487,7 +5959,6 @@ class SageConnector: doc.Write() logger.info(f" Client {commande_data['client']['code']} associé") - # ===== RÉFÉRENCE EXTERNE (optionnelle) ===== if commande_data.get("reference"): try: doc.DO_Ref = commande_data["reference"] @@ -6495,7 +5966,6 @@ class SageConnector: except: pass - # ===== LIGNES ===== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -6510,7 +5980,6 @@ class SageConnector: f"--- Ligne {idx}: {ligne_data['article_code']} ---" ) - # 📍 ÉTAPE 1: Charger l'article RÉEL depuis Sage persist_article = factory_article.ReadReference( ligne_data["article_code"] ) @@ -6525,18 +5994,15 @@ class SageConnector: ) article_obj.Read() - # ÉTAPE 2: Récupérer le prix de vente RÉEL prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") logger.info(f" Prix Sage: {prix_sage}€") - # TOLÉRER prix = 0 (articles de service, etc.) if prix_sage == 0: logger.warning( f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" ) - # 📍 ÉTAPE 3: Créer la ligne ligne_persist = factory_lignes.Create() try: @@ -6548,7 +6014,6 @@ class SageConnector: ligne_persist, "IBODocumentVenteLigne3" ) - # SetDefaultArticleReference quantite = float(ligne_data["quantite"]) try: @@ -6574,31 +6039,24 @@ class SageConnector: ligne_obj.DL_Qte = quantite logger.warning("Configuration manuelle appliquée") - # ÉTAPE 4: Vérifier le prix automatique prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) logger.info(f" Prix auto chargé: {prix_auto}€") - # 💵 ÉTAPE 5: Ajuster le prix si nécessaire prix_a_utiliser = ligne_data.get("prix_unitaire_ht") if prix_a_utiliser is not None and prix_a_utiliser > 0: - # Prix personnalisé fourni ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) logger.info(f" Prix personnalisé: {prix_a_utiliser}€") elif prix_auto == 0 and prix_sage > 0: - # Pas de prix auto mais prix Sage existe ligne_obj.DL_PrixUnitaire = float(prix_sage) logger.info(f" Prix Sage forcé: {prix_sage}€") elif prix_auto > 0: - # Prix auto OK logger.info(f" Prix auto conservé: {prix_auto}€") - # SINON: Prix reste à 0 (toléré pour services, etc.) prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) montant_ligne = quantite * prix_final logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") - # Remise remise = ligne_data.get("remise_pourcentage", 0) if remise > 0: try: @@ -6613,11 +6071,9 @@ class SageConnector: except Exception as e: logger.warning(f"Remise non appliquée: {e}") - # ÉTAPE 6: Écrire la ligne ligne_obj.Write() logger.info(f" Ligne {idx} écrite") - # VÉRIFICATION try: ligne_obj.Read() prix_enregistre = float( @@ -6632,14 +6088,12 @@ class SageConnector: except Exception as e: logger.warning(f"Impossible de vérifier: {e}") - # ===== VALIDATION ===== doc.Write() process.Process() if transaction_active: self.cial.CptaApplication.CommitTrans() - # ===== RÉCUPÉRATION NUMÉRO ===== time.sleep(2) numero_commande = None @@ -6657,7 +6111,6 @@ class SageConnector: if not numero_commande: numero_commande = getattr(doc, "DO_Piece", "") - # ===== RELECTURE ===== factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece( settings.SAGE_TYPE_BON_COMMANDE, numero_commande @@ -6726,15 +6179,11 @@ class SageConnector: with self._com_context(), self._lock_com: logger.info(f" === MODIFICATION COMMANDE {numero} ===") - # ======================================== - # ÉTAPE 1 : CHARGER LE DOCUMENT - # ======================================== logger.info(" Chargement document...") factory = self.cial.FactoryDocumentVente persist = None - # Chercher le document for type_test in [10, 3, settings.SAGE_TYPE_BON_COMMANDE]: try: persist_test = factory.ReadPiece(type_test, numero) @@ -6756,9 +6205,6 @@ class SageConnector: logger.info(f" Type={type_reel}, Statut={statut_actuel}") - # ======================================== - # ÉTAPE 2 : VÉRIFIER CLIENT INITIAL - # ======================================== client_code_initial = "" try: client_obj = getattr(doc, "Client", None) @@ -6774,7 +6220,6 @@ class SageConnector: if not client_code_initial: raise ValueError(" Client introuvable dans le document") - # Compter les lignes initiales nb_lignes_initial = 0 try: factory_lignes = getattr( @@ -6795,9 +6240,6 @@ class SageConnector: except Exception as e: logger.warning(f" Erreur comptage lignes: {e}") - # ======================================== - # ÉTAPE 3 : DÉTERMINER LES MODIFICATIONS - # ======================================== champs_modifies = [] modif_date = "date_commande" in commande_data @@ -6815,7 +6257,6 @@ class SageConnector: logger.info(f" Référence: {modif_ref}") logger.info(f" Lignes: {modif_lignes}") - # ===== EXTRAIRE référence et statut pour les traiter à la fin si lignes modifiées ===== commande_data_temp = commande_data.copy() reference_a_modifier = None statut_a_modifier = None @@ -6835,9 +6276,6 @@ class SageConnector: ) modif_statut = False - # ======================================== - # ÉTAPE 4 : TEST WRITE() BASIQUE - # ======================================== logger.info(" Test Write() basique (sans modification)...") try: @@ -6845,7 +6283,6 @@ class SageConnector: 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() @@ -6866,9 +6303,6 @@ class SageConnector: f"Document verrouillé, impossible de modifier: {e}" ) - # ======================================== - # ÉTAPE 5 : MODIFICATIONS SIMPLES (pas de lignes) - # ======================================== if not modif_lignes and ( modif_date or modif_date_livraison @@ -6914,7 +6348,6 @@ class SageConnector: 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() @@ -6922,7 +6355,6 @@ class SageConnector: doc.Read() - # Vérifier client client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() @@ -6946,13 +6378,9 @@ class SageConnector: 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( @@ -6983,15 +6411,11 @@ class SageConnector: 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) @@ -7001,20 +6425,15 @@ class SageConnector: ) 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): @@ -7022,7 +6441,6 @@ class SageConnector: f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---" ) - # Charger l'article persist_article = factory_article.ReadReference( ligne_data["article_code"] ) @@ -7036,7 +6454,6 @@ class SageConnector: ) article_obj.Read() - # Créer nouvelle ligne ligne_persist = factory_lignes.Create() try: @@ -7050,7 +6467,6 @@ class SageConnector: quantite = float(ligne_data["quantite"]) - # Associer article try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite @@ -7062,13 +6478,11 @@ class SageConnector: 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( @@ -7078,13 +6492,11 @@ class SageConnector: 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") @@ -7095,7 +6507,6 @@ class SageConnector: doc.Read() - # Vérifier client client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() @@ -7106,9 +6517,6 @@ class SageConnector: 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", "") @@ -7132,9 +6540,6 @@ class SageConnector: 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) @@ -7157,9 +6562,6 @@ class SageConnector: except Exception as e: logger.warning(f"Impossible de modifier le statut: {e}") - # ======================================== - # ÉTAPE 7 : RELECTURE ET RETOUR - # ======================================== logger.info(" Relecture finale...") import time @@ -7168,7 +6570,6 @@ class SageConnector: doc.Read() - # Vérifier client final client_obj_final = getattr(doc, "Client", None) if client_obj_final: client_obj_final.Read() @@ -7247,7 +6648,6 @@ class SageConnector: pass try: - # ===== CRÉATION DOCUMENT LIVRAISON (type 30) ===== process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_BON_LIVRAISON ) @@ -7260,13 +6660,10 @@ class SageConnector: logger.info(" Document livraison créé") - # ===== DATES ===== - # Date du document (DO_Date) doc.DO_Date = pywintypes.Time( self.normaliser_date(livraison_data.get("date_livraison")) ) - # Date de livraison prévue chez le client (DO_DateLivr) if ( "date_livraison_prevue" in livraison_data and livraison_data["date_livraison_prevue"] @@ -7280,7 +6677,6 @@ class SageConnector: f" Date livraison prévue: {livraison_data['date_livraison_prevue']}" ) - # ===== CLIENT (CRITIQUE) ===== factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( livraison_data["client"]["code"] @@ -7299,7 +6695,6 @@ class SageConnector: doc.Write() logger.info(f" Client {livraison_data['client']['code']} associé") - # ===== RÉFÉRENCE EXTERNE (optionnelle) ===== if livraison_data.get("reference"): try: doc.DO_Ref = livraison_data["reference"] @@ -7307,7 +6702,6 @@ class SageConnector: except: pass - # ===== LIGNES ===== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -7324,7 +6718,6 @@ class SageConnector: f"--- Ligne {idx}: {ligne_data['article_code']} ---" ) - # Charger l'article RÉEL depuis Sage persist_article = factory_article.ReadReference( ligne_data["article_code"] ) @@ -7339,7 +6732,6 @@ class SageConnector: ) article_obj.Read() - # Récupérer le prix de vente RÉEL prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") logger.info(f" Prix Sage: {prix_sage}€") @@ -7349,7 +6741,6 @@ class SageConnector: f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" ) - # Créer la ligne ligne_persist = factory_lignes.Create() try: @@ -7386,11 +6777,9 @@ class SageConnector: ligne_obj.DL_Qte = quantite logger.warning("Configuration manuelle appliquée") - # Vérifier le prix automatique prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) logger.info(f" Prix auto chargé: {prix_auto}€") - # Ajuster le prix si nécessaire prix_a_utiliser = ligne_data.get("prix_unitaire_ht") if prix_a_utiliser is not None and prix_a_utiliser > 0: @@ -7406,7 +6795,6 @@ class SageConnector: montant_ligne = quantite * prix_final logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") - # Remise remise = ligne_data.get("remise_pourcentage", 0) if remise > 0: try: @@ -7421,18 +6809,15 @@ class SageConnector: except Exception as e: logger.warning(f"Remise non appliquée: {e}") - # Écrire la ligne ligne_obj.Write() logger.info(f" Ligne {idx} écrite") - # ===== VALIDATION ===== doc.Write() process.Process() if transaction_active: self.cial.CptaApplication.CommitTrans() - # ===== RÉCUPÉRATION NUMÉRO ===== time.sleep(2) numero_livraison = None @@ -7450,7 +6835,6 @@ class SageConnector: if not numero_livraison: numero_livraison = getattr(doc, "DO_Piece", "") - # ===== RELECTURE ===== factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece( settings.SAGE_TYPE_BON_LIVRAISON, numero_livraison @@ -7466,7 +6850,6 @@ class SageConnector: total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) reference_finale = getattr(doc_final, "DO_Ref", "") - # Dates date_livraison_prevue_final = None try: @@ -7526,15 +6909,11 @@ class SageConnector: with self._com_context(), self._lock_com: logger.info(f" === MODIFICATION LIVRAISON {numero} ===") - # ======================================== - # ÉTAPE 1 : CHARGER LE DOCUMENT - # ======================================== logger.info(" Chargement document...") factory = self.cial.FactoryDocumentVente persist = None - # Chercher le document for type_test in [30, settings.SAGE_TYPE_BON_LIVRAISON]: try: persist_test = factory.ReadPiece(type_test, numero) @@ -7555,14 +6934,12 @@ class SageConnector: 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( @@ -7583,9 +6960,6 @@ class SageConnector: except Exception as e: logger.warning(f" Erreur comptage lignes: {e}") - # ======================================== - # ÉTAPE 2 : DÉTERMINER LES MODIFICATIONS - # ======================================== champs_modifies = [] modif_date = "date_livraison" in livraison_data @@ -7603,7 +6977,6 @@ class SageConnector: logger.info(f" Référence: {modif_ref}") logger.info(f" Lignes: {modif_lignes}") - # ===== EXTRAIRE référence et statut pour les traiter à la fin si lignes modifiées ===== livraison_data_temp = livraison_data.copy() reference_a_modifier = None statut_a_modifier = None @@ -7623,9 +6996,6 @@ class SageConnector: ) modif_statut = False - # ======================================== - # ÉTAPE 3 : MODIFICATIONS SIMPLES (pas de lignes) - # ======================================== if not modif_lignes and ( modif_date or modif_date_livraison_prevue @@ -7677,13 +7047,9 @@ class SageConnector: 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( @@ -7716,13 +7082,9 @@ class SageConnector: 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) @@ -7737,13 +7099,9 @@ class SageConnector: 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): @@ -7751,7 +7109,6 @@ class SageConnector: f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---" ) - # Charger l'article persist_article = factory_article.ReadReference( ligne_data["article_code"] ) @@ -7765,7 +7122,6 @@ class SageConnector: ) article_obj.Read() - # Créer nouvelle ligne ligne_persist = factory_lignes.Create() try: @@ -7779,7 +7135,6 @@ class SageConnector: quantite = float(ligne_data["quantite"]) - # Associer article try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite @@ -7791,13 +7146,11 @@ class SageConnector: 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( @@ -7807,13 +7160,11 @@ class SageConnector: 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") @@ -7826,9 +7177,6 @@ class SageConnector: 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", "") @@ -7854,9 +7202,6 @@ class SageConnector: except Exception as e: logger.warning(f"Impossible de modifier la référence: {e}") - # ======================================== - # ÉTAPE 4.6 : MODIFIER STATUT (EN DERNIER) - # ======================================== if statut_a_modifier is not None: try: statut_actuel = getattr(doc, "DO_Statut", 0) @@ -7881,9 +7226,6 @@ class SageConnector: except Exception as e: logger.warning(f"Impossible de modifier le statut: {e}") - # ======================================== - # ÉTAPE 5 : RELECTURE ET RETOUR - # ======================================== logger.info(" Relecture finale...") import time @@ -7897,7 +7239,6 @@ class SageConnector: reference_finale = getattr(doc, "DO_Ref", "") statut_final = getattr(doc, "DO_Statut", 0) - # Extraire les dates date_livraison_prevue_final = None try: @@ -7967,7 +7308,6 @@ class SageConnector: pass try: - # ===== CRÉATION DOCUMENT AVOIR (type 50) ===== process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_BON_AVOIR ) @@ -7980,7 +7320,6 @@ class SageConnector: logger.info(" Document avoir créé") - # ===== DATES ===== doc.DO_Date = pywintypes.Time( self.normaliser_date(avoir_data.get("date_avoir")) ) @@ -7993,7 +7332,6 @@ class SageConnector: f" Date livraison: {avoir_data['date_livraison']}" ) - # ===== CLIENT (CRITIQUE) ===== factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( avoir_data["client"]["code"] @@ -8012,7 +7350,6 @@ class SageConnector: doc.Write() logger.info(f" Client {avoir_data['client']['code']} associé") - # ===== RÉFÉRENCE EXTERNE (optionnelle) ===== if avoir_data.get("reference"): try: doc.DO_Ref = avoir_data["reference"] @@ -8020,7 +7357,6 @@ class SageConnector: except: pass - # ===== LIGNES ===== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -8035,7 +7371,6 @@ class SageConnector: f"--- Ligne {idx}: {ligne_data['article_code']} ---" ) - # Charger l'article RÉEL depuis Sage persist_article = factory_article.ReadReference( ligne_data["article_code"] ) @@ -8050,7 +7385,6 @@ class SageConnector: ) article_obj.Read() - # Récupérer le prix de vente RÉEL prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") logger.info(f" Prix Sage: {prix_sage}€") @@ -8060,7 +7394,6 @@ class SageConnector: f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" ) - # Créer la ligne ligne_persist = factory_lignes.Create() try: @@ -8097,11 +7430,9 @@ class SageConnector: ligne_obj.DL_Qte = quantite logger.warning("Configuration manuelle appliquée") - # Vérifier le prix automatique prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) logger.info(f" Prix auto chargé: {prix_auto}€") - # Ajuster le prix si nécessaire prix_a_utiliser = ligne_data.get("prix_unitaire_ht") if prix_a_utiliser is not None and prix_a_utiliser > 0: @@ -8117,7 +7448,6 @@ class SageConnector: montant_ligne = quantite * prix_final logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") - # Remise remise = ligne_data.get("remise_pourcentage", 0) if remise > 0: try: @@ -8132,18 +7462,15 @@ class SageConnector: except Exception as e: logger.warning(f"Remise non appliquée: {e}") - # Écrire la ligne ligne_obj.Write() logger.info(f" Ligne {idx} écrite") - # ===== VALIDATION ===== doc.Write() process.Process() if transaction_active: self.cial.CptaApplication.CommitTrans() - # ===== RÉCUPÉRATION NUMÉRO ===== time.sleep(2) numero_avoir = None @@ -8161,7 +7488,6 @@ class SageConnector: if not numero_avoir: numero_avoir = getattr(doc, "DO_Piece", "") - # ===== RELECTURE ===== factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece( settings.SAGE_TYPE_BON_AVOIR, numero_avoir @@ -8177,7 +7503,6 @@ class SageConnector: total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) reference_finale = getattr(doc_final, "DO_Ref", "") - # Dates date_livraison_final = None try: @@ -8232,15 +7557,11 @@ class SageConnector: with self._com_context(), self._lock_com: logger.info(f" === MODIFICATION AVOIR {numero} ===") - # ======================================== - # ÉTAPE 1 : CHARGER LE DOCUMENT - # ======================================== logger.info(" Chargement document...") factory = self.cial.FactoryDocumentVente persist = None - # Chercher le document for type_test in [50, settings.SAGE_TYPE_BON_AVOIR]: try: persist_test = factory.ReadPiece(type_test, numero) @@ -8262,16 +7583,12 @@ class SageConnector: 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) @@ -8287,7 +7604,6 @@ class SageConnector: if not client_code_initial: raise ValueError(" Client introuvable dans le document") - # Compter les lignes initiales nb_lignes_initial = 0 try: factory_lignes = getattr( @@ -8308,9 +7624,6 @@ class SageConnector: 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 @@ -8328,7 +7641,6 @@ class SageConnector: logger.info(f" Référence: {modif_ref}") logger.info(f" Lignes: {modif_lignes}") - # ===== EXTRAIRE référence et statut pour les traiter à la fin si lignes modifiées ===== avoir_data_temp = avoir_data.copy() reference_a_modifier = None statut_a_modifier = None @@ -8348,9 +7660,6 @@ class SageConnector: ) modif_statut = False - # ======================================== - # ÉTAPE 4 : TEST WRITE() BASIQUE - # ======================================== logger.info(" Test Write() basique (sans modification)...") try: @@ -8358,7 +7667,6 @@ class SageConnector: 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() @@ -8379,9 +7687,6 @@ class SageConnector: f"Document verrouillé, impossible de modifier: {e}" ) - # ======================================== - # ÉTAPE 5 : MODIFICATIONS SIMPLES (pas de lignes) - # ======================================== if not modif_lignes and ( modif_date or modif_date_livraison @@ -8425,7 +7730,6 @@ class SageConnector: 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() @@ -8433,7 +7737,6 @@ class SageConnector: doc.Read() - # Vérifier client client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() @@ -8457,13 +7760,9 @@ class SageConnector: 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")) @@ -8492,15 +7791,11 @@ class SageConnector: 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) @@ -8510,20 +7805,15 @@ class SageConnector: ) 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): @@ -8531,7 +7821,6 @@ class SageConnector: f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---" ) - # Charger l'article persist_article = factory_article.ReadReference( ligne_data["article_code"] ) @@ -8545,7 +7834,6 @@ class SageConnector: ) article_obj.Read() - # Créer nouvelle ligne ligne_persist = factory_lignes.Create() try: @@ -8559,7 +7847,6 @@ class SageConnector: quantite = float(ligne_data["quantite"]) - # Associer article try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite @@ -8571,13 +7858,11 @@ class SageConnector: 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( @@ -8587,13 +7872,11 @@ class SageConnector: 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") @@ -8604,7 +7887,6 @@ class SageConnector: doc.Read() - # Vérifier client client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() @@ -8615,9 +7897,6 @@ class SageConnector: 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", "") @@ -8643,9 +7922,6 @@ class SageConnector: 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) @@ -8670,9 +7946,6 @@ class SageConnector: except Exception as e: logger.warning(f"Impossible de modifier le statut: {e}") - # ======================================== - # ÉTAPE 7 : RELECTURE ET RETOUR - # ======================================== logger.info(" Relecture finale...") import time @@ -8681,7 +7954,6 @@ class SageConnector: doc.Read() - # Vérifier client final client_obj_final = getattr(doc, "Client", None) if client_obj_final: client_obj_final.Read() @@ -8694,7 +7966,6 @@ class SageConnector: reference_finale = getattr(doc, "DO_Ref", "") statut_final = getattr(doc, "DO_Statut", 0) - # Extraire les dates date_livraison_final = None try: @@ -8764,7 +8035,6 @@ class SageConnector: pass try: - # ===== CRÉATION DOCUMENT FACTURE (type 60) ===== process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_FACTURE ) @@ -8777,7 +8047,6 @@ class SageConnector: logger.info(" Document facture créé") - # ===== DATES ===== doc.DO_Date = pywintypes.Time( self.normaliser_date(facture_data.get("date_facture")) ) @@ -8793,7 +8062,6 @@ class SageConnector: f" Date livraison: {facture_data['date_livraison']}" ) - # ===== CLIENT (CRITIQUE) ===== factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( facture_data["client"]["code"] @@ -8812,7 +8080,6 @@ class SageConnector: doc.Write() logger.info(f" Client {facture_data['client']['code']} associé") - # ===== RÉFÉRENCE EXTERNE (optionnelle) ===== if facture_data.get("reference"): try: doc.DO_Ref = facture_data["reference"] @@ -8820,13 +8087,10 @@ class SageConnector: except: pass - # ===== CHAMPS SPÉCIFIQUES FACTURES ===== logger.info(" Configuration champs spécifiques factures...") - # Code journal (si disponible) try: if hasattr(doc, "DO_CodeJournal"): - # Essayer de récupérer le code journal par défaut try: param_societe = ( self.cial.CptaApplication.ParametreSociete @@ -8842,7 +8106,6 @@ class SageConnector: except Exception as e: logger.debug(f" Code journal: {e}") - # Souche (si disponible) try: if hasattr(doc, "DO_Souche"): doc.DO_Souche = 0 @@ -8850,7 +8113,6 @@ class SageConnector: except: pass - # Régime (si disponible) try: if hasattr(doc, "DO_Regime"): doc.DO_Regime = 0 @@ -8858,7 +8120,6 @@ class SageConnector: except: pass - # ===== LIGNES ===== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -8873,7 +8134,6 @@ class SageConnector: f"--- Ligne {idx}: {ligne_data['article_code']} ---" ) - # Charger l'article RÉEL depuis Sage persist_article = factory_article.ReadReference( ligne_data["article_code"] ) @@ -8888,7 +8148,6 @@ class SageConnector: ) article_obj.Read() - # Récupérer le prix de vente RÉEL prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") logger.info(f" Prix Sage: {prix_sage}€") @@ -8898,7 +8157,6 @@ class SageConnector: f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" ) - # Créer la ligne ligne_persist = factory_lignes.Create() try: @@ -8935,11 +8193,9 @@ class SageConnector: ligne_obj.DL_Qte = quantite logger.warning("Configuration manuelle appliquée") - # Vérifier le prix automatique prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) logger.info(f" Prix auto chargé: {prix_auto}€") - # Ajuster le prix si nécessaire prix_a_utiliser = ligne_data.get("prix_unitaire_ht") if prix_a_utiliser is not None and prix_a_utiliser > 0: @@ -8955,7 +8211,6 @@ class SageConnector: montant_ligne = quantite * prix_final logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") - # Remise remise = ligne_data.get("remise_pourcentage", 0) if remise > 0: try: @@ -8970,14 +8225,11 @@ class SageConnector: except Exception as e: logger.warning(f"Remise non appliquée: {e}") - # Écrire la ligne ligne_obj.Write() logger.info(f" Ligne {idx} écrite") - # ===== VALIDATION FINALE ===== logger.info(" Validation facture...") - # Réassocier le client avant validation (critique pour factures) try: doc.SetClient(client_obj) logger.debug(" Client réassocié avant validation") @@ -8996,7 +8248,6 @@ class SageConnector: self.cial.CptaApplication.CommitTrans() logger.info(" Transaction committée") - # ===== RÉCUPÉRATION NUMÉRO ===== time.sleep(2) numero_facture = None @@ -9019,7 +8270,6 @@ class SageConnector: logger.info(f" Numéro facture: {numero_facture}") - # ===== RELECTURE ===== factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece( settings.SAGE_TYPE_FACTURE, numero_facture @@ -9035,7 +8285,6 @@ class SageConnector: total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) reference_finale = getattr(doc_final, "DO_Ref", "") - # Dates date_livraison_final = None try: @@ -9091,15 +8340,11 @@ class SageConnector: with self._com_context(), self._lock_com: logger.info(f" === MODIFICATION FACTURE {numero} ===") - # ======================================== - # ÉTAPE 1 : CHARGER LE DOCUMENT - # ======================================== logger.info(" Chargement document...") factory = self.cial.FactoryDocumentVente persist = None - # Chercher le document for type_test in [60, 5, settings.SAGE_TYPE_FACTURE]: try: persist_test = factory.ReadPiece(type_test, numero) @@ -9121,16 +8366,12 @@ class SageConnector: 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) @@ -9146,7 +8387,6 @@ class SageConnector: if not client_code_initial: raise ValueError(" Client introuvable dans le document") - # Compter les lignes initiales nb_lignes_initial = 0 try: factory_lignes = getattr( @@ -9167,9 +8407,6 @@ class SageConnector: 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 @@ -9187,7 +8424,6 @@ class SageConnector: logger.info(f" Référence: {modif_ref}") logger.info(f" Lignes: {modif_lignes}") - # ===== EXTRAIRE référence et statut pour les traiter à la fin si lignes modifiées ===== facture_data_temp = facture_data.copy() reference_a_modifier = None statut_a_modifier = None @@ -9207,9 +8443,6 @@ class SageConnector: ) modif_statut = False - # ======================================== - # ÉTAPE 4 : TEST WRITE() BASIQUE - # ======================================== logger.info(" Test Write() basique (sans modification)...") try: @@ -9217,7 +8450,6 @@ class SageConnector: 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() @@ -9238,9 +8470,6 @@ class SageConnector: f"Document verrouillé, impossible de modifier: {e}" ) - # ======================================== - # ÉTAPE 5 : MODIFICATIONS SIMPLES (pas de lignes) - # ======================================== if not modif_lignes and ( modif_date or modif_date_livraison @@ -9284,7 +8513,6 @@ class SageConnector: 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() @@ -9292,7 +8520,6 @@ class SageConnector: doc.Read() - # Vérifier client client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() @@ -9316,13 +8543,9 @@ class SageConnector: 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")) @@ -9351,15 +8574,11 @@ class SageConnector: 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) @@ -9369,20 +8588,15 @@ class SageConnector: ) 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): @@ -9390,7 +8604,6 @@ class SageConnector: f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---" ) - # Charger l'article persist_article = factory_article.ReadReference( ligne_data["article_code"] ) @@ -9404,7 +8617,6 @@ class SageConnector: ) article_obj.Read() - # Créer nouvelle ligne ligne_persist = factory_lignes.Create() try: @@ -9418,7 +8630,6 @@ class SageConnector: quantite = float(ligne_data["quantite"]) - # Associer article try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite @@ -9430,13 +8641,11 @@ class SageConnector: 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( @@ -9446,13 +8655,11 @@ class SageConnector: 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") @@ -9463,7 +8670,6 @@ class SageConnector: doc.Read() - # Vérifier client client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() @@ -9474,9 +8680,6 @@ class SageConnector: 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", "") @@ -9502,9 +8705,6 @@ class SageConnector: 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) @@ -9529,9 +8729,6 @@ class SageConnector: except Exception as e: logger.warning(f"Impossible de modifier le statut: {e}") - # ======================================== - # ÉTAPE 7 : RELECTURE ET RETOUR - # ======================================== logger.info(" Relecture finale...") import time @@ -9540,7 +8737,6 @@ class SageConnector: doc.Read() - # Vérifier client final client_obj_final = getattr(doc, "Client", None) if client_obj_final: client_obj_final.Read() @@ -9553,7 +8749,6 @@ class SageConnector: reference_finale = getattr(doc, "DO_Ref", "") statut_final = getattr(doc, "DO_Statut", 0) - # Extraire les dates date_livraison_final = None try: @@ -9609,7 +8804,6 @@ class SageConnector: try: logger.info("[ARTICLE] === CREATION ARTICLE ===") - # Transaction transaction_active = False try: self.cial.CptaApplication.BeginTrans() @@ -9619,9 +8813,6 @@ class SageConnector: logger.debug(f"BeginTrans non disponible : {e}") try: - # ======================================== - # ÉTAPE 0 : DÉCOUVRIR DÉPÔTS - # ======================================== depots_disponibles = [] depot_a_utiliser = None depot_code_demande = article_data.get("depot_code") @@ -9685,9 +8876,6 @@ class SageConnector: f"[DEPOT] Utilisation : '{depot_a_utiliser['code']}' ({depot_a_utiliser['intitule']})" ) - # ======================================== - # ÉTAPE 1 : VALIDATION & NETTOYAGE - # ======================================== reference = article_data.get("reference", "").upper().strip() if not reference: raise ValueError("La référence est obligatoire") @@ -9704,7 +8892,6 @@ class SageConnector: if len(designation) > 69: designation = designation[:69] - # Récupération des STOCKS stock_reel = article_data.get("stock_reel", 0.0) stock_mini = article_data.get("stock_mini", 0.0) stock_maxi = article_data.get("stock_maxi", 0.0) @@ -9715,9 +8902,6 @@ class SageConnector: logger.info(f"[ARTICLE] Stock mini demandé : {stock_mini}") logger.info(f"[ARTICLE] Stock maxi demandé : {stock_maxi}") - # ======================================== - # ÉTAPE 2 : VÉRIFIER SI EXISTE DÉJÀ - # ======================================== factory = self.cial.FactoryArticle try: article_existant = factory.ReadReference(reference) @@ -9737,20 +8921,13 @@ class SageConnector: logger.error(f"[ARTICLE] Erreur vérification : {e}") raise - # ======================================== - # ÉTAPE 3 : CRÉER L'ARTICLE - # ======================================== persist = factory.Create() article = win32com.client.CastTo(persist, "IBOArticle3") article.SetDefault() - # Champs de base article.AR_Ref = reference article.AR_Design = designation - # ======================================== - # ÉTAPE 4 : TROUVER ARTICLE MODÈLE VIA SQL - # ======================================== logger.info("[MODELE] Recherche article modèle via SQL...") article_modele_ref = None @@ -9780,7 +8957,6 @@ class SageConnector: except Exception as e: logger.warning(f" [SQL] Erreur recherche article : {e}") - # Charger l'article modèle via COM if article_modele_ref: try: persist_modele = factory.ReadReference(article_modele_ref) @@ -9804,12 +8980,8 @@ class SageConnector: "Créez au moins un article manuellement dans Sage pour servir de modèle." ) - # ======================================== - # ÉTAPE 5 : COPIER UNITÉ + FAMILLE - # ======================================== logger.info("[OBJETS] Copie Unite + Famille depuis modèle...") - # Unite unite_trouvee = False try: unite_obj = getattr(article_modele, "Unite", None) @@ -9827,7 +8999,6 @@ class SageConnector: "Impossible de copier l'unité de vente depuis le modèle" ) - # Famille famille_trouvee = False famille_code_personnalise = article_data.get("famille") famille_obj = None @@ -9838,7 +9009,6 @@ class SageConnector: ) try: - # Vérifier existence via SQL famille_existe_sql = False famille_code_exact = None @@ -9875,7 +9045,6 @@ class SageConnector: except Exception as e_sql: logger.warning(f" [SQL] Erreur : {e_sql}") - # Charger via COM if famille_existe_sql and famille_code_exact: factory_famille = self.cial.FactoryFamille try: @@ -9936,7 +9105,6 @@ class SageConnector: f" [WARN] Famille personnalisée non chargée : {str(e)[:100]}" ) - # Si pas de famille perso, copier depuis le modèle if not famille_trouvee: try: famille_obj = getattr(article_modele, "Famille", None) @@ -9949,23 +9117,15 @@ class SageConnector: except Exception as e: logger.debug(f" Famille non copiable : {str(e)[:80]}") - # ======================================== - # ÉTAPE 6 : CHAMPS OBLIGATOIRES - # ======================================== logger.info("[CHAMPS] Copie champs obligatoires depuis modèle...") - # Types et natures article.AR_Type = int(getattr(article_modele, "AR_Type", 0)) article.AR_Nature = int(getattr(article_modele, "AR_Nature", 0)) article.AR_Nomencl = int(getattr(article_modele, "AR_Nomencl", 0)) - # Suivi stock (forcé à 2 = FIFO/LIFO) article.AR_SuiviStock = 2 logger.info(f" [OK] AR_SuiviStock=2 (FIFO/LIFO)") - # ======================================== - # ÉTAPE 7 : PRIX - # ======================================== prix_vente = article_data.get("prix_vente") if prix_vente is not None: try: @@ -9990,17 +9150,11 @@ class SageConnector: except Exception as e: logger.warning(f" Prix achat erreur : {str(e)[:100]}") - # ======================================== - # ÉTAPE 8 : CODE EAN - # ======================================== code_ean = article_data.get("code_ean") if code_ean: article.AR_CodeBarre = str(code_ean) logger.info(f" Code EAN/Barre : {code_ean}") - # ======================================== - # ÉTAPE 9 : DESCRIPTION - # ======================================== description = article_data.get("description") if description: try: @@ -10009,9 +9163,6 @@ class SageConnector: except: pass - # ======================================== - # ÉTAPE 10 : ÉCRITURE ARTICLE - # ======================================== logger.info("[ARTICLE] Écriture dans Sage...") try: @@ -10030,13 +9181,9 @@ class SageConnector: logger.error(f" [ERREUR] Write() échoué : {error_detail}") raise RuntimeError(f"Échec création article : {error_detail}") - # ======================================== - # ÉTAPE 11 : DÉFINIR LE STOCK DANS F_ARTSTOCK (CRITIQUE) - # ======================================== stock_defini = False stock_erreur = None - # Vérifier si on a des valeurs de stock à définir has_stock_values = stock_reel or stock_mini or stock_maxi if has_stock_values: @@ -10047,7 +9194,6 @@ class SageConnector: try: depot_obj = depot_a_utiliser["objet"] - # Chercher FactoryArticleStock ou FactoryDepotStock factory_stock = None for factory_name in [ "FactoryArticleStock", @@ -10070,22 +9216,18 @@ class SageConnector: "Factory de stock introuvable sur le dépôt" ) - # Créer l'entrée de stock dans F_ARTSTOCK stock_persist = factory_stock.Create() stock_obj = win32com.client.CastTo( stock_persist, "IBODepotStock3" ) stock_obj.SetDefault() - # Référence article stock_obj.AR_Ref = reference - # Stock réel if stock_reel: stock_obj.AS_QteSto = float(stock_reel) logger.info(f" AS_QteSto = {stock_reel}") - # Stock minimum if stock_mini: try: stock_obj.AS_QteMini = float(stock_mini) @@ -10093,7 +9235,6 @@ class SageConnector: except Exception as e: logger.warning(f" AS_QteMini non défini : {e}") - # Stock maximum if stock_maxi: try: stock_obj.AS_QteMaxi = float(stock_maxi) @@ -10115,9 +9256,6 @@ class SageConnector: exc_info=True, ) - # ======================================== - # ÉTAPE 12 : COMMIT - # ======================================== if transaction_active: try: self.cial.CptaApplication.CommitTrans() @@ -10127,9 +9265,6 @@ class SageConnector: except Exception as e: logger.warning(f"[COMMIT] Erreur commit : {e}") - # ======================================== - # ÉTAPE 13 : VÉRIFICATION & RELECTURE - # ======================================== logger.info("[VERIF] Relecture article créé...") article_cree_persist = factory.ReadReference(reference) @@ -10143,9 +9278,6 @@ class SageConnector: ) article_cree.Read() - # ======================================== - # ÉTAPE 14 : VÉRIFIER LE STOCK DANS F_ARTSTOCK VIA SQL - # ======================================== stocks_par_depot = [] stock_total = 0.0 @@ -10153,7 +9285,6 @@ class SageConnector: with self._get_sql_connection() as conn: cursor = conn.cursor() - # Vérifier si le stock a été créé dans F_ARTSTOCK cursor.execute( """ SELECT @@ -10207,25 +9338,16 @@ class SageConnector: f"[OK] ARTICLE CREE : {reference} - Stock total : {stock_total}" ) - # ======================================== - # ÉTAPE 15 : EXTRACTION COMPLÈTE - # ======================================== logger.info("[EXTRACTION] Extraction complète de l'article créé...") - # Utiliser _extraire_article() pour avoir TOUS les champs resultat = self._extraire_article(article_cree) if not resultat: - # Fallback si extraction échoue resultat = { "reference": reference, "designation": designation, } - # ======================================== - # ÉTAPE 16 : FORCER LES VALEURS DE STOCK DEPUIS F_ARTSTOCK - # ======================================== - # 1. STOCK (forcer les valeurs depuis F_ARTSTOCK) resultat["stock_reel"] = stock_total if stock_mini: @@ -10234,28 +9356,23 @@ class SageConnector: if stock_maxi: resultat["stock_maxi"] = float(stock_maxi) - # Stock disponible = stock réel (article neuf, pas de réservation) resultat["stock_disponible"] = stock_total resultat["stock_reserve"] = 0.0 resultat["stock_commande"] = 0.0 - # 2. PRIX if prix_vente is not None: resultat["prix_vente"] = float(prix_vente) if prix_achat is not None: resultat["prix_achat"] = float(prix_achat) - # 3. DESCRIPTION if description: resultat["description"] = description - # 4. CODE EAN if code_ean: resultat["code_ean"] = str(code_ean) resultat["code_barre"] = str(code_ean) - # 5. FAMILLE if famille_code_personnalise and famille_trouvee: resultat["famille_code"] = famille_code_personnalise try: @@ -10267,7 +9384,6 @@ class SageConnector: except: pass - # 6. INFOS DÉPÔTS if stocks_par_depot: resultat["stocks_par_depot"] = stocks_par_depot resultat["depot_principal"] = { @@ -10275,10 +9391,8 @@ class SageConnector: "intitule": depot_a_utiliser["intitule"], } - # 7. SUIVI DE STOCK resultat["suivi_stock_active"] = stock_defini - # 8. AVERTISSEMENT SI STOCK NON DÉFINI if has_stock_values and not stock_defini and stock_erreur: resultat["avertissement"] = ( f"Stock demandé mais non défini dans F_ARTSTOCK : {stock_erreur}" @@ -10320,9 +9434,6 @@ class SageConnector: with self._com_context(), self._lock_com: logger.info(f"[ARTICLE] === MODIFICATION {reference} ===") - # ======================================== - # ÉTAPE 1 : CHARGER L'ARTICLE EXISTANT - # ======================================== factory_article = self.cial.FactoryArticle persist = factory_article.ReadReference(reference.upper()) @@ -10335,16 +9446,10 @@ class SageConnector: designation_actuelle = getattr(article, "AR_Design", "") logger.info(f"[ARTICLE] Trouvé : {reference} - {designation_actuelle}") - # ======================================== - # ÉTAPE 2 : METTRE À JOUR LES CHAMPS - # ======================================== logger.info("[ARTICLE] Mise à jour des champs...") champs_modifies = [] - # ======================================== - # 🆕 FAMILLE (NOUVEAU - avec scanner List) - # ======================================== if "famille" in article_data and article_data["famille"]: famille_code_demande = article_data["famille"].upper().strip() logger.info( @@ -10352,9 +9457,6 @@ class SageConnector: ) try: - # ======================================== - # VÉRIFIER EXISTENCE VIA SQL - # ======================================== famille_existe_sql = False famille_code_exact = None famille_type = None @@ -10381,7 +9483,6 @@ class SageConnector: famille_type = row.FA_Type if len(row) > 1 else 0 famille_existe_sql = True - # Vérifier le type if famille_type == 1: raise ValueError( f"La famille '{famille_code_demande}' est de type 'Total' " @@ -10403,16 +9504,12 @@ class SageConnector: logger.warning(f" [SQL] Erreur : {e}") raise ValueError(f"Impossible de vérifier la famille : {e}") - # ======================================== - # CHARGER VIA COM (SCANNER) - # ======================================== if famille_existe_sql and famille_code_exact: logger.info(f" [COM] Recherche via scanner...") factory_famille = self.cial.FactoryFamille famille_obj = None - # Scanner List() try: index = 1 max_scan = 1000 @@ -10435,7 +9532,6 @@ class SageConnector: ) if code_test == famille_code_exact.upper(): - # TROUVÉ ! famille_obj = fam_test logger.info( f" [OK] Famille trouvée à l'index {index}" @@ -10456,7 +9552,6 @@ class SageConnector: f" [COM] Scanner échoué : {str(e)[:200]}" ) - # Assigner la famille if famille_obj: famille_obj.Read() article.Famille = famille_obj @@ -10476,18 +9571,12 @@ class SageConnector: logger.error(f" [ERREUR] Changement famille : {e}") raise ValueError(f"Impossible de changer la famille : {str(e)}") - # ======================================== - # DÉSIGNATION - # ======================================== if "designation" in article_data: designation = str(article_data["designation"])[:69].strip() article.AR_Design = designation champs_modifies.append(f"designation") logger.info(f" [OK] Désignation : {designation}") - # ======================================== - # PRIX DE VENTE - # ======================================== if "prix_vente" in article_data: try: prix_vente = float(article_data["prix_vente"]) @@ -10497,14 +9586,10 @@ class SageConnector: except Exception as e: logger.warning(f" [WARN] Prix vente : {e}") - # ======================================== - # PRIX D'ACHAT - # ======================================== if "prix_achat" in article_data: try: prix_achat = float(article_data["prix_achat"]) - # Double tentative (AR_PrixAch / AR_PrixAchat) try: article.AR_PrixAch = prix_achat champs_modifies.append("prix_achat") @@ -10521,9 +9606,6 @@ class SageConnector: except Exception as e: logger.warning(f" [WARN] Prix achat : {e}") - # ======================================== - # STOCK RÉEL (NIVEAU ARTICLE) - # ======================================== if "stock_reel" in article_data: try: stock_reel = float(article_data["stock_reel"]) @@ -10543,9 +9625,6 @@ class SageConnector: logger.error(f" [ERREUR] Stock : {e}") raise ValueError(f"Impossible de modifier le stock: {e}") - # ======================================== - # STOCK MINI/MAXI (NIVEAU ARTICLE) - # ======================================== if "stock_mini" in article_data: try: stock_mini = float(article_data["stock_mini"]) @@ -10564,9 +9643,6 @@ class SageConnector: except Exception as e: logger.warning(f" [WARN] Stock maxi : {e}") - # ======================================== - # CODE EAN - # ======================================== if "code_ean" in article_data: try: code_ean = str(article_data["code_ean"])[:13].strip() @@ -10576,9 +9652,6 @@ class SageConnector: except Exception as e: logger.warning(f" [WARN] Code EAN : {e}") - # ======================================== - # DESCRIPTION - # ======================================== if "description" in article_data: try: description = str(article_data["description"])[:255].strip() @@ -10588,9 +9661,6 @@ class SageConnector: except Exception as e: logger.warning(f" [WARN] Description : {e}") - # ======================================== - # VÉRIFICATION - # ======================================== if not champs_modifies: logger.warning("[ARTICLE] Aucun champ à modifier") return self._extraire_article(article) @@ -10599,9 +9669,6 @@ class SageConnector: f"[ARTICLE] Champs à modifier : {', '.join(champs_modifies)}" ) - # ======================================== - # ÉCRITURE - # ======================================== logger.info("[ARTICLE] Écriture des modifications...") try: @@ -10623,26 +9690,20 @@ class SageConnector: logger.error(f"[ARTICLE] Erreur Write() : {error_detail}") raise RuntimeError(f"Échec modification : {error_detail}") - # ======================================== - # RELECTURE ET EXTRACTION - # ======================================== article.Read() logger.info( f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)" ) - # Extraction complète resultat = self._extraire_article(article) if not resultat: - # Fallback si extraction échoue resultat = { "reference": reference, "designation": getattr(article, "AR_Design", ""), } - # Enrichir avec les valeurs qu'on vient de modifier if "prix_vente" in article_data: resultat["prix_vente"] = float(article_data["prix_vente"]) @@ -10695,9 +9756,6 @@ class SageConnector: try: logger.info("[FAMILLE] === CRÉATION FAMILLE (TYPE DÉTAIL) ===") - # ======================================== - # VALIDATION - # ======================================== code = famille_data.get("code", "").upper().strip() if not code: raise ValueError("Le code famille est obligatoire") @@ -10717,20 +9775,15 @@ class SageConnector: logger.info(f"[FAMILLE] Code : {code}") logger.info(f"[FAMILLE] Intitulé : {intitule}") - # NOUVEAU : Avertir si l'utilisateur demande un type Total type_demande = famille_data.get("type", 0) if type_demande == 1: logger.warning( "[FAMILLE] Type 'Total' demandé mais IGNORÉ - Création en type Détail uniquement" ) - # ======================================== - # VÉRIFIER SI EXISTE DÉJÀ - # ======================================== factory_famille = self.cial.FactoryFamille try: - # Scanner pour vérifier l'existence index = 1 while index <= 1000: try: @@ -10758,25 +9811,19 @@ class SageConnector: except ValueError: raise - # ======================================== - # CRÉER LA FAMILLE - # ======================================== persist = factory_famille.Create() famille = win32com.client.CastTo(persist, "IBOFamille3") famille.SetDefault() - # Champs obligatoires famille.FA_CodeFamille = code famille.FA_Intitule = intitule - # CRITIQUE : FORCER Type = 0 (Détail) try: famille.FA_Type = 0 # Toujours Détail logger.info(f"[FAMILLE] Type : 0 (Détail)") except Exception as e: logger.warning(f"[FAMILLE] FA_Type non défini : {e}") - # Comptes généraux (optionnels) compte_achat = famille_data.get("compte_achat") if compte_achat: try: @@ -10811,9 +9858,6 @@ class SageConnector: except Exception as e: logger.warning(f"[FAMILLE] Compte vente non défini : {e}") - # ======================================== - # ÉCRIRE DANS SAGE - # ======================================== logger.info("[FAMILLE] Écriture dans Sage...") try: @@ -10834,9 +9878,6 @@ class SageConnector: logger.error(f"[FAMILLE] Erreur Write() : {error_detail}") raise RuntimeError(f"Échec création famille : {error_detail}") - # ======================================== - # RELIRE ET RETOURNER - # ======================================== famille.Read() resultat = { @@ -10877,7 +9918,6 @@ class SageConnector: logger.info("[SQL] Détection des colonnes de F_FAMILLE...") - # Requête de test pour récupérer les métadonnées cursor.execute("SELECT TOP 1 * FROM F_FAMILLE") colonnes_disponibles = [column[0] for column in cursor.description] @@ -10897,7 +9937,6 @@ class SageConnector: "FA_Raccourci", ] - # Ne garder QUE les colonnes qui existent vraiment colonnes_a_lire = [ col for col in colonnes_souhaitees if col in colonnes_disponibles ] @@ -10924,7 +9963,6 @@ class SageConnector: else: logger.info("[SQL] Filtre : TOUS les types (Détail + Total)") - # Filtre texte (si fourni) if filtre: conditions_filtre = [] @@ -10939,7 +9977,6 @@ class SageConnector: if conditions_filtre: query += " AND (" + " OR ".join(conditions_filtre) + ")" - # Tri if "FA_Intitule" in colonnes_a_lire: query += " ORDER BY FA_Intitule" elif "FA_CodeFamille" in colonnes_a_lire: @@ -10953,7 +9990,6 @@ class SageConnector: for row in rows: famille = {} - # Remplir avec les colonnes disponibles for idx, colonne in enumerate(colonnes_a_lire): valeur = row[idx] @@ -10962,14 +9998,12 @@ class SageConnector: famille[colonne] = valeur - # Alias if "FA_CodeFamille" in famille: famille["code"] = famille["FA_CodeFamille"] if "FA_Intitule" in famille: famille["intitule"] = famille["FA_Intitule"] - # Type lisible if "FA_Type" in famille: type_val = famille["FA_Type"] famille["type"] = type_val @@ -10980,7 +10014,6 @@ class SageConnector: famille["type_libelle"] = "Détail" famille["est_total"] = False - # Autres champs famille["unite_vente"] = str(famille.get("FA_UniteVen", "")) famille["coef"] = ( float(famille.get("FA_Coef", 0.0)) @@ -11026,11 +10059,9 @@ class SageConnector: with self._get_sql_connection() as conn: cursor = conn.cursor() - # Détecter les colonnes disponibles cursor.execute("SELECT TOP 1 * FROM F_FAMILLE") colonnes_disponibles = [col[0] for col in cursor.description] - # Construire la requête selon les colonnes disponibles colonnes_select = ["FA_CodeFamille", "FA_Intitule"] if "FA_Type" in colonnes_disponibles: @@ -11054,7 +10085,6 @@ class SageConnector: famille_code_exact = self._safe_strip(row.FA_CodeFamille) famille_intitule_sql = self._safe_strip(row.FA_Intitule) - # Type (si disponible) if "FA_Type" in colonnes_disponibles and len(row) > 2: famille_type_sql = row.FA_Type @@ -11068,7 +10098,6 @@ class SageConnector: raise except Exception as e: logger.warning(f" [SQL] Erreur : {e}") - # Continuer quand même avec COM if not famille_code_exact: famille_code_exact = code_recherche @@ -11101,7 +10130,6 @@ class SageConnector: ) if code_test == famille_code_exact: - # TROUVÉE ! famille_obj = fam_test index_trouve = index logger.info(f" [OK] Famille trouvée à l'index {index}") @@ -11133,13 +10161,11 @@ class SageConnector: famille_obj.Read() - # Champs de base resultat = { "code": getattr(famille_obj, "FA_CodeFamille", "").strip(), "intitule": getattr(famille_obj, "FA_Intitule", "").strip(), } - # Type try: fa_type = getattr(famille_obj, "FA_Type", 0) resultat["type"] = fa_type @@ -11147,7 +10173,6 @@ class SageConnector: resultat["est_total"] = fa_type == 1 resultat["est_detail"] = fa_type == 0 - # Avertissement si famille Total if fa_type == 1: resultat["avertissement"] = ( "Cette famille est de type 'Total' (agrégation comptable) " @@ -11162,7 +10187,6 @@ class SageConnector: resultat["est_total"] = False resultat["est_detail"] = True - # Unité de vente try: resultat["unite_vente"] = getattr( famille_obj, "FA_UniteVen", "" @@ -11170,20 +10194,17 @@ class SageConnector: except: resultat["unite_vente"] = "" - # Coefficient try: coef = getattr(famille_obj, "FA_Coef", None) resultat["coef"] = float(coef) if coef is not None else 0.0 except: resultat["coef"] = 0.0 - # Nature try: resultat["nature"] = getattr(famille_obj, "FA_Nature", 0) except: resultat["nature"] = 0 - # Centrale d'achat try: central = getattr(famille_obj, "FA_Central", None) resultat["est_centrale"] = ( @@ -11192,7 +10213,6 @@ class SageConnector: except: resultat["est_centrale"] = False - # Statistique try: stat = getattr(famille_obj, "FA_Stat", None) resultat["est_statistique"] = ( @@ -11201,7 +10221,6 @@ class SageConnector: except: resultat["est_statistique"] = False - # Raccourci try: resultat["raccourci"] = getattr( famille_obj, "FA_Raccourci", "" @@ -11209,10 +10228,6 @@ class SageConnector: except: resultat["raccourci"] = "" - # ======================================== - # COMPTES GÉNÉRAUX - # ======================================== - # Compte achat try: compte_achat_obj = getattr(famille_obj, "CompteGAchat", None) if compte_achat_obj: @@ -11225,7 +10240,6 @@ class SageConnector: except: resultat["compte_achat"] = "" - # Compte vente try: compte_vente_obj = getattr(famille_obj, "CompteGVente", None) if compte_vente_obj: @@ -11238,10 +10252,8 @@ class SageConnector: except: resultat["compte_vente"] = "" - # Index de lecture resultat["index_com"] = index_trouve - # Dates (si disponibles) try: date_creation = getattr(famille_obj, "cbCreation", None) resultat["date_creation"] = ( @@ -11258,7 +10270,6 @@ class SageConnector: except: resultat["date_modification"] = "" - # Compter les articles de cette famille via SQL try: with self._get_sql_connection() as conn: cursor = conn.cursor() @@ -11311,7 +10322,6 @@ class SageConnector: with self._com_context(), self._lock_com: logger.info(f"[STOCK] === CRÉATION ENTRÉE STOCK (COM pur) ===") - # Démarrer transaction transaction_active = False try: self.cial.CptaApplication.BeginTrans() @@ -11321,9 +10331,6 @@ class SageConnector: pass try: - # ======================================== - # ÉTAPE 1 : CRÉER LE DOCUMENT D'ENTRÉE - # ======================================== factory_doc = self.cial.FactoryDocumentStock persist_doc = factory_doc.CreateType(180) # 180 = Entrée doc = win32com.client.CastTo(persist_doc, "IBODocumentStock3") @@ -11343,16 +10350,12 @@ class SageConnector: doc.Write() logger.info(f"[STOCK] Document créé") - # ======================================== - # ÉTAPE 2 : PRÉPARER POUR LES STOCKS MINI/MAXI - # ======================================== factory_article = self.cial.FactoryArticle factory_depot = self.cial.FactoryDepot stocks_mis_a_jour = [] depot_principal = None - # Trouver un dépôt principal try: persist_depot = factory_depot.List(1) if persist_depot: @@ -11366,9 +10369,6 @@ class SageConnector: except Exception as e: logger.warning(f"Erreur chargement dépôt: {e}") - # ======================================== - # ÉTAPE 3 : TRAITER CHAQUE LIGNE (MOUVEMENT + STOCK) - # ======================================== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -11382,7 +10382,6 @@ class SageConnector: logger.info(f"[STOCK] Ligne {idx}: {article_ref} x {quantite}") - # A. CHARGER L'ARTICLE persist_article = factory_article.ReadReference(article_ref) if not persist_article: raise ValueError(f"Article {article_ref} introuvable") @@ -11392,7 +10391,6 @@ class SageConnector: ) article_obj.Read() - # B. CRÉER LA LIGNE DE MOUVEMENT ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( @@ -11405,7 +10403,6 @@ class SageConnector: ligne_obj.SetDefault() - # Lier l'article au mouvement try: ligne_obj.SetDefaultArticleReference( article_ref, float(quantite) @@ -11420,7 +10417,6 @@ class SageConnector: f"Impossible de lier l'article {article_ref}" ) - # Prix prix = ligne_data.get("prix_unitaire") if prix: try: @@ -11428,24 +10424,18 @@ class SageConnector: except: pass - # Écrire la ligne ligne_obj.Write() - # ======================================== - # ÉTAPE 4 : GÉRER LES STOCKS MINI/MAXI (COM PUR) - # ======================================== if stock_mini is not None or stock_maxi is not None: logger.info( f"[STOCK] Ajustement stock pour {article_ref}..." ) try: - # MÉTHODE A : Via Article.FactoryArticleStock (LA BONNE MÉTHODE) logger.info( f" [COM] Méthode A : Article.FactoryArticleStock" ) - # 1. Charger l'article COMPLET avec sa factory factory_article = self.cial.FactoryArticle persist_article_full = factory_article.ReadReference( article_ref @@ -11455,7 +10445,6 @@ class SageConnector: ) article_full.Read() - # 2. Accéder à la FactoryArticleStock de l'article factory_article_stock = None try: factory_article_stock = ( @@ -11468,7 +10457,6 @@ class SageConnector: ) if factory_article_stock: - # 3. Chercher si le stock existe déjà stock_trouve = None index_stock = 1 @@ -11485,7 +10473,6 @@ class SageConnector: ) stock_obj.Read() - # Vérifier le dépôt depot_stock = None try: depot_stock = getattr( @@ -11500,7 +10487,6 @@ class SageConnector: f" Dépôt {index_stock}: {depot_code}" ) - # Si c'est le dépôt principal ou le premier trouvé if ( not stock_trouve or depot_code @@ -11524,7 +10510,6 @@ class SageConnector: ) index_stock += 1 - # 4. Si pas trouvé, créer un nouveau stock if not stock_trouve: try: stock_persist = ( @@ -11535,7 +10520,6 @@ class SageConnector: ) stock_trouve.SetDefault() - # Lier au dépôt principal si disponible if depot_principal: try: stock_trouve.Depot = depot_principal @@ -11552,18 +10536,14 @@ class SageConnector: ) raise - # 5. METTRE À JOUR LES STOCKS MINI/MAXI if stock_trouve: - # Sauvegarder l'état avant modification try: stock_trouve.Read() except: pass - # STOCK MINI if stock_mini is not None: try: - # Essayer différentes propriétés possibles for prop_name in [ "AS_QteMini", "AS_Mini", @@ -11592,10 +10572,8 @@ class SageConnector: f" Stock mini non défini: {e}" ) - # STOCK MAXI if stock_maxi is not None: try: - # Essayer différentes propriétés possibles for prop_name in [ "AS_QteMaxi", "AS_Maxi", @@ -11624,7 +10602,6 @@ class SageConnector: f" Stock maxi non défini: {e}" ) - # 6. SAUVEGARDER try: stock_trouve.Write() logger.info( @@ -11636,7 +10613,6 @@ class SageConnector: ) raise - # MÉTHODE B : Alternative via DepotStock si A échoue if depot_principal and ( stock_mini is not None or stock_maxi is not None ): @@ -11663,7 +10639,6 @@ class SageConnector: continue if factory_depot_stock: - # Chercher le stock existant stock_depot_trouve = None index_ds = 1 @@ -11696,7 +10671,6 @@ class SageConnector: except: index_ds += 1 - # Si pas trouvé, créer if not stock_depot_trouve: try: stock_ds_persist = ( @@ -11720,7 +10694,6 @@ class SageConnector: f" Impossible de créer DepotStock: {e}" ) - # Mettre à jour if stock_depot_trouve: if stock_mini is not None: try: @@ -11766,7 +10739,6 @@ class SageConnector: f"[STOCK] Erreur ajustement stock: {e}", exc_info=True, ) - # Ne pas bloquer si l'ajustement échoue stocks_mis_a_jour.append( { @@ -11777,37 +10749,28 @@ class SageConnector: } ) - # ======================================== - # ÉTAPE 5 : FINALISER LE DOCUMENT - # ======================================== doc.Write() doc.Read() numero = getattr(doc, "DO_Piece", "") logger.info(f"[STOCK] Document finalisé: {numero}") - # ======================================== - # ÉTAPE 6 : VÉRIFICATION VIA COM - # ======================================== logger.info(f"[STOCK] Vérification finale via COM...") for stock_info in stocks_mis_a_jour: article_ref = stock_info["article_ref"] try: - # Recharger l'article pour voir les stocks persist_article = factory_article.ReadReference(article_ref) article_verif = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_verif.Read() - # Lire les attributs de stock stock_total = 0.0 stock_mini_lu = 0.0 stock_maxi_lu = 0.0 - # Essayer différents attributs for attr in ["AR_Stock", "AS_QteSto", "Stock"]: try: val = getattr(article_verif, attr, None) @@ -11851,7 +10814,6 @@ class SageConnector: f"[VERIF] Erreur vérification {article_ref}: {e}" ) - # Commit if transaction_active: try: self.cial.CptaApplication.CommitTrans() @@ -11889,7 +10851,6 @@ class SageConnector: with self._com_context(), self._lock_com: logger.info(f"[STOCK] Lecture stock : {reference}") - # Charger l'article factory_article = self.cial.FactoryArticle persist_article = factory_article.ReadReference(reference.upper()) @@ -11916,9 +10877,6 @@ class SageConnector: "methode_lecture": None, } - # ======================================== - # MÉTHODE 1 : Via Depot.FactoryDepotStock (RAPIDE - 1-2 sec) - # ======================================== logger.info("[STOCK] Tentative Méthode 1 : Depot.FactoryDepotStock...") try: @@ -11926,7 +10884,6 @@ class SageConnector: index_depot = 1 stocks_trouves = [] - # OPTIMISATION : Limiter à 20 dépôts max (au lieu de 100) while index_depot <= 20: try: persist_depot = factory_depot.List(index_depot) @@ -11951,7 +10908,6 @@ class SageConnector: index_depot += 1 continue - # Chercher FactoryDepotStock factory_depot_stock = None for factory_name in [ @@ -11968,7 +10924,6 @@ class SageConnector: pass if factory_depot_stock: - # OPTIMISATION : Limiter le scan à 1000 stocks par dépôt index_stock = 1 while index_stock <= 1000: @@ -11984,10 +10939,8 @@ class SageConnector: ) stock.Read() - # Vérifier si c'est notre article article_ref_stock = "" - # Essayer différents attributs for attr_ref in [ "AR_Ref", "AS_Article", @@ -12003,7 +10956,6 @@ class SageConnector: except: pass - # Si pas trouvé, essayer via l'objet Article if not article_ref_stock: try: article_obj = getattr( @@ -12022,12 +10974,10 @@ class SageConnector: pass if article_ref_stock == reference.upper(): - # TROUVÉ ! quantite = 0.0 qte_mini = 0.0 qte_maxi = 0.0 - # Essayer différents attributs de quantité for attr_qte in [ "AS_QteSto", "AS_Qte", @@ -12042,7 +10992,6 @@ class SageConnector: except: pass - # Qte mini/maxi try: qte_mini = float( getattr(stock, "AS_QteMini", 0.0) @@ -12103,15 +11052,11 @@ class SageConnector: except Exception as e: logger.warning(f"[STOCK] Méthode 1 échouée : {e}") - # ======================================== - # MÉTHODE 2 : Via attributs Article (RAPIDE - < 1 sec) - # ======================================== logger.info("[STOCK] Tentative Méthode 2 : Attributs Article...") try: stock_trouve = False - # Essayer différents attributs for attr_stock in ["AR_Stock", "AR_QteSto", "AR_QteStock"]: try: val = getattr(article, attr_stock, None) @@ -12134,12 +11079,8 @@ class SageConnector: except Exception as e: logger.warning(f"[STOCK] Méthode 2 échouée : {e}") - # ======================================== - # MÉTHODE 3 : Calcul depuis mouvements (LENT - DÉSACTIVÉ PAR DÉFAUT) - # ======================================== if not calcul_complet: - # Méthodes rapides ont échoué, mais calcul complet non demandé logger.warning( f"[STOCK] Méthodes rapides échouées pour {reference}" ) @@ -12156,13 +11097,10 @@ class SageConnector: return stock_info - # ATTENTION : Cette partie est ULTRA-LENTE (20+ minutes) logger.warning( "[STOCK] CALCUL DEPUIS MOUVEMENTS ACTIVÉ - PEUT PRENDRE 20+ MINUTES" ) - # [Le reste du code de calcul depuis mouvements reste inchangé...] - # ... (code existant pour la méthode 3) except ValueError: raise @@ -12182,15 +11120,11 @@ class SageConnector: pass try: - # ======================================== - # ÉTAPE 1 : CRÉER LE DOCUMENT - # ======================================== factory = self.cial.FactoryDocumentStock persist = factory.CreateType(181) # 181 = Sortie doc = win32com.client.CastTo(persist, "IBODocumentStock3") doc.SetDefault() - # Date date_mouv = sortie_data.get("date_mouvement") if isinstance(date_mouv, date): @@ -12200,7 +11134,6 @@ class SageConnector: else: doc.DO_Date = pywintypes.Time(datetime.now()) - # Référence if sortie_data.get("reference"): doc.DO_Ref = sortie_data["reference"] @@ -12209,9 +11142,6 @@ class SageConnector: f"[STOCK] Document créé : {getattr(doc, 'DO_Piece', '?')}" ) - # ======================================== - # ÉTAPE 2 : FACTORY LIGNES - # ======================================== try: factory_lignes = doc.FactoryDocumentLigne except: @@ -12220,9 +11150,6 @@ class SageConnector: factory_article = self.cial.FactoryArticle stocks_mis_a_jour = [] - # ======================================== - # ÉTAPE 3 : TRAITER CHAQUE LIGNE - # ======================================== for idx, ligne_data in enumerate(sortie_data["lignes"], 1): article_ref = ligne_data["article_ref"].upper() quantite = ligne_data["quantite"] @@ -12231,7 +11158,6 @@ class SageConnector: f"[STOCK] ======== LIGNE {idx} : {article_ref} x {quantite} ========" ) - # Charger l'article persist_article = factory_article.ReadReference(article_ref) if not persist_article: raise ValueError(f"Article {article_ref} introuvable") @@ -12247,7 +11173,6 @@ class SageConnector: logger.info(f"[STOCK] Article : {ar_design}") logger.info(f"[STOCK] AR_SuiviStock : {ar_suivi}") - # VÉRIFIER LE STOCK DISPONIBLE stock_dispo = self.verifier_stock_suffisant( article_ref, quantite, None ) @@ -12262,7 +11187,6 @@ class SageConnector: f"[STOCK] Stock disponible : {stock_dispo['stock_disponible']}" ) - # Gérer le lot numero_lot = ligne_data.get("numero_lot") if ar_suivi == 1: # CMUP @@ -12279,9 +11203,6 @@ class SageConnector: f"[STOCK] FIFO/LIFO : Lot auto-généré '{numero_lot}'" ) - # ======================================== - # CRÉER LA LIGNE - # ======================================== ligne_persist = factory_lignes.Create() try: @@ -12295,9 +11216,6 @@ class SageConnector: ligne_obj.SetDefault() - # ======================================== - # LIAISON ARTICLE - # ======================================== article_lie = False try: @@ -12321,9 +11239,6 @@ class SageConnector: f"Impossible de lier l'article {article_ref}" ) - # ======================================== - # LOT (si FIFO/LIFO) - # ======================================== if numero_lot and ar_suivi == 2: try: ligne_obj.SetDefaultLot(numero_lot) @@ -12335,7 +11250,6 @@ class SageConnector: except: pass - # Prix prix = ligne_data.get("prix_unitaire") if prix: try: @@ -12343,13 +11257,9 @@ class SageConnector: except: pass - # ======================================== - # ÉCRIRE LA LIGNE - # ======================================== ligne_obj.Write() logger.info(f"[STOCK] Write() réussi") - # Vérification ligne_obj.Read() ref_verifiee = article_ref # Supposer OK si Write() réussi @@ -12377,16 +11287,12 @@ class SageConnector: } ) - # ======================================== - # FINALISER - # ======================================== doc.Write() doc.Read() numero = getattr(doc, "DO_Piece", "") logger.info(f"[STOCK] Document finalisé : {numero}") - # Commit try: self.cial.CptaApplication.CommitTrans() logger.info(f"[STOCK] Transaction committée") @@ -12419,7 +11325,6 @@ class SageConnector: with self._com_context(), self._lock_com: factory = self.cial.FactoryDocumentStock - # Chercher le document persist = None index = 1 @@ -12451,7 +11356,6 @@ class SageConnector: doc = win32com.client.CastTo(persist, "IBODocumentStock3") doc.Read() - # Infos du document do_type = getattr(doc, "DO_Type", -1) types_mouvements = { @@ -12470,7 +11374,6 @@ class SageConnector: "lignes": [], } - # Lire les lignes try: factory_lignes = getattr( doc, "FactoryDocumentLigne", None @@ -12495,7 +11398,6 @@ class SageConnector: ligne.Read() - # Récupérer la référence article via l'objet Article article_ref = "" try: article_obj = getattr(ligne, "Article", None) @@ -12548,12 +11450,10 @@ class SageConnector: with self._get_sql_connection() as conn: cursor = conn.cursor() - # LOCK pour éviter les race conditions cursor.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE") cursor.execute("BEGIN TRANSACTION") try: - # Lire stock avec lock cursor.execute( """ SELECT SUM(AS_QteSto)