diff --git a/sage_connector.py b/sage_connector.py index 5470965..7c663c2 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -5720,16 +5720,67 @@ class SageConnector: raise RuntimeError(f"Erreur technique: {e}") def modifier_client(self, code: str, client_data: Dict) -> Dict: + """ + Modification client Sage - Version complète alignée sur creer_client + """ if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: - # ======================================== - # ÉTAPE 1 : CHARGER LE CLIENT EXISTANT - # ======================================== - logger.info(f" Recherche client {code}...") + logger.info("=" * 80) + 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 "" + return str(value)[:max_len].strip() + + def safe_int(value, default=None): + if value is None: + return default + try: + return int(value) + except (ValueError, TypeError): + return default + + def safe_float(value, default=None): + if value is None: + return default + try: + return float(value) + except (ValueError, TypeError): + return default + + def try_set_attribute(obj, attr_name, value, variants=None): + """Essaie de définir un attribut avec plusieurs variantes de noms""" + if variants is None: + variants = [attr_name] + else: + variants = [attr_name] + variants + + for variant in variants: + try: + if hasattr(obj, variant): + setattr(obj, variant, value) + logger.debug(f" {variant} = {value} [OK]") + return True + except Exception as e: + logger.debug(f" {variant} echec: {str(e)[:50]}") + + return False + + champs_modifies = [] + + # ============================================================ + # ÉTAPE 1 : CHARGER LE CLIENT EXISTANT + # ============================================================ + logger.info("[ETAPE 1] CHARGEMENT CLIENT") + factory_client = self.cial.CptaApplication.FactoryClient persist = factory_client.ReadNumero(code) @@ -5739,151 +5790,501 @@ class SageConnector: client = win32com.client.CastTo(persist, "IBOClient3") client.Read() - logger.info( - f" Client {code} trouvé: {getattr(client, 'CT_Intitule', '')}" - ) + logger.info(f" Client chargé: {getattr(client, 'CT_Intitule', '')}") - # ======================================== - # ÉTAPE 2 : METTRE À JOUR LES CHAMPS FOURNIS - # ======================================== - logger.info(" Mise à jour des champs...") - - champs_modifies = [] - - # Intitulé + # ============================================================ + # ÉTAPE 2 : IDENTIFICATION + # ============================================================ + logger.info("[ETAPE 2] IDENTIFICATION") + if "intitule" in client_data: - intitule = str(client_data["intitule"])[:69].strip() + intitule = clean_str(client_data["intitule"], 69) client.CT_Intitule = intitule - champs_modifies.append(f"intitule='{intitule}'") - - # Adresse - if any( - k in client_data - for k in ["adresse", "code_postal", "ville", "pays"] - ): + champs_modifies.append("intitule") + logger.info(f" CT_Intitule = {intitule}") + + if "qualite" in client_data: + qualite = clean_str(client_data["qualite"], 17) + if try_set_attribute(client, "CT_Qualite", qualite): + champs_modifies.append("qualite") + + if "classement" in client_data: + if try_set_attribute(client, "CT_Classement", clean_str(client_data["classement"], 17)): + champs_modifies.append("classement") + + 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: + logger.warning(f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]") + else: + if try_set_attribute(client, "CT_Raccourci", raccourci): + champs_modifies.append("raccourci") + except: + if try_set_attribute(client, "CT_Raccourci", raccourci): + champs_modifies.append("raccourci") + + if "siret" in client_data: + if try_set_attribute(client, "CT_Siret", clean_str(client_data["siret"], 15)): + champs_modifies.append("siret") + + if "tva_intra" in client_data: + if try_set_attribute(client, "CT_Identifiant", clean_str(client_data["tva_intra"], 25)): + champs_modifies.append("tva_intra") + + if "code_naf" in client_data: + 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)") + logger.info(f" CT_Contact (client) = {contact_nom} [OK]") + 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 + champs_modifies.append("contact (adresse)") + logger.info(f" Contact (adresse) = {contact_nom} [OK]") + except Exception as e: + logger.warning(f" Contact (adresse) [ECHEC: {e}]") + + # Autres champs adresse adresse_obj = client.Adresse - + if "adresse" in client_data: - adresse = str(client_data["adresse"])[:35].strip() - adresse_obj.Adresse = adresse + adresse_obj.Adresse = clean_str(client_data["adresse"], 35) champs_modifies.append("adresse") - + + if "complement" in client_data: + adresse_obj.Complement = clean_str(client_data["complement"], 35) + champs_modifies.append("complement") + if "code_postal" in client_data: - cp = str(client_data["code_postal"])[:9].strip() - adresse_obj.CodePostal = cp + adresse_obj.CodePostal = clean_str(client_data["code_postal"], 9) champs_modifies.append("code_postal") - + if "ville" in client_data: - ville = str(client_data["ville"])[:35].strip() - adresse_obj.Ville = ville + adresse_obj.Ville = clean_str(client_data["ville"], 35) champs_modifies.append("ville") - + + if "region" in client_data: + adresse_obj.CodeRegion = clean_str(client_data["region"], 25) + champs_modifies.append("region") + if "pays" in client_data: - pays = str(client_data["pays"])[:35].strip() - adresse_obj.Pays = pays + adresse_obj.Pays = clean_str(client_data["pays"], 35) champs_modifies.append("pays") - + + logger.info(f" Adresse mise à jour ({len([k for k in adresse_keys if k in client_data])} champs)") + except Exception as e: - logger.warning(f"Erreur mise à jour adresse: {e}") + logger.error(f" Adresse erreur: {e}") - # Télécom - if "email" in client_data or "telephone" in client_data: + # ============================================================ + # ÉTAPE 4 : TELECOM + # ============================================================ + telecom_keys = ["telephone", "telecopie", "email", "site_web", "portable", "facebook", "linkedin"] + + if any(k in client_data for k in telecom_keys): + logger.info("[ETAPE 4] TELECOM") + try: telecom_obj = client.Telecom - - if "email" in client_data: - email = str(client_data["email"])[:69].strip() - telecom_obj.EMail = email - champs_modifies.append("email") - + if "telephone" in client_data: - tel = str(client_data["telephone"])[:21].strip() - telecom_obj.Telephone = tel + telecom_obj.Telephone = clean_str(client_data["telephone"], 21) champs_modifies.append("telephone") - + + if "telecopie" in client_data: + telecom_obj.Telecopie = clean_str(client_data["telecopie"], 21) + champs_modifies.append("telecopie") + + if "email" in client_data: + telecom_obj.EMail = clean_str(client_data["email"], 69) + champs_modifies.append("email") + + if "site_web" in client_data: + telecom_obj.Site = clean_str(client_data["site_web"], 69) + champs_modifies.append("site_web") + + if "portable" in client_data: + portable = clean_str(client_data["portable"], 21) + if try_set_attribute(telecom_obj, "Portable", portable): + champs_modifies.append("portable") + + if "facebook" in client_data: + facebook = clean_str(client_data["facebook"], 69) + if not try_set_attribute(telecom_obj, "Facebook", facebook): + try_set_attribute(client, "CT_Facebook", facebook) + champs_modifies.append("facebook") + + if "linkedin" in client_data: + linkedin = clean_str(client_data["linkedin"], 69) + if not try_set_attribute(telecom_obj, "LinkedIn", linkedin): + try_set_attribute(client, "CT_LinkedIn", linkedin) + champs_modifies.append("linkedin") + + logger.info(f" Telecom mis à jour ({len([k for k in telecom_keys if k in client_data])} champs)") + except Exception as e: - logger.warning(f"Erreur mise à jour télécom: {e}") + logger.error(f" Telecom erreur: {e}") - # SIRET - if "siret" in client_data: + # ============================================================ + # ÉTAPE 5 : COMPTE GENERAL + # ============================================================ + if "compte_general" in client_data: + logger.info("[ETAPE 5] COMPTE GENERAL") + + compte = clean_str(client_data["compte_general"], 13) + factory_compte = self.cial.CptaApplication.FactoryCompteG + try: - siret = str(client_data["siret"])[:14].strip() - client.CT_Siret = siret - champs_modifies.append("siret") + persist_compte = factory_compte.ReadNumero(compte) + if persist_compte: + compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3") + compte_obj.Read() + + type_compte = getattr(compte_obj, 'CG_Type', None) + if type_compte == 0: + client.CompteGPrinc = compte_obj + champs_modifies.append("compte_general") + logger.info(f" CompteGPrinc = {compte} [OK]") + else: + logger.warning(f" Compte {compte} - Type {type_compte} incompatible") except Exception as e: - logger.warning(f"Erreur mise à jour SIRET: {e}") + logger.warning(f" CompteGPrinc erreur: {e}") - # TVA Intracommunautaire - if "tva_intra" in client_data: - try: - tva = str(client_data["tva_intra"])[:25].strip() - client.CT_Identifiant = tva - champs_modifies.append("tva_intra") - except Exception as e: - logger.warning(f"Erreur mise à jour TVA: {e}") + # ============================================================ + # ÉTAPE 6 : CATEGORIES + # ============================================================ + if "categorie_tarifaire" in client_data or "categorie_comptable" in client_data: + logger.info("[ETAPE 6] CATEGORIES") + + if "categorie_tarifaire" in client_data: + try: + cat_id = str(client_data["categorie_tarifaire"]) + factory_cat_tarif = self.cial.CptaApplication.FactoryCategorieTarif + persist_cat = factory_cat_tarif.ReadIntitule(cat_id) + + if persist_cat: + cat_tarif_obj = win32com.client.CastTo(persist_cat, "IBOCategorieTarif3") + cat_tarif_obj.Read() + client.CatTarif = cat_tarif_obj + champs_modifies.append("categorie_tarifaire") + logger.info(f" CatTarif = {cat_id} [OK]") + except Exception as e: + logger.warning(f" CatTarif erreur: {e}") + + if "categorie_comptable" in client_data: + try: + cat_id = str(client_data["categorie_comptable"]) + factory_cat_compta = self.cial.CptaApplication.FactoryCategorieCompta + persist_cat = factory_cat_compta.ReadIntitule(cat_id) + + if persist_cat: + cat_compta_obj = win32com.client.CastTo(persist_cat, "IBOCategorieCompta3") + cat_compta_obj.Read() + client.CatCompta = cat_compta_obj + champs_modifies.append("categorie_comptable") + logger.info(f" CatCompta = {cat_id} [OK]") + 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}" + if key in client_data: + if not taux_modifies: + logger.info("[ETAPE 7] TAUX") + taux_modifies = True + + val = safe_float(client_data[key]) + 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 + + stat01 = client_data.get("statistique01") or client_data.get("secteur") + if stat01: + if not stat_modifies: + logger.info("[ETAPE 8] STATISTIQUES") + stat_modifies = True + + if try_set_attribute(client, "CT_Statistique01", clean_str(stat01, 21)): + champs_modifies.append("statistique01") + + for i in range(2, 11): + key = f"statistique{i:02d}" + if key in client_data: + if not stat_modifies: + logger.info("[ETAPE 8] STATISTIQUES") + stat_modifies = True + + 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): + logger.info("[ETAPE 9] COMMERCIAL") + + if "encours_autorise" in client_data: + if try_set_attribute(client, "CT_Encours", safe_float(client_data["encours_autorise"])): + champs_modifies.append("encours_autorise") + + if "assurance_credit" in client_data: + if try_set_attribute(client, "CT_Assurance", safe_float(client_data["assurance_credit"])): + champs_modifies.append("assurance_credit") + + if "langue" in client_data: + if try_set_attribute(client, "CT_Langue", safe_int(client_data["langue"])): + champs_modifies.append("langue") + + if "commercial_code" in client_data: + co_no = safe_int(client_data["commercial_code"]) + if not try_set_attribute(client, "CO_No", co_no): + try: + factory_collab = self.cial.CptaApplication.FactoryCollaborateur + persist_collab = factory_collab.ReadIntitule(str(co_no)) + if persist_collab: + collab_obj = win32com.client.CastTo(persist_collab, "IBOCollaborateur3") + collab_obj.Read() + client.Collaborateur = collab_obj + champs_modifies.append("commercial_code") + logger.info(f" Collaborateur = {co_no} [OK]") + 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", + "exclure_relance", "exclure_penalites", "bon_a_payer" + ] + + if any(k in client_data for k in facturation_keys): + logger.info("[ETAPE 10] FACTURATION") + + if "lettrage_auto" in client_data: + if try_set_attribute(client, "CT_Lettrage", 1 if client_data["lettrage_auto"] else 0): + champs_modifies.append("lettrage_auto") + + if "est_actif" in client_data: + if try_set_attribute(client, "CT_Sommeil", 0 if client_data["est_actif"] else 1): + champs_modifies.append("est_actif") + + if "type_facture" in client_data: + if try_set_attribute(client, "CT_Facture", safe_int(client_data["type_facture"])): + champs_modifies.append("type_facture") + + if "est_prospect" in client_data: + if try_set_attribute(client, "CT_Prospect", 1 if client_data["est_prospect"] else 0): + champs_modifies.append("est_prospect") + + factu_map = { + "CT_BLFact": "bl_en_facture", + "CT_Saut": "saut_page", + "CT_ValidEch": "validation_echeance", + "CT_ControlEnc": "controle_encours", + "CT_NotRappel": "exclure_relance", + "CT_NotPenal": "exclure_penalites", + "CT_BonAPayer": "bon_a_payer", + } + + for attr, key in factu_map.items(): + if key in client_data: + 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): + logger.info("[ETAPE 11] LOGISTIQUE") + + logistique_map = { + "CT_PrioriteLivr": "priorite_livraison", + "CT_LivrPartielle": "livraison_partielle", + "CT_DelaiTransport": "delai_transport", + "CT_DelaiAppro": "delai_appro", + } + + for attr, key in logistique_map.items(): + if key in client_data: + 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", + "sv_objet_maj", "ca_annuel", "sv_chiffre_affaires", "sv_resultat" + ] + + 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): + try: + factory_mr = self.cial.CptaApplication.FactoryModeRegl + persist_mr = factory_mr.ReadIntitule(str(mr_no)) + if persist_mr: + mr_obj = win32com.client.CastTo(persist_mr, "IBOModeRegl3") + mr_obj.Read() + client.ModeRegl = mr_obj + champs_modifies.append("mode_reglement_code") + logger.info(f" ModeRegl = {mr_no} [OK]") + 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: + client.CT_Surveillance = surveillance + champs_modifies.append("surveillance_active") + logger.info(f" CT_Surveillance = {surveillance} [OK]") + 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: + client.CT_Coface = coface + champs_modifies.append("coface") + logger.info(f" CT_Coface = {coface} [OK]") + 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") + + if "effectif" in client_data: + if try_set_attribute(client, "CT_SvEffectif", clean_str(client_data["effectif"], 11)): + champs_modifies.append("effectif") + + if "sv_regularite" in client_data: + if try_set_attribute(client, "CT_SvRegul", clean_str(client_data["sv_regularite"], 3)): + champs_modifies.append("sv_regularite") + + if "sv_cotation" in client_data: + if try_set_attribute(client, "CT_SvCotation", clean_str(client_data["sv_cotation"], 5)): + champs_modifies.append("sv_cotation") + + if "sv_objet_maj" in client_data: + if try_set_attribute(client, "CT_SvObjetMaj", clean_str(client_data["sv_objet_maj"], 61)): + champs_modifies.append("sv_objet_maj") + + ca = client_data.get("ca_annuel") or client_data.get("sv_chiffre_affaires") + if ca: + if try_set_attribute(client, "CT_SvCA", safe_float(ca)): + champs_modifies.append("ca_annuel/sv_chiffre_affaires") + + if "sv_resultat" in client_data: + 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) - logger.info(f" Champs à modifier: {', '.join(champs_modifies)}") - - # ======================================== - # ÉTAPE 3 : ÉCRIRE LES MODIFICATIONS - # ======================================== - logger.info(" Écriture des modifications...") + logger.info("=" * 80) + logger.info(f"[WRITE] {len(champs_modifies)} champs modifiés:") + for i, champ in enumerate(champs_modifies, 1): + logger.info(f" {i}. {champ}") + logger.info("=" * 80) try: client.Write() - logger.info(" Write() réussi !") - + client.Read() + logger.info("[OK] Write réussi") except Exception as e: error_detail = str(e) - try: sage_error = self.cial.CptaApplication.LastError if sage_error: - error_detail = ( - f"{sage_error.Description} (Code: {sage_error.Number})" - ) + error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" except: pass + + logger.error(f"[ERREUR] {error_detail}") + raise RuntimeError(f"Echec Write(): {error_detail}") - logger.error(f" Erreur Write(): {error_detail}") - raise RuntimeError(f"Échec modification: {error_detail}") - - # ======================================== - # ÉTAPE 4 : RELIRE ET RETOURNER - # ======================================== - client.Read() - - logger.info( - f" CLIENT MODIFIÉ: {code} ({len(champs_modifies)} champs) " - ) - - # Refresh cache + logger.info("=" * 80) + logger.info(f"[SUCCES] CLIENT MODIFIÉ: {code} ({len(champs_modifies)} champs)") + logger.info("=" * 80) return self._extraire_client(client) except ValueError as e: - logger.error(f" Erreur métier: {e}") + logger.error(f"[ERREUR VALIDATION] {e}") raise - except Exception as e: - logger.error(f" Erreur modification client: {e}", exc_info=True) - - error_message = str(e) - if self.cial: - try: - err = self.cial.CptaApplication.LastError - if err: - error_message = f"Erreur Sage: {err.Description}" - except: - pass - - raise RuntimeError(f"Erreur technique Sage: {error_message}") - + logger.error(f"[ERREUR] {e}", exc_info=True) + raise RuntimeError(f"Erreur technique: {e}") + def creer_commande_enrichi(self, commande_data: dict) -> Dict: """ Crée une commande dans Sage avec support des dates.