diff --git a/main.py b/main.py index 30eeb13..2e5606f 100644 --- a/main.py +++ b/main.py @@ -260,16 +260,16 @@ def devis_list( raise HTTPException(500, str(e)) -@app.post("/sage/devis/statut", dependencies=[Depends(verify_token)]) -def changer_statut_devis_endpoint(numero: str, nouveau_statut: int): - """Change le statut d'un devis""" +@app.post("/sage/document/statut", dependencies=[Depends(verify_token)]) +def changer_statut_document(numero: str, type_doc: int, nouveau_statut: int): + """Change le statut d'un document""" try: with sage._com_context(), sage._lock_com: factory = sage.cial.FactoryDocumentVente - persist = factory.ReadPiece(0, numero) + persist = factory.ReadPiece(type_doc, numero) if not persist: - raise HTTPException(404, f"Devis {numero} introuvable") + raise HTTPException(404, f"Document {numero} introuvable") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() @@ -278,7 +278,7 @@ def changer_statut_devis_endpoint(numero: str, nouveau_statut: int): doc.DO_Statut = nouveau_statut doc.Write() - logger.info(f" Statut devis {numero}: {statut_actuel} → {nouveau_statut}") + logger.info(f" Statut document {numero}: {statut_actuel} → {nouveau_statut}") return { "success": True, diff --git a/sage_connector.py b/sage_connector.py index 6697620..842448c 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -1,20 +1,14 @@ import win32com.client import pythoncom # AJOUT CRITIQUE -from datetime import datetime, timedelta, date -from typing import Dict, List, Optional, Any +from datetime import datetime, date +from typing import Dict, List, Optional import threading import time import logging -from config import settings, validate_settings +from config import settings import pyodbc from contextlib import contextmanager import pywintypes -import os -import glob -import tempfile -from dataclasses import dataclass, field -import zlib -import struct from utils.articles.articles_data_sql import ( _enrichir_stock_emplacements, @@ -52,6 +46,7 @@ from utils.functions.functions import ( _clean_str, _try_set_attribute, normaliser_date, + _get_type_libelle ) from utils.functions.items_to_dict import ( @@ -75,9 +70,13 @@ from utils.documents.documents_data_sql import ( ) from utils.documents.devis.devis_extraction import _extraire_infos_devis -from utils.documents.devis.devis_check import _relire_devis, _recuperer_numero_devis +from utils.documents.devis.devis_check import _recuperer_numero_devis, _rechercher_devis_dans_liste -from utils.tiers.contacts.contacts import _get_contacts_client +from utils.tiers.contacts.contacts import ( + _get_contacts_client, + _chercher_contact_en_base, + _lire_contact_depuis_base + ) logger = logging.getLogger(__name__) @@ -1517,13 +1516,12 @@ class SageConnector: cursor = conn.cursor() return _lire_document_sql(cursor, numero, type_doc=5) - def creer_devis_enrichi(self, devis_data: dict, forcer_brouillon: bool = False): + def creer_devis_enrichi(self, devis_data: dict): if not self.cial: raise RuntimeError("Connexion Sage non établie") logger.info( f" Début création devis pour client {devis_data['client']['code']} " - f"(brouillon={forcer_brouillon})" ) try: @@ -1574,13 +1572,8 @@ class SageConnector: doc.SetDefaultClient(client_obj) logger.info(f" Client {devis_data['client']['code']} associé") - - if forcer_brouillon: - doc.DO_Statut = 0 - logger.info(" Statut défini: 0 (Brouillon)") - else: - doc.DO_Statut = 2 - logger.info(" Statut défini: 2 (Accepté)") + doc.DO_Statut = 0 + #logger.info(" Statut défini: 0 (Accepté)") doc.Write() @@ -1673,15 +1666,11 @@ class SageConnector: doc.Write() - if not forcer_brouillon: - logger.info(" Lancement Process()...") + try: process.Process() - else: - try: - process.Process() - logger.info(" Process() appelé (brouillon)") - except: - logger.debug("Process() ignoré pour brouillon") + logger.info(" Process() appelé (brouillon)") + except: + logger.debug("Process() ignoré pour brouillon") numero_devis = _recuperer_numero_devis(process, doc) @@ -1728,8 +1717,8 @@ class SageConnector: time.sleep(0.5) - doc_final_data = _relire_devis( - numero_devis, devis_data, forcer_brouillon + doc_final_data = self._relire_devis( + numero_devis, devis_data ) logger.info( @@ -1751,6 +1740,64 @@ class SageConnector: logger.error(f" ERREUR CRÉATION DEVIS: {e}", exc_info=True) raise RuntimeError(f"Échec création devis: {str(e)}") + def _relire_devis(self, numero_devis, devis_data): + """Relit le devis créé et extrait les informations finales.""" + factory_doc = self.cial.FactoryDocumentVente + persist_reread = factory_doc.ReadPiece(0, numero_devis) + + if not persist_reread: + logger.debug("ReadPiece échoué, recherche dans List()...") + persist_reread = _rechercher_devis_dans_liste( + numero_devis, factory_doc + ) + + if persist_reread: + doc_final = win32com.client.CastTo(persist_reread, "IBODocumentVente3") + doc_final.Read() + + total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) + total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) + statut_final = getattr(doc_final, "DO_Statut", 0) + reference_final = getattr(doc_final, "DO_Ref", "") + + date_livraison_final = None + + try: + date_livr = getattr(doc_final, "DO_DateLivr", None) + if date_livr: + date_livraison_final = date_livr.strftime("%Y-%m-%d") + except: + pass + else: + total_calcule = sum( + l.get("montant_ligne_ht", 0) for l in devis_data["lignes"] + ) + total_ht = total_calcule + total_ttc = round(total_calcule * 1.20, 2) + statut_final = 0 + reference_final = devis_data.get("reference", "") + date_livraison_final = devis_data.get("date_livraison") + + logger.info(f" Total HT: {total_ht}€") + logger.info(f" Total TTC: {total_ttc}€") + logger.info(f" Statut final: {statut_final}") + if reference_final: + logger.info(f" Référence: {reference_final}") + if date_livraison_final: + logger.info(f" Date livraison: {date_livraison_final}") + + return { + "numero_devis": numero_devis, + "total_ht": total_ht, + "total_ttc": total_ttc, + "nb_lignes": len(devis_data["lignes"]), + "client_code": devis_data["client"]["code"], + "date_devis": str(devis_data.get("date_devis", "")), + "date_livraison": date_livraison_final, + "reference": reference_final, + "statut": statut_final, + } + def modifier_devis(self, numero: str, devis_data: Dict) -> Dict: logger.info("=" * 100) logger.info("=" * 100) @@ -2369,7 +2416,6 @@ class SageConnector: numero_source, type_source, type_cible, - ignorer_controle_stock=False, conserver_document_source=True, verifier_doublons=True, ): @@ -2394,26 +2440,28 @@ class SageConnector: if (type_source, type_cible) not in transformations_valides: raise ValueError( f"Transformation non autorisée: " - f"{self._get_type_libelle(type_source)} → {self._get_type_libelle(type_cible)}" + f"{_get_type_libelle(type_source)} → {_get_type_libelle(type_cible)}" ) module, methode = transformations_valides[(type_source, type_cible)] logger.info(f"[TRANSFORM] Méthode: Transformation.{module}.{methode}()") - if verifier_doublons: - logger.info("[TRANSFORM] Vérification des doublons...") - verif = peut_etre_transforme(numero_source, type_source, type_cible) - - if not verif["possible"]: - docs = [d["numero"] for d in verif.get("documents_existants", [])] - raise ValueError( - f"{verif['raison']}. Document(s) existant(s): {', '.join(docs)}" - ) - - logger.info("[TRANSFORM] Aucun doublon détecté") - try: - with self._com_context(), self._lock_com: + with self._com_context(), self._lock_com, self._get_sql_connection() as conn: + cursor = conn.cursor() + + if verifier_doublons: + logger.info("[TRANSFORM] Vérification des doublons...") + verif = peut_etre_transforme(cursor, numero_source, type_source, type_cible) + + if not verif["possible"]: + docs = [d["numero"] for d in verif.get("documents_existants", [])] + raise ValueError( + f"{verif['raison']}. Document(s) existant(s): {', '.join(docs)}" + ) + + logger.info("[TRANSFORM] Aucun doublon détecté") + factory = self.cial.FactoryDocumentVente logger.info(f"[TRANSFORM] Lecture de {numero_source}...") @@ -2838,7 +2886,6 @@ class SageConnector: return _lire_document_sql(cursor, numero, type_doc=50) def lire_livraison(self, numero): - """Lit UNE livraison via SQL (avec lignes)""" with self._get_sql_connection() as conn: cursor = conn.cursor() return _lire_document_sql(cursor, numero, type_doc=30) @@ -2848,7 +2895,7 @@ class SageConnector: raise RuntimeError("Connexion Sage non établie") try: - with self._com_context(), self._lock_com: + with self._com_context(), self._lock_com, self._get_sql_connection() as conn: logger.info("=" * 80) logger.info("[CREATION CONTACT F_CONTACTT]") logger.info("=" * 80) @@ -2865,6 +2912,7 @@ class SageConnector: logger.info(f" CLIENT: {numero_client}") logger.info(f" CONTACT: {prenom} {nom}") + # Chargement du client logger.info(f"[1] Chargement du client: {numero_client}") factory_client = self.cial.CptaApplication.FactoryClient try: @@ -2878,19 +2926,19 @@ class SageConnector: except Exception as e: raise ValueError(f"Client {numero_client} introuvable: {e}") + # Création du contact logger.info("[2] Creation via FactoryTiersContact") if not hasattr(client_obj, "FactoryTiersContact"): raise RuntimeError("FactoryTiersContact non trouvee sur le client") factory_contact = client_obj.FactoryTiersContact - logger.info( - f" OK FactoryTiersContact: {type(factory_contact).__name__}" - ) + logger.info(f" OK FactoryTiersContact: {type(factory_contact).__name__}") persist = factory_contact.Create() logger.info(f" Objet cree: {type(persist).__name__}") + # Cast vers l'interface contact contact = None interfaces_a_tester = [ "IBOTiersContact3", @@ -2916,10 +2964,9 @@ class SageConnector: if not contact: logger.error(" ERROR Aucun cast ne fonctionne") - raise RuntimeError( - "Impossible de caster vers une interface contact valide" - ) + raise RuntimeError("Impossible de caster vers une interface contact valide") + # Configuration des champs logger.info("[3] Configuration du contact") if hasattr(contact, "_prop_map_put_"): @@ -2967,6 +3014,7 @@ class SageConnector: except Exception as e: logger.warning(f" WARN ServiceContact: {e}") + # Coordonnées Telecom logger.info("[4] Coordonnees (Telecom)") if hasattr(contact, "Telecom"): @@ -2997,6 +3045,7 @@ class SageConnector: except Exception as e: logger.warning(f" WARN Erreur Telecom: {e}") + # Réseaux sociaux logger.info("[5] Reseaux sociaux") if contact_data.get("facebook"): @@ -3029,35 +3078,90 @@ class SageConnector: except Exception as e: logger.warning(f" WARN SetDefault(): {e}") + # ============================================================ + # ENREGISTREMENT AVEC GESTION DU BUG SAGE "existe déjà" + # ============================================================ logger.info("[6] Enregistrement du contact") - try: - contact.Write() - logger.info(" OK Write() reussi") - - contact.Read() - logger.info(" OK Read() reussi") - 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})" - ) - except: - pass - logger.error(f" ERROR Write: {error_detail}") - raise RuntimeError(f"Echec enregistrement: {error_detail}") - + + contact_cree_malgre_erreur = False contact_no = None n_contact = None + erreur_com = None + try: - contact_no = getattr(contact, "CT_No", None) - n_contact = getattr(contact, "N_Contact", None) - logger.info(f" Contact CT_No={contact_no}, N_Contact={n_contact}") - except: - pass + contact.Write() + logger.info(" Write() reussi") + + # Si Write() réussit, relire immédiatement pour avoir les IDs + try: + contact.Read() + logger.info(" Read() reussi") + except Exception as read_err: + logger.warning(f" WARN Read() échoué: {read_err}") + + # Essayer de récupérer les identifiants via l'objet COM + try: + contact_no = getattr(contact, "CT_No", None) + n_contact = getattr(contact, "N_Contact", None) + logger.info(f" IDs COM: CT_No={contact_no}, N_Contact={n_contact}") + except: + pass + + # Si les IDs ne sont pas disponibles via COM, chercher en base + if not contact_no: + logger.info(" 🔍 CT_No non disponible via COM - Recherche en base...") + import time + time.sleep(0.3) + + contact_sql = _chercher_contact_en_base( + conn, + numero_client=numero_client, + nom=nom, + prenom=prenom if prenom else None + ) + + if contact_sql: + logger.info(f" Contact trouvé en base: CT_No={contact_sql['contact_numero']}") + contact_no = contact_sql['contact_numero'] + n_contact = contact_sql['n_contact'] + else: + logger.warning(" Contact non trouvé en base immédiatement") + + except Exception as e: + erreur_com = str(e) + logger.warning(f" Write() a levé une exception: {erreur_com}") + + # Vérifier si c'est l'erreur "existe déjà" + if "existe déjà" in erreur_com.lower() or "already exists" in erreur_com.lower(): + logger.info(" 🔍 Erreur 'existe déjà' détectée - Vérification en base...") + + # Pause pour laisser Sage finaliser l'écriture + import time + time.sleep(0.5) + + # Vérifier si le contact a été créé malgré l'erreur + contact_sql = _chercher_contact_en_base( + conn, + numero_client=numero_client, + nom=nom, + prenom=prenom if prenom else None + ) + + if contact_sql: + logger.info(f" Contact CRÉÉ malgré l'erreur COM !") + logger.info(f" CT_No={contact_sql['contact_numero']}, N_Contact={contact_sql['n_contact']}") + contact_cree_malgre_erreur = True + contact_no = contact_sql['contact_numero'] + n_contact = contact_sql['n_contact'] + else: + logger.error(" Contact NON trouvé en base - Erreur réelle") + raise RuntimeError(f"Echec enregistrement: {erreur_com}") + else: + # Autre type d'erreur - on la remonte + logger.error(f" Erreur Write non gérée: {erreur_com}") + raise RuntimeError(f"Echec enregistrement: {erreur_com}") + # Définir comme contact par défaut si demandé est_defaut = contact_data.get("est_defaut", False) if est_defaut and (contact_no or n_contact): logger.info("[7] Definition comme contact par defaut") @@ -3065,9 +3169,7 @@ class SageConnector: nom_complet = f"{prenom} {nom}".strip() if prenom else nom persist_client = factory_client.ReadNumero(numero_client) - client_obj = win32com.client.CastTo( - persist_client, "IBOClient3" - ) + client_obj = win32com.client.CastTo(persist_client, "IBOClient3") client_obj.Read() client_obj.CT_Contact = nom_complet @@ -3087,29 +3189,90 @@ class SageConnector: logger.warning(f" WARN Echec: {e}") est_defaut = False + # Construction du retour logger.info("=" * 80) - logger.info(f"[SUCCES] Contact cree: {prenom} {nom}") - logger.info(f" Lie au client {numero_client}") + if contact_cree_malgre_erreur: + logger.info(f"[SUCCES] Contact créé MALGRÉ erreur COM: {prenom} {nom}") + logger.info(f" (Bug connu de Sage 100c - Le contact est bien en base)") + else: + logger.info(f"[SUCCES] Contact créé: {prenom} {nom}") + logger.info(f" Lié au client {numero_client}") if contact_no: logger.info(f" CT_No={contact_no}") logger.info("=" * 80) - contact_dict = _contact_to_dict( - contact, - numero_client=numero_client, - contact_numero=contact_no, - n_contact=n_contact, - ) + logger.info("[7] Construction du retour") + contact_dict = None + + # Stratégie 1 : Si on a les identifiants, relire depuis la base + if contact_no: + logger.info(f" Stratégie 1: Lecture base (CT_No={contact_no})") + try: + contact_dict = _lire_contact_depuis_base( + conn, + numero_client=numero_client, + contact_no=contact_no + ) + + if contact_dict: + logger.info(f" Lecture base réussie: {len(contact_dict)} champs") + logger.info(f" Type: {type(contact_dict)}") + logger.info(f" Keys: {list(contact_dict.keys())}") + logger.info(f" Sample: numero={contact_dict.get('numero')}, nom={contact_dict.get('nom')}") + else: + logger.warning(" _lire_contact_depuis_base() retourne None") + except Exception as e: + logger.error(f" Erreur lecture base: {e}", exc_info=True) + contact_dict = None + + # Stratégie 2 : Fallback sur l'objet COM (_contact_to_dict) + if not contact_dict: + logger.info(" Stratégie 2: Lecture objet COM (_contact_to_dict)") + try: + contact_dict = _contact_to_dict( + contact, + numero_client=numero_client, + contact_numero=contact_no, + n_contact=n_contact, + ) + if contact_dict: + logger.info(f" _contact_to_dict réussi: {len(contact_dict)} champs") + else: + logger.warning(" _contact_to_dict() retourne None/vide") + except Exception as e: + logger.error(f" Erreur _contact_to_dict: {e}", exc_info=True) + contact_dict = None + + # Stratégie 3 : Construction manuelle en dernier recours + if not contact_dict: + logger.info(" Stratégie 3: Construction manuelle (fallback)") + contact_dict = self._construire_contact_minimal( + numero_client=numero_client, + contact_no=contact_no, + n_contact=n_contact, + nom=nom, + prenom=prenom, + contact_data=contact_data + ) + logger.info(f" Contact minimal construit: {len(contact_dict)} champs") + + # Vérification finale avant retour + if not contact_dict or not isinstance(contact_dict, dict): + logger.error(f" ERREUR: contact_dict invalide: type={type(contact_dict)}, value={contact_dict}") + raise RuntimeError("Impossible de construire le dictionnaire de retour") + + # Ajouter le flag est_defaut contact_dict["est_defaut"] = est_defaut - - logger.info("=" * 80) - logger.info("[DEBUG RETOUR]") - logger.info(f" numero_client = {numero_client}") - logger.info(f" contact_no = {contact_no}") - logger.info(f" n_contact = {n_contact}") - logger.info(f" contact_dict COMPLET = {contact_dict}") - logger.info("=" * 80) - + + # Log final détaillé + logger.info(f" 📦 DICT FINAL AVANT RETURN:") + logger.info(f" Type: {type(contact_dict)}") + logger.info(f" Len: {len(contact_dict)}") + logger.info(f" Keys: {list(contact_dict.keys())}") + for key, value in contact_dict.items(): + logger.info(f" {key}: {value} (type: {type(value).__name__})") + + logger.info(f" 🚀 RETURN contact_dict") return contact_dict except ValueError as e: @@ -3119,6 +3282,49 @@ class SageConnector: logger.error(f"[ERREUR] {e}", exc_info=True) raise RuntimeError(f"Erreur technique: {e}") + def _construire_contact_minimal( + self, + numero_client: str, + contact_no: Optional[int], + n_contact: Optional[int], + nom: str, + prenom: Optional[str], + contact_data: Dict + ) -> Dict: + logger.info(f" _construire_contact_minimal(client={numero_client}, CT_No={contact_no})") + + civilite_map_reverse = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"} + civilite_input = contact_data.get("civilite") + civilite_code = None + + if civilite_input: + if isinstance(civilite_input, int): + civilite_code = civilite_map_reverse.get(civilite_input) + else: + civilite_code = civilite_input + + result = { + "numero": numero_client, + "contact_numero": contact_no, + "n_contact": n_contact, + "civilite": civilite_code, + "nom": nom, + "prenom": prenom, + "fonction": contact_data.get("fonction"), + "service_code": contact_data.get("service_code"), + "telephone": contact_data.get("telephone"), + "portable": contact_data.get("portable"), + "telecopie": contact_data.get("telecopie"), + "email": contact_data.get("email"), + "facebook": contact_data.get("facebook"), + "linkedin": contact_data.get("linkedin"), + "skype": contact_data.get("skype"), + "est_defaut": False, + } + + logger.info(f" Contact minimal: numero={result['numero']}, nom={result['nom']}, email={result['email']}") + return result + def modifier_contact(self, numero: str, contact_numero: int, updates: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -3129,8 +3335,12 @@ class SageConnector: logger.info(f"[MODIFICATION CONTACT] CT_No={contact_numero}") logger.info("=" * 80) + # ============================================================ + # [1] CHARGEMENT DU CLIENT + # ============================================================ logger.info("[1] Chargement du client") factory_client = self.cial.CptaApplication.FactoryClient + try: persist_client = factory_client.ReadNumero(numero) if not persist_client: @@ -3138,51 +3348,182 @@ class SageConnector: client_obj = win32com.client.CastTo(persist_client, "IBOClient3") client_obj.Read() - logger.info(f" OK Client charge") + logger.info(f" OK Client charge: {client_obj.CT_Intitule}") except Exception as e: raise ValueError(f"Client {numero} introuvable: {e}") + # ============================================================ + # [2] CHARGEMENT DU CONTACT (multi-stratégies) + # ============================================================ logger.info("[2] Chargement du contact") - + + contact = None nom_recherche = None prenom_recherche = None - + + # Récupérer d'abord les infos depuis la base SQL (lecture seule) try: with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( - "SELECT CT_Nom, CT_Prenom FROM F_CONTACTT WHERE CT_Num = ? AND CT_No = ?", + """SELECT CT_No, CT_Nom, CT_Prenom, cbMarq + FROM F_CONTACTT + WHERE CT_Num = ? AND CT_No = ?""", [numero, contact_numero], ) row = cursor.fetchone() if not row: - raise ValueError( - f"Contact CT_No={contact_numero} non trouve" - ) + raise ValueError(f"Contact CT_No={contact_numero} non trouve en base") + ct_no_sql = row.CT_No nom_recherche = row.CT_Nom.strip() if row.CT_Nom else "" - prenom_recherche = ( - row.CT_Prenom.strip() if row.CT_Prenom else "" - ) + prenom_recherche = row.CT_Prenom.strip() if row.CT_Prenom else "" + cbmarq_sql = row.cbMarq - logger.info( - f" Contact trouve en SQL: {prenom_recherche} {nom_recherche}" - ) + logger.info(f" Contact SQL: CT_No={ct_no_sql}, cbMarq={cbmarq_sql}") + logger.info(f" Nom='{nom_recherche}', Prenom='{prenom_recherche}'") + except ValueError: + raise except Exception as e: - raise ValueError(f"Contact introuvable: {e}") + raise ValueError(f"Erreur lecture contact en base: {e}") - factory_dossier = self.cial.CptaApplication.FactoryDossierContact - persist = factory_dossier.ReadNomPrenom(nom_recherche, prenom_recherche) + # --- Stratégie 1 : Via FactoryTiersContact du client (parcours) --- + # NOTE: CT_No et N_Contact sont None sur IBOTiersContact3, on matche par nom/prénom + logger.info(" Strategie 1: Parcours FactoryTiersContact du client...") + try: + factory_contact = client_obj.FactoryTiersContact + + if hasattr(factory_contact, "List"): + liste_contacts = factory_contact.List + + if liste_contacts and hasattr(liste_contacts, "Count"): + count = liste_contacts.Count + logger.info(f" {count} contact(s) pour ce client") + + for i in range(count): + try: + item = liste_contacts.Item(i + 1) + + temp_contact = None + for iface in ["IBOTiersContact3", "IBOTiersContact", "IBOContactT3"]: + try: + temp_contact = win32com.client.CastTo(item, iface) + break + except: + continue + + if not temp_contact: + continue + + temp_contact.Read() + + nom_com = getattr(temp_contact, "Nom", "") or "" + prenom_com = getattr(temp_contact, "Prenom", "") or "" + + # Correspondance par nom/prénom (insensible à la casse) + if (nom_com.strip().lower() == nom_recherche.lower() and + prenom_com.strip().lower() == prenom_recherche.lower()): + contact = temp_contact + logger.info(f" OK Contact trouve a l'index {i+1}: '{prenom_com}' '{nom_com}'") + break + + except Exception as e: + logger.debug(f" Item {i+1} erreur: {e}") + continue + + except Exception as e: + logger.warning(f" Strategie 1 echouee: {e}") - if not persist: - raise ValueError(f"Contact non trouvable via ReadNomPrenom") + # --- Stratégie 2 : Via FactoryDossierContact.ReadNomPrenom --- + if not contact: + logger.info(" Strategie 2: FactoryDossierContact.ReadNomPrenom...") + try: + factory_dossier = self.cial.CptaApplication.FactoryDossierContact + persist = factory_dossier.ReadNomPrenom(nom_recherche, prenom_recherche) + + if persist: + contact = win32com.client.CastTo(persist, "IBOTiersContact3") + contact.Read() + logger.info(f" OK Contact charge via ReadNomPrenom") + + except Exception as e: + logger.warning(f" Strategie 2 echouee: {e}") + + # --- Stratégie 3 : Variations nom/prenom --- + if not contact: + logger.info(" Strategie 3: Variations nom/prenom...") + try: + factory_dossier = self.cial.CptaApplication.FactoryDossierContact + + variations = [ + (nom_recherche.upper(), prenom_recherche.upper()), + (nom_recherche.lower(), prenom_recherche.lower()), + (nom_recherche.capitalize(), prenom_recherche.capitalize()), + (nom_recherche, ""), + ] + + for nom_var, prenom_var in variations: + try: + persist = factory_dossier.ReadNomPrenom(nom_var, prenom_var) + if persist: + contact = win32com.client.CastTo(persist, "IBOTiersContact3") + contact.Read() + logger.info(f" OK Contact trouve avec: '{nom_var}'/'{prenom_var}'") + break + except: + continue + + except Exception as e: + logger.warning(f" Strategie 3 echouee: {e}") + + # --- Stratégie 4 : Parcours global FactoryDossierContact --- + if not contact: + logger.info(" Strategie 4: Parcours global FactoryDossierContact...") + try: + factory_dossier = self.cial.CptaApplication.FactoryDossierContact + + if hasattr(factory_dossier, "List"): + liste = factory_dossier.List + + if liste and hasattr(liste, "Count"): + count = min(liste.Count, 500) + logger.info(f" Parcours de {count} contacts...") + + for i in range(count): + try: + item = liste.Item(i + 1) + temp = win32com.client.CastTo(item, "IBOTiersContact3") + temp.Read() + + nom = getattr(temp, "Nom", "") or "" + prenom = getattr(temp, "Prenom", "") or "" + + if (nom.strip().lower() == nom_recherche.lower() and + prenom.strip().lower() == prenom_recherche.lower()): + contact = temp + logger.info(f" OK Contact trouve a l'index global {i+1}") + break + except: + continue + + except Exception as e: + logger.warning(f" Strategie 4 echouee: {e}") + + # Échec total + if not contact: + logger.error(f" ECHEC: Impossible de charger le contact CT_No={contact_numero}") + raise ValueError( + f"Contact CT_No={contact_numero} introuvable via COM. " + f"Nom='{nom_recherche}', Prenom='{prenom_recherche}'" + ) - contact = win32com.client.CastTo(persist, "IBOTiersContact3") - contact.Read() logger.info(f" OK Contact charge: {contact.Nom}") + # ============================================================ + # [3] APPLICATION DES MODIFICATIONS + # ============================================================ logger.info("[3] Application des modifications") modifications_appliquees = [] @@ -3194,8 +3535,8 @@ class SageConnector: contact.Civilite = civilite_code logger.info(f" Civilite = {civilite_code}") modifications_appliquees.append("civilite") - except: - pass + except Exception as e: + logger.warning(f" WARN Civilite: {e}") if "nom" in updates: nom = _clean_str(updates["nom"], 35) @@ -3204,8 +3545,8 @@ class SageConnector: contact.Nom = nom logger.info(f" Nom = {nom}") modifications_appliquees.append("nom") - except: - pass + except Exception as e: + logger.warning(f" WARN Nom: {e}") if "prenom" in updates: prenom = _clean_str(updates["prenom"], 35) @@ -3213,8 +3554,8 @@ class SageConnector: contact.Prenom = prenom logger.info(f" Prenom = {prenom}") modifications_appliquees.append("prenom") - except: - pass + except Exception as e: + logger.warning(f" WARN Prenom: {e}") if "fonction" in updates: fonction = _clean_str(updates["fonction"], 35) @@ -3222,8 +3563,8 @@ class SageConnector: contact.Fonction = fonction logger.info(f" Fonction = {fonction}") modifications_appliquees.append("fonction") - except: - pass + except Exception as e: + logger.warning(f" WARN Fonction: {e}") if "service_code" in updates: service = _safe_int(updates["service_code"]) @@ -3232,8 +3573,8 @@ class SageConnector: contact.ServiceContact = service logger.info(f" ServiceContact = {service}") modifications_appliquees.append("service_code") - except: - pass + except Exception as e: + logger.warning(f" WARN ServiceContact: {e}") if hasattr(contact, "Telecom"): try: @@ -3262,8 +3603,9 @@ class SageConnector: if _try_set_attribute(telecom, "Telecopie", fax): logger.info(f" Telecopie = {fax}") modifications_appliquees.append("telecopie") - except: - pass + + except Exception as e: + logger.warning(f" WARN Telecom: {e}") if "facebook" in updates: facebook = _clean_str(updates["facebook"], 69) @@ -3271,8 +3613,8 @@ class SageConnector: contact.Facebook = facebook logger.info(f" Facebook = {facebook}") modifications_appliquees.append("facebook") - except: - pass + except Exception as e: + logger.warning(f" WARN Facebook: {e}") if "linkedin" in updates: linkedin = _clean_str(updates["linkedin"], 69) @@ -3280,8 +3622,8 @@ class SageConnector: contact.LinkedIn = linkedin logger.info(f" LinkedIn = {linkedin}") modifications_appliquees.append("linkedin") - except: - pass + except Exception as e: + logger.warning(f" WARN LinkedIn: {e}") if "skype" in updates: skype = _clean_str(updates["skype"], 69) @@ -3289,36 +3631,44 @@ class SageConnector: contact.Skype = skype logger.info(f" Skype = {skype}") modifications_appliquees.append("skype") - except: - pass + except Exception as e: + logger.warning(f" WARN Skype: {e}") + logger.info(f" Modifications preparees: {', '.join(modifications_appliquees) if modifications_appliquees else 'aucune'}") + + # ============================================================ + # [4] ENREGISTREMENT + # ============================================================ logger.info("[4] Enregistrement") + try: contact.Write() - contact.Read() - logger.info(" OK Write() reussi") + logger.info(" Write() reussi") 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" ERROR Write: {error_detail}") raise RuntimeError(f"Echec modification contact: {error_detail}") - logger.info( - f" Modifications appliquees: {', '.join(modifications_appliquees)}" - ) + try: + contact.Read() + logger.info(" Read() reussi") + except Exception as e: + logger.warning(f" WARN Read() apres Write: {e}") + # ============================================================ + # [5] GESTION CONTACT PAR DÉFAUT + # ============================================================ est_defaut_demande = updates.get("est_defaut") est_actuellement_defaut = False - if est_defaut_demande is not None and est_defaut_demande: - logger.info("[5] Gestion contact par defaut") + if est_defaut_demande: + logger.info("[5] Definition comme contact par defaut") try: nom_complet = ( f"{contact.Prenom} {contact.Nom}".strip() @@ -3327,9 +3677,7 @@ class SageConnector: ) persist_client = factory_client.ReadNumero(numero) - client_obj = win32com.client.CastTo( - persist_client, "IBOClient3" - ) + client_obj = win32com.client.CastTo(persist_client, "IBOClient3") client_obj.Read() client_obj.CT_Contact = nom_complet @@ -3339,8 +3687,8 @@ class SageConnector: try: client_obj.CT_NoContact = contact_numero logger.info(f" CT_NoContact = {contact_numero}") - except: - pass + except Exception as e: + logger.warning(f" WARN CT_NoContact: {e}") client_obj.Write() client_obj.Read() @@ -3348,20 +3696,80 @@ class SageConnector: est_actuellement_defaut = True except Exception as e: - logger.warning(f" WARN Echec: {e}") + logger.warning(f" WARN Echec definition contact defaut: {e}") + + # ============================================================ + # [6] CONSTRUCTION DU RETOUR (lecture SQL uniquement) + # ============================================================ + logger.info("[6] Construction du retour") + + contact_dict = None + + # Lecture depuis la base + try: + with self._get_sql_connection() as conn: + contact_dict = _lire_contact_depuis_base( + conn, + numero_client=numero, + contact_no=contact_numero + ) + if contact_dict: + logger.info(f" Lecture base reussie") + except Exception as e: + logger.warning(f" Lecture base echouee: {e}") + + # Fallback via objet COM + if not contact_dict: + try: + contact_dict = _contact_to_dict( + contact, + numero_client=numero, + contact_numero=contact_numero, + n_contact=None, + ) + if contact_dict: + logger.info(f" _contact_to_dict reussi") + except Exception as e: + logger.warning(f" _contact_to_dict echoue: {e}") + + # Construction manuelle + if not contact_dict: + logger.info(" Construction manuelle du retour") + contact_dict = { + "numero": numero, + "contact_numero": contact_numero, + "n_contact": None, + "civilite": None, + "nom": getattr(contact, "Nom", nom_recherche), + "prenom": getattr(contact, "Prenom", prenom_recherche), + "fonction": getattr(contact, "Fonction", None), + "service_code": None, + "telephone": None, + "portable": None, + "telecopie": None, + "email": None, + "facebook": None, + "linkedin": None, + "skype": None, + } + + if hasattr(contact, "Telecom"): + try: + telecom = contact.Telecom + contact_dict["telephone"] = getattr(telecom, "Telephone", None) + contact_dict["portable"] = getattr(telecom, "Portable", None) + contact_dict["email"] = getattr(telecom, "EMail", None) + contact_dict["telecopie"] = getattr(telecom, "Telecopie", None) + except: + pass + + contact_dict["est_defaut"] = est_actuellement_defaut logger.info("=" * 80) logger.info(f"[SUCCES] Contact modifie: CT_No={contact_numero}") + logger.info(f" Modifications: {', '.join(modifications_appliquees) if modifications_appliquees else 'aucune'}") logger.info("=" * 80) - contact_dict = _contact_to_dict( - contact, - numero_client=numero, - contact_numero=contact_numero, - n_contact=None, - ) - contact_dict["est_defaut"] = est_actuellement_defaut - return contact_dict except ValueError as e: @@ -3370,7 +3778,7 @@ class SageConnector: except Exception as e: logger.error(f"[ERREUR] {e}", exc_info=True) raise RuntimeError(f"Erreur technique: {e}") - + def definir_contact_defaut(self, numero: str, contact_numero: int) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -3581,9 +3989,6 @@ class SageConnector: raise RuntimeError(f"Erreur technique: {e}") def creer_client(self, client_data: Dict) -> Dict: - """ - Creation client Sage - Version corrigée pour erreur cohérence - """ if not self.cial: raise RuntimeError("Connexion Sage non etablie") @@ -4184,9 +4589,6 @@ 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") @@ -8830,455 +9232,455 @@ class SageConnector: raise RuntimeError(f"Erreur technique Sage : {error_message}") - def lister_toutes_familles( - self, filtre: str = "", inclure_totaux: bool = True - ) -> List[Dict]: - """Liste toutes les familles avec leurs comptes comptables et fournisseur principal""" - try: - with self._get_sql_connection() as conn: - cursor = conn.cursor() + def lister_toutes_familles( + self, filtre: str = "", inclure_totaux: bool = True + ) -> List[Dict]: + """Liste toutes les familles avec leurs comptes comptables et fournisseur principal""" + try: + with self._get_sql_connection() as conn: + cursor = conn.cursor() - logger.info( - "[SQL] Chargement des familles avec F_FAMCOMPTA et F_FAMFOURNISS..." - ) + logger.info( + "[SQL] Chargement des familles avec F_FAMCOMPTA et F_FAMFOURNISS..." + ) - query = """ - SELECT - -- F_FAMILLE - Identification - f.FA_CodeFamille, - f.FA_Type, - f.FA_Intitule, - f.FA_UniteVen, - f.FA_Coef, - f.FA_SuiviStock, - f.FA_Garantie, - f.FA_Central, - - -- F_FAMILLE - Statistiques - f.FA_Stat01, - f.FA_Stat02, - f.FA_Stat03, - f.FA_Stat04, - f.FA_Stat05, - - -- F_FAMILLE - Fiscal et gestion - f.FA_CodeFiscal, - f.FA_Pays, - f.FA_UnitePoids, - f.FA_Escompte, - f.FA_Delai, - f.FA_HorsStat, - f.FA_VteDebit, - f.FA_NotImp, - - -- F_FAMILLE - Frais annexes (3 frais avec 3 remises chacun) - f.FA_Frais01FR_Denomination, - f.FA_Frais01FR_Rem01REM_Valeur, - f.FA_Frais01FR_Rem01REM_Type, - f.FA_Frais01FR_Rem02REM_Valeur, - f.FA_Frais01FR_Rem02REM_Type, - f.FA_Frais01FR_Rem03REM_Valeur, - f.FA_Frais01FR_Rem03REM_Type, - f.FA_Frais02FR_Denomination, - f.FA_Frais02FR_Rem01REM_Valeur, - f.FA_Frais02FR_Rem01REM_Type, - f.FA_Frais02FR_Rem02REM_Valeur, - f.FA_Frais02FR_Rem02REM_Type, - f.FA_Frais02FR_Rem03REM_Valeur, - f.FA_Frais02FR_Rem03REM_Type, - f.FA_Frais03FR_Denomination, - f.FA_Frais03FR_Rem01REM_Valeur, - f.FA_Frais03FR_Rem01REM_Type, - f.FA_Frais03FR_Rem02REM_Valeur, - f.FA_Frais03FR_Rem02REM_Type, - f.FA_Frais03FR_Rem03REM_Valeur, - f.FA_Frais03FR_Rem03REM_Type, - - -- F_FAMILLE - Options diverses - f.FA_Contremarque, - f.FA_FactPoids, - f.FA_FactForfait, - f.FA_Publie, - f.FA_RacineRef, - f.FA_RacineCB, - - -- F_FAMILLE - Catégories - f.CL_No1, - f.CL_No2, - f.CL_No3, - f.CL_No4, - - -- F_FAMILLE - Gestion avancée - f.FA_Nature, - f.FA_NbColis, - f.FA_SousTraitance, - f.FA_Fictif, - f.FA_Criticite, - - -- F_FAMILLE - Métadonnées système - f.cbMarq, - f.cbCreateur, - f.cbModification, - f.cbCreation, - f.cbCreationUser, - - -- F_FAMCOMPTA Vente (FCP_Type = 0) - vte.FCP_ComptaCPT_CompteG, - vte.FCP_ComptaCPT_CompteA, - vte.FCP_ComptaCPT_Taxe1, - vte.FCP_ComptaCPT_Taxe2, - vte.FCP_ComptaCPT_Taxe3, - vte.FCP_ComptaCPT_Date1, - vte.FCP_ComptaCPT_Date2, - vte.FCP_ComptaCPT_Date3, - vte.FCP_TypeFacture, - - -- F_FAMCOMPTA Achat (FCP_Type = 1) - ach.FCP_ComptaCPT_CompteG, - ach.FCP_ComptaCPT_CompteA, - ach.FCP_ComptaCPT_Taxe1, - ach.FCP_ComptaCPT_Taxe2, - ach.FCP_ComptaCPT_Taxe3, - ach.FCP_ComptaCPT_Date1, - ach.FCP_ComptaCPT_Date2, - ach.FCP_ComptaCPT_Date3, - ach.FCP_TypeFacture, - - -- F_FAMCOMPTA Stock (FCP_Type = 2) - sto.FCP_ComptaCPT_CompteG, - sto.FCP_ComptaCPT_CompteA, - - -- F_FAMFOURNISS (fournisseur principal FF_Principal=1) - ff.CT_Num, - ff.FF_Unite, - ff.FF_Conversion, - ff.FF_DelaiAppro, - ff.FF_Garantie, - ff.FF_Colisage, - ff.FF_QteMini, - ff.FF_QteMont, - ff.EG_Champ, - ff.FF_Devise, - ff.FF_Remise, - ff.FF_ConvDiv, - ff.FF_TypeRem, - - -- Nombre d'articles - ISNULL(COUNT(DISTINCT a.AR_Ref), 0) as nb_articles - - FROM F_FAMILLE f + query = """ + SELECT + -- F_FAMILLE - Identification + f.FA_CodeFamille, + f.FA_Type, + f.FA_Intitule, + f.FA_UniteVen, + f.FA_Coef, + f.FA_SuiviStock, + f.FA_Garantie, + f.FA_Central, - -- Jointures comptables - LEFT JOIN F_FAMCOMPTA vte ON f.FA_CodeFamille = vte.FA_CodeFamille - AND vte.FCP_Type = 0 -- Vente - AND vte.FCP_Champ = 1 -- Compte principal - - LEFT JOIN F_FAMCOMPTA ach ON f.FA_CodeFamille = ach.FA_CodeFamille - AND ach.FCP_Type = 1 -- Achat - AND ach.FCP_Champ = 1 - - LEFT JOIN F_FAMCOMPTA sto ON f.FA_CodeFamille = sto.FA_CodeFamille - AND sto.FCP_Type = 2 -- Stock - AND sto.FCP_Champ = 1 + -- F_FAMILLE - Statistiques + f.FA_Stat01, + f.FA_Stat02, + f.FA_Stat03, + f.FA_Stat04, + f.FA_Stat05, - -- Fournisseur principal - LEFT JOIN F_FAMFOURNISS ff ON f.FA_CodeFamille = ff.FA_CodeFamille - AND ff.FF_Principal = 1 + -- F_FAMILLE - Fiscal et gestion + f.FA_CodeFiscal, + f.FA_Pays, + f.FA_UnitePoids, + f.FA_Escompte, + f.FA_Delai, + f.FA_HorsStat, + f.FA_VteDebit, + f.FA_NotImp, + + -- F_FAMILLE - Frais annexes (3 frais avec 3 remises chacun) + f.FA_Frais01FR_Denomination, + f.FA_Frais01FR_Rem01REM_Valeur, + f.FA_Frais01FR_Rem01REM_Type, + f.FA_Frais01FR_Rem02REM_Valeur, + f.FA_Frais01FR_Rem02REM_Type, + f.FA_Frais01FR_Rem03REM_Valeur, + f.FA_Frais01FR_Rem03REM_Type, + f.FA_Frais02FR_Denomination, + f.FA_Frais02FR_Rem01REM_Valeur, + f.FA_Frais02FR_Rem01REM_Type, + f.FA_Frais02FR_Rem02REM_Valeur, + f.FA_Frais02FR_Rem02REM_Type, + f.FA_Frais02FR_Rem03REM_Valeur, + f.FA_Frais02FR_Rem03REM_Type, + f.FA_Frais03FR_Denomination, + f.FA_Frais03FR_Rem01REM_Valeur, + f.FA_Frais03FR_Rem01REM_Type, + f.FA_Frais03FR_Rem02REM_Valeur, + f.FA_Frais03FR_Rem02REM_Type, + f.FA_Frais03FR_Rem03REM_Valeur, + f.FA_Frais03FR_Rem03REM_Type, + + -- F_FAMILLE - Options diverses + f.FA_Contremarque, + f.FA_FactPoids, + f.FA_FactForfait, + f.FA_Publie, + f.FA_RacineRef, + f.FA_RacineCB, + + -- F_FAMILLE - Catégories + f.CL_No1, + f.CL_No2, + f.CL_No3, + f.CL_No4, + + -- F_FAMILLE - Gestion avancée + f.FA_Nature, + f.FA_NbColis, + f.FA_SousTraitance, + f.FA_Fictif, + f.FA_Criticite, + + -- F_FAMILLE - Métadonnées système + f.cbMarq, + f.cbCreateur, + f.cbModification, + f.cbCreation, + f.cbCreationUser, + + -- F_FAMCOMPTA Vente (FCP_Type = 0) + vte.FCP_ComptaCPT_CompteG, + vte.FCP_ComptaCPT_CompteA, + vte.FCP_ComptaCPT_Taxe1, + vte.FCP_ComptaCPT_Taxe2, + vte.FCP_ComptaCPT_Taxe3, + vte.FCP_ComptaCPT_Date1, + vte.FCP_ComptaCPT_Date2, + vte.FCP_ComptaCPT_Date3, + vte.FCP_TypeFacture, + + -- F_FAMCOMPTA Achat (FCP_Type = 1) + ach.FCP_ComptaCPT_CompteG, + ach.FCP_ComptaCPT_CompteA, + ach.FCP_ComptaCPT_Taxe1, + ach.FCP_ComptaCPT_Taxe2, + ach.FCP_ComptaCPT_Taxe3, + ach.FCP_ComptaCPT_Date1, + ach.FCP_ComptaCPT_Date2, + ach.FCP_ComptaCPT_Date3, + ach.FCP_TypeFacture, + + -- F_FAMCOMPTA Stock (FCP_Type = 2) + sto.FCP_ComptaCPT_CompteG, + sto.FCP_ComptaCPT_CompteA, + + -- F_FAMFOURNISS (fournisseur principal FF_Principal=1) + ff.CT_Num, + ff.FF_Unite, + ff.FF_Conversion, + ff.FF_DelaiAppro, + ff.FF_Garantie, + ff.FF_Colisage, + ff.FF_QteMini, + ff.FF_QteMont, + ff.EG_Champ, + ff.FF_Devise, + ff.FF_Remise, + ff.FF_ConvDiv, + ff.FF_TypeRem, -- Nombre d'articles - LEFT JOIN F_ARTICLE a ON f.FA_CodeFamille = a.FA_CodeFamille + ISNULL(COUNT(DISTINCT a.AR_Ref), 0) as nb_articles - WHERE 1=1 - """ + FROM F_FAMILLE f + + -- Jointures comptables + LEFT JOIN F_FAMCOMPTA vte ON f.FA_CodeFamille = vte.FA_CodeFamille + AND vte.FCP_Type = 0 -- Vente + AND vte.FCP_Champ = 1 -- Compte principal + + LEFT JOIN F_FAMCOMPTA ach ON f.FA_CodeFamille = ach.FA_CodeFamille + AND ach.FCP_Type = 1 -- Achat + AND ach.FCP_Champ = 1 + + LEFT JOIN F_FAMCOMPTA sto ON f.FA_CodeFamille = sto.FA_CodeFamille + AND sto.FCP_Type = 2 -- Stock + AND sto.FCP_Champ = 1 + + -- Fournisseur principal + LEFT JOIN F_FAMFOURNISS ff ON f.FA_CodeFamille = ff.FA_CodeFamille + AND ff.FF_Principal = 1 + + -- Nombre d'articles + LEFT JOIN F_ARTICLE a ON f.FA_CodeFamille = a.FA_CodeFamille + + WHERE 1=1 + """ - params = [] + params = [] - if not inclure_totaux: - query += " AND f.FA_Type = 0" - logger.info("[SQL] Filtre : FA_Type = 0 (Détail uniquement)") - - if filtre: - query += """ - AND ( - f.FA_CodeFamille LIKE ? - OR f.FA_Intitule LIKE ? - ) - """ - params.extend([f"%{filtre}%", f"%{filtre}%"]) + if not inclure_totaux: + query += " AND f.FA_Type = 0" + logger.info("[SQL] Filtre : FA_Type = 0 (Détail uniquement)") + if filtre: query += """ - GROUP BY - f.FA_CodeFamille, f.FA_Type, f.FA_Intitule, f.FA_UniteVen, f.FA_Coef, - f.FA_SuiviStock, f.FA_Garantie, f.FA_Central, - f.FA_Stat01, f.FA_Stat02, f.FA_Stat03, f.FA_Stat04, f.FA_Stat05, - f.FA_CodeFiscal, f.FA_Pays, f.FA_UnitePoids, f.FA_Escompte, f.FA_Delai, - f.FA_HorsStat, f.FA_VteDebit, f.FA_NotImp, - f.FA_Frais01FR_Denomination, f.FA_Frais01FR_Rem01REM_Valeur, f.FA_Frais01FR_Rem01REM_Type, - f.FA_Frais01FR_Rem02REM_Valeur, f.FA_Frais01FR_Rem02REM_Type, - f.FA_Frais01FR_Rem03REM_Valeur, f.FA_Frais01FR_Rem03REM_Type, - f.FA_Frais02FR_Denomination, f.FA_Frais02FR_Rem01REM_Valeur, f.FA_Frais02FR_Rem01REM_Type, - f.FA_Frais02FR_Rem02REM_Valeur, f.FA_Frais02FR_Rem02REM_Type, - f.FA_Frais02FR_Rem03REM_Valeur, f.FA_Frais02FR_Rem03REM_Type, - f.FA_Frais03FR_Denomination, f.FA_Frais03FR_Rem01REM_Valeur, f.FA_Frais03FR_Rem01REM_Type, - f.FA_Frais03FR_Rem02REM_Valeur, f.FA_Frais03FR_Rem02REM_Type, - f.FA_Frais03FR_Rem03REM_Valeur, f.FA_Frais03FR_Rem03REM_Type, - f.FA_Contremarque, f.FA_FactPoids, f.FA_FactForfait, f.FA_Publie, - f.FA_RacineRef, f.FA_RacineCB, - f.CL_No1, f.CL_No2, f.CL_No3, f.CL_No4, - f.FA_Nature, f.FA_NbColis, f.FA_SousTraitance, f.FA_Fictif, f.FA_Criticite, - f.cbMarq, f.cbCreateur, f.cbModification, f.cbCreation, f.cbCreationUser, - vte.FCP_ComptaCPT_CompteG, vte.FCP_ComptaCPT_CompteA, - vte.FCP_ComptaCPT_Taxe1, vte.FCP_ComptaCPT_Taxe2, vte.FCP_ComptaCPT_Taxe3, - vte.FCP_ComptaCPT_Date1, vte.FCP_ComptaCPT_Date2, vte.FCP_ComptaCPT_Date3, - vte.FCP_TypeFacture, - ach.FCP_ComptaCPT_CompteG, ach.FCP_ComptaCPT_CompteA, - ach.FCP_ComptaCPT_Taxe1, ach.FCP_ComptaCPT_Taxe2, ach.FCP_ComptaCPT_Taxe3, - ach.FCP_ComptaCPT_Date1, ach.FCP_ComptaCPT_Date2, ach.FCP_ComptaCPT_Date3, - ach.FCP_TypeFacture, - sto.FCP_ComptaCPT_CompteG, sto.FCP_ComptaCPT_CompteA, - ff.CT_Num, ff.FF_Unite, ff.FF_Conversion, ff.FF_DelaiAppro, - ff.FF_Garantie, ff.FF_Colisage, ff.FF_QteMini, ff.FF_QteMont, - ff.EG_Champ, ff.FF_Devise, ff.FF_Remise, ff.FF_ConvDiv, ff.FF_TypeRem - ORDER BY f.FA_Intitule + AND ( + f.FA_CodeFamille LIKE ? + OR f.FA_Intitule LIKE ? + ) """ + params.extend([f"%{filtre}%", f"%{filtre}%"]) - cursor.execute(query, params) - rows = cursor.fetchall() + query += """ + GROUP BY + f.FA_CodeFamille, f.FA_Type, f.FA_Intitule, f.FA_UniteVen, f.FA_Coef, + f.FA_SuiviStock, f.FA_Garantie, f.FA_Central, + f.FA_Stat01, f.FA_Stat02, f.FA_Stat03, f.FA_Stat04, f.FA_Stat05, + f.FA_CodeFiscal, f.FA_Pays, f.FA_UnitePoids, f.FA_Escompte, f.FA_Delai, + f.FA_HorsStat, f.FA_VteDebit, f.FA_NotImp, + f.FA_Frais01FR_Denomination, f.FA_Frais01FR_Rem01REM_Valeur, f.FA_Frais01FR_Rem01REM_Type, + f.FA_Frais01FR_Rem02REM_Valeur, f.FA_Frais01FR_Rem02REM_Type, + f.FA_Frais01FR_Rem03REM_Valeur, f.FA_Frais01FR_Rem03REM_Type, + f.FA_Frais02FR_Denomination, f.FA_Frais02FR_Rem01REM_Valeur, f.FA_Frais02FR_Rem01REM_Type, + f.FA_Frais02FR_Rem02REM_Valeur, f.FA_Frais02FR_Rem02REM_Type, + f.FA_Frais02FR_Rem03REM_Valeur, f.FA_Frais02FR_Rem03REM_Type, + f.FA_Frais03FR_Denomination, f.FA_Frais03FR_Rem01REM_Valeur, f.FA_Frais03FR_Rem01REM_Type, + f.FA_Frais03FR_Rem02REM_Valeur, f.FA_Frais03FR_Rem02REM_Type, + f.FA_Frais03FR_Rem03REM_Valeur, f.FA_Frais03FR_Rem03REM_Type, + f.FA_Contremarque, f.FA_FactPoids, f.FA_FactForfait, f.FA_Publie, + f.FA_RacineRef, f.FA_RacineCB, + f.CL_No1, f.CL_No2, f.CL_No3, f.CL_No4, + f.FA_Nature, f.FA_NbColis, f.FA_SousTraitance, f.FA_Fictif, f.FA_Criticite, + f.cbMarq, f.cbCreateur, f.cbModification, f.cbCreation, f.cbCreationUser, + vte.FCP_ComptaCPT_CompteG, vte.FCP_ComptaCPT_CompteA, + vte.FCP_ComptaCPT_Taxe1, vte.FCP_ComptaCPT_Taxe2, vte.FCP_ComptaCPT_Taxe3, + vte.FCP_ComptaCPT_Date1, vte.FCP_ComptaCPT_Date2, vte.FCP_ComptaCPT_Date3, + vte.FCP_TypeFacture, + ach.FCP_ComptaCPT_CompteG, ach.FCP_ComptaCPT_CompteA, + ach.FCP_ComptaCPT_Taxe1, ach.FCP_ComptaCPT_Taxe2, ach.FCP_ComptaCPT_Taxe3, + ach.FCP_ComptaCPT_Date1, ach.FCP_ComptaCPT_Date2, ach.FCP_ComptaCPT_Date3, + ach.FCP_TypeFacture, + sto.FCP_ComptaCPT_CompteG, sto.FCP_ComptaCPT_CompteA, + ff.CT_Num, ff.FF_Unite, ff.FF_Conversion, ff.FF_DelaiAppro, + ff.FF_Garantie, ff.FF_Colisage, ff.FF_QteMini, ff.FF_QteMont, + ff.EG_Champ, ff.FF_Devise, ff.FF_Remise, ff.FF_ConvDiv, ff.FF_TypeRem + ORDER BY f.FA_Intitule + """ - def to_str(val): - """Convertit en string, gère None et int""" - if val is None: - return "" - return str(val).strip() if isinstance(val, str) else str(val) + cursor.execute(query, params) + rows = cursor.fetchall() - def to_float(val): - """Convertit en float, gère None""" - if val is None or val == "": - return 0.0 - try: - return float(val) - except (ValueError, TypeError): - return 0.0 + def to_str(val): + """Convertit en string, gère None et int""" + if val is None: + return "" + return str(val).strip() if isinstance(val, str) else str(val) - def to_int(val): - """Convertit en int, gère None""" - if val is None or val == "": - return 0 - try: - return int(val) - except (ValueError, TypeError): - return 0 + def to_float(val): + """Convertit en float, gère None""" + if val is None or val == "": + return 0.0 + try: + return float(val) + except (ValueError, TypeError): + return 0.0 - def to_bool(val): - """Convertit en bool""" - if val is None: - return False - if isinstance(val, bool): - return val - if isinstance(val, int): - return val != 0 - return bool(val) + def to_int(val): + """Convertit en int, gère None""" + if val is None or val == "": + return 0 + try: + return int(val) + except (ValueError, TypeError): + return 0 - familles = [] + def to_bool(val): + """Convertit en bool""" + if val is None: + return False + if isinstance(val, bool): + return val + if isinstance(val, int): + return val != 0 + return bool(val) - for row in rows: - idx = 0 + familles = [] - famille = { - "code": to_str(row[idx]), - "type": to_int(row[idx + 1]), - "intitule": to_str(row[idx + 2]), - "unite_vente": to_str(row[idx + 3]), - "coef": to_float(row[idx + 4]), - "suivi_stock": to_bool(row[idx + 5]), - "garantie": to_int(row[idx + 6]), - "est_centrale": to_bool(row[idx + 7]), + for row in rows: + idx = 0 + + famille = { + "code": to_str(row[idx]), + "type": to_int(row[idx + 1]), + "intitule": to_str(row[idx + 2]), + "unite_vente": to_str(row[idx + 3]), + "coef": to_float(row[idx + 4]), + "suivi_stock": to_bool(row[idx + 5]), + "garantie": to_int(row[idx + 6]), + "est_centrale": to_bool(row[idx + 7]), + } + idx += 8 + + famille.update( + { + "stat_01": to_str(row[idx]), + "stat_02": to_str(row[idx + 1]), + "stat_03": to_str(row[idx + 2]), + "stat_04": to_str(row[idx + 3]), + "stat_05": to_str(row[idx + 4]), } - idx += 8 - - famille.update( - { - "stat_01": to_str(row[idx]), - "stat_02": to_str(row[idx + 1]), - "stat_03": to_str(row[idx + 2]), - "stat_04": to_str(row[idx + 3]), - "stat_05": to_str(row[idx + 4]), - } - ) - idx += 5 - - famille.update( - { - "code_fiscal": to_str(row[idx]), - "pays": to_str(row[idx + 1]), - "unite_poids": to_str(row[idx + 2]), - "escompte": to_bool(row[idx + 3]), - "delai": to_int(row[idx + 4]), - "hors_statistique": to_bool(row[idx + 5]), - "vente_debit": to_bool(row[idx + 6]), - "non_imprimable": to_bool(row[idx + 7]), - } - ) - idx += 8 - - famille.update( - { - "frais_01_libelle": to_str(row[idx]), - "frais_01_remise_1_valeur": to_float(row[idx + 1]), - "frais_01_remise_1_type": to_int(row[idx + 2]), - "frais_01_remise_2_valeur": to_float(row[idx + 3]), - "frais_01_remise_2_type": to_int(row[idx + 4]), - "frais_01_remise_3_valeur": to_float(row[idx + 5]), - "frais_01_remise_3_type": to_int(row[idx + 6]), - "frais_02_libelle": to_str(row[idx + 7]), - "frais_02_remise_1_valeur": to_float(row[idx + 8]), - "frais_02_remise_1_type": to_int(row[idx + 9]), - "frais_02_remise_2_valeur": to_float(row[idx + 10]), - "frais_02_remise_2_type": to_int(row[idx + 11]), - "frais_02_remise_3_valeur": to_float(row[idx + 12]), - "frais_02_remise_3_type": to_int(row[idx + 13]), - "frais_03_libelle": to_str(row[idx + 14]), - "frais_03_remise_1_valeur": to_float(row[idx + 15]), - "frais_03_remise_1_type": to_int(row[idx + 16]), - "frais_03_remise_2_valeur": to_float(row[idx + 17]), - "frais_03_remise_2_type": to_int(row[idx + 18]), - "frais_03_remise_3_valeur": to_float(row[idx + 19]), - "frais_03_remise_3_type": to_int(row[idx + 20]), - } - ) - idx += 21 - - famille.update( - { - "contremarque": to_bool(row[idx]), - "fact_poids": to_bool(row[idx + 1]), - "fact_forfait": to_bool(row[idx + 2]), - "publie": to_bool(row[idx + 3]), - "racine_reference": to_str(row[idx + 4]), - "racine_code_barre": to_str(row[idx + 5]), - } - ) - idx += 6 - - famille.update( - { - "categorie_1": to_int(row[idx]), - "categorie_2": to_int(row[idx + 1]), - "categorie_3": to_int(row[idx + 2]), - "categorie_4": to_int(row[idx + 3]), - } - ) - idx += 4 - - famille.update( - { - "nature": to_int(row[idx]), - "nb_colis": to_int(row[idx + 1]), - "sous_traitance": to_bool(row[idx + 2]), - "fictif": to_bool(row[idx + 3]), - "criticite": to_int(row[idx + 4]), - } - ) - idx += 5 - - famille.update( - { - "cb_marq": to_int(row[idx]), - "cb_createur": to_str(row[idx + 1]), - "cb_modification": row[ - idx + 2 - ], # datetime - garder tel quel - "cb_creation": row[ - idx + 3 - ], # datetime - garder tel quel - "cb_creation_user": to_str(row[idx + 4]), - } - ) - idx += 5 - - famille.update( - { - "compte_vente": to_str(row[idx]), - "compte_auxiliaire_vente": to_str(row[idx + 1]), - "tva_vente_1": to_str(row[idx + 2]), - "tva_vente_2": to_str(row[idx + 3]), - "tva_vente_3": to_str(row[idx + 4]), - "tva_vente_date_1": row[idx + 5], # datetime - "tva_vente_date_2": row[idx + 6], - "tva_vente_date_3": row[idx + 7], - "type_facture_vente": to_int(row[idx + 8]), - } - ) - idx += 9 - - famille.update( - { - "compte_achat": to_str(row[idx]), - "compte_auxiliaire_achat": to_str(row[idx + 1]), - "tva_achat_1": to_str(row[idx + 2]), - "tva_achat_2": to_str(row[idx + 3]), - "tva_achat_3": to_str(row[idx + 4]), - "tva_achat_date_1": row[idx + 5], - "tva_achat_date_2": row[idx + 6], - "tva_achat_date_3": row[idx + 7], - "type_facture_achat": to_int(row[idx + 8]), - } - ) - idx += 9 - - famille.update( - { - "compte_stock": to_str(row[idx]), - "compte_auxiliaire_stock": to_str(row[idx + 1]), - } - ) - idx += 2 - - famille.update( - { - "fournisseur_principal": to_str(row[idx]), - "fournisseur_unite": to_str(row[idx + 1]), - "fournisseur_conversion": to_float(row[idx + 2]), - "fournisseur_delai_appro": to_int(row[idx + 3]), - "fournisseur_garantie": to_int(row[idx + 4]), - "fournisseur_colisage": to_int(row[idx + 5]), - "fournisseur_qte_mini": to_float(row[idx + 6]), - "fournisseur_qte_mont": to_float(row[idx + 7]), - "fournisseur_enumere_gamme": to_int(row[idx + 8]), - "fournisseur_devise": to_int(row[idx + 9]), - "fournisseur_remise": to_float(row[idx + 10]), - "fournisseur_conv_div": to_float(row[idx + 11]), - "fournisseur_type_remise": to_int(row[idx + 12]), - } - ) - idx += 13 - - famille["nb_articles"] = to_int(row[idx]) - - famille["type_libelle"] = ( - "Total" if famille["type"] == 1 else "Détail" - ) - famille["est_total"] = famille["type"] == 1 - famille["est_detail"] = famille["type"] == 0 - - famille["FA_CodeFamille"] = famille["code"] - famille["FA_Intitule"] = famille["intitule"] - famille["FA_Type"] = famille["type"] - famille["CG_NumVte"] = famille["compte_vente"] - famille["CG_NumAch"] = famille["compte_achat"] - - familles.append(famille) - - type_msg = ( - "DÉTAIL uniquement" if not inclure_totaux else "TOUS types" ) - logger.info(f" {len(familles)} familles chargées ({type_msg})") + idx += 5 - return familles + famille.update( + { + "code_fiscal": to_str(row[idx]), + "pays": to_str(row[idx + 1]), + "unite_poids": to_str(row[idx + 2]), + "escompte": to_bool(row[idx + 3]), + "delai": to_int(row[idx + 4]), + "hors_statistique": to_bool(row[idx + 5]), + "vente_debit": to_bool(row[idx + 6]), + "non_imprimable": to_bool(row[idx + 7]), + } + ) + idx += 8 - except Exception as e: - logger.error(f"Erreur SQL familles: {e}", exc_info=True) - raise RuntimeError(f"Erreur lecture familles: {str(e)}") + famille.update( + { + "frais_01_libelle": to_str(row[idx]), + "frais_01_remise_1_valeur": to_float(row[idx + 1]), + "frais_01_remise_1_type": to_int(row[idx + 2]), + "frais_01_remise_2_valeur": to_float(row[idx + 3]), + "frais_01_remise_2_type": to_int(row[idx + 4]), + "frais_01_remise_3_valeur": to_float(row[idx + 5]), + "frais_01_remise_3_type": to_int(row[idx + 6]), + "frais_02_libelle": to_str(row[idx + 7]), + "frais_02_remise_1_valeur": to_float(row[idx + 8]), + "frais_02_remise_1_type": to_int(row[idx + 9]), + "frais_02_remise_2_valeur": to_float(row[idx + 10]), + "frais_02_remise_2_type": to_int(row[idx + 11]), + "frais_02_remise_3_valeur": to_float(row[idx + 12]), + "frais_02_remise_3_type": to_int(row[idx + 13]), + "frais_03_libelle": to_str(row[idx + 14]), + "frais_03_remise_1_valeur": to_float(row[idx + 15]), + "frais_03_remise_1_type": to_int(row[idx + 16]), + "frais_03_remise_2_valeur": to_float(row[idx + 17]), + "frais_03_remise_2_type": to_int(row[idx + 18]), + "frais_03_remise_3_valeur": to_float(row[idx + 19]), + "frais_03_remise_3_type": to_int(row[idx + 20]), + } + ) + idx += 21 + + famille.update( + { + "contremarque": to_bool(row[idx]), + "fact_poids": to_bool(row[idx + 1]), + "fact_forfait": to_bool(row[idx + 2]), + "publie": to_bool(row[idx + 3]), + "racine_reference": to_str(row[idx + 4]), + "racine_code_barre": to_str(row[idx + 5]), + } + ) + idx += 6 + + famille.update( + { + "categorie_1": to_int(row[idx]), + "categorie_2": to_int(row[idx + 1]), + "categorie_3": to_int(row[idx + 2]), + "categorie_4": to_int(row[idx + 3]), + } + ) + idx += 4 + + famille.update( + { + "nature": to_int(row[idx]), + "nb_colis": to_int(row[idx + 1]), + "sous_traitance": to_bool(row[idx + 2]), + "fictif": to_bool(row[idx + 3]), + "criticite": to_int(row[idx + 4]), + } + ) + idx += 5 + + famille.update( + { + "cb_marq": to_int(row[idx]), + "cb_createur": to_str(row[idx + 1]), + "cb_modification": row[ + idx + 2 + ], # datetime - garder tel quel + "cb_creation": row[ + idx + 3 + ], # datetime - garder tel quel + "cb_creation_user": to_str(row[idx + 4]), + } + ) + idx += 5 + + famille.update( + { + "compte_vente": to_str(row[idx]), + "compte_auxiliaire_vente": to_str(row[idx + 1]), + "tva_vente_1": to_str(row[idx + 2]), + "tva_vente_2": to_str(row[idx + 3]), + "tva_vente_3": to_str(row[idx + 4]), + "tva_vente_date_1": row[idx + 5], # datetime + "tva_vente_date_2": row[idx + 6], + "tva_vente_date_3": row[idx + 7], + "type_facture_vente": to_int(row[idx + 8]), + } + ) + idx += 9 + + famille.update( + { + "compte_achat": to_str(row[idx]), + "compte_auxiliaire_achat": to_str(row[idx + 1]), + "tva_achat_1": to_str(row[idx + 2]), + "tva_achat_2": to_str(row[idx + 3]), + "tva_achat_3": to_str(row[idx + 4]), + "tva_achat_date_1": row[idx + 5], + "tva_achat_date_2": row[idx + 6], + "tva_achat_date_3": row[idx + 7], + "type_facture_achat": to_int(row[idx + 8]), + } + ) + idx += 9 + + famille.update( + { + "compte_stock": to_str(row[idx]), + "compte_auxiliaire_stock": to_str(row[idx + 1]), + } + ) + idx += 2 + + famille.update( + { + "fournisseur_principal": to_str(row[idx]), + "fournisseur_unite": to_str(row[idx + 1]), + "fournisseur_conversion": to_float(row[idx + 2]), + "fournisseur_delai_appro": to_int(row[idx + 3]), + "fournisseur_garantie": to_int(row[idx + 4]), + "fournisseur_colisage": to_int(row[idx + 5]), + "fournisseur_qte_mini": to_float(row[idx + 6]), + "fournisseur_qte_mont": to_float(row[idx + 7]), + "fournisseur_enumere_gamme": to_int(row[idx + 8]), + "fournisseur_devise": to_int(row[idx + 9]), + "fournisseur_remise": to_float(row[idx + 10]), + "fournisseur_conv_div": to_float(row[idx + 11]), + "fournisseur_type_remise": to_int(row[idx + 12]), + } + ) + idx += 13 + + famille["nb_articles"] = to_int(row[idx]) + + famille["type_libelle"] = ( + "Total" if famille["type"] == 1 else "Détail" + ) + famille["est_total"] = famille["type"] == 1 + famille["est_detail"] = famille["type"] == 0 + + famille["FA_CodeFamille"] = famille["code"] + famille["FA_Intitule"] = famille["intitule"] + famille["FA_Type"] = famille["type"] + famille["CG_NumVte"] = famille["compte_vente"] + famille["CG_NumAch"] = famille["compte_achat"] + + familles.append(famille) + + type_msg = ( + "DÉTAIL uniquement" if not inclure_totaux else "TOUS types" + ) + logger.info(f" {len(familles)} familles chargées ({type_msg})") + + return familles + + except Exception as e: + logger.error(f"Erreur SQL familles: {e}", exc_info=True) + raise RuntimeError(f"Erreur lecture familles: {str(e)}") def lire_famille(self, code: str) -> Dict: try: @@ -9977,266 +10379,6 @@ class SageConnector: logger.error(f"[STOCK] ERREUR GLOBALE : {e}", exc_info=True) raise ValueError(f"Erreur création entrée stock : {str(e)}") - def lire_stock_article(self, reference: str, calcul_complet: bool = False) -> Dict: - try: - with self._com_context(), self._lock_com: - logger.info(f"[STOCK] Lecture stock : {reference}") - - factory_article = self.cial.FactoryArticle - persist_article = factory_article.ReadReference(reference.upper()) - - if not persist_article: - raise ValueError(f"Article {reference} introuvable") - - article = win32com.client.CastTo(persist_article, "IBOArticle3") - article.Read() - - ar_suivi = getattr(article, "AR_SuiviStock", 0) - ar_design = getattr(article, "AR_Design", reference) - - stock_info = { - "article": reference.upper(), - "designation": ar_design, - "stock_total": 0.0, - "suivi_stock": ar_suivi, - "suivi_libelle": { - 0: "Aucun suivi", - 1: "CMUP (sans lot)", - 2: "FIFO/LIFO (avec lot)", - }.get(ar_suivi, f"Code {ar_suivi}"), - "depots": [], - "methode_lecture": None, - } - - logger.info("[STOCK] Tentative Méthode 1 : Depot.FactoryDepotStock...") - - try: - factory_depot = self.cial.FactoryDepot - index_depot = 1 - stocks_trouves = [] - - while index_depot <= 20: - try: - persist_depot = factory_depot.List(index_depot) - if persist_depot is None: - break - - depot = win32com.client.CastTo(persist_depot, "IBODepot3") - depot.Read() - - depot_code = "" - depot_intitule = "" - - try: - depot_code = getattr(depot, "DE_Code", "").strip() - depot_intitule = getattr( - depot, "DE_Intitule", f"Dépôt {depot_code}" - ) - except: - pass - - if not depot_code: - index_depot += 1 - continue - - factory_depot_stock = None - - for factory_name in [ - "FactoryDepotStock", - "FactoryArticleStock", - ]: - try: - factory_depot_stock = getattr( - depot, factory_name, None - ) - if factory_depot_stock: - break - except: - pass - - if factory_depot_stock: - index_stock = 1 - - while index_stock <= 1000: - try: - stock_persist = factory_depot_stock.List( - index_stock - ) - if stock_persist is None: - break - - stock = win32com.client.CastTo( - stock_persist, "IBODepotStock3" - ) - stock.Read() - - article_ref_stock = "" - - for attr_ref in [ - "AR_Ref", - "AS_Article", - "Article_Ref", - ]: - try: - val = getattr(stock, attr_ref, None) - if val: - article_ref_stock = ( - str(val).strip().upper() - ) - break - except: - pass - - if not article_ref_stock: - try: - article_obj = getattr( - stock, "Article", None - ) - if article_obj: - article_obj.Read() - article_ref_stock = ( - getattr( - article_obj, "AR_Ref", "" - ) - .strip() - .upper() - ) - except: - pass - - if article_ref_stock == reference.upper(): - quantite = 0.0 - qte_mini = 0.0 - qte_maxi = 0.0 - - for attr_qte in [ - "AS_QteSto", - "AS_Qte", - "QteSto", - "Quantite", - ]: - try: - val = getattr(stock, attr_qte, None) - if val is not None: - quantite = float(val) - break - except: - pass - - try: - qte_mini = float( - getattr(stock, "AS_QteMini", 0.0) - ) - except: - pass - - try: - qte_maxi = float( - getattr(stock, "AS_QteMaxi", 0.0) - ) - except: - pass - - stocks_trouves.append( - { - "code": depot_code, - "intitule": depot_intitule, - "quantite": quantite, - "qte_mini": qte_mini, - "qte_maxi": qte_maxi, - } - ) - - logger.info( - f"[STOCK] Trouvé dépôt {depot_code} : {quantite} unités" - ) - break - - index_stock += 1 - - except Exception as e: - if "Accès refusé" in str(e): - break - index_stock += 1 - - index_depot += 1 - - except Exception as e: - if "Accès refusé" in str(e): - break - index_depot += 1 - - if stocks_trouves: - stock_info["depots"] = stocks_trouves - stock_info["stock_total"] = sum( - d["quantite"] for d in stocks_trouves - ) - stock_info["methode_lecture"] = ( - "Depot.FactoryDepotStock (RAPIDE)" - ) - - logger.info( - f"[STOCK] Méthode 1 réussie : {stock_info['stock_total']} unités" - ) - return stock_info - - except Exception as e: - logger.warning(f"[STOCK] Méthode 1 échouée : {e}") - - logger.info("[STOCK] Tentative Méthode 2 : Attributs Article...") - - try: - stock_trouve = False - - for attr_stock in ["AR_Stock", "AR_QteSto", "AR_QteStock"]: - try: - val = getattr(article, attr_stock, None) - if val is not None: - stock_info["stock_total"] = float(val) - stock_info["methode_lecture"] = ( - f"Article.{attr_stock} (RAPIDE)" - ) - stock_trouve = True - logger.info( - f"[STOCK] Méthode 2 réussie via {attr_stock}" - ) - break - except: - pass - - if stock_trouve: - return stock_info - - except Exception as e: - logger.warning(f"[STOCK] Méthode 2 échouée : {e}") - - if not calcul_complet: - logger.warning( - f"[STOCK] Méthodes rapides échouées pour {reference}" - ) - - stock_info["methode_lecture"] = "ÉCHEC - Méthodes rapides échouées" - stock_info["stock_total"] = 0.0 - stock_info["note"] = ( - "Les méthodes rapides de lecture de stock ont échoué. " - "Options disponibles :\n" - "1. Utiliser GET /sage/diagnostic/stock-rapide-test/{reference} pour un diagnostic détaillé\n" - "2. Appeler lire_stock_article(reference, calcul_complet=True) pour un calcul depuis mouvements (LENT - 20+ min)\n" - "3. Vérifier que l'article a bien un suivi de stock activé (AR_SuiviStock)" - ) - - return stock_info - - logger.warning( - "[STOCK] CALCUL DEPUIS MOUVEMENTS ACTIVÉ - PEUT PRENDRE 20+ MINUTES" - ) - - except ValueError: - raise - except Exception as e: - logger.error(f"[STOCK] Erreur lecture : {e}", exc_info=True) - raise ValueError(f"Erreur lecture stock : {str(e)}") - def creer_sortie_stock(self, sortie_data: Dict) -> Dict: try: with ( diff --git a/utils/documents/devis/devis_check.py b/utils/documents/devis/devis_check.py index 4bd22d1..4e74a18 100644 --- a/utils/documents/devis/devis_check.py +++ b/utils/documents/devis/devis_check.py @@ -28,7 +28,7 @@ def _rechercher_devis_dans_liste(numero_devis, factory_doc): return None -def _recuperer_numero_devis(self, process, doc): +def _recuperer_numero_devis(process, doc): """Récupère le numéro du devis créé via plusieurs méthodes.""" numero_devis = None @@ -55,66 +55,7 @@ def _recuperer_numero_devis(self, process, doc): return numero_devis -def _relire_devis(self, numero_devis, devis_data, forcer_brouillon): - """Relit le devis créé et extrait les informations finales.""" - factory_doc = self.cial.FactoryDocumentVente - persist_reread = factory_doc.ReadPiece(0, numero_devis) - - if not persist_reread: - logger.debug("ReadPiece échoué, recherche dans List()...") - persist_reread = _rechercher_devis_dans_liste( - numero_devis, factory_doc - ) - - if persist_reread: - doc_final = win32com.client.CastTo(persist_reread, "IBODocumentVente3") - doc_final.Read() - - total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) - total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) - statut_final = getattr(doc_final, "DO_Statut", 0) - reference_final = getattr(doc_final, "DO_Ref", "") - - date_livraison_final = None - - try: - date_livr = getattr(doc_final, "DO_DateLivr", None) - if date_livr: - date_livraison_final = date_livr.strftime("%Y-%m-%d") - except: - pass - else: - total_calcule = sum( - l.get("montant_ligne_ht", 0) for l in devis_data["lignes"] - ) - total_ht = total_calcule - total_ttc = round(total_calcule * 1.20, 2) - statut_final = 0 if forcer_brouillon else 2 - reference_final = devis_data.get("reference", "") - date_livraison_final = devis_data.get("date_livraison") - - logger.info(f" Total HT: {total_ht}€") - logger.info(f" Total TTC: {total_ttc}€") - logger.info(f" Statut final: {statut_final}") - if reference_final: - logger.info(f" Référence: {reference_final}") - if date_livraison_final: - logger.info(f" Date livraison: {date_livraison_final}") - - return { - "numero_devis": numero_devis, - "total_ht": total_ht, - "total_ttc": total_ttc, - "nb_lignes": len(devis_data["lignes"]), - "client_code": devis_data["client"]["code"], - "date_devis": str(devis_data.get("date_devis", "")), - "date_livraison": date_livraison_final, - "reference": reference_final, - "statut": statut_final, - } - - __all__ = [ "_recuperer_numero_devis", - "_relire_devis" + "_rechercher_devis_dans_liste" ] \ No newline at end of file diff --git a/utils/functions/sage_utilities.py b/utils/functions/sage_utilities.py index 0c8eaef..a5f809a 100644 --- a/utils/functions/sage_utilities.py +++ b/utils/functions/sage_utilities.py @@ -114,7 +114,7 @@ def verifier_si_deja_transforme_sql(numero_source, cursor, type_source): logger.error(f"[VERIF] Erreur vérification: {e}") return {"deja_transforme": False, "documents_cibles": []} -def peut_etre_transforme(numero_source, type_source, type_cible): +def peut_etre_transforme(cursor, numero_source, type_source, type_cible): """Version corrigée avec normalisation""" type_source = _normaliser_type_document(type_source) type_cible = _normaliser_type_document(type_cible) @@ -124,7 +124,7 @@ def peut_etre_transforme(numero_source, type_source, type_cible): f"(type {type_source}) → type {type_cible}" ) - verif = verifier_si_deja_transforme_sql(numero_source, type_source) + verif = verifier_si_deja_transforme_sql(cursor, numero_source, type_source) docs_meme_type = [ d for d in verif["documents_cibles"] if d["type"] == type_cible diff --git a/utils/tiers/__init__.py b/utils/tiers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/tiers/contacts/contacts.py b/utils/tiers/contacts/contacts.py index 7b2d8ef..ac0878c 100644 --- a/utils/tiers/contacts/contacts.py +++ b/utils/tiers/contacts/contacts.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Any +from typing import Dict, Optional from utils.functions.items_to_dict import _row_to_contact_dict import logging from utils.functions.functions import _safe_strip @@ -56,7 +56,113 @@ def _get_contacts_client(numero: str, conn) -> list: except Exception as e: logger.warning(f" Impossible de récupérer contacts pour {numero}: {e}") return [] + +def _chercher_contact_en_base( + conn, + numero_client: str, + nom: str, + prenom: Optional[str] = None +) -> Optional[Dict]: + try: + cursor = conn.cursor() + + if prenom: + query = """ + SELECT TOP 1 CT_No, N_Contact + FROM F_CONTACTT + WHERE CT_Num = ? + AND LTRIM(RTRIM(CT_Nom)) = ? + AND LTRIM(RTRIM(CT_Prenom)) = ? + ORDER BY CT_No DESC + """ + cursor.execute(query, (numero_client.upper(), nom.strip(), prenom.strip())) + else: + query = """ + SELECT TOP 1 CT_No, N_Contact + FROM F_CONTACTT + WHERE CT_Num = ? + AND LTRIM(RTRIM(CT_Nom)) = ? + AND (CT_Prenom IS NULL OR LTRIM(RTRIM(CT_Prenom)) = '') + ORDER BY CT_No DESC + """ + cursor.execute(query, (numero_client.upper(), nom.strip())) + + row = cursor.fetchone() + + if row: + return { + "contact_numero": row.CT_No, + "n_contact": row.N_Contact + } + + return None + + except Exception as e: + logger.warning(f"Erreur recherche contact en base: {e}") + return None + +def _lire_contact_depuis_base( + conn, + numero_client: str, + contact_no: int +) -> Optional[Dict]: + try: + cursor = conn.cursor() + + query = """ + SELECT + CT_Num, CT_No, N_Contact, + CT_Civilite, CT_Nom, CT_Prenom, CT_Fonction, + N_Service, + CT_Telephone, CT_TelPortable, CT_Telecopie, CT_EMail, + CT_Facebook, CT_LinkedIn, CT_Skype + FROM F_CONTACTT + WHERE CT_Num = ? AND CT_No = ? + """ + + logger.info(f" Execution SQL: CT_Num='{numero_client.upper()}', CT_No={contact_no}") + cursor.execute(query, (numero_client.upper(), contact_no)) + + row = cursor.fetchone() + + if not row: + logger.warning(f" Aucune ligne retournée pour CT_Num={numero_client.upper()}, CT_No={contact_no}") + return None + + logger.info(f" Ligne SQL trouvée: Nom={row.CT_Nom}, Prenom={row.CT_Prenom}") + + civilite_map = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"} + + result = { + "numero": _safe_strip(row.CT_Num), + "contact_numero": row.CT_No, + "n_contact": row.N_Contact, + "civilite": civilite_map.get(row.CT_Civilite, None), + "nom": _safe_strip(row.CT_Nom), + "prenom": _safe_strip(row.CT_Prenom), + "fonction": _safe_strip(row.CT_Fonction), + "service_code": row.N_Service, + "telephone": _safe_strip(row.CT_Telephone), + "portable": _safe_strip(row.CT_TelPortable), + "telecopie": _safe_strip(row.CT_Telecopie), + "email": _safe_strip(row.CT_EMail), + "facebook": _safe_strip(row.CT_Facebook), + "linkedin": _safe_strip(row.CT_LinkedIn), + "skype": _safe_strip(row.CT_Skype), + "est_defaut": False, + } + + logger.info(f" Dict construit: numero={result['numero']}, nom={result['nom']}") + return result + + except Exception as e: + logger.error(f" Exception dans : {e}", exc_info=True) + return None + + __all__ = [ - "_get_contacts_client" + "_get_contacts_client", + "_chercher_contact_en_base", + "_lire_contact_depuis_base" ] \ No newline at end of file