From 97a2bc01f0dbfeed1770e608c154e93093b8755f Mon Sep 17 00:00:00 2001 From: fanilo Date: Mon, 5 Jan 2026 21:00:58 +0100 Subject: [PATCH] Added better handling for collaborator --- sage_connector.py | 612 ++++++++++++++++++++++++++-------------------- 1 file changed, 350 insertions(+), 262 deletions(-) diff --git a/sage_connector.py b/sage_connector.py index 5eec384..bd2f2f4 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -7390,310 +7390,398 @@ class SageConnector: return None def creer_collaborateur(self, data: dict) -> dict: - """Crée un nouveau collaborateur via COM Sage""" + """Crée un nouveau collaborateur via COM Sage (BOI)""" if not self.cial: raise RuntimeError("Connexion Sage non établie") - logger.info( - f"📝 Début création collaborateur: {data.get('nom')} {data.get('prenom', '')}" - ) + # Validation préalable + if not data.get("nom"): + raise ValueError("Le champ 'nom' est obligatoire") + + nom_upper = str(data["nom"]).upper().strip()[:35] + prenom = str(data.get("prenom", "")).strip()[:35] if data.get("prenom") else "" + + logger.info(f"\n{'=' * 70}") + logger.info(f"📝 CRÉATION COLLABORATEUR: {nom_upper} {prenom}") + logger.info(f"{'=' * 70}") try: with self._com_context(), self._lock_com: - transaction_active = False - - try: - # Démarrage transaction - try: - self.cial.CptaApplication.BeginTrans() - transaction_active = True - logger.debug("✓ Transaction Sage démarrée") - except Exception as e: - logger.warning(f"BeginTrans échoué (non critique): {e}") - - # ===== FACTORY COLLABORATEUR ===== - factory_collab = self.cial.FactoryCollaborateur - persist_collab = factory_collab.Create() - - # Cast vers l'interface IBOCollaborateur3 - try: - collab_obj = win32com.client.CastTo( - persist_collab, "IBOCollaborateur3" - ) - except Exception: - # Fallback si IBOCollaborateur3 n'existe pas - collab_obj = win32com.client.CastTo( - persist_collab, "IBOCollaborateur" - ) - - logger.info("✓ Objet collaborateur créé") - - # ===== MAPPING ET AFFECTATION CHAMPS ===== - mapping = { - "nom": "CO_Nom", - "prenom": "CO_Prenom", - "fonction": "CO_Fonction", - "adresse": "CO_Adresse", - "complement": "CO_Complement", - "code_postal": "CO_CodePostal", - "ville": "CO_Ville", - "region": "CO_CodeRegion", - "pays": "CO_Pays", - "service": "CO_Service", - "telephone": "CO_Telephone", - "telecopie": "CO_Telecopie", - "email": "CO_EMail", - "tel_portable": "CO_TelPortable", - "matricule": "CO_Matricule", - "facebook": "CO_Facebook", - "linkedin": "CO_LinkedIn", - "skype": "CO_Skype", - "chef_ventes_numero": "CO_NoChefVentes", - } - - # Champs texte - for py_field, sage_field in mapping.items(): - if py_field in data and data[py_field] is not None: - try: - setattr(collab_obj, sage_field, str(data[py_field])) - logger.debug(f" ✓ {sage_field}: {data[py_field]}") - except Exception as e: - logger.warning(f" ⚠ {sage_field} non défini: {e}") - - # ===== CHAMPS BOOLÉENS ===== - bool_mapping = { - "est_vendeur": "CO_Vendeur", - "est_caissier": "CO_Caissier", - "est_acheteur": "CO_Acheteur", - "est_chef_ventes": "CO_ChefVentes", - } - - for py_field, sage_field in bool_mapping.items(): - if py_field in data: - try: - valeur = 1 if data[py_field] else 0 - setattr(collab_obj, sage_field, valeur) - logger.debug(f" ✓ {sage_field}: {valeur}") - except Exception as e: - logger.warning(f" ⚠ {sage_field} non défini: {e}") - - # ===== CO_SOMMEIL (inversé: est_actif → 0=actif, 1=inactif) ===== - if "est_actif" in data: - try: - valeur_sommeil = 0 if data["est_actif"] else 1 - collab_obj.CO_Sommeil = valeur_sommeil - logger.debug( - f" ✓ CO_Sommeil: {valeur_sommeil} (actif={data['est_actif']})" - ) - except Exception as e: - logger.warning(f" ⚠ CO_Sommeil non défini: {e}") - - # ===== WRITE ===== - collab_obj.Write() - logger.info("💾 Collaborateur écrit dans Sage") - - # Commit transaction - if transaction_active: - try: - self.cial.CptaApplication.CommitTrans() - logger.info("✓ Transaction committée") - except Exception: - pass - - # ===== RÉCUPÉRATION DU NUMÉRO ===== - try: - collab_obj.Read() - numero = getattr(collab_obj, "CO_No", None) - except Exception: - numero = None - - if not numero: - raise RuntimeError( - "Numéro de collaborateur vide après création" - ) - - logger.info(f"📄 Collaborateur créé: CO_No={numero}") - - # ===== RELECTURE POUR RETOUR COMPLET ===== - collaborateur_final = self.lire_collaborateur(numero) - - if not collaborateur_final: - raise RuntimeError( - f"Impossible de relire le collaborateur {numero}" - ) - - logger.info( - f"✅ COLLABORATEUR CRÉÉ: {numero} - " - f"{collaborateur_final['nom']} {collaborateur_final.get('prenom', '')}" + # ===== VÉRIFICATION DOUBLON VIA SQL ===== + logger.info("🔍 Vérification doublon...") + with self._get_sql_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT CO_No FROM F_COLLABORATEUR WHERE CO_Nom = ? AND CO_Prenom = ?", + (nom_upper, prenom), ) + existing = cursor.fetchone() + if existing: + raise ValueError( + f"Le collaborateur '{nom_upper} {prenom}' existe déjà (N°{existing[0]})" + ) + logger.info("✓ Pas de doublon") - return collaborateur_final + # ===== FACTORY + CREATE ===== + try: + factory = self.cial.FactoryCollaborateur + except AttributeError: + factory = self.cial.CptaApplication.FactoryCollaborateur - except Exception: - if transaction_active: + persist = factory.Create() + + # Cast vers interface + collab = None + for iface in [ + "IBOCollaborateur3", + "IBOCollaborateur2", + "IBOCollaborateur", + ]: + try: + collab = win32com.client.CastTo(persist, iface) + logger.info(f"✓ Cast vers {iface}") + break + except: + pass + if not collab: + collab = persist + + # ===== SETDEFAULT ===== + try: + collab.SetDefault() + logger.info("✓ SetDefault()") + except Exception as e: + logger.warning(f"SetDefault() ignoré: {e}") + + # ===== HELPER ===== + def safe_set(obj, attr, value, max_len=None): + """Affecte une valeur de manière sécurisée""" + if value is None or value == "": + return False + try: + val = str(value) + if max_len: + val = val[:max_len] + setattr(obj, attr, val) + logger.debug(f" ✓ {attr} = '{val}'") + return True + except Exception as e: + logger.warning(f" ✗ {attr}: {e}") + return False + + # ===== CHAMPS DIRECTS SUR COLLABORATEUR ===== + logger.info("📝 Champs directs...") + + # Obligatoire + safe_set(collab, "Nom", nom_upper, 35) + + # Optionnels + safe_set(collab, "Prenom", prenom, 35) + safe_set(collab, "Fonction", data.get("fonction"), 35) + safe_set(collab, "Service", data.get("service"), 35) + safe_set(collab, "Matricule", data.get("matricule"), 10) + safe_set(collab, "Facebook", data.get("facebook"), 35) + safe_set(collab, "LinkedIn", data.get("linkedin"), 35) + safe_set(collab, "Skype", data.get("skype"), 35) + + # ===== SOUS-OBJET ADRESSE ===== + logger.info("📍 Adresse...") + try: + adresse_obj = collab.Adresse + safe_set(adresse_obj, "Adresse", data.get("adresse"), 35) + safe_set(adresse_obj, "Complement", data.get("complement"), 35) + safe_set(adresse_obj, "CodePostal", data.get("code_postal"), 9) + safe_set(adresse_obj, "Ville", data.get("ville"), 35) + safe_set(adresse_obj, "CodeRegion", data.get("code_region"), 25) + safe_set(adresse_obj, "Pays", data.get("pays"), 35) + except Exception as e: + logger.warning(f"⚠️ Erreur Adresse: {e}") + + # ===== SOUS-OBJET TELECOM ===== + logger.info("📞 Telecom...") + try: + telecom_obj = collab.Telecom + safe_set(telecom_obj, "Telephone", data.get("telephone"), 21) + safe_set(telecom_obj, "Telecopie", data.get("telecopie"), 21) + safe_set(telecom_obj, "EMail", data.get("email"), 69) + safe_set(telecom_obj, "Portable", data.get("tel_portable"), 21) + except Exception as e: + logger.warning(f"⚠️ Erreur Telecom: {e}") + + # ===== CHAMPS BOOLÉENS (seulement si True) ===== + logger.info("🔘 Booléens...") + if data.get("vendeur") is True: + try: + collab.Vendeur = True + logger.debug(" ✓ Vendeur = True") + except: + pass + if data.get("caissier") is True: + try: + collab.Caissier = True + except: + pass + if data.get("acheteur") is True: + try: + collab.Acheteur = True + except: + pass + if data.get("sommeil") is True: + try: + collab.Sommeil = True + except: + pass + if data.get("chef_ventes") is True: + try: + collab.ChefVentes = True + except: + pass + + # ===== WRITE ===== + logger.info("💾 Write()...") + try: + collab.Write() + logger.info("✅ Write() RÉUSSI!") + except Exception as e: + logger.error(f"❌ Write() échoué: {e}") + raise RuntimeError(f"Échec Write(): {e}") + + # ===== RÉCUPÉRATION DU NUMÉRO ===== + numero_cree = None + + # Via Read() + try: + collab.Read() + for attr in ["No", "CO_No", "Numero"]: try: - self.cial.CptaApplication.RollbackTrans() - logger.error("❌ Transaction annulée (rollback)") - except Exception: + val = getattr(collab, attr) + if val and isinstance(val, int): + numero_cree = val + break + except: pass - raise + except: + pass + # Via SQL si pas trouvé + if not numero_cree: + try: + with self._get_sql_connection() as conn: + cursor = conn.cursor() + query = f"SELECT CO_No FROM F_COLLABORATEUR WHERE CO_Nom = '{nom_upper}'" + if prenom: + query += f" AND CO_Prenom = '{prenom}'" + cursor.execute(query) + row = cursor.fetchone() + if row: + numero_cree = row[0] + except Exception as e: + logger.warning(f"SQL récup numéro: {e}") + + logger.info(f"\n{'=' * 70}") + logger.info( + f"✅ COLLABORATEUR CRÉÉ: N°{numero_cree} - {nom_upper} {prenom}" + ) + logger.info(f"{'=' * 70}") + + # Retourner le collaborateur + if numero_cree: + return self.lire_collaborateur(numero_cree) + else: + return {"nom": nom_upper, "prenom": prenom, "status": "créé"} + + except ValueError as e: + logger.warning(f"⚠️ Validation: {e}") + raise except Exception as e: - logger.error(f"❌ ERREUR CRÉATION COLLABORATEUR: {e}", exc_info=True) + logger.error(f"❌ Erreur création collaborateur: {e}", exc_info=True) raise RuntimeError(f"Échec création collaborateur: {str(e)}") def modifier_collaborateur(self, numero: int, data: dict) -> dict: - """Modifie un collaborateur existant via COM Sage""" + """Modifie un collaborateur existant via COM Sage (BOI)""" if not self.cial: raise RuntimeError("Connexion Sage non établie") - logger.info(f"📝 Début modification collaborateur CO_No={numero}") + logger.info(f"\n{'=' * 70}") + logger.info(f"📝 MODIFICATION COLLABORATEUR N°{numero}") + logger.info(f"{'=' * 70}") try: with self._com_context(), self._lock_com: - transaction_active = False - + # ===== LECTURE DU COLLABORATEUR EXISTANT ===== try: - # Démarrage transaction + factory = self.cial.FactoryCollaborateur + except AttributeError: + factory = self.cial.CptaApplication.FactoryCollaborateur + + # Lire par numéro + try: + persist = factory.ReadNumero(numero) + except Exception as e: + raise ValueError(f"Collaborateur {numero} introuvable: {e}") + + if not persist: + raise ValueError(f"Collaborateur {numero} introuvable") + + # Cast vers interface + collab = None + for iface in [ + "IBOCollaborateur3", + "IBOCollaborateur2", + "IBOCollaborateur", + ]: try: - self.cial.CptaApplication.BeginTrans() - transaction_active = True - logger.debug("✓ Transaction Sage démarrée") - except Exception as e: - logger.warning(f"BeginTrans échoué (non critique): {e}") + collab = win32com.client.CastTo(persist, iface) + logger.info(f"✓ Cast vers {iface}") + break + except: + pass + if not collab: + collab = persist - # ===== LECTURE DU COLLABORATEUR EXISTANT ===== - factory_collab = self.cial.FactoryCollaborateur - persist_collab = factory_collab.ReadNumero(numero) - - if not persist_collab: - raise ValueError(f"Collaborateur {numero} introuvable") - - # Cast vers l'interface IBOCollaborateur3 - try: - collab_obj = win32com.client.CastTo( - persist_collab, "IBOCollaborateur3" - ) - except Exception: - collab_obj = win32com.client.CastTo( - persist_collab, "IBOCollaborateur" - ) - - collab_obj.Read() + # Charger les données actuelles + try: + collab.Read() logger.info(f"✓ Collaborateur {numero} chargé") + except Exception as e: + logger.warning(f"Read() ignoré: {e}") - # ===== MAPPING ET MODIFICATION CHAMPS ===== - mapping = { - "nom": "CO_Nom", - "prenom": "CO_Prenom", - "fonction": "CO_Fonction", - "adresse": "CO_Adresse", - "complement": "CO_Complement", - "code_postal": "CO_CodePostal", - "ville": "CO_Ville", - "region": "CO_CodeRegion", - "pays": "CO_Pays", - "service": "CO_Service", - "telephone": "CO_Telephone", - "telecopie": "CO_Telecopie", - "email": "CO_EMail", - "tel_portable": "CO_TelPortable", - "matricule": "CO_Matricule", - "facebook": "CO_Facebook", - "linkedin": "CO_LinkedIn", - "skype": "CO_Skype", - "chef_ventes_numero": "CO_NoChefVentes", + # ===== HELPER ===== + def safe_set(obj, attr, value, max_len=None): + """Affecte une valeur de manière sécurisée""" + if value is None: + return False + try: + val = str(value) if not isinstance(value, bool) else value + if max_len and isinstance(val, str): + val = val[:max_len] + setattr(obj, attr, val) + logger.debug(f" ✓ {attr} = '{val}'") + return True + except Exception as e: + logger.warning(f" ✗ {attr}: {e}") + return False + + champs_modifies = [] + + # ===== CHAMPS DIRECTS SUR COLLABORATEUR ===== + logger.info("📝 Champs directs...") + + champs_directs = { + "nom": ("Nom", 35), + "prenom": ("Prenom", 35), + "fonction": ("Fonction", 35), + "service": ("Service", 35), + "matricule": ("Matricule", 10), + "facebook": ("Facebook", 35), + "linkedin": ("LinkedIn", 35), + "skype": ("Skype", 35), + } + + for py_field, (sage_attr, max_len) in champs_directs.items(): + if py_field in data: + val = data[py_field] + # Cas spécial: nom en majuscules + if py_field == "nom" and val: + val = str(val).upper().strip() + if safe_set(collab, sage_attr, val, max_len): + champs_modifies.append(sage_attr) + + # ===== SOUS-OBJET ADRESSE ===== + logger.info("📍 Adresse...") + try: + adresse_obj = collab.Adresse + + champs_adresse = { + "adresse": ("Adresse", 35), + "complement": ("Complement", 35), + "code_postal": ("CodePostal", 9), + "ville": ("Ville", 35), + "code_region": ("CodeRegion", 25), + "pays": ("Pays", 35), } - champs_modifies = [] - - # Champs texte - for py_field, sage_field in mapping.items(): + for py_field, (sage_attr, max_len) in champs_adresse.items(): if py_field in data: - try: - valeur = ( - str(data[py_field]) - if data[py_field] is not None - else "" - ) - setattr(collab_obj, sage_field, valeur) - champs_modifies.append(sage_field) - logger.debug(f" ✓ {sage_field}: {valeur}") - except Exception as e: - logger.warning(f" ⚠ {sage_field} non modifié: {e}") + if safe_set( + adresse_obj, sage_attr, data[py_field], max_len + ): + champs_modifies.append(f"Adresse.{sage_attr}") - # ===== CHAMPS BOOLÉENS ===== - bool_mapping = { - "est_vendeur": "CO_Vendeur", - "est_caissier": "CO_Caissier", - "est_acheteur": "CO_Acheteur", - "est_chef_ventes": "CO_ChefVentes", + except Exception as e: + logger.warning(f"⚠️ Erreur accès Adresse: {e}") + + # ===== SOUS-OBJET TELECOM ===== + logger.info("📞 Telecom...") + try: + telecom_obj = collab.Telecom + + champs_telecom = { + "telephone": ("Telephone", 21), + "telecopie": ("Telecopie", 21), + "email": ("EMail", 69), + "tel_portable": ("Portable", 21), } - for py_field, sage_field in bool_mapping.items(): + for py_field, (sage_attr, max_len) in champs_telecom.items(): if py_field in data: - try: - valeur = 1 if data[py_field] else 0 - setattr(collab_obj, sage_field, valeur) - champs_modifies.append(sage_field) - logger.debug(f" ✓ {sage_field}: {valeur}") - except Exception as e: - logger.warning(f" ⚠ {sage_field} non modifié: {e}") + if safe_set( + telecom_obj, sage_attr, data[py_field], max_len + ): + champs_modifies.append(f"Telecom.{sage_attr}") - # ===== CO_SOMMEIL (inversé) ===== - if "est_actif" in data: + except Exception as e: + logger.warning(f"⚠️ Erreur accès Telecom: {e}") + + # ===== CHAMPS BOOLÉENS ===== + logger.info("🔘 Booléens...") + + champs_bool = { + "vendeur": "Vendeur", + "caissier": "Caissier", + "acheteur": "Acheteur", + "sommeil": "Sommeil", + "chef_ventes": "ChefVentes", + } + + for py_field, sage_attr in champs_bool.items(): + if py_field in data and data[py_field] is not None: try: - valeur_sommeil = 0 if data["est_actif"] else 1 - collab_obj.CO_Sommeil = valeur_sommeil - champs_modifies.append("CO_Sommeil") - logger.debug(f" ✓ CO_Sommeil: {valeur_sommeil}") + val = bool(data[py_field]) + setattr(collab, sage_attr, val) + champs_modifies.append(sage_attr) + logger.debug(f" ✓ {sage_attr} = {val}") except Exception as e: - logger.warning(f" ⚠ CO_Sommeil non modifié: {e}") + logger.warning(f" ✗ {sage_attr}: {e}") - if not champs_modifies: - logger.info("ℹ Aucun champ à modifier") - return self.lire_collaborateur(numero) + # ===== VÉRIFICATION ===== + if not champs_modifies: + logger.info("ℹ️ Aucun champ à modifier") + return self.lire_collaborateur(numero) - # ===== WRITE ===== - collab_obj.Write() - logger.info( - f"💾 Collaborateur modifié ({len(champs_modifies)} champs)" - ) + logger.info( + f"📋 {len(champs_modifies)} champ(s) à modifier: {champs_modifies}" + ) - # Commit transaction - if transaction_active: - try: - self.cial.CptaApplication.CommitTrans() - logger.info("✓ Transaction committée") - except Exception: - pass + # ===== WRITE ===== + logger.info("💾 Write()...") + try: + collab.Write() + logger.info("✅ Write() RÉUSSI!") + except Exception as e: + logger.error(f"❌ Write() échoué: {e}") + raise RuntimeError(f"Échec Write(): {e}") - # ===== RELECTURE POUR RETOUR COMPLET ===== - collaborateur_final = self.lire_collaborateur(numero) + # ===== RETOUR ===== + logger.info(f"\n{'=' * 70}") + logger.info(f"✅ COLLABORATEUR MODIFIÉ: N°{numero}") + logger.info(f"{'=' * 70}") - if not collaborateur_final: - raise RuntimeError( - f"Impossible de relire le collaborateur {numero}" - ) - - logger.info( - f"✅ COLLABORATEUR MODIFIÉ: {numero} - " - f"{collaborateur_final['nom']} {collaborateur_final.get('prenom', '')}" - ) - - return collaborateur_final - - except Exception: - if transaction_active: - try: - self.cial.CptaApplication.RollbackTrans() - logger.error("❌ Transaction annulée (rollback)") - except Exception: - pass - raise + return self.lire_collaborateur(numero) + except ValueError as e: + logger.warning(f"⚠️ Validation: {e}") + raise except Exception as e: - logger.error( - f"❌ ERREUR MODIFICATION COLLABORATEUR {numero}: {e}", exc_info=True - ) + logger.error(f"❌ Erreur modification collaborateur: {e}", exc_info=True) raise RuntimeError(f"Échec modification collaborateur: {str(e)}")