diff --git a/sage_connector.py b/sage_connector.py index 42dc660..6df70f5 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -2378,54 +2378,87 @@ class SageConnector: def creer_client(self, client_data: Dict) -> Dict: """ Crée un nouveau client dans Sage 100c via l'API COM. - ✅ Gestion stricte des longueurs de champs + ✅ Validation STRICTE des longueurs + Diagnostic exhaustif """ if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: - # 1. Factory Client + # ======================================== + # ÉTAPE 0 : VALIDATION PRÉALABLE + # ======================================== + logger.info("🔍 === VALIDATION DES DONNÉES ===") + + # Intitulé obligatoire + if not client_data.get("intitule"): + raise ValueError("Le champ 'intitule' est obligatoire") + + # Tronquer TOUS les champs texte + intitule = str(client_data["intitule"])[:69].strip() + num_prop = str(client_data.get("num", "")).upper()[:17].strip() + compte = str(client_data.get("compte_collectif", "411000"))[:13].strip() + + adresse = str(client_data.get("adresse", ""))[:35].strip() + code_postal = str(client_data.get("code_postal", ""))[:9].strip() + ville = str(client_data.get("ville", ""))[:35].strip() + pays = str(client_data.get("pays", ""))[:35].strip() + + telephone = str(client_data.get("telephone", ""))[:21].strip() + email = str(client_data.get("email", ""))[:69].strip() + + siret = str(client_data.get("siret", ""))[:14].strip() + tva_intra = str(client_data.get("tva_intra", ""))[:25].strip() + + # Log des valeurs tronquées + logger.info(f" intitule: '{intitule}' (len={len(intitule)}/69)") + logger.info(f" num: '{num_prop or 'AUTO'}' (len={len(num_prop)}/17)") + logger.info(f" compte: '{compte}' (len={len(compte)}/13)") + logger.info(f" adresse: '{adresse}' (len={len(adresse)}/35)") + logger.info(f" code_postal: '{code_postal}' (len={len(code_postal)}/9)") + logger.info(f" ville: '{ville}' (len={len(ville)}/35)") + logger.info(f" pays: '{pays}' (len={len(pays)}/35)") + logger.info(f" telephone: '{telephone}' (len={len(telephone)}/21)") + logger.info(f" email: '{email}' (len={len(email)}/69)") + logger.info(f" siret: '{siret}' (len={len(siret)}/14)") + logger.info(f" tva_intra: '{tva_intra}' (len={len(tva_intra)}/25)") + + # ======================================== + # ÉTAPE 1 : CRÉATION OBJET CLIENT + # ======================================== factory_client = self.cial.CptaApplication.FactoryClient persist = factory_client.Create() if not persist: raise RuntimeError("Factory.Create() a retourné None") - # Cast vers IBOClient3 client = win32com.client.CastTo(persist, "IBOClient3") - logger.debug(f"✅ Cast réussi vers IBOClient3") + logger.info("✅ Objet client créé") - # 2. Remplissage avec VALIDATION DES LONGUEURS - logger.info(f"📝 Création client: {client_data['intitule']}") + # ======================================== + # ÉTAPE 2 : CHAMPS OBLIGATOIRES MINIMAUX + # ======================================== + logger.info("📝 Définition champs obligatoires...") - # ===== CT_Intitule - OBLIGATOIRE (max 69 car) ===== - intitule = client_data["intitule"][:69] # Tronquer si nécessaire - try: - client.CT_Intitule = intitule - logger.debug(f"✅ CT_Intitule: {intitule}") - except Exception as e: - raise RuntimeError(f"Impossible de définir CT_Intitule: {e}") + # 1. Intitulé (OBLIGATOIRE) + client.CT_Intitule = intitule + logger.debug(f" ✅ CT_Intitule: '{intitule}'") - # ===== CT_Num - Optionnel (max 17 car, ALPHA MAJ) ===== - num_prop = client_data.get("num", "") - if num_prop: - num_prop = num_prop.upper()[:17] # Majuscules + tronquer - try: - client.CT_Num = num_prop - logger.debug(f"✅ CT_Num: {num_prop}") - except Exception as e: - logger.warning(f"⚠️ CT_Num non défini: {e}") + # 2. Type (OBLIGATOIRE - 0=Client) + client.CT_Type = 0 + logger.debug(" ✅ CT_Type: 0 (Client)") - # ===== CT_CompteG - Compte collectif (max 13 car) ===== - compte = client_data.get("compte_collectif", "411000")[:13] - try: + # 3. Compte collectif + if compte: client.CT_CompteG = compte - logger.debug(f"✅ CT_CompteG: {compte}") - except Exception as e: - logger.warning(f"⚠️ CT_CompteG non défini: {e}") + logger.debug(f" ✅ CT_CompteG: '{compte}'") - # ===== Catégories (valeurs numériques, pas de pb de longueur) ===== + # 4. Numéro client (optionnel, auto si vide) + if num_prop: + client.CT_Num = num_prop + logger.debug(f" ✅ CT_Num: '{num_prop}'") + + # 5. Catégories (valeurs par défaut) try: client.N_CatTarif = 1 client.N_CatCompta = 1 @@ -2433,97 +2466,134 @@ class SageConnector: client.N_Expedition = 1 client.N_Condition = 1 client.N_Risque = 1 - logger.debug("✅ Catégories initialisées") + logger.debug(" ✅ Catégories initialisées") except Exception as e: - logger.warning(f"⚠️ Catégories: {e}") - - # ===== CT_NumPayeur (max 17 car) ===== + logger.warning(f" ⚠️ Catégories: {e}") + + # ======================================== + # ÉTAPE 3 : CHAMPS OPTIONNELS + # ======================================== + logger.info("📝 Définition champs optionnels...") + + # Adresse + if any([adresse, code_postal, ville, pays]): + try: + adresse_obj = getattr(client, "Adresse", None) + if adresse_obj: + if adresse: + adresse_obj.Adresse = adresse + if code_postal: + adresse_obj.CodePostal = code_postal + if ville: + adresse_obj.Ville = ville + if pays: + adresse_obj.Pays = pays + logger.debug(" ✅ Adresse définie") + except Exception as e: + logger.warning(f" ⚠️ Adresse: {e}") + + # Télécom + if telephone or email: + try: + telecom_obj = getattr(client, "Telecom", None) + if telecom_obj: + if telephone: + telecom_obj.Telephone = telephone + if email: + telecom_obj.EMail = email + logger.debug(" ✅ Télécom défini") + except Exception as e: + logger.warning(f" ⚠️ Télécom: {e}") + + # Identifiants + if siret: + try: + client.CT_Siret = siret + logger.debug(f" ✅ SIRET: '{siret}'") + except Exception as e: + logger.warning(f" ⚠️ SIRET: {e}") + + if tva_intra: + try: + client.CT_Identifiant = tva_intra + logger.debug(f" ✅ TVA: '{tva_intra}'") + except Exception as e: + logger.warning(f" ⚠️ TVA: {e}") + + # NumPayeur (= NumClient si défini) if num_prop: try: - client.CT_NumPayeur = num_prop # Déjà tronqué plus haut - logger.debug(f"✅ CT_NumPayeur: {num_prop}") + client.CT_NumPayeur = num_prop + logger.debug(f" ✅ CT_NumPayeur: '{num_prop}'") except Exception as e: - logger.warning(f"⚠️ CT_NumPayeur: {e}") + logger.warning(f" ⚠️ CT_NumPayeur: {e}") - # ===== ADRESSE (tous max 35 car sauf code postal = 9) ===== - try: - adresse_obj = getattr(client, "Adresse", None) - if adresse_obj: - if client_data.get("adresse"): - adresse_obj.Adresse = client_data["adresse"][:35] - if client_data.get("code_postal"): - adresse_obj.CodePostal = client_data["code_postal"][:9] - if client_data.get("ville"): - adresse_obj.Ville = client_data["ville"][:35] - if client_data.get("pays"): - adresse_obj.Pays = client_data["pays"][:35] - logger.debug("✅ Adresse définie") - except Exception as e: - logger.warning(f"⚠️ Adresse: {e}") - - # ===== TELECOM (Téléphone max 21, Email max 69) ===== - try: - telecom_obj = getattr(client, "Telecom", None) - if telecom_obj: - if client_data.get("telephone"): - telecom_obj.Telephone = client_data["telephone"][:21] - if client_data.get("email"): - telecom_obj.EMail = client_data["email"][:69] - logger.debug("✅ Télécom défini") - except Exception as e: - logger.warning(f"⚠️ Télécom: {e}") - - # ===== IDENTIFIANTS (SIRET max 14, TVA max 25) ===== - try: - if client_data.get("siret"): - client.CT_Siret = client_data["siret"][:14] - logger.debug(f"✅ SIRET: {client_data['siret'][:14]}") - if client_data.get("tva_intra"): - client.CT_Identifiant = client_data["tva_intra"][:25] - logger.debug(f"✅ TVA: {client_data['tva_intra'][:25]}") - except Exception as e: - logger.warning(f"⚠️ Identifiants: {e}") - - # 3. DIAGNOSTIC PRÉ-WRITE (pour debug) + # ======================================== + # ÉTAPE 4 : DIAGNOSTIC PRÉ-WRITE + # ======================================== logger.info("🔍 === DIAGNOSTIC PRÉ-WRITE ===") - try: - logger.info(f" CT_Intitule: '{client.CT_Intitule}' (len={len(client.CT_Intitule)})") - logger.info(f" CT_Num: '{getattr(client, 'CT_Num', 'AUTO')}'") - logger.info(f" CT_CompteG: '{getattr(client, 'CT_CompteG', 'N/A')}'") - logger.info(f" CT_Type: {getattr(client, 'CT_Type', '?')}") - except Exception as e: - logger.warning(f"⚠️ Erreur diagnostic: {e}") - # 4. Écriture en base + champs_critiques = [ + "CT_Intitule", "CT_Num", "CT_Type", "CT_CompteG", + "CT_NumPayeur", "N_CatTarif", "N_CatCompta" + ] + + for champ in champs_critiques: + try: + val = getattr(client, champ, "N/A") + longueur = len(str(val)) if val not in [None, "N/A"] else 0 + logger.info(f" {champ}: {repr(val)} (len={longueur})") + except Exception as e: + logger.warning(f" {champ}: Erreur lecture - {e}") + + # ======================================== + # ÉTAPE 5 : ÉCRITURE EN BASE + # ======================================== logger.info("💾 Écriture du client dans Sage...") + try: client.Write() logger.info("✅ Write() réussi !") + except Exception as e: # Récupérer erreur Sage détaillée error_detail = str(e) + sage_error_desc = None + try: sage_error = self.cial.CptaApplication.LastError if sage_error: - error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" - logger.error(f"❌ Erreur Sage détaillée: {error_detail}") + sage_error_desc = sage_error.Description + error_detail = f"{sage_error_desc} (Code: {sage_error.Number})" + logger.error(f"❌ Erreur Sage: {error_detail}") except: pass - # Log des valeurs pour debug - logger.error("❌ ÉCHEC Write() - Dump des champs définis:") + # Dump COMPLET des champs pour debug + logger.error("❌ ÉCHEC Write() - DUMP COMPLET:") + for attr in dir(client): - if attr.startswith("CT_") or attr.startswith("N_"): + if (attr.startswith("CT_") or + attr.startswith("N_") or + attr.startswith("cbMarq")): try: val = getattr(client, attr, None) - if val and not callable(val): - logger.error(f" {attr}: {val} (len={len(str(val))})") + if val is not None and not callable(val): + longueur = len(str(val)) if isinstance(val, str) else "N/A" + logger.error(f" {attr}: {repr(val)} (type={type(val).__name__}, len={longueur})") except: pass + # Tester si c'est un doublon + if sage_error_desc and ("doublon" in sage_error_desc.lower() or + "existe" in sage_error_desc.lower()): + raise ValueError(f"Ce client existe déjà: {sage_error_desc}") + raise RuntimeError(f"Échec Write(): {error_detail}") - # 5. Relire pour récupérer le numéro auto-généré + # ======================================== + # ÉTAPE 6 : RELECTURE & FINALISATION + # ======================================== try: client.Read() except Exception as e: @@ -2534,30 +2604,42 @@ class SageConnector: if not num_final: raise RuntimeError("CT_Num vide après Write()") - # Si CT_NumPayeur était vide, le définir maintenant - if not num_prop: + # Si auto-numérotation, définir CT_NumPayeur maintenant + if not num_prop and num_final: try: client.CT_NumPayeur = num_final client.Write() logger.debug(f"✅ CT_NumPayeur auto-défini: {num_final}") except Exception as e: - logger.warning(f"⚠️ CT_NumPayeur: {e}") + logger.warning(f"⚠️ CT_NumPayeur final: {e}") - logger.info(f"✅✅✅ CLIENT CRÉÉ: {num_final} - {client.CT_Intitule} ✅✅✅") + logger.info(f"✅✅✅ CLIENT CRÉÉ: {num_final} - {intitule} ✅✅✅") - # 6. Refresh cache + # ======================================== + # ÉTAPE 7 : REFRESH CACHE + # ======================================== self._refresh_cache_clients() return { "numero": num_final, - "intitule": client.CT_Intitule, - "compte_collectif": getattr(client, "CT_CompteG", ""), - "type": getattr(client, "CT_Type", 0), - "adresse": client_data.get("adresse"), - "ville": client_data.get("ville"), - "email": client_data.get("email") + "intitule": intitule, + "compte_collectif": compte, + "type": 0, + "adresse": adresse or None, + "code_postal": code_postal or None, + "ville": ville or None, + "pays": pays or None, + "email": email or None, + "telephone": telephone or None, + "siret": siret or None, + "tva_intra": tva_intra or None } - + + except ValueError as e: + # Erreur métier (doublon, validation) + logger.error(f"❌ Erreur métier: {e}") + raise + except Exception as e: logger.error(f"❌ Erreur création client: {e}", exc_info=True) @@ -2567,10 +2649,6 @@ class SageConnector: err = self.cial.CptaApplication.LastError if err: error_message = f"Erreur Sage: {err.Description}" - if "doublon" in err.Description.lower() or "existe" in err.Description.lower(): - raise ValueError(f"Ce client existe déjà. {error_message}") - except ValueError: - raise except: pass