import win32com.client import pythoncom from datetime import datetime, date from typing import Dict, List, Optional import threading import time import logging from config import settings import pyodbc from contextlib import contextmanager import pywintypes from utils.articles.articles_data_sql import ( _enrichir_stock_emplacements, _enrichir_gammes_articles, _enrichir_tarifs_clients, _enrichir_nomenclature, _enrichir_compta_articles, _enrichir_fournisseurs_multiples, _enrichir_depots_details, _enrichir_emplacements_details, _enrichir_gammes_enumeres, _enrichir_references_enumerees, _enrichir_medias_articles, _enrichir_prix_gammes, _enrichir_conditionnements, _mapper_article_depuis_row, _enrichir_stocks_articles, _enrichir_fournisseurs_articles, _enrichir_familles_articles, _enrichir_tva_articles, ) from utils.tiers.clients.clients_data import ( _extraire_client, _cast_client, ) from utils.articles.stock_check import verifier_stock_suffisant from utils.articles.articles_data_com import _extraire_article from utils.tiers.tiers_data_sql import _build_tiers_select_query from utils.functions.functions import ( _safe_strip, _safe_int, _clean_str, _try_set_attribute, normaliser_date, _get_type_libelle, ) from utils.functions.items_to_dict import ( _contact_to_dict, _row_to_contact_dict, _row_to_tiers_dict, ) from utils.functions.sage_utilities import ( _verifier_devis_non_transforme, peut_etre_transforme, lire_erreurs_sage, ) from utils.documents.documents_data_sql import ( _afficher_etat_document, _compter_lignes_document, _rechercher_devis_par_numero, _lire_document_sql, _lister_documents_avec_lignes_sql, ) from utils.documents.devis.devis_extraction import _extraire_infos_devis from utils.documents.devis.devis_check import ( _recuperer_numero_devis, _rechercher_devis_dans_liste, ) from utils.tiers.contacts.contacts import ( _get_contacts_client, _chercher_contact_en_base, _lire_contact_depuis_base, ) from utils import ( valider_donnees_creation, mapper_champ_api_vers_sage, CHAMPS_STOCK_INITIAL, CHAMPS_ASSIGNABLES_CREATION, CHAMPS_ASSIGNABLES_MODIFICATION, CHAMPS_OBJETS_SPECIAUX, valider_champ, valider_donnees_modification, obtenir_champs_assignables, ) logger = logging.getLogger(__name__) class SageConnector: def __init__(self, chemin_base, utilisateur="", mot_de_passe=""): self.chemin_base = chemin_base self.utilisateur = utilisateur self.mot_de_passe = mot_de_passe self.cial = None self.sql_server = "OV-FDDDC6\\SAGE100" self.sql_database = "BIJOU" self.sql_conn_string = ( f"DRIVER={{ODBC Driver 17 for SQL Server}};" f"SERVER={self.sql_server};" f"DATABASE={self.sql_database};" f"Trusted_Connection=yes;" f"Encrypt=no;" ) self._lock_com = threading.RLock() self._thread_local = threading.local() @contextmanager def _com_context(self): if not hasattr(self._thread_local, "com_initialized"): try: pythoncom.CoInitialize() self._thread_local.com_initialized = True logger.debug( f"COM initialisé pour thread {threading.current_thread().name}" ) except Exception as e: logger.error(f"Erreur initialisation COM: {e}") raise try: yield finally: pass @contextmanager def _get_sql_connection(self): """Context manager pour connexions SQL""" conn = None try: conn = pyodbc.connect(self.sql_conn_string, timeout=10) yield conn except pyodbc.Error as e: logger.error(f" Erreur SQL: {e}") raise RuntimeError(f"Erreur SQL: {str(e)}") finally: if conn: conn.close() def _cleanup_com_thread(self): """Nettoie COM pour le thread actuel (à appeler à la fin)""" if hasattr(self._thread_local, "com_initialized"): try: pythoncom.CoUninitialize() delattr(self._thread_local, "com_initialized") logger.debug( f"COM nettoyé pour thread {threading.current_thread().name}" ) except Exception: pass def connecter(self): """Connexion initiale à Sage - VERSION HYBRIDE""" try: with self._com_context(): self.cial = win32com.client.gencache.EnsureDispatch( "Objets100c.Cial.Stream" ) self.cial.Name = self.chemin_base self.cial.Loggable.UserName = self.utilisateur self.cial.Loggable.UserPwd = self.mot_de_passe self.cial.Open() logger.info(f" Connexion COM Sage réussie: {self.chemin_base}") try: with self._get_sql_connection() as conn: conn.cursor() except Exception as e: logger.warning(f"SQL non disponible: {e}") logger.warning(" Les lectures utiliseront COM (plus lent)") return True except Exception as e: logger.error(f" Erreur connexion Sage: {e}", exc_info=True) return False def deconnecter(self): """Déconnexion propre""" if self.cial: try: with self._com_context(): self.cial.Close() logger.info("Connexion Sage fermée") except Exception: pass def lister_tous_fournisseurs(self, filtre=""): try: with self._get_sql_connection() as conn: cursor = conn.cursor() query = """ SELECT -- IDENTIFICATION (9) CT_Num, CT_Intitule, CT_Type, CT_Qualite, CT_Classement, CT_Raccourci, CT_Siret, CT_Identifiant, CT_Ape, -- ADRESSE (7) CT_Contact, CT_Adresse, CT_Complement, CT_CodePostal, CT_Ville, CT_CodeRegion, CT_Pays, -- TELECOM (6) CT_Telephone, CT_Telecopie, CT_EMail, CT_Site, CT_Facebook, CT_LinkedIn, -- TAUX (4) CT_Taux01, CT_Taux02, CT_Taux03, CT_Taux04, -- STATISTIQUES (10) CT_Statistique01, CT_Statistique02, CT_Statistique03, CT_Statistique04, CT_Statistique05, CT_Statistique06, CT_Statistique07, CT_Statistique08, CT_Statistique09, CT_Statistique10, -- COMMERCIAL (4) CT_Encours, CT_Assurance, CT_Langue, CO_No, -- FACTURATION (11) CT_Lettrage, CT_Sommeil, CT_Facture, CT_Prospect, CT_BLFact, CT_Saut, CT_ValidEch, CT_ControlEnc, CT_NotRappel, CT_NotPenal, CT_BonAPayer, -- LOGISTIQUE (4) CT_PrioriteLivr, CT_LivrPartielle, CT_DelaiTransport, CT_DelaiAppro, -- COMMENTAIRE (1) CT_Commentaire, -- ANALYTIQUE (1) CA_Num, -- ORGANISATION / SURVEILLANCE (10) MR_No, CT_Surveillance, CT_Coface, CT_SvFormeJuri, CT_SvEffectif, CT_SvRegul, CT_SvCotation, CT_SvObjetMaj, CT_SvCA, CT_SvResultat, -- COMPTE GENERAL ET CATEGORIES (3) CG_NumPrinc, N_CatTarif, N_CatCompta FROM F_COMPTET WHERE CT_Type = 1 """ params = [] if filtre: query += " AND (CT_Num LIKE ? OR CT_Intitule LIKE ?)" params.extend([f"%{filtre}%", f"%{filtre}%"]) query += " ORDER BY CT_Intitule" cursor.execute(query, params) rows = cursor.fetchall() fournisseurs = [] for row in rows: fournisseur = { "numero": _safe_strip(row.CT_Num), "intitule": _safe_strip(row.CT_Intitule), "type_tiers": row.CT_Type, "qualite": _safe_strip(row.CT_Qualite), "classement": _safe_strip(row.CT_Classement), "raccourci": _safe_strip(row.CT_Raccourci), "siret": _safe_strip(row.CT_Siret), "tva_intra": _safe_strip(row.CT_Identifiant), "code_naf": _safe_strip(row.CT_Ape), "contact": _safe_strip(row.CT_Contact), "adresse": _safe_strip(row.CT_Adresse), "complement": _safe_strip(row.CT_Complement), "code_postal": _safe_strip(row.CT_CodePostal), "ville": _safe_strip(row.CT_Ville), "region": _safe_strip(row.CT_CodeRegion), "pays": _safe_strip(row.CT_Pays), "telephone": _safe_strip(row.CT_Telephone), "telecopie": _safe_strip(row.CT_Telecopie), "email": _safe_strip(row.CT_EMail), "site_web": _safe_strip(row.CT_Site), "facebook": _safe_strip(row.CT_Facebook), "linkedin": _safe_strip(row.CT_LinkedIn), "taux01": row.CT_Taux01, "taux02": row.CT_Taux02, "taux03": row.CT_Taux03, "taux04": row.CT_Taux04, "statistique01": _safe_strip(row.CT_Statistique01), "statistique02": _safe_strip(row.CT_Statistique02), "statistique03": _safe_strip(row.CT_Statistique03), "statistique04": _safe_strip(row.CT_Statistique04), "statistique05": _safe_strip(row.CT_Statistique05), "statistique06": _safe_strip(row.CT_Statistique06), "statistique07": _safe_strip(row.CT_Statistique07), "statistique08": _safe_strip(row.CT_Statistique08), "statistique09": _safe_strip(row.CT_Statistique09), "statistique10": _safe_strip(row.CT_Statistique10), "encours_autorise": row.CT_Encours, "assurance_credit": row.CT_Assurance, "langue": row.CT_Langue, "commercial_code": row.CO_No, "lettrage_auto": (row.CT_Lettrage == 1), "est_actif": (row.CT_Sommeil == 0), "type_facture": row.CT_Facture, "est_prospect": (row.CT_Prospect == 1), "bl_en_facture": row.CT_BLFact, "saut_page": row.CT_Saut, "validation_echeance": row.CT_ValidEch, "controle_encours": row.CT_ControlEnc, "exclure_relance": (row.CT_NotRappel == 1), "exclure_penalites": (row.CT_NotPenal == 1), "bon_a_payer": row.CT_BonAPayer, "priorite_livraison": row.CT_PrioriteLivr, "livraison_partielle": row.CT_LivrPartielle, "delai_transport": row.CT_DelaiTransport, "delai_appro": row.CT_DelaiAppro, "commentaire": _safe_strip(row.CT_Commentaire), "section_analytique": _safe_strip(row.CA_Num), "mode_reglement_code": row.MR_No, "surveillance_active": (row.CT_Surveillance == 1), "coface": _safe_strip(row.CT_Coface), "forme_juridique": _safe_strip(row.CT_SvFormeJuri), "effectif": _safe_strip(row.CT_SvEffectif), "sv_regularite": _safe_strip(row.CT_SvRegul), "sv_cotation": _safe_strip(row.CT_SvCotation), "sv_objet_maj": _safe_strip(row.CT_SvObjetMaj), "sv_chiffre_affaires": row.CT_SvCA, "sv_resultat": row.CT_SvResultat, "compte_general": _safe_strip(row.CG_NumPrinc), "categorie_tarif": row.N_CatTarif, "categorie_compta": row.N_CatCompta, } fournisseur["contacts"] = _get_contacts_client(row.CT_Num, conn) fournisseurs.append(fournisseur) logger.info( f" SQL: {len(fournisseurs)} fournisseurs avec {len(fournisseur)} champs" ) return fournisseurs except Exception as e: logger.error(f" Erreur SQL fournisseurs: {e}") raise RuntimeError(f"Erreur lecture fournisseurs: {str(e)}") def lire_fournisseur(self, code_fournisseur): try: with self._get_sql_connection() as conn: cursor = conn.cursor() query = """ SELECT -- IDENTIFICATION (9) CT_Num, CT_Intitule, CT_Type, CT_Qualite, CT_Classement, CT_Raccourci, CT_Siret, CT_Identifiant, CT_Ape, -- ADRESSE (7) CT_Contact, CT_Adresse, CT_Complement, CT_CodePostal, CT_Ville, CT_CodeRegion, CT_Pays, -- TELECOM (6) CT_Telephone, CT_Telecopie, CT_EMail, CT_Site, CT_Facebook, CT_LinkedIn, -- TAUX (4) CT_Taux01, CT_Taux02, CT_Taux03, CT_Taux04, -- STATISTIQUES (10) CT_Statistique01, CT_Statistique02, CT_Statistique03, CT_Statistique04, CT_Statistique05, CT_Statistique06, CT_Statistique07, CT_Statistique08, CT_Statistique09, CT_Statistique10, -- COMMERCIAL (4) CT_Encours, CT_Assurance, CT_Langue, CO_No, -- FACTURATION (11) CT_Lettrage, CT_Sommeil, CT_Facture, CT_Prospect, CT_BLFact, CT_Saut, CT_ValidEch, CT_ControlEnc, CT_NotRappel, CT_NotPenal, CT_BonAPayer, -- LOGISTIQUE (4) CT_PrioriteLivr, CT_LivrPartielle, CT_DelaiTransport, CT_DelaiAppro, -- COMMENTAIRE (1) CT_Commentaire, -- ANALYTIQUE (1) CA_Num, -- ORGANISATION / SURVEILLANCE (10) MR_No, CT_Surveillance, CT_Coface, CT_SvFormeJuri, CT_SvEffectif, CT_SvRegul, CT_SvCotation, CT_SvObjetMaj, CT_SvCA, CT_SvResultat, -- COMPTE GENERAL ET CATEGORIES (3) CG_NumPrinc, N_CatTarif, N_CatCompta FROM F_COMPTET WHERE CT_Num = ? AND CT_Type = 1 """ cursor.execute(query, (code_fournisseur.upper(),)) row = cursor.fetchone() if not row: return None fournisseur = { "numero": _safe_strip(row.CT_Num), "intitule": _safe_strip(row.CT_Intitule), "type_tiers": row.CT_Type, "qualite": _safe_strip(row.CT_Qualite), "classement": _safe_strip(row.CT_Classement), "raccourci": _safe_strip(row.CT_Raccourci), "siret": _safe_strip(row.CT_Siret), "tva_intra": _safe_strip(row.CT_Identifiant), "code_naf": _safe_strip(row.CT_Ape), "contact": _safe_strip(row.CT_Contact), "adresse": _safe_strip(row.CT_Adresse), "complement": _safe_strip(row.CT_Complement), "code_postal": _safe_strip(row.CT_CodePostal), "ville": _safe_strip(row.CT_Ville), "region": _safe_strip(row.CT_CodeRegion), "pays": _safe_strip(row.CT_Pays), "telephone": _safe_strip(row.CT_Telephone), "telecopie": _safe_strip(row.CT_Telecopie), "email": _safe_strip(row.CT_EMail), "site_web": _safe_strip(row.CT_Site), "facebook": _safe_strip(row.CT_Facebook), "linkedin": _safe_strip(row.CT_LinkedIn), "taux01": row.CT_Taux01, "taux02": row.CT_Taux02, "taux03": row.CT_Taux03, "taux04": row.CT_Taux04, "statistique01": _safe_strip(row.CT_Statistique01), "statistique02": _safe_strip(row.CT_Statistique02), "statistique03": _safe_strip(row.CT_Statistique03), "statistique04": _safe_strip(row.CT_Statistique04), "statistique05": _safe_strip(row.CT_Statistique05), "statistique06": _safe_strip(row.CT_Statistique06), "statistique07": _safe_strip(row.CT_Statistique07), "statistique08": _safe_strip(row.CT_Statistique08), "statistique09": _safe_strip(row.CT_Statistique09), "statistique10": _safe_strip(row.CT_Statistique10), "encours_autorise": row.CT_Encours, "assurance_credit": row.CT_Assurance, "langue": row.CT_Langue, "commercial_code": row.CO_No, "lettrage_auto": (row.CT_Lettrage == 1), "est_actif": (row.CT_Sommeil == 0), "type_facture": row.CT_Facture, "est_prospect": (row.CT_Prospect == 1), "bl_en_facture": row.CT_BLFact, "saut_page": row.CT_Saut, "validation_echeance": row.CT_ValidEch, "controle_encours": row.CT_ControlEnc, "exclure_relance": (row.CT_NotRappel == 1), "exclure_penalites": (row.CT_NotPenal == 1), "bon_a_payer": row.CT_BonAPayer, "priorite_livraison": row.CT_PrioriteLivr, "livraison_partielle": row.CT_LivrPartielle, "delai_transport": row.CT_DelaiTransport, "delai_appro": row.CT_DelaiAppro, "commentaire": _safe_strip(row.CT_Commentaire), "section_analytique": _safe_strip(row.CA_Num), "mode_reglement_code": row.MR_No, "surveillance_active": (row.CT_Surveillance == 1), "coface": _safe_strip(row.CT_Coface), "forme_juridique": _safe_strip(row.CT_SvFormeJuri), "effectif": _safe_strip(row.CT_SvEffectif), "sv_regularite": _safe_strip(row.CT_SvRegul), "sv_cotation": _safe_strip(row.CT_SvCotation), "sv_objet_maj": _safe_strip(row.CT_SvObjetMaj), "sv_chiffre_affaires": row.CT_SvCA, "sv_resultat": row.CT_SvResultat, "compte_general": _safe_strip(row.CG_NumPrinc), "categorie_tarif": row.N_CatTarif, "categorie_compta": row.N_CatCompta, } fournisseur["contacts"] = _get_contacts_client(row.CT_Num, conn) logger.info( f" SQL: Fournisseur {code_fournisseur} avec {len(fournisseur)} champs" ) return fournisseur except Exception as e: logger.error(f" Erreur SQL fournisseur {code_fournisseur}: {e}") return None def creer_fournisseur(self, fournisseur_data: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: logger.info(" === VALIDATION DONNÉES FOURNISSEUR ===") if not fournisseur_data.get("intitule"): raise ValueError("Le champ 'intitule' est obligatoire") intitule = str(fournisseur_data["intitule"])[:69].strip() num_prop = ( str(fournisseur_data.get("num", "")).upper()[:17].strip() if fournisseur_data.get("num") else "" ) compte = str(fournisseur_data.get("compte_collectif", "4010000"))[ :13 ].strip() adresse = str(fournisseur_data.get("adresse", ""))[:35].strip() code_postal = str(fournisseur_data.get("code_postal", ""))[:9].strip() ville = str(fournisseur_data.get("ville", ""))[:35].strip() pays = str(fournisseur_data.get("pays", ""))[:35].strip() telephone = str(fournisseur_data.get("telephone", ""))[:21].strip() email = str(fournisseur_data.get("email", ""))[:69].strip() siret = str(fournisseur_data.get("siret", ""))[:14].strip() tva_intra = str(fournisseur_data.get("tva_intra", ""))[:25].strip() logger.info(f" intitule: '{intitule}' (len={len(intitule)})") logger.info(f" num: '{num_prop or 'AUTO'}' (len={len(num_prop)})") logger.info(f" compte: '{compte}' (len={len(compte)})") factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur persist = factory_fournisseur.Create() fournisseur = win32com.client.CastTo(persist, "IBOFournisseur3") fournisseur.SetDefault() logger.info(" Objet fournisseur créé et initialisé") logger.info(" Définition des champs obligatoires...") fournisseur.CT_Intitule = intitule logger.debug(f" CT_Intitule: '{intitule}'") try: fournisseur.CT_Type = 1 logger.debug(" CT_Type: 1 (Fournisseur)") except Exception: logger.debug(" CT_Type non défini (géré par FactoryFournisseur)") try: fournisseur.CT_Qualite = "FOU" logger.debug(" CT_Qualite: 'FOU'") except Exception: logger.debug(" CT_Qualite non défini (pas critique)") try: factory_compte = self.cial.CptaApplication.FactoryCompteG persist_compte = factory_compte.ReadNumero(compte) if persist_compte: compte_obj = win32com.client.CastTo( persist_compte, "IBOCompteG3" ) compte_obj.Read() fournisseur.CompteGPrinc = compte_obj logger.debug(f" CompteGPrinc: objet '{compte}' assigné") else: logger.warning( f" Compte {compte} introuvable - utilisation défaut" ) except Exception as e: logger.warning(f" Erreur CompteGPrinc: {e}") if num_prop: fournisseur.CT_Num = num_prop logger.debug(f" CT_Num fourni: '{num_prop}'") else: try: if hasattr(fournisseur, "SetDefaultNumPiece"): fournisseur.SetDefaultNumPiece() num_genere = getattr(fournisseur, "CT_Num", "") logger.debug(f" CT_Num auto-généré: '{num_genere}'") else: num_genere = factory_fournisseur.GetNextNumero() if num_genere: fournisseur.CT_Num = num_genere logger.debug( f" CT_Num auto (GetNextNumero): '{num_genere}'" ) else: import time num_genere = f"FOUR{int(time.time()) % 1000000}" fournisseur.CT_Num = num_genere logger.warning(f" CT_Num fallback: '{num_genere}'") except Exception as e: logger.error(f" Impossible de générer CT_Num: {e}") raise ValueError( "Impossible de générer le numéro fournisseur automatiquement" ) try: if hasattr(fournisseur, "N_CatTarif"): fournisseur.N_CatTarif = 1 if hasattr(fournisseur, "N_CatCompta"): fournisseur.N_CatCompta = 1 if hasattr(fournisseur, "N_Period"): fournisseur.N_Period = 1 logger.debug(" Catégories (N_*) initialisées") except Exception as e: logger.warning(f" Catégories: {e}") logger.info(" Définition champs optionnels...") if any([adresse, code_postal, ville, pays]): try: adresse_obj = fournisseur.Adresse if adresse: adresse_obj.Adresse = adresse if code_postal: adresse_obj.CodePostal = code_postal if ville: adresse_obj.Ville = ville if pays: adresse_obj.Pays = pays logger.debug(" Adresse définie") except Exception as e: logger.warning(f" Adresse: {e}") if telephone or email: try: telecom_obj = fournisseur.Telecom if telephone: telecom_obj.Telephone = telephone if email: telecom_obj.EMail = email logger.debug(" Télécom défini") except Exception as e: logger.warning(f" Télécom: {e}") if siret: try: fournisseur.CT_Siret = siret logger.debug(f" SIRET: '{siret}'") except Exception as e: logger.warning(f" SIRET: {e}") if tva_intra: try: fournisseur.CT_Identifiant = tva_intra logger.debug(f" TVA intra: '{tva_intra}'") except Exception as e: logger.warning(f" TVA: {e}") try: if hasattr(fournisseur, "CT_Lettrage"): fournisseur.CT_Lettrage = True if hasattr(fournisseur, "CT_Sommeil"): fournisseur.CT_Sommeil = False logger.debug(" Options par défaut définies") except Exception as e: logger.debug(f" Options: {e}") logger.info(" === DIAGNOSTIC PRÉ-WRITE ===") num_avant_write = getattr(fournisseur, "CT_Num", "") if not num_avant_write: logger.error(" CRITIQUE: CT_Num toujours vide !") raise ValueError("Le numéro fournisseur (CT_Num) est obligatoire") logger.info(f" CT_Num confirmé: '{num_avant_write}'") logger.info(" Écriture du fournisseur dans Sage...") try: fournisseur.Write() logger.info(" 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})" ) logger.error(f" Erreur Sage: {error_detail}") except Exception: pass if ( "doublon" in error_detail.lower() or "existe" in error_detail.lower() ): raise ValueError(f"Ce fournisseur existe déjà : {error_detail}") raise RuntimeError(f"Échec Write(): {error_detail}") try: fournisseur.Read() except Exception as e: logger.warning(f"Impossible de relire: {e}") num_final = getattr(fournisseur, "CT_Num", "") if not num_final: raise RuntimeError("CT_Num vide après Write()") logger.info(f" FOURNISSEUR CRÉÉ: {num_final} - {intitule} ") resultat = { "numero": num_final, "intitule": intitule, "compte_collectif": compte, "type": 1, "est_fournisseur": True, "adresse": adresse or None, "code_postal": code_postal or None, "ville": ville or None, "pays": pays or None, "email": email or None, "telephone": telephone or None, "siret": siret or None, "tva_intra": tva_intra or None, } return resultat except ValueError as e: logger.error(f" Erreur métier: {e}") raise except Exception as e: logger.error(f" Erreur création fournisseur: {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 Exception: pass raise RuntimeError(f"Erreur technique Sage: {error_message}") def modifier_fournisseur(self, code: str, fournisseur_data: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: logger.info(f" Recherche fournisseur {code}...") factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur persist = factory_fournisseur.ReadNumero(code) if not persist: raise ValueError(f"Fournisseur {code} introuvable") fournisseur = _cast_client(persist) if not fournisseur: raise ValueError(f"Impossible de charger le fournisseur {code}") logger.info( f" Fournisseur {code} trouvé: {getattr(fournisseur, 'CT_Intitule', '')}" ) logger.info(" Mise à jour des champs...") champs_modifies = [] if "intitule" in fournisseur_data: intitule = str(fournisseur_data["intitule"])[:69].strip() fournisseur.CT_Intitule = intitule champs_modifies.append(f"intitule='{intitule}'") if any( k in fournisseur_data for k in ["adresse", "code_postal", "ville", "pays"] ): try: adresse_obj = fournisseur.Adresse if "adresse" in fournisseur_data: adresse = str(fournisseur_data["adresse"])[:35].strip() adresse_obj.Adresse = adresse champs_modifies.append("adresse") if "code_postal" in fournisseur_data: cp = str(fournisseur_data["code_postal"])[:9].strip() adresse_obj.CodePostal = cp champs_modifies.append("code_postal") if "ville" in fournisseur_data: ville = str(fournisseur_data["ville"])[:35].strip() adresse_obj.Ville = ville champs_modifies.append("ville") if "pays" in fournisseur_data: pays = str(fournisseur_data["pays"])[:35].strip() adresse_obj.Pays = pays champs_modifies.append("pays") except Exception as e: logger.warning(f"Erreur mise à jour adresse: {e}") if "email" in fournisseur_data or "telephone" in fournisseur_data: try: telecom_obj = fournisseur.Telecom if "email" in fournisseur_data: email = str(fournisseur_data["email"])[:69].strip() telecom_obj.EMail = email champs_modifies.append("email") if "telephone" in fournisseur_data: tel = str(fournisseur_data["telephone"])[:21].strip() telecom_obj.Telephone = tel champs_modifies.append("telephone") except Exception as e: logger.warning(f"Erreur mise à jour télécom: {e}") if "siret" in fournisseur_data: try: siret = str(fournisseur_data["siret"])[:14].strip() fournisseur.CT_Siret = siret champs_modifies.append("siret") except Exception as e: logger.warning(f"Erreur mise à jour SIRET: {e}") if "tva_intra" in fournisseur_data: try: tva = str(fournisseur_data["tva_intra"])[:25].strip() fournisseur.CT_Identifiant = tva champs_modifies.append("tva_intra") except Exception as e: logger.warning(f"Erreur mise à jour TVA: {e}") if not champs_modifies: logger.warning("Aucun champ à modifier") return { "numero": getattr(fournisseur, "CT_Num", "").strip(), "intitule": getattr(fournisseur, "CT_Intitule", "").strip(), "type": 1, "est_fournisseur": True, } logger.info(f" Champs à modifier: {', '.join(champs_modifies)}") logger.info(" Écriture des modifications...") try: fournisseur.Write() logger.info(" 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})" ) except Exception: pass logger.error(f" Erreur Write(): {error_detail}") raise RuntimeError(f"Échec modification: {error_detail}") fournisseur.Read() logger.info( f" FOURNISSEUR MODIFIÉ: {code} ({len(champs_modifies)} champs) " ) numero = getattr(fournisseur, "CT_Num", "").strip() intitule = getattr(fournisseur, "CT_Intitule", "").strip() data = { "numero": numero, "intitule": intitule, "type": 1, "est_fournisseur": True, } try: adresse_obj = getattr(fournisseur, "Adresse", None) if adresse_obj: data["adresse"] = getattr(adresse_obj, "Adresse", "").strip() data["code_postal"] = getattr( adresse_obj, "CodePostal", "" ).strip() data["ville"] = getattr(adresse_obj, "Ville", "").strip() except Exception: data["adresse"] = "" data["code_postal"] = "" data["ville"] = "" try: telecom_obj = getattr(fournisseur, "Telecom", None) if telecom_obj: data["telephone"] = getattr( telecom_obj, "Telephone", "" ).strip() data["email"] = getattr(telecom_obj, "EMail", "").strip() except Exception: data["telephone"] = "" data["email"] = "" return data except ValueError as e: logger.error(f" Erreur métier: {e}") raise except Exception as e: logger.error(f" Erreur modification fournisseur: {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 Exception: pass raise RuntimeError(f"Erreur technique Sage: {error_message}") def lister_tous_clients(self, filtre=""): """ Liste tous les clients avec TOUS les champs gérés par creer_client Symétrie complète GET/POST """ try: with self._get_sql_connection() as conn: cursor = conn.cursor() query = """ SELECT -- IDENTIFICATION (8) CT_Num, CT_Intitule, CT_Type, CT_Qualite, CT_Classement, CT_Raccourci, CT_Siret, CT_Identifiant, CT_Ape, -- ADRESSE (7) CT_Contact, CT_Adresse, CT_Complement, CT_CodePostal, CT_Ville, CT_CodeRegion, CT_Pays, -- TELECOM (7) CT_Telephone, CT_Telecopie, CT_EMail, CT_Site, CT_Facebook, CT_LinkedIn, -- TAUX (4) CT_Taux01, CT_Taux02, CT_Taux03, CT_Taux04, -- STATISTIQUES (10) CT_Statistique01, CT_Statistique02, CT_Statistique03, CT_Statistique04, CT_Statistique05, CT_Statistique06, CT_Statistique07, CT_Statistique08, CT_Statistique09, CT_Statistique10, -- COMMERCIAL (4) CT_Encours, CT_Assurance, CT_Langue, CO_No, -- FACTURATION (11) CT_Lettrage, CT_Sommeil, CT_Facture, CT_Prospect, CT_BLFact, CT_Saut, CT_ValidEch, CT_ControlEnc, CT_NotRappel, CT_NotPenal, CT_BonAPayer, -- LOGISTIQUE (4) CT_PrioriteLivr, CT_LivrPartielle, CT_DelaiTransport, CT_DelaiAppro, -- COMMENTAIRE (1) CT_Commentaire, -- ANALYTIQUE (1) CA_Num, -- ORGANISATION / SURVEILLANCE (10) MR_No, CT_Surveillance, CT_Coface, CT_SvFormeJuri, CT_SvEffectif, CT_SvRegul, CT_SvCotation, CT_SvObjetMaj, CT_SvCA, CT_SvResultat, -- COMPTE GENERAL ET CATEGORIES (3) CG_NumPrinc, N_CatTarif, N_CatCompta FROM F_COMPTET WHERE CT_Type = 0 """ params = [] if filtre: query += " AND (CT_Num LIKE ? OR CT_Intitule LIKE ?)" params.extend([f"%{filtre}%", f"%{filtre}%"]) query += " ORDER BY CT_Intitule" cursor.execute(query, params) rows = cursor.fetchall() clients = [] for row in rows: client = { "numero": _safe_strip(row.CT_Num), "intitule": _safe_strip(row.CT_Intitule), "type_tiers": row.CT_Type, "qualite": _safe_strip(row.CT_Qualite), "classement": _safe_strip(row.CT_Classement), "raccourci": _safe_strip(row.CT_Raccourci), "siret": _safe_strip(row.CT_Siret), "tva_intra": _safe_strip(row.CT_Identifiant), "code_naf": _safe_strip(row.CT_Ape), "contact": _safe_strip(row.CT_Contact), "adresse": _safe_strip(row.CT_Adresse), "complement": _safe_strip(row.CT_Complement), "code_postal": _safe_strip(row.CT_CodePostal), "ville": _safe_strip(row.CT_Ville), "region": _safe_strip(row.CT_CodeRegion), "pays": _safe_strip(row.CT_Pays), "telephone": _safe_strip(row.CT_Telephone), "telecopie": _safe_strip(row.CT_Telecopie), "email": _safe_strip(row.CT_EMail), "site_web": _safe_strip(row.CT_Site), "facebook": _safe_strip(row.CT_Facebook), "linkedin": _safe_strip(row.CT_LinkedIn), "taux01": row.CT_Taux01, "taux02": row.CT_Taux02, "taux03": row.CT_Taux03, "taux04": row.CT_Taux04, "statistique01": _safe_strip(row.CT_Statistique01), "statistique02": _safe_strip(row.CT_Statistique02), "statistique03": _safe_strip(row.CT_Statistique03), "statistique04": _safe_strip(row.CT_Statistique04), "statistique05": _safe_strip(row.CT_Statistique05), "statistique06": _safe_strip(row.CT_Statistique06), "statistique07": _safe_strip(row.CT_Statistique07), "statistique08": _safe_strip(row.CT_Statistique08), "statistique09": _safe_strip(row.CT_Statistique09), "statistique10": _safe_strip(row.CT_Statistique10), "encours_autorise": row.CT_Encours, "assurance_credit": row.CT_Assurance, "langue": row.CT_Langue, "commercial_code": row.CO_No, "lettrage_auto": (row.CT_Lettrage == 1), "est_actif": (row.CT_Sommeil == 0), "type_facture": row.CT_Facture, "est_prospect": (row.CT_Prospect == 1), "bl_en_facture": row.CT_BLFact, "saut_page": row.CT_Saut, "validation_echeance": row.CT_ValidEch, "controle_encours": row.CT_ControlEnc, "exclure_relance": (row.CT_NotRappel == 1), "exclure_penalites": (row.CT_NotPenal == 1), "bon_a_payer": row.CT_BonAPayer, "priorite_livraison": row.CT_PrioriteLivr, "livraison_partielle": row.CT_LivrPartielle, "delai_transport": row.CT_DelaiTransport, "delai_appro": row.CT_DelaiAppro, "commentaire": _safe_strip(row.CT_Commentaire), "section_analytique": _safe_strip(row.CA_Num), "mode_reglement_code": row.MR_No, "surveillance_active": (row.CT_Surveillance == 1), "coface": _safe_strip(row.CT_Coface), "forme_juridique": _safe_strip(row.CT_SvFormeJuri), "effectif": _safe_strip(row.CT_SvEffectif), "sv_regularite": _safe_strip(row.CT_SvRegul), "sv_cotation": _safe_strip(row.CT_SvCotation), "sv_objet_maj": _safe_strip(row.CT_SvObjetMaj), "sv_chiffre_affaires": row.CT_SvCA, "sv_resultat": row.CT_SvResultat, "compte_general": _safe_strip(row.CG_NumPrinc), "categorie_tarif": row.N_CatTarif, "categorie_compta": row.N_CatCompta, } client["contacts"] = _get_contacts_client(row.CT_Num, conn) clients.append(client) logger.info(f" SQL: {len(clients)} clients avec {len(client)} champs") return clients except Exception as e: logger.error(f" Erreur SQL clients: {e}") raise RuntimeError(f"Erreur lecture clients: {str(e)}") def lire_client(self, code_client): """ Lit un client avec TOUS les champs (identique à lister_tous_clients) Symétrie complète GET/POST """ try: with self._get_sql_connection() as conn: cursor = conn.cursor() query = """ SELECT -- IDENTIFICATION (8) CT_Num, CT_Intitule, CT_Type, CT_Qualite, CT_Classement, CT_Raccourci, CT_Siret, CT_Identifiant, CT_Ape, -- ADRESSE (7) CT_Contact, CT_Adresse, CT_Complement, CT_CodePostal, CT_Ville, CT_CodeRegion, CT_Pays, -- TELECOM (7) CT_Telephone, CT_Telecopie, CT_EMail, CT_Site, CT_Facebook, CT_LinkedIn, -- TAUX (4) CT_Taux01, CT_Taux02, CT_Taux03, CT_Taux04, -- STATISTIQUES (10) CT_Statistique01, CT_Statistique02, CT_Statistique03, CT_Statistique04, CT_Statistique05, CT_Statistique06, CT_Statistique07, CT_Statistique08, CT_Statistique09, CT_Statistique10, -- COMMERCIAL (4) CT_Encours, CT_Assurance, CT_Langue, CO_No, -- FACTURATION (11) CT_Lettrage, CT_Sommeil, CT_Facture, CT_Prospect, CT_BLFact, CT_Saut, CT_ValidEch, CT_ControlEnc, CT_NotRappel, CT_NotPenal, CT_BonAPayer, -- LOGISTIQUE (4) CT_PrioriteLivr, CT_LivrPartielle, CT_DelaiTransport, CT_DelaiAppro, -- COMMENTAIRE (1) CT_Commentaire, -- ANALYTIQUE (1) CA_Num, -- ORGANISATION / SURVEILLANCE (10) MR_No, CT_Surveillance, CT_Coface, CT_SvFormeJuri, CT_SvEffectif, CT_SvRegul, CT_SvCotation, CT_SvObjetMaj, CT_SvCA, CT_SvResultat, -- COMPTE GENERAL ET CATEGORIES (3) CG_NumPrinc, N_CatTarif, N_CatCompta FROM F_COMPTET WHERE CT_Num = ? AND CT_Type = 0 """ cursor.execute(query, (code_client.upper(),)) row = cursor.fetchone() if not row: return None client = { "numero": _safe_strip(row.CT_Num), "intitule": _safe_strip(row.CT_Intitule), "type_tiers": row.CT_Type, "qualite": _safe_strip(row.CT_Qualite), "classement": _safe_strip(row.CT_Classement), "raccourci": _safe_strip(row.CT_Raccourci), "siret": _safe_strip(row.CT_Siret), "tva_intra": _safe_strip(row.CT_Identifiant), "code_naf": _safe_strip(row.CT_Ape), "contact": _safe_strip(row.CT_Contact), "adresse": _safe_strip(row.CT_Adresse), "complement": _safe_strip(row.CT_Complement), "code_postal": _safe_strip(row.CT_CodePostal), "ville": _safe_strip(row.CT_Ville), "region": _safe_strip(row.CT_CodeRegion), "pays": _safe_strip(row.CT_Pays), "telephone": _safe_strip(row.CT_Telephone), "telecopie": _safe_strip(row.CT_Telecopie), "email": _safe_strip(row.CT_EMail), "site_web": _safe_strip(row.CT_Site), "facebook": _safe_strip(row.CT_Facebook), "linkedin": _safe_strip(row.CT_LinkedIn), "taux01": row.CT_Taux01, "taux02": row.CT_Taux02, "taux03": row.CT_Taux03, "taux04": row.CT_Taux04, "statistique01": _safe_strip(row.CT_Statistique01), "statistique02": _safe_strip(row.CT_Statistique02), "statistique03": _safe_strip(row.CT_Statistique03), "statistique04": _safe_strip(row.CT_Statistique04), "statistique05": _safe_strip(row.CT_Statistique05), "statistique06": _safe_strip(row.CT_Statistique06), "statistique07": _safe_strip(row.CT_Statistique07), "statistique08": _safe_strip(row.CT_Statistique08), "statistique09": _safe_strip(row.CT_Statistique09), "statistique10": _safe_strip(row.CT_Statistique10), "encours_autorise": row.CT_Encours, "assurance_credit": row.CT_Assurance, "langue": row.CT_Langue, "commercial_code": row.CO_No, "lettrage_auto": (row.CT_Lettrage == 1), "est_actif": (row.CT_Sommeil == 0), "type_facture": row.CT_Facture, "est_prospect": (row.CT_Prospect == 1), "bl_en_facture": row.CT_BLFact, "saut_page": row.CT_Saut, "validation_echeance": row.CT_ValidEch, "controle_encours": row.CT_ControlEnc, "exclure_relance": (row.CT_NotRappel == 1), "exclure_penalites": (row.CT_NotPenal == 1), "bon_a_payer": row.CT_BonAPayer, "priorite_livraison": row.CT_PrioriteLivr, "livraison_partielle": row.CT_LivrPartielle, "delai_transport": row.CT_DelaiTransport, "delai_appro": row.CT_DelaiAppro, "commentaire": _safe_strip(row.CT_Commentaire), "section_analytique": _safe_strip(row.CA_Num), "mode_reglement_code": row.MR_No, "surveillance_active": (row.CT_Surveillance == 1), "coface": _safe_strip(row.CT_Coface), "forme_juridique": _safe_strip(row.CT_SvFormeJuri), "effectif": _safe_strip(row.CT_SvEffectif), "sv_regularite": _safe_strip(row.CT_SvRegul), "sv_cotation": _safe_strip(row.CT_SvCotation), "sv_objet_maj": _safe_strip(row.CT_SvObjetMaj), "sv_chiffre_affaires": row.CT_SvCA, "sv_resultat": row.CT_SvResultat, "compte_general": _safe_strip(row.CG_NumPrinc), "categorie_tarif": row.N_CatTarif, "categorie_compta": row.N_CatCompta, } client["contacts"] = _get_contacts_client(row.CT_Num, conn) logger.info(f" SQL: Client {code_client} avec {len(client)} champs") return client except Exception as e: logger.error(f" Erreur SQL client {code_client}: {e}") return None def lister_tous_articles(self, filtre=""): try: with self._get_sql_connection() as conn: cursor = conn.cursor() logger.info("[SQL] Détection des colonnes de F_ARTICLE...") cursor.execute("SELECT TOP 1 * FROM F_ARTICLE") colonnes_disponibles = [column[0] for column in cursor.description] logger.info( f"[SQL] Colonnes F_ARTICLE trouvées : {len(colonnes_disponibles)}" ) colonnes_config = { "AR_Ref": "reference", "AR_Design": "designation", "AR_CodeBarre": "code_barre", "AR_EdiCode": "edi_code", "AR_Raccourci": "raccourci", "AR_PrixVen": "prix_vente", "AR_PrixAch": "prix_achat", "AR_Coef": "coef", "AR_PUNet": "prix_net", "AR_PrixAchNouv": "prix_achat_nouveau", "AR_CoefNouv": "coef_nouveau", "AR_PrixVenNouv": "prix_vente_nouveau", "AR_DateApplication": "date_application_prix", "AR_CoutStd": "cout_standard", "AR_UniteVen": "unite_vente", "AR_UnitePoids": "unite_poids", "AR_PoidsNet": "poids_net", "AR_PoidsBrut": "poids_brut", "AR_Gamme1": "gamme_1", "AR_Gamme2": "gamme_2", "FA_CodeFamille": "famille_code", "AR_Type": "type_article", "AR_Nature": "nature", "AR_Garantie": "garantie", "AR_CodeFiscal": "code_fiscal", "AR_Pays": "pays", "CO_No": "fournisseur_principal", "AR_Condition": "conditionnement", "AR_NbColis": "nb_colis", "AR_Prevision": "prevision", "AR_SuiviStock": "suivi_stock", "AR_Nomencl": "nomenclature", "AR_QteComp": "qte_composant", "AR_QteOperatoire": "qte_operatoire", "AR_Sommeil": "sommeil", "AR_Substitut": "article_substitut", "AR_Escompte": "soumis_escompte", "AR_Delai": "delai", "AR_Stat01": "stat_01", "AR_Stat02": "stat_02", "AR_Stat03": "stat_03", "AR_Stat04": "stat_04", "AR_Stat05": "stat_05", "AR_HorsStat": "hors_statistique", "CL_No1": "categorie_1", "CL_No2": "categorie_2", "CL_No3": "categorie_3", "CL_No4": "categorie_4", "AR_DateModif": "date_modification", "AR_VteDebit": "vente_debit", "AR_NotImp": "non_imprimable", "AR_Transfere": "transfere", "AR_Publie": "publie", "AR_Contremarque": "contremarque", "AR_FactPoids": "fact_poids", "AR_FactForfait": "fact_forfait", "AR_SaisieVar": "saisie_variable", "AR_Fictif": "fictif", "AR_SousTraitance": "sous_traitance", "AR_Criticite": "criticite", "RP_CodeDefaut": "reprise_code_defaut", "AR_DelaiFabrication": "delai_fabrication", "AR_DelaiPeremption": "delai_peremption", "AR_DelaiSecurite": "delai_securite", "AR_TypeLancement": "type_lancement", "AR_Cycle": "cycle", "AR_Photo": "photo", "AR_Langue1": "langue_1", "AR_Langue2": "langue_2", "AR_Frais01FR_Denomination": "frais_01_denomination", "AR_Frais02FR_Denomination": "frais_02_denomination", "AR_Frais03FR_Denomination": "frais_03_denomination", "Marque commerciale": "marque_commerciale", "Objectif / Qtés vendues": "objectif_qtes_vendues", "Pourcentage teneur en or": "pourcentage_or", "1ère commercialisation": "premiere_commercialisation", "AR_InterdireCommande": "interdire_commande", "AR_Exclure": "exclure", } colonnes_a_lire = [ col_sql for col_sql in colonnes_config.keys() if col_sql in colonnes_disponibles ] if not colonnes_a_lire: logger.error("[SQL] Aucune colonne mappée trouvée !") colonnes_a_lire = ["AR_Ref", "AR_Design", "AR_PrixVen"] logger.info(f"[SQL] Colonnes sélectionnées : {len(colonnes_a_lire)}") colonnes_sql = [] for col in colonnes_a_lire: if " " in col or "/" in col or "è" in col: colonnes_sql.append(f"[{col}]") else: colonnes_sql.append(col) colonnes_str = ", ".join(colonnes_sql) query = f"SELECT {colonnes_str} FROM F_ARTICLE WHERE 1=1" params = [] if filtre: conditions = [] if "AR_Ref" in colonnes_a_lire: conditions.append("AR_Ref LIKE ?") params.append(f"%{filtre}%") if "AR_Design" in colonnes_a_lire: conditions.append("AR_Design LIKE ?") params.append(f"%{filtre}%") if "AR_CodeBarre" in colonnes_a_lire: conditions.append("AR_CodeBarre LIKE ?") params.append(f"%{filtre}%") if conditions: query += " AND (" + " OR ".join(conditions) + ")" query += " ORDER BY AR_Ref" logger.debug(f"[SQL] Requête : {query[:200]}...") cursor.execute(query, params) rows = cursor.fetchall() logger.info(f"[SQL] {len(rows)} lignes récupérées") articles = [] for row in rows: row_data = {} for idx, col_sql in enumerate(colonnes_a_lire): valeur = row[idx] if isinstance(valeur, str): valeur = valeur.strip() row_data[col_sql] = valeur if "Marque commerciale" in row_data: logger.debug( f"[DEBUG] Marque commerciale trouvée: {row_data['Marque commerciale']}" ) article_data = _mapper_article_depuis_row(row_data, colonnes_config) articles.append(article_data) articles = _enrichir_stocks_articles(articles, cursor) articles = _enrichir_familles_articles(articles, cursor) articles = _enrichir_fournisseurs_articles(articles, cursor) articles = _enrichir_tva_articles(articles, cursor) articles = _enrichir_stock_emplacements(articles, cursor) articles = _enrichir_gammes_articles(articles, cursor) articles = _enrichir_tarifs_clients(articles, cursor) articles = _enrichir_nomenclature(articles, cursor) articles = _enrichir_compta_articles(articles, cursor) articles = _enrichir_fournisseurs_multiples(articles, cursor) articles = _enrichir_depots_details(articles, cursor) articles = _enrichir_emplacements_details(articles, cursor) articles = _enrichir_gammes_enumeres(articles, cursor) articles = _enrichir_references_enumerees(articles, cursor) articles = _enrichir_medias_articles(articles, cursor) articles = _enrichir_prix_gammes(articles, cursor) articles = _enrichir_conditionnements(articles, cursor) return articles except Exception as e: logger.error(f" Erreur SQL articles: {e}", exc_info=True) raise RuntimeError(f"Erreur lecture articles: {str(e)}") def lire_article(self, reference): """ Lit un article complet depuis SQL avec enrichissements Version alignée sur lister_tous_articles """ try: with self._get_sql_connection() as conn: cursor = conn.cursor() # === DÉTECTION DES COLONNES (identique à lister_tous_articles) === logger.info(f"[SQL] Lecture article {reference}...") cursor.execute("SELECT TOP 1 * FROM F_ARTICLE") colonnes_disponibles = [column[0] for column in cursor.description] # Configuration du mapping (identique à lister_tous_articles) colonnes_config = { "AR_Ref": "reference", "AR_Design": "designation", "AR_CodeBarre": "code_barre", "AR_EdiCode": "edi_code", "AR_Raccourci": "raccourci", "AR_PrixVen": "prix_vente", "AR_PrixAch": "prix_achat", "AR_Coef": "coef", "AR_PUNet": "prix_net", "AR_PrixAchNouv": "prix_achat_nouveau", "AR_CoefNouv": "coef_nouveau", "AR_PrixVenNouv": "prix_vente_nouveau", "AR_DateApplication": "date_application_prix", "AR_CoutStd": "cout_standard", "AR_UniteVen": "unite_vente", "AR_UnitePoids": "unite_poids", "AR_PoidsNet": "poids_net", "AR_PoidsBrut": "poids_brut", "AR_Gamme1": "gamme_1", "AR_Gamme2": "gamme_2", "FA_CodeFamille": "famille_code", "AR_Type": "type_article", "AR_Nature": "nature", "AR_Garantie": "garantie", "AR_CodeFiscal": "code_fiscal", "AR_Pays": "pays", "CO_No": "fournisseur_principal", "AR_Condition": "conditionnement", "AR_NbColis": "nb_colis", "AR_Prevision": "prevision", "AR_SuiviStock": "suivi_stock", "AR_Nomencl": "nomenclature", "AR_QteComp": "qte_composant", "AR_QteOperatoire": "qte_operatoire", "AR_Sommeil": "sommeil", "AR_Substitut": "article_substitut", "AR_Escompte": "soumis_escompte", "AR_Delai": "delai", "AR_Stat01": "stat_01", "AR_Stat02": "stat_02", "AR_Stat03": "stat_03", "AR_Stat04": "stat_04", "AR_Stat05": "stat_05", "AR_HorsStat": "hors_statistique", "CL_No1": "categorie_1", "CL_No2": "categorie_2", "CL_No3": "categorie_3", "CL_No4": "categorie_4", "AR_DateModif": "date_modification", "AR_VteDebit": "vente_debit", "AR_NotImp": "non_imprimable", "AR_Transfere": "transfere", "AR_Publie": "publie", "AR_Contremarque": "contremarque", "AR_FactPoids": "fact_poids", "AR_FactForfait": "fact_forfait", "AR_SaisieVar": "saisie_variable", "AR_Fictif": "fictif", "AR_SousTraitance": "sous_traitance", "AR_Criticite": "criticite", "RP_CodeDefaut": "reprise_code_defaut", "AR_DelaiFabrication": "delai_fabrication", "AR_DelaiPeremption": "delai_peremption", "AR_DelaiSecurite": "delai_securite", "AR_TypeLancement": "type_lancement", "AR_Cycle": "cycle", "AR_Photo": "photo", "AR_Langue1": "langue_1", "AR_Langue2": "langue_2", "AR_Frais01FR_Denomination": "frais_01_denomination", "AR_Frais02FR_Denomination": "frais_02_denomination", "AR_Frais03FR_Denomination": "frais_03_denomination", "Marque commerciale": "marque_commerciale", "Objectif / Qtés vendues": "objectif_qtes_vendues", "Pourcentage teneur en or": "pourcentage_or", "1ère commercialisation": "premiere_commercialisation", "AR_InterdireCommande": "interdire_commande", "AR_Exclure": "exclure", } # Sélection des colonnes disponibles colonnes_a_lire = [ col_sql for col_sql in colonnes_config.keys() if col_sql in colonnes_disponibles ] if not colonnes_a_lire: logger.error("[SQL] Aucune colonne mappée trouvée !") return None # Construction de la requête SQL avec échappement des noms de colonnes colonnes_sql = [] for col in colonnes_a_lire: if " " in col or "/" in col or "è" in col: colonnes_sql.append(f"[{col}]") else: colonnes_sql.append(col) colonnes_str = ", ".join(colonnes_sql) query = f"SELECT {colonnes_str} FROM F_ARTICLE WHERE AR_Ref = ?" logger.debug(f"[SQL] Requête : {query[:200]}...") cursor.execute(query, (reference.upper(),)) row = cursor.fetchone() if not row: logger.info(f"[SQL] Article {reference} non trouvé") return None # Construction du dictionnaire row_data row_data = {} for idx, col_sql in enumerate(colonnes_a_lire): valeur = row[idx] if isinstance(valeur, str): valeur = valeur.strip() row_data[col_sql] = valeur # Mapping de l'article (fonction partagée) article = _mapper_article_depuis_row(row_data, colonnes_config) # Enrichissements (dans le même ordre que lister_tous_articles) articles = [ article ] # Liste d'un seul article pour les fonctions d'enrichissement articles = _enrichir_stocks_articles(articles, cursor) articles = _enrichir_familles_articles(articles, cursor) articles = _enrichir_fournisseurs_articles(articles, cursor) articles = _enrichir_tva_articles(articles, cursor) articles = _enrichir_stock_emplacements(articles, cursor) articles = _enrichir_gammes_articles(articles, cursor) articles = _enrichir_tarifs_clients(articles, cursor) articles = _enrichir_nomenclature(articles, cursor) articles = _enrichir_compta_articles(articles, cursor) articles = _enrichir_fournisseurs_multiples(articles, cursor) articles = _enrichir_depots_details(articles, cursor) articles = _enrichir_emplacements_details(articles, cursor) articles = _enrichir_gammes_enumeres(articles, cursor) articles = _enrichir_references_enumerees(articles, cursor) articles = _enrichir_medias_articles(articles, cursor) articles = _enrichir_prix_gammes(articles, cursor) articles = _enrichir_conditionnements(articles, cursor) logger.info(f"✓ Article {reference} lu avec succès") return articles[0] except Exception as e: logger.error(f" Erreur SQL article {reference}: {e}", exc_info=True) return None def obtenir_contact(self, numero: str, contact_numero: int) -> Optional[Dict]: """ Récupère un contact spécifique par son CT_No """ try: with self._get_sql_connection() as conn: 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 = ? """ cursor.execute(query, [numero, contact_numero]) row = cursor.fetchone() if not row: return None return _row_to_contact_dict(row) except Exception as e: logger.error(f"Erreur obtention contact: {e}") raise RuntimeError(f"Erreur lecture contact: {str(e)}") def lister_tous_devis_cache(self, filtre=""): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lister_documents_avec_lignes_sql(cursor, type_doc=0, filtre=filtre) def lire_devis_cache(self, numero): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lire_document_sql(cursor, numero, type_doc=0) def lister_toutes_commandes_cache(self, filtre=""): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lister_documents_avec_lignes_sql(cursor, type_doc=1, filtre=filtre) def lire_commande_cache(self, numero): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lire_document_sql(cursor, numero, type_doc=1) def lister_toutes_factures_cache(self, filtre=""): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lister_documents_avec_lignes_sql(cursor, type_doc=6, filtre=filtre) def lire_facture_cache(self, numero): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lire_document_sql(cursor, numero, type_doc=6) def lister_tous_fournisseurs_cache(self, filtre=""): return self.lister_tous_fournisseurs() def lire_fournisseur_cache(self, code): return self.lire_fournisseur() def lister_toutes_livraisons_cache(self, filtre=""): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lister_documents_avec_lignes_sql(cursor, type_doc=3, filtre=filtre) def lire_livraison_cache(self, numero): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lire_document_sql(cursor, numero, type_doc=3) def lister_tous_avoirs_cache(self, filtre=""): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lister_documents_avec_lignes_sql(cursor, type_doc=5, filtre=filtre) def lire_avoir_cache(self, numero): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lire_document_sql(cursor, numero, type_doc=5) 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']} " ) try: with self._com_context(), self._lock_com: transaction_active = False try: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug(" Transaction Sage démarrée") except Exception as e: logger.warning(f"BeginTrans échoué: {e}") try: process = self.cial.CreateProcess_Document(0) doc = process.Document try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") except Exception: pass logger.info(" Document devis créé") doc.DO_Date = pywintypes.Time( normaliser_date(devis_data.get("date_devis")) ) if "date_livraison" in devis_data and devis_data["date_livraison"]: doc.DO_DateLivr = pywintypes.Time( normaliser_date(devis_data["date_livraison"]) ) factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( devis_data["client"]["code"] ) if not persist_client: raise ValueError( f" Client {devis_data['client']['code']} introuvable" ) client_obj = _cast_client(persist_client) if not client_obj: raise ValueError( f" Impossible de charger le client {devis_data['client']['code']}" ) doc.SetDefaultClient(client_obj) logger.info(f" Client {devis_data['client']['code']} associé") doc.DO_Statut = 0 doc.Write() try: factory_lignes = doc.FactoryDocumentLigne except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle logger.info(f" Ajout de {len(devis_data['lignes'])} lignes...") for idx, ligne_data in enumerate(devis_data["lignes"], 1): logger.debug( f"--- Ligne {idx}: {ligne_data['article_code']} ---" ) persist_article = factory_article.ReadReference( ligne_data["article_code"] ) if not persist_article: raise ValueError( f" Article {ligne_data['article_code']} introuvable" ) article_obj = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_obj.Read() prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") if prix_sage == 0: logger.warning( f"Article {ligne_data['article_code']} a un prix = 0€" ) ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) except Exception: try: ligne_obj.SetDefaultArticle(article_obj, quantite) except Exception: ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") ) ligne_obj.DL_Qte = quantite prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) prix_a_utiliser = ligne_data.get("prix_unitaire_ht") if prix_a_utiliser is not None and prix_a_utiliser > 0: ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) elif prix_auto == 0: if prix_sage == 0: raise ValueError( f"Prix nul pour article {ligne_data['article_code']}" ) ligne_obj.DL_PrixUnitaire = float(prix_sage) remise = ligne_data.get("remise_pourcentage", 0) if remise > 0: try: ligne_obj.DL_Remise01REM_Valeur = float(remise) ligne_obj.DL_Remise01REM_Type = 0 except Exception as e: logger.warning(f"Remise non appliquée: {e}") ligne_obj.Write() logger.info(f" {len(devis_data['lignes'])} lignes écrites") doc.Write() try: process.Process() logger.info(" Process() appelé (brouillon)") except Exception: logger.debug("Process() ignoré pour brouillon") numero_devis = _recuperer_numero_devis(process, doc) if not numero_devis: raise RuntimeError(" Numéro devis vide après création") logger.info(f" Numéro: {numero_devis}") if transaction_active: try: self.cial.CptaApplication.CommitTrans() logger.info(" Transaction committée") except Exception: pass import time time.sleep(0.5) if "reference" in devis_data and devis_data["reference"]: try: logger.info( f" Application de la référence: {devis_data['reference']}" ) doc_reload = self._charger_devis(numero_devis) nouvelle_reference = devis_data["reference"] doc_reload.DO_Ref = ( str(nouvelle_reference) if nouvelle_reference else "" ) doc_reload.Write() time.sleep(0.5) doc_reload.Read() logger.info(f" Référence définie: {nouvelle_reference}") except Exception as e: logger.warning( f"Impossible de définir la référence: {e}", exc_info=True, ) time.sleep(0.5) doc_final_data = self._relire_devis(numero_devis, devis_data) logger.info( f" DEVIS CRÉÉ: {numero_devis} - {doc_final_data['total_ttc']}€ TTC " ) return doc_final_data except Exception: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() logger.error(" Transaction annulée (rollback)") except Exception: pass raise except Exception as e: 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 Exception: pass else: total_calcule = sum( ligne.get("montant_ligne_ht", 0) for ligne 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) logger.info(f" NOUVELLE MÉTHODE modifier_devis() APPELÉE POUR {numero} ") logger.info(f" Données reçues: {devis_data}") logger.info("=" * 100) if not self.cial: logger.error(" Connexion Sage non établie") raise RuntimeError("Connexion Sage non établie") try: with ( self._com_context(), self._lock_com, self._get_sql_connection() as conn, ): cursor = conn.cursor() logger.info("") logger.info("=" * 80) logger.info(f" [ÉTAPE 1] CHARGEMENT DU DEVIS {numero}") logger.info("=" * 80) doc = self._charger_devis(numero) logger.info(f" Devis {numero} chargé avec succès") logger.info("") _afficher_etat_document(doc, "📸 ÉTAT INITIAL") logger.info(" Vérification statut transformation...") _verifier_devis_non_transforme(numero, doc, cursor) logger.info(" Devis non transformé - modification autorisée") logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 2] ANALYSE DOCUMENT ACTUEL") logger.info("=" * 80) client_code_initial = "" try: client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_code_initial = getattr(client_obj, "CT_Num", "").strip() logger.info(f" Client: {client_code_initial}") else: logger.warning(" Objet Client non trouvé") except Exception as e: logger.warning(f" Impossible de lire le client: {e}") nb_lignes_initial = _compter_lignes_document(doc) logger.info(f" Nombre de lignes actuelles: {nb_lignes_initial}") logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 3] ANALYSE MODIFICATIONS DEMANDÉES") logger.info("=" * 80) modif_date = "date_devis" in devis_data modif_date_livraison = "date_livraison" in devis_data modif_statut = "statut" in devis_data modif_ref = "reference" in devis_data modif_lignes = ( "lignes" in devis_data and devis_data["lignes"] is not None ) logger.info(f" Date devis: {modif_date}") if modif_date: logger.info(f" → Valeur: {devis_data['date_devis']}") logger.info(f" Date livraison: {modif_date_livraison}") if modif_date_livraison: logger.info(f" → Valeur: {devis_data['date_livraison']}") logger.info(f" Référence: {modif_ref}") if modif_ref: logger.info(f" → Valeur: '{devis_data['reference']}'") logger.info(f" Statut: {modif_statut}") if modif_statut: logger.info(f" → Valeur: {devis_data['statut']}") logger.info(f" Lignes: {modif_lignes}") if modif_lignes: logger.info(f" → Nombre: {len(devis_data['lignes'])}") for i, ligne in enumerate(devis_data["lignes"], 1): logger.info( f" → Ligne {i}: {ligne.get('article_code')} (Qté: {ligne.get('quantite')})" ) devis_data_temp = devis_data.copy() reference_a_modifier = None statut_a_modifier = None if modif_lignes: logger.info("") logger.info( " STRATÉGIE: Report référence/statut APRÈS modification lignes" ) if modif_ref: reference_a_modifier = devis_data_temp.pop("reference") logger.info(f" Référence '{reference_a_modifier}' reportée") modif_ref = False if modif_statut: statut_a_modifier = devis_data_temp.pop("statut") logger.info(f" Statut {statut_a_modifier} reporté") modif_statut = False logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 4] TEST WRITE() BASIQUE") logger.info("=" * 80) logger.info("Test sans modification pour vérifier le verrouillage...") try: doc.Write() logger.info(" Write() basique OK - Document NON verrouillé") time.sleep(0.3) doc.Read() logger.info(" Read() après Write() OK") except Exception as e: logger.error(f" Write() basique ÉCHOUE: {e}") logger.error(" ABANDON: Document VERROUILLÉ ou problème COM") raise ValueError(f"Document verrouillé: {e}") champs_modifies = [] if not modif_lignes and ( modif_date or modif_date_livraison or modif_statut or modif_ref ): logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 5A] MODIFICATIONS SIMPLES (sans lignes)") logger.info("=" * 80) if modif_date: logger.info("") logger.info(" Modification DATE_DEVIS...") try: ancienne_date = getattr(doc, "DO_Date", None) ancienne_date_str = ( ancienne_date.strftime("%Y-%m-%d") if ancienne_date else "None" ) logger.info(f" Actuelle: {ancienne_date_str}") nouvelle_date = normaliser_date( devis_data_temp["date_devis"] ) nouvelle_date_str = nouvelle_date.strftime("%Y-%m-%d") logger.info(f" Cible: {nouvelle_date_str}") doc.DO_Date = pywintypes.Time(nouvelle_date) logger.info(" doc.DO_Date affecté") champs_modifies.append("date_devis") logger.info( f" Date devis sera modifiée: {ancienne_date_str} → {nouvelle_date_str}" ) except Exception as e: logger.error(f" Erreur date devis: {e}", exc_info=True) if modif_date_livraison: logger.info("") logger.info(" Modification DATE_LIVRAISON...") try: ancienne_date_livr = getattr(doc, "DO_DateLivr", None) ancienne_date_livr_str = ( ancienne_date_livr.strftime("%Y-%m-%d") if ancienne_date_livr else "None" ) logger.info(f" Actuelle: {ancienne_date_livr_str}") if devis_data_temp["date_livraison"]: nouvelle_date_livr = normaliser_date( devis_data_temp["date_livraison"] ) nouvelle_date_livr_str = nouvelle_date_livr.strftime( "%Y-%m-%d" ) logger.info(f" Cible: {nouvelle_date_livr_str}") doc.DO_DateLivr = pywintypes.Time(nouvelle_date_livr) logger.info(" doc.DO_DateLivr affecté") else: logger.info(" Cible: Effacement (None)") doc.DO_DateLivr = None logger.info(" doc.DO_DateLivr = None") champs_modifies.append("date_livraison") logger.info(" Date livraison sera modifiée") except Exception as e: logger.error( f" Erreur date livraison: {e}", exc_info=True ) if modif_ref: logger.info("") logger.info(" Modification RÉFÉRENCE...") try: ancienne_ref = getattr(doc, "DO_Ref", "") logger.info(f" Actuelle: '{ancienne_ref}'") nouvelle_ref = ( str(devis_data_temp["reference"]) if devis_data_temp["reference"] else "" ) logger.info(f" Cible: '{nouvelle_ref}'") doc.DO_Ref = nouvelle_ref logger.info(" doc.DO_Ref affecté") champs_modifies.append("reference") logger.info( f" Référence sera modifiée: '{ancienne_ref}' → '{nouvelle_ref}'" ) except Exception as e: logger.error(f" Erreur référence: {e}", exc_info=True) if modif_statut: logger.info("") logger.info(" Modification STATUT...") try: statut_actuel = getattr(doc, "DO_Statut", 0) logger.info(f" Actuel: {statut_actuel}") nouveau_statut = int(devis_data_temp["statut"]) logger.info(f" Cible: {nouveau_statut}") if nouveau_statut in [0, 1, 2, 3]: doc.DO_Statut = nouveau_statut logger.info(" doc.DO_Statut affecté") champs_modifies.append("statut") logger.info( f" Statut sera modifié: {statut_actuel} → {nouveau_statut}" ) else: logger.warning( f" Statut {nouveau_statut} invalide (doit être 0,1,2 ou 3)" ) except Exception as e: logger.error(f" Erreur statut: {e}", exc_info=True) logger.info("") logger.info(" Write() modifications simples...") try: doc.Write() logger.info(" Write() réussi") time.sleep(0.5) doc.Read() logger.info(" Read() après Write() OK") except Exception as e: logger.error(f" Write() a échoué: {e}", exc_info=True) raise elif modif_lignes: logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 5B] REMPLACEMENT COMPLET DES LIGNES") logger.info("=" * 80) if modif_date: logger.info(" Modification date devis (avant lignes)...") try: nouvelle_date = normaliser_date( devis_data_temp["date_devis"] ) doc.DO_Date = pywintypes.Time(nouvelle_date) champs_modifies.append("date_devis") logger.info( f" Date: {nouvelle_date.strftime('%Y-%m-%d')}" ) except Exception as e: logger.error(f" Erreur: {e}") if modif_date_livraison: logger.info(" Modification date livraison (avant lignes)...") try: if devis_data_temp["date_livraison"]: nouvelle_date_livr = normaliser_date( devis_data_temp["date_livraison"] ) doc.DO_DateLivr = pywintypes.Time(nouvelle_date_livr) logger.info( f" Date livraison: {nouvelle_date_livr.strftime('%Y-%m-%d')}" ) else: doc.DO_DateLivr = None logger.info(" Date livraison effacée") champs_modifies.append("date_livraison") except Exception as e: logger.error(f" Erreur: {e}") nouvelles_lignes = devis_data["lignes"] nb_nouvelles = len(nouvelles_lignes) logger.info("") logger.info( f" Remplacement: {nb_lignes_initial} lignes → {nb_nouvelles} lignes" ) try: factory_lignes = doc.FactoryDocumentLigne except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle if nb_lignes_initial > 0: logger.info("") logger.info( f" Suppression de {nb_lignes_initial} lignes existantes..." ) for idx in range(nb_lignes_initial, 0, -1): try: ligne_p = factory_lignes.List(idx) if ligne_p: try: ligne = win32com.client.CastTo( ligne_p, "IBODocumentLigne3" ) except Exception: ligne = win32com.client.CastTo( ligne_p, "IBODocumentVenteLigne3" ) ligne.Read() ligne.Remove() logger.debug(f" Ligne {idx} supprimée") except Exception as e: logger.warning(f" Ligne {idx} non supprimée: {e}") logger.info(f" {nb_lignes_initial} lignes supprimées") logger.info("") logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...") for idx, ligne_data in enumerate(nouvelles_lignes, 1): article_code = ligne_data["article_code"] quantite = float(ligne_data["quantite"]) logger.info("") logger.info(f" Ligne {idx}/{nb_nouvelles}: {article_code}") logger.info(f" Quantité: {quantite}") if ligne_data.get("prix_unitaire_ht"): logger.info( f" Prix HT: {ligne_data['prix_unitaire_ht']}€" ) if ligne_data.get("remise_pourcentage"): logger.info( f" Remise: {ligne_data['remise_pourcentage']}%" ) try: persist_article = factory_article.ReadReference( article_code ) if not persist_article: raise ValueError(f"Article {article_code} INTROUVABLE") article_obj = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_obj.Read() logger.info(" Article chargé") ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) try: ligne_obj.SetDefaultArticleReference( article_code, quantite ) logger.info( " Article associé via SetDefaultArticleReference" ) except Exception: try: ligne_obj.SetDefaultArticle(article_obj, quantite) logger.info( " Article associé via SetDefaultArticle" ) except Exception: ligne_obj.DL_Design = ligne_data.get( "designation", "" ) ligne_obj.DL_Qte = quantite logger.info(" Article associé manuellement") if ligne_data.get("prix_unitaire_ht"): ligne_obj.DL_PrixUnitaire = float( ligne_data["prix_unitaire_ht"] ) logger.info(" Prix unitaire défini") if ligne_data.get("remise_pourcentage", 0) > 0: try: ligne_obj.DL_Remise01REM_Valeur = float( ligne_data["remise_pourcentage"] ) ligne_obj.DL_Remise01REM_Type = 0 logger.info(" Remise définie") except Exception: logger.debug(" Remise non supportée") ligne_obj.Write() logger.info(f" Ligne {idx} créée avec succès") except Exception as e: logger.error( f" ERREUR ligne {idx}: {e}", exc_info=True ) raise logger.info("") logger.info(f" {nb_nouvelles} lignes créées") logger.info("") logger.info(" Write() après remplacement lignes...") try: doc.Write() logger.info(" Write() réussi") time.sleep(0.5) doc.Read() logger.info(" Read() après Write() OK") except Exception as e: logger.error(f" Write() a échoué: {e}", exc_info=True) raise champs_modifies.append("lignes") if reference_a_modifier is not None: logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 6] MODIFICATION RÉFÉRENCE (après lignes)") logger.info("=" * 80) try: ancienne_ref = getattr(doc, "DO_Ref", "") nouvelle_ref = ( str(reference_a_modifier) if reference_a_modifier else "" ) logger.info(f" Actuelle: '{ancienne_ref}'") logger.info(f" Cible: '{nouvelle_ref}'") doc.DO_Ref = nouvelle_ref logger.info(" doc.DO_Ref affecté") doc.Write() logger.info(" Write()") time.sleep(0.5) doc.Read() logger.info(" Read()") champs_modifies.append("reference") logger.info( f" Référence modifiée: '{ancienne_ref}' → '{nouvelle_ref}'" ) except Exception as e: logger.error(f" Erreur référence: {e}", exc_info=True) if statut_a_modifier is not None: logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 7] MODIFICATION STATUT (en dernier)") logger.info("=" * 80) try: statut_actuel = getattr(doc, "DO_Statut", 0) nouveau_statut = int(statut_a_modifier) logger.info(f" Actuel: {statut_actuel}") logger.info(f" Cible: {nouveau_statut}") if nouveau_statut != statut_actuel and nouveau_statut in [ 0, 1, 2, 3, ]: doc.DO_Statut = nouveau_statut logger.info(" doc.DO_Statut affecté") doc.Write() logger.info(" Write()") time.sleep(0.5) doc.Read() logger.info(" Read()") champs_modifies.append("statut") logger.info( f" Statut modifié: {statut_actuel} → {nouveau_statut}" ) else: logger.info( " Pas de modification (identique ou invalide)" ) except Exception as e: logger.error(f" Erreur statut: {e}", exc_info=True) logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 8] VALIDATION FINALE") logger.info("=" * 80) try: doc.Write() logger.info(" Write() final") except Exception as e: logger.warning(f" Write() final: {e}") time.sleep(0.5) doc.Read() logger.info(" Read() final") logger.info("") _afficher_etat_document(doc, "📸 ÉTAT FINAL") logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 9] EXTRACTION RÉSULTAT") logger.info("=" * 80) resultat = _extraire_infos_devis(doc, numero, champs_modifies) logger.info(" Résultat extrait:") logger.info(f" Numéro: {resultat['numero']}") logger.info(f" Référence: '{resultat['reference']}'") logger.info(f" Date devis: {resultat['date_devis']}") logger.info(f" Date livraison: {resultat['date_livraison']}") logger.info(f" Statut: {resultat['statut']}") logger.info(f" Total HT: {resultat['total_ht']}€") logger.info(f" Total TTC: {resultat['total_ttc']}€") logger.info(f" Champs modifiés: {resultat['champs_modifies']}") logger.info("") logger.info("=" * 100) logger.info(f" MODIFICATION DEVIS {numero} TERMINÉE AVEC SUCCÈS ") logger.info("=" * 100) return resultat except ValueError as e: logger.error(f" ERREUR MÉTIER: {e}") raise except Exception as e: logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True) raise RuntimeError(f"Erreur technique Sage: {str(e)}") def _charger_devis(self, numero: str): """Charge un devis depuis Sage.""" logger.info(f" Chargement devis {numero}...") factory = self.cial.FactoryDocumentVente logger.info(" Tentative ReadPiece(0, numero)...") persist = factory.ReadPiece(0, numero) if not persist: logger.warning(" ReadPiece a échoué, recherche dans la liste...") persist = _rechercher_devis_par_numero(numero, factory) if not persist: raise ValueError(f" Devis {numero} INTROUVABLE") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() logger.info(f" Devis {numero} chargé") return doc def lire_devis(self, numero_devis): try: with self._get_sql_connection() as conn: cursor = conn.cursor() devis = _lire_document_sql(cursor, numero_devis, type_doc=0) if not devis: return None return devis except Exception as e: logger.error(f" Erreur SQL lecture devis {numero_devis}: {e}") return None def lire_document(self, numero, type_doc): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lire_document_sql(cursor, numero, type_doc) def transformer_document( self, numero_source, type_source, type_cible, conserver_document_source=True, verifier_doublons=True, ): if not self.cial: raise RuntimeError("Connexion Sage non établie") type_source = int(type_source) type_cible = int(type_cible) logger.info( f"[TRANSFORM] Transformation: {numero_source} ({type_source}) → type {type_cible}" ) transformations_valides = { (0, 10): ("Vente", "CreateProcess_Commander"), (0, 60): ("Vente", "CreateProcess_Facturer"), (10, 30): ("Vente", "CreateProcess_Livrer"), (10, 60): ("Vente", "CreateProcess_Facturer"), (30, 60): ("Vente", "CreateProcess_Facturer"), } if (type_source, type_cible) not in transformations_valides: raise ValueError( f"Transformation non autorisée: " 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}()") try: 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}...") if not factory.ExistPiece(type_source, numero_source): raise ValueError(f"Document {numero_source} introuvable") persist_source = factory.ReadPiece(type_source, numero_source) if not persist_source: raise ValueError(f"Impossible de lire {numero_source}") doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3") doc_source.Read() statut_source = getattr(doc_source, "DO_Statut", 0) nb_lignes_source = 0 try: factory_lignes = getattr(doc_source, "FactoryDocumentLigne", None) if factory_lignes: lignes_list = factory_lignes.List nb_lignes_source = lignes_list.Count if lignes_list else 0 except Exception: pass logger.info( f"[TRANSFORM] Source: statut={statut_source}, {nb_lignes_source} ligne(s)" ) if nb_lignes_source == 0: raise ValueError(f"Document {numero_source} vide (0 lignes)") logger.info("[TRANSFORM] 🔧 Création du transformer...") transformation = getattr(self.cial, "Transformation", None) if not transformation: raise RuntimeError("API Transformation non disponible") module_obj = getattr(transformation, module, None) if not module_obj: raise RuntimeError(f"Module {module} non disponible") methode_func = getattr(module_obj, methode, None) if not methode_func: raise RuntimeError(f"Méthode {methode} non disponible") transformer = methode_func() if not transformer: raise RuntimeError("Échec création transformer") logger.info("[TRANSFORM] Transformer créé") logger.info("[TRANSFORM] Configuration...") if hasattr(transformer, "ConserveDocuments"): try: transformer.ConserveDocuments = conserver_document_source logger.info( f"[TRANSFORM] ConserveDocuments = {conserver_document_source}" ) except Exception as e: logger.warning( f"[TRANSFORM] ConserveDocuments non modifiable: {e}" ) logger.info("[TRANSFORM] Ajout du document...") try: transformer.AddDocument(doc_source) logger.info("[TRANSFORM] Document ajouté") except Exception as e: raise RuntimeError(f"Impossible d'ajouter le document: {e}") try: can_process = getattr(transformer, "CanProcess", False) logger.info(f"[TRANSFORM] CanProcess: {can_process}") except Exception: can_process = True if not can_process: erreurs = lire_erreurs_sage(transformer, "Transformer") if erreurs: msgs = [f"{e['field']}: {e['description']}" for e in erreurs] raise RuntimeError( f"Transformation impossible: {' | '.join(msgs)}" ) raise RuntimeError("Transformation impossible (CanProcess=False)") transaction_active = False try: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug("[TRANSFORM] Transaction démarrée") except Exception: pass try: logger.info("[TRANSFORM] Process()...") try: transformer.Process() logger.info("[TRANSFORM] Process() réussi") except Exception as e: logger.error(f"[TRANSFORM] Erreur Process(): {e}") erreurs = lire_erreurs_sage(transformer, "Transformer") if erreurs: msgs = [ f"{e['field']}: {e['description']}" for e in erreurs ] raise RuntimeError(f"Échec: {' | '.join(msgs)}") raise RuntimeError(f"Échec transformation: {e}") logger.info("[TRANSFORM] Récupération des résultats...") list_results = getattr(transformer, "ListDocumentsResult", None) if not list_results: raise RuntimeError("ListDocumentsResult non disponible") documents_crees = [] index = 1 while index <= 100: try: doc_result = list_results.Item(index) if doc_result is None: break doc_result = win32com.client.CastTo( doc_result, "IBODocumentVente3" ) doc_result.Read() numero_cible = getattr(doc_result, "DO_Piece", "").strip() total_ht = float(getattr(doc_result, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc_result, "DO_TotalTTC", 0.0)) nb_lignes = 0 try: factory_lignes_result = getattr( doc_result, "FactoryDocumentLigne", None ) if factory_lignes_result: lignes_list = factory_lignes_result.List nb_lignes = lignes_list.Count if lignes_list else 0 except Exception: pass documents_crees.append( { "numero": numero_cible, "total_ht": total_ht, "total_ttc": total_ttc, "nb_lignes": nb_lignes, } ) logger.info( f"[TRANSFORM] Document créé: {numero_cible} " f"({nb_lignes} lignes, {total_ht}€ HT)" ) index += 1 except Exception: logger.debug(f"Fin de liste à index {index}") break if not documents_crees: raise RuntimeError("Aucun document créé après Process()") if transaction_active: try: self.cial.CptaApplication.CommitTrans() logger.debug("[TRANSFORM] Transaction committée") except Exception: pass time.sleep(1.5) doc_principal = documents_crees[0] logger.info( f"[TRANSFORM] SUCCÈS: {numero_source} → {doc_principal['numero']}" ) logger.info( f"[TRANSFORM] • {doc_principal['nb_lignes']} ligne(s)" ) logger.info( f"[TRANSFORM] • {doc_principal['total_ht']}€ HT / " f"{doc_principal['total_ttc']}€ TTC" ) return { "success": True, "document_source": numero_source, "document_cible": doc_principal["numero"], "type_source": type_source, "type_cible": type_cible, "nb_documents_crees": len(documents_crees), "documents": documents_crees, "nb_lignes": doc_principal["nb_lignes"], "total_ht": doc_principal["total_ht"], "total_ttc": doc_principal["total_ttc"], "methode_transformation": f"{module}.{methode}", } except Exception: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() logger.error("[TRANSFORM] Transaction annulée (rollback)") except Exception: pass raise except ValueError as e: logger.error(f"[TRANSFORM] Erreur métier: {e}") raise except RuntimeError as e: logger.error(f"[TRANSFORM] Erreur technique: {e}") raise except Exception as e: logger.error(f"[TRANSFORM] Erreur inattendue: {e}", exc_info=True) raise RuntimeError(f"Échec transformation: {str(e)}") def mettre_a_jour_champ_libre(self, doc_id, type_doc, nom_champ, valeur): """Mise à jour champ libre pour Universign ID""" try: with self._com_context(), self._lock_com: factory = self.cial.FactoryDocumentVente persist = factory.ReadPiece(type_doc, doc_id) if persist: doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() try: setattr(doc, f"DO_{nom_champ}", valeur) doc.Write() logger.debug(f"Champ libre {nom_champ} = {valeur} sur {doc_id}") return True except Exception as e: logger.warning(f"Impossible de mettre à jour {nom_champ}: {e}") except Exception as e: logger.error(f"Erreur MAJ champ libre: {e}") return False def _lire_client_obj(self, code_client): """Retourne l'objet client Sage brut (pour remises)""" if not self.cial: return None try: with self._com_context(), self._lock_com: factory = self.cial.CptaApplication.FactoryClient persist = factory.ReadNumero(code_client) if persist: return _cast_client(persist) except Exception: pass return None def lire_contact_principal_client(self, code_client): if not self.cial: return None try: with self._com_context(), self._lock_com: factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero(code_client) if not persist_client: return None client = _cast_client(persist_client) if not client: return None contact_info = { "client_code": code_client, "client_intitule": getattr(client, "CT_Intitule", ""), "email": None, "nom": None, "telephone": None, } try: telecom = getattr(client, "Telecom", None) if telecom: contact_info["email"] = getattr(telecom, "EMail", "") contact_info["telephone"] = getattr(telecom, "Telephone", "") except Exception: pass try: contact_info["nom"] = ( getattr(client, "CT_Contact", "") or contact_info["client_intitule"] ) except Exception: contact_info["nom"] = contact_info["client_intitule"] return contact_info except Exception as e: logger.error(f"Erreur lecture contact client {code_client}: {e}") return None def mettre_a_jour_derniere_relance(self, doc_id, type_doc): date_relance = datetime.now().strftime("%Y-%m-%d %H:%M:%S") return self.mettre_a_jour_champ_libre( doc_id, type_doc, "DerniereRelance", date_relance ) def lister_tous_prospects(self, filtre=""): try: with self._get_sql_connection() as conn: cursor = conn.cursor() query = """ SELECT CT_Num, CT_Intitule, CT_Adresse, CT_Ville, CT_CodePostal, CT_Telephone, CT_EMail FROM F_COMPTET WHERE CT_Type = 0 AND CT_Prospect = 1 """ params = [] if filtre: query += " AND (CT_Num LIKE ? OR CT_Intitule LIKE ?)" params.extend([f"%{filtre}%", f"%{filtre}%"]) query += " ORDER BY CT_Intitule" cursor.execute(query, params) rows = cursor.fetchall() prospects = [] for row in rows: prospects.append( { "numero": _safe_strip(row.CT_Num), "intitule": _safe_strip(row.CT_Intitule), "adresse": _safe_strip(row.CT_Adresse), "ville": _safe_strip(row.CT_Ville), "code_postal": _safe_strip(row.CT_CodePostal), "telephone": _safe_strip(row.CT_Telephone), "email": _safe_strip(row.CT_EMail), "type": 0, "est_prospect": True, } ) logger.info(f" SQL: {len(prospects)} prospects") return prospects except Exception as e: logger.error(f" Erreur SQL prospects: {e}") return [] def lire_prospect(self, code_prospect): try: with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( """ SELECT CT_Num, CT_Intitule, CT_Type, CT_Qualite, CT_Adresse, CT_Complement, CT_Ville, CT_CodePostal, CT_Pays, CT_Telephone, CT_Portable, CT_EMail, CT_Telecopie, CT_Siret, CT_Identifiant, CT_Sommeil, CT_Prospect, CT_Contact, CT_FormeJuridique, CT_Secteur FROM F_COMPTET WHERE CT_Num = ? AND CT_Type = 0 AND CT_Prospect = 1 """, (code_prospect.upper(),), ) row = cursor.fetchone() if not row: return None return { "numero": _safe_strip(row.CT_Num), "intitule": _safe_strip(row.CT_Intitule), "type": 0, "qualite": _safe_strip(row.CT_Qualite), "est_prospect": True, "adresse": _safe_strip(row.CT_Adresse), "complement": _safe_strip(row.CT_Complement), "ville": _safe_strip(row.CT_Ville), "code_postal": _safe_strip(row.CT_CodePostal), "pays": _safe_strip(row.CT_Pays), "telephone": _safe_strip(row.CT_Telephone), "portable": _safe_strip(row.CT_Portable), "email": _safe_strip(row.CT_EMail), "telecopie": _safe_strip(row.CT_Telecopie), "siret": _safe_strip(row.CT_Siret), "tva_intra": _safe_strip(row.CT_Identifiant), "est_actif": (row.CT_Sommeil == 0), "contact": _safe_strip(row.CT_Contact), "forme_juridique": _safe_strip(row.CT_FormeJuridique), "secteur": _safe_strip(row.CT_Secteur), } except Exception as e: logger.error(f" Erreur SQL prospect {code_prospect}: {e}") return None def lire_avoir(self, numero): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lire_document_sql(cursor, numero, type_doc=50) def lire_livraison(self, numero): with self._get_sql_connection() as conn: cursor = conn.cursor() return _lire_document_sql(cursor, numero, type_doc=30) def creer_contact(self, contact_data: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") try: 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) if not contact_data.get("numero"): raise ValueError("numero (code client) obligatoire") if not contact_data.get("nom"): raise ValueError("nom obligatoire") numero_client = _clean_str(contact_data["numero"], 17).upper() nom = _clean_str(contact_data["nom"], 35) prenom = _clean_str(contact_data.get("prenom", ""), 35) logger.info(f" CLIENT: {numero_client}") logger.info(f" CONTACT: {prenom} {nom}") logger.info(f"[1] Chargement du client: {numero_client}") factory_client = self.cial.CptaApplication.FactoryClient try: persist_client = factory_client.ReadNumero(numero_client) if not persist_client: raise ValueError(f"Client {numero_client} non trouve") client_obj = win32com.client.CastTo(persist_client, "IBOClient3") client_obj.Read() logger.info(" OK Client charge") except Exception as e: raise ValueError(f"Client {numero_client} introuvable: {e}") 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__}" ) persist = factory_contact.Create() logger.info(f" Objet cree: {type(persist).__name__}") contact = None interfaces_a_tester = [ "IBOTiersContact3", "IBOTiersContact", "IBOContactT3", "IBOContactT", ] for interface_name in interfaces_a_tester: try: temp = win32com.client.CastTo(persist, interface_name) if hasattr(temp, "_prop_map_put_"): props = list(temp._prop_map_put_.keys()) logger.info(f" Test {interface_name}: props={props[:15]}") if "Nom" in props or "CT_Nom" in props: contact = temp logger.info(f" OK Cast reussi vers {interface_name}") break except Exception as e: logger.debug(f" {interface_name}: {str(e)[:50]}") if not contact: logger.error(" ERROR Aucun cast ne fonctionne") raise RuntimeError( "Impossible de caster vers une interface contact valide" ) logger.info("[3] Configuration du contact") if hasattr(contact, "_prop_map_put_"): props = list(contact._prop_map_put_.keys()) logger.info(f" Proprietes disponibles: {props}") try: contact.Nom = nom logger.info(f" OK Nom = {nom}") except Exception as e: logger.error(f" ERROR Impossible de definir Nom: {e}") raise RuntimeError(f"Echec definition Nom: {e}") if prenom: try: contact.Prenom = prenom logger.info(f" OK Prenom = {prenom}") except Exception as e: logger.warning(f" WARN Prenom: {e}") if contact_data.get("civilite"): civilite_map = {"M.": 0, "Mme": 1, "Mlle": 2, "Societe": 3} civilite_code = civilite_map.get(contact_data["civilite"]) if civilite_code is not None: try: contact.Civilite = civilite_code logger.info(f" OK Civilite = {civilite_code}") except Exception as e: logger.warning(f" WARN Civilite: {e}") if contact_data.get("fonction"): fonction = _clean_str(contact_data["fonction"], 35) try: contact.Fonction = fonction logger.info(f" OK Fonction = {fonction}") except Exception as e: logger.warning(f" WARN Fonction: {e}") if contact_data.get("service_code") is not None: try: service = _safe_int(contact_data["service_code"]) if service is not None and hasattr(contact, "ServiceContact"): contact.ServiceContact = service logger.info(f" OK ServiceContact = {service}") except Exception as e: logger.warning(f" WARN ServiceContact: {e}") logger.info("[4] Coordonnees (Telecom)") if hasattr(contact, "Telecom"): try: telecom = contact.Telecom logger.info(f" Type Telecom: {type(telecom).__name__}") if contact_data.get("telephone"): telephone = _clean_str(contact_data["telephone"], 21) if _try_set_attribute(telecom, "Telephone", telephone): logger.info(f" Telephone = {telephone}") if contact_data.get("portable"): portable = _clean_str(contact_data["portable"], 21) if _try_set_attribute(telecom, "Portable", portable): logger.info(f" Portable = {portable}") if contact_data.get("email"): email = _clean_str(contact_data["email"], 69) if _try_set_attribute(telecom, "EMail", email): logger.info(f" EMail = {email}") if contact_data.get("telecopie"): fax = _clean_str(contact_data["telecopie"], 21) if _try_set_attribute(telecom, "Telecopie", fax): logger.info(f" Telecopie = {fax}") except Exception as e: logger.warning(f" WARN Erreur Telecom: {e}") logger.info("[5] Reseaux sociaux") if contact_data.get("facebook"): facebook = _clean_str(contact_data["facebook"], 69) try: contact.Facebook = facebook logger.info(f" Facebook = {facebook}") except Exception: pass if contact_data.get("linkedin"): linkedin = _clean_str(contact_data["linkedin"], 69) try: contact.LinkedIn = linkedin logger.info(f" LinkedIn = {linkedin}") except Exception: pass if contact_data.get("skype"): skype = _clean_str(contact_data["skype"], 69) try: contact.Skype = skype logger.info(f" Skype = {skype}") except Exception: pass try: contact.SetDefault() logger.info(" OK SetDefault() applique") except Exception as e: logger.warning(f" WARN SetDefault(): {e}") logger.info("[6] Enregistrement du contact") contact_cree_malgre_erreur = False contact_no = None n_contact = None erreur_com = None try: contact.Write() logger.info(" Write() reussi") try: contact.Read() logger.info(" Read() reussi") except Exception as read_err: logger.warning(f" WARN Read() échoué: {read_err}") 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 Exception: pass 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}") 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..." ) import time time.sleep(0.5) contact_sql = _chercher_contact_en_base( conn, numero_client=numero_client, nom=nom, prenom=prenom if prenom else None, ) if contact_sql: logger.info(" 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: logger.error(f" Erreur Write non gérée: {erreur_com}") raise RuntimeError(f"Echec enregistrement: {erreur_com}") 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") try: 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.Read() client_obj.CT_Contact = nom_complet logger.info(f" CT_Contact = '{nom_complet}'") if contact_no and hasattr(client_obj, "CT_NoContact"): try: client_obj.CT_NoContact = contact_no logger.info(f" CT_NoContact = {contact_no}") except Exception: pass client_obj.Write() client_obj.Read() logger.info(" OK Contact par defaut defini") except Exception as e: logger.warning(f" WARN Echec: {e}") est_defaut = False logger.info("=" * 80) if contact_cree_malgre_erreur: logger.info( f"[SUCCES] Contact créé MALGRÉ erreur COM: {prenom} {nom}" ) logger.info( " (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) logger.info("[7] Construction du retour") contact_dict = None 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 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 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" ) 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" ) contact_dict["est_defaut"] = est_defaut logger.info(" 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(" RETURN contact_dict") return contact_dict except ValueError as e: logger.error(f"[ERREUR VALIDATION] {e}") raise except Exception as e: 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") try: with self._com_context(), self._lock_com: logger.info("=" * 80) logger.info(f"[MODIFICATION CONTACT] CT_No={contact_numero}") logger.info("=" * 80) logger.info("[1] Chargement du client") factory_client = self.cial.CptaApplication.FactoryClient try: persist_client = factory_client.ReadNumero(numero) if not persist_client: raise ValueError(f"Client {numero} non trouve") client_obj = win32com.client.CastTo(persist_client, "IBOClient3") client_obj.Read() logger.info(f" OK Client charge: {client_obj.CT_Intitule}") except Exception as e: raise ValueError(f"Client {numero} introuvable: {e}") logger.info("[2] Chargement du contact") contact = None nom_recherche = None prenom_recherche = None try: with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( """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 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 "" ) cbmarq_sql = row.cbMarq 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"Erreur lecture contact en base: {e}") 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 Exception: continue if not temp_contact: continue temp_contact.Read() nom_com = getattr(temp_contact, "Nom", "") or "" prenom_com = ( getattr(temp_contact, "Prenom", "") or "" ) 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 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(" OK Contact charge via ReadNomPrenom") except Exception as e: logger.warning(f" Strategie 2 echouee: {e}") 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 Exception: continue except Exception as e: logger.warning(f" Strategie 3 echouee: {e}") 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 Exception: continue except Exception as e: logger.warning(f" Strategie 4 echouee: {e}") 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}'" ) logger.info(f" OK Contact charge: {contact.Nom}") logger.info("[3] Application des modifications") modifications_appliquees = [] if "civilite" in updates: civilite_map = {"M.": 0, "Mme": 1, "Mlle": 2, "Societe": 3} civilite_code = civilite_map.get(updates["civilite"]) if civilite_code is not None: try: contact.Civilite = civilite_code logger.info(f" Civilite = {civilite_code}") modifications_appliquees.append("civilite") except Exception as e: logger.warning(f" WARN Civilite: {e}") if "nom" in updates: nom = _clean_str(updates["nom"], 35) if nom: try: contact.Nom = nom logger.info(f" Nom = {nom}") modifications_appliquees.append("nom") except Exception as e: logger.warning(f" WARN Nom: {e}") if "prenom" in updates: prenom = _clean_str(updates["prenom"], 35) try: contact.Prenom = prenom logger.info(f" Prenom = {prenom}") modifications_appliquees.append("prenom") except Exception as e: logger.warning(f" WARN Prenom: {e}") if "fonction" in updates: fonction = _clean_str(updates["fonction"], 35) try: contact.Fonction = fonction logger.info(f" Fonction = {fonction}") modifications_appliquees.append("fonction") except Exception as e: logger.warning(f" WARN Fonction: {e}") if "service_code" in updates: service = _safe_int(updates["service_code"]) if service is not None and hasattr(contact, "ServiceContact"): try: contact.ServiceContact = service logger.info(f" ServiceContact = {service}") modifications_appliquees.append("service_code") except Exception as e: logger.warning(f" WARN ServiceContact: {e}") if hasattr(contact, "Telecom"): try: telecom = contact.Telecom if "telephone" in updates: telephone = _clean_str(updates["telephone"], 21) if _try_set_attribute(telecom, "Telephone", telephone): logger.info(f" Telephone = {telephone}") modifications_appliquees.append("telephone") if "portable" in updates: portable = _clean_str(updates["portable"], 21) if _try_set_attribute(telecom, "Portable", portable): logger.info(f" Portable = {portable}") modifications_appliquees.append("portable") if "email" in updates: email = _clean_str(updates["email"], 69) if _try_set_attribute(telecom, "EMail", email): logger.info(f" EMail = {email}") modifications_appliquees.append("email") if "telecopie" in updates: fax = _clean_str(updates["telecopie"], 21) if _try_set_attribute(telecom, "Telecopie", fax): logger.info(f" Telecopie = {fax}") modifications_appliquees.append("telecopie") except Exception as e: logger.warning(f" WARN Telecom: {e}") if "facebook" in updates: facebook = _clean_str(updates["facebook"], 69) try: contact.Facebook = facebook logger.info(f" Facebook = {facebook}") modifications_appliquees.append("facebook") except Exception as e: logger.warning(f" WARN Facebook: {e}") if "linkedin" in updates: linkedin = _clean_str(updates["linkedin"], 69) try: contact.LinkedIn = linkedin logger.info(f" LinkedIn = {linkedin}") modifications_appliquees.append("linkedin") except Exception as e: logger.warning(f" WARN LinkedIn: {e}") if "skype" in updates: skype = _clean_str(updates["skype"], 69) try: contact.Skype = skype logger.info(f" Skype = {skype}") modifications_appliquees.append("skype") except Exception as e: logger.warning(f" WARN Skype: {e}") logger.info( f" Modifications preparees: {', '.join(modifications_appliquees) if modifications_appliquees else 'aucune'}" ) logger.info("[4] Enregistrement") try: contact.Write() 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})" ) except Exception: pass logger.error(f" ERROR Write: {error_detail}") raise RuntimeError(f"Echec modification contact: {error_detail}") try: contact.Read() logger.info(" Read() reussi") except Exception as e: logger.warning(f" WARN Read() apres Write: {e}") est_defaut_demande = updates.get("est_defaut") est_actuellement_defaut = False if est_defaut_demande: logger.info("[5] Definition comme contact par defaut") try: nom_complet = ( f"{contact.Prenom} {contact.Nom}".strip() if contact.Prenom else contact.Nom ) persist_client = factory_client.ReadNumero(numero) client_obj = win32com.client.CastTo( persist_client, "IBOClient3" ) client_obj.Read() client_obj.CT_Contact = nom_complet logger.info(f" CT_Contact = '{nom_complet}'") if hasattr(client_obj, "CT_NoContact"): try: client_obj.CT_NoContact = contact_numero logger.info(f" CT_NoContact = {contact_numero}") except Exception as e: logger.warning(f" WARN CT_NoContact: {e}") client_obj.Write() client_obj.Read() logger.info(" OK Contact par defaut defini") est_actuellement_defaut = True except Exception as e: logger.warning(f" WARN Echec definition contact defaut: {e}") logger.info("[6] Construction du retour") contact_dict = None 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(" Lecture base reussie") except Exception as e: logger.warning(f" Lecture base echouee: {e}") 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(" _contact_to_dict reussi") except Exception as e: logger.warning(f" _contact_to_dict echoue: {e}") 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 Exception: 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) return contact_dict except ValueError as e: logger.error(f"[ERREUR VALIDATION] {e}") raise 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") try: with self._com_context(), self._lock_com: logger.info("=" * 80) logger.info( f"[DEFINIR CONTACT PAR DEFAUT] Client={numero}, Contact={contact_numero}" ) logger.info("=" * 80) logger.info("[1] Recuperation infos contact") nom_contact = None prenom_contact = None 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 = ?", [numero, contact_numero], ) row = cursor.fetchone() if not row: raise ValueError( f"Contact CT_No={contact_numero} non trouve" ) nom_contact = row.CT_Nom.strip() if row.CT_Nom else "" prenom_contact = row.CT_Prenom.strip() if row.CT_Prenom else "" nom_complet = ( f"{prenom_contact} {nom_contact}".strip() if prenom_contact else nom_contact ) logger.info(f" OK Contact trouve: {nom_complet}") except Exception as e: raise ValueError(f"Contact introuvable: {e}") logger.info("[2] Chargement du client") factory_client = self.cial.CptaApplication.FactoryClient try: persist_client = factory_client.ReadNumero(numero) if not persist_client: raise ValueError(f"Client {numero} non trouve") client = win32com.client.CastTo(persist_client, "IBOClient3") client.Read() logger.info(f" OK Client charge: {client.CT_Intitule}") except Exception as e: raise ValueError(f"Client introuvable: {e}") logger.info("[3] Definition du contact par defaut") ancien_contact = getattr(client, "CT_Contact", "") client.CT_Contact = nom_complet logger.info(f" CT_Contact: '{ancien_contact}' -> '{nom_complet}'") if hasattr(client, "CT_NoContact"): try: client.CT_NoContact = contact_numero logger.info(f" CT_NoContact = {contact_numero}") except Exception: pass logger.info("[4] Enregistrement") try: client.Write() client.Read() logger.info(" OK Client mis a jour") 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 Exception: pass raise RuntimeError(f"Echec mise a jour: {error_detail}") logger.info("=" * 80) logger.info(f"[SUCCES] Contact par defaut: {nom_complet}") logger.info("=" * 80) return { "numero": numero, "contact_numero": contact_numero, "contact_nom": nom_complet, "client_intitule": client.CT_Intitule, "est_defaut": True, "date_modification": datetime.now().isoformat(), } except ValueError as e: logger.error(f"[ERREUR VALIDATION] {e}") raise except Exception as e: logger.error(f"[ERREUR] {e}", exc_info=True) raise RuntimeError(f"Erreur technique: {e}") def lister_contacts(self, numero: str) -> List[Dict]: try: with self._get_sql_connection() as conn: return _get_contacts_client(numero, conn) except Exception as e: logger.error(f"Erreur liste contacts: {e}") raise RuntimeError(f"Erreur lecture contacts: {str(e)}") def supprimer_contact(self, numero: str, contact_numero: int) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: logger.info("=" * 80) logger.info(f"[SUPPRESSION CONTACT] CT_No={contact_numero}") logger.info("=" * 80) logger.info("[1] Recuperation infos contact") nom_contact = None prenom_contact = None 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 = ?", [numero, contact_numero], ) row = cursor.fetchone() if not row: raise ValueError( f"Contact CT_No={contact_numero} non trouve" ) nom_contact = row.CT_Nom.strip() if row.CT_Nom else "" prenom_contact = row.CT_Prenom.strip() if row.CT_Prenom else "" logger.info( f" OK Contact trouve: {prenom_contact} {nom_contact}" ) except Exception as e: raise ValueError(f"Contact introuvable: {e}") logger.info("[2] Chargement du contact") factory_dossier = self.cial.CptaApplication.FactoryDossierContact try: persist = factory_dossier.ReadNomPrenom(nom_contact, prenom_contact) if not persist: raise ValueError("Contact non trouvable via ReadNomPrenom") contact = win32com.client.CastTo(persist, "IBOTiersContact3") contact.Read() logger.info(f" OK Contact charge: {contact.Nom}") except Exception as e: raise ValueError(f"Contact introuvable: {e}") logger.info("[3] Suppression") try: contact.Remove() logger.info(" OK Remove() 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 Exception: pass logger.error(f" ERROR Remove: {error_detail}") raise RuntimeError(f"Echec suppression contact: {error_detail}") logger.info("=" * 80) logger.info( f"[SUCCES] Contact supprime: {prenom_contact} {nom_contact}" ) logger.info("=" * 80) return { "numero": numero, "contact_numero": contact_numero, "nom": nom_contact, "prenom": prenom_contact, "supprime": True, "date_suppression": datetime.now().isoformat(), } except ValueError as e: logger.error(f"[ERREUR VALIDATION] {e}") raise except Exception as e: logger.error(f"[ERREUR] {e}", exc_info=True) raise RuntimeError(f"Erreur technique: {e}") def creer_client(self, client_data: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non etablie") try: with self._com_context(), self._lock_com: logger.info("=" * 80) logger.info("[CREATION CLIENT SAGE - DIAGNOSTIC COMPLET]") logger.info("=" * 80) 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 definir 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 if not client_data.get("intitule"): raise ValueError("intitule obligatoire") if not client_data.get("numero"): raise ValueError("numero obligatoire") intitule = clean_str(client_data["intitule"], 69) numero = clean_str(client_data["numero"], 17).upper() type_tiers = safe_int(client_data.get("type_tiers"), 0) logger.info("[ETAPE 1] CREATION OBJET") factory_map = { 0: ("FactoryClient", "IBOClient3"), 1: ("FactoryFourniss", "IBOFournisseur3"), 2: ("FactorySalarie", "IBOSalarie3"), 3: ("FactoryAutre", "IBOAutre3"), } factory_name, interface_name = factory_map[type_tiers] factory = getattr(self.cial.CptaApplication, factory_name) persist = factory.Create() client = win32com.client.CastTo(persist, interface_name) logger.info(f" Objet cree: {interface_name}") logger.info("[ETAPE 2] CONFIGURATION OBLIGATOIRE") client.CT_Intitule = intitule client.CT_Num = numero logger.info(f" CT_Num = {numero}") logger.info(f" CT_Intitule = {intitule}") qualite = clean_str(client_data.get("qualite", "CLI"), 17) if qualite: client.CT_Qualite = qualite logger.info(f" CT_Qualite = {qualite}") client.SetDefault() logger.info(" SetDefault() applique") if client_data.get("raccourci"): raccourci = clean_str(client_data["raccourci"], 7).upper().strip() try: factory_client = self.cial.CptaApplication.FactoryClient exist_client = factory_client.ReadRaccourci(raccourci) if exist_client: logger.warning( f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]" ) else: client.CT_Raccourci = raccourci logger.info(f" CT_Raccourci = {raccourci} [OK]") except Exception: try: client.CT_Raccourci = raccourci logger.info(f" CT_Raccourci = {raccourci} [OK]") except Exception as e2: logger.warning( f" CT_Raccourci = {raccourci} [ECHEC: {e2}]" ) try: if not hasattr(client, "CT_Type") or client.CT_Type is None: client.CT_Type = type_tiers logger.info(f" CT_Type force a {type_tiers}") except Exception: pass COMPTES_DEFAUT = {0: "4110000", 1: "4010000", 2: "421", 3: "471"} compte = clean_str( client_data.get("compte_general") or COMPTES_DEFAUT.get(type_tiers, "4110000"), 13, ) factory_compte = self.cial.CptaApplication.FactoryCompteG compte_trouve = False comptes_a_tester = [ compte, COMPTES_DEFAUT.get(type_tiers, "4110000"), "4110000", "411000", "411", "4010000", "401000", "401", ] for test_compte in comptes_a_tester: try: persist_compte = factory_compte.ReadNumero(test_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 compte = test_compte compte_trouve = True logger.info( f" CompteGPrinc = {test_compte} (Type: {type_compte}) [OK]" ) break else: logger.debug( f" Compte {test_compte} - Type {type_compte} incompatible" ) except Exception as e: logger.debug(f" Compte {test_compte} - erreur: {e}") if not compte_trouve: raise RuntimeError("Aucun compte general valide trouve") logger.info(" Configuration categories:") try: factory_cat_tarif = self.cial.CptaApplication.FactoryCategorieTarif for cat_id in ["0", "1"]: try: 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 logger.info(f" CatTarif = {cat_id} [OK]") break except Exception: continue except Exception as e: logger.warning(f" CatTarif erreur: {e}") try: factory_cat_compta = ( self.cial.CptaApplication.FactoryCategorieCompta ) for cat_id in ["0", "1"]: try: 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 logger.info(f" CatCompta = {cat_id} [OK]") break except Exception: continue except Exception as e: logger.warning(f" CatCompta erreur: {e}") logger.info("[ETAPE 3] IDENTIFICATION") if client_data.get("classement"): try_set_attribute( client, "CT_Classement", clean_str(client_data["classement"], 17), ) if client_data.get("raccourci"): raccourci = clean_str(client_data["raccourci"], 7).upper() try_set_attribute(client, "CT_Raccourci", raccourci) if client_data.get("siret"): try_set_attribute( client, "CT_Siret", clean_str(client_data["siret"], 15) ) if client_data.get("tva_intra"): try_set_attribute( client, "CT_Identifiant", clean_str(client_data["tva_intra"], 25), ) if client_data.get("code_naf"): try_set_attribute( client, "CT_Ape", clean_str(client_data["code_naf"], 7) ) logger.info("[ETAPE 4] ADRESSE") if client_data.get("contact"): contact_nom = clean_str(client_data["contact"], 35) try: client.CT_Contact = contact_nom logger.info(f" CT_Contact (client) = {contact_nom} [OK]") except Exception as e: logger.warning(f" CT_Contact (client) [ECHEC: {e}]") try: adresse_obj = client.Adresse adresse_obj.Contact = contact_nom logger.info(f" Contact (adresse) = {contact_nom} [OK]") except Exception as e: logger.warning(f" Contact (adresse) [ECHEC: {e}]") try: adresse_obj = client.Adresse logger.info(" Objet Adresse OK") if client_data.get("adresse"): adresse_obj.Adresse = clean_str(client_data["adresse"], 35) if client_data.get("complement"): adresse_obj.Complement = clean_str( client_data["complement"], 35 ) if client_data.get("code_postal"): adresse_obj.CodePostal = clean_str( client_data["code_postal"], 9 ) if client_data.get("ville"): adresse_obj.Ville = clean_str(client_data["ville"], 35) if client_data.get("region"): adresse_obj.CodeRegion = clean_str(client_data["region"], 25) if client_data.get("pays"): adresse_obj.Pays = clean_str(client_data["pays"], 35) except Exception as e: logger.error(f" Adresse erreur: {e}") logger.info("[ETAPE 5] TELECOM") try: telecom_obj = client.Telecom logger.info(" Objet Telecom OK") if client_data.get("telephone"): telecom_obj.Telephone = clean_str(client_data["telephone"], 21) if client_data.get("telecopie"): telecom_obj.Telecopie = clean_str(client_data["telecopie"], 21) if client_data.get("email"): telecom_obj.EMail = clean_str(client_data["email"], 69) if client_data.get("site_web"): telecom_obj.Site = clean_str(client_data["site_web"], 69) if client_data.get("portable"): portable = clean_str(client_data["portable"], 21) try_set_attribute(telecom_obj, "Portable", portable) logger.info(f" Portable = {portable}") if client_data.get("facebook"): facebook = clean_str(client_data["facebook"], 69) if not try_set_attribute(telecom_obj, "Facebook", facebook): try_set_attribute(client, "CT_Facebook", facebook) logger.info(f" Facebook = {facebook}") if client_data.get("linkedin"): linkedin = clean_str(client_data["linkedin"], 69) if not try_set_attribute(telecom_obj, "LinkedIn", linkedin): try_set_attribute(client, "CT_LinkedIn", linkedin) logger.info(f" LinkedIn = {linkedin}") except Exception as e: logger.error(f" Telecom erreur: {e}") logger.info("[ETAPE 6] TAUX") for i in range(1, 5): val = client_data.get(f"taux{i:02d}") if val is not None: try_set_attribute(client, f"CT_Taux{i:02d}", safe_float(val)) logger.info("[ETAPE 7] STATISTIQUES") stat01 = client_data.get("statistique01") or client_data.get("secteur") if stat01: try_set_attribute(client, "CT_Statistique01", clean_str(stat01, 21)) for i in range(2, 11): val = client_data.get(f"statistique{i:02d}") if val: try_set_attribute( client, f"CT_Statistique{i:02d}", clean_str(val, 21) ) logger.info("[ETAPE 8] COMMERCIAL") if client_data.get("encours_autorise"): try_set_attribute( client, "CT_Encours", safe_float(client_data["encours_autorise"]), ) if client_data.get("assurance_credit"): try_set_attribute( client, "CT_Assurance", safe_float(client_data["assurance_credit"]), ) if client_data.get("langue") is not None: try_set_attribute( client, "CT_Langue", safe_int(client_data["langue"]) ) if client_data.get("commercial_code") is not None: co_no = safe_int(client_data["commercial_code"]) if not try_set_attribute(client, "CO_No", co_no): 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 logger.debug( f" Collaborateur (objet) = {co_no} [OK]" ) except Exception as e: logger.debug(f" Collaborateur echec: {e}") logger.info("[ETAPE 9] FACTURATION") try_set_attribute( client, "CT_Lettrage", 1 if client_data.get("lettrage_auto", True) else 0, ) try_set_attribute( client, "CT_Sommeil", 0 if client_data.get("est_actif", True) else 1 ) try_set_attribute( client, "CT_Facture", safe_int(client_data.get("type_facture", 1)) ) if client_data.get("est_prospect") is not None: try_set_attribute( client, "CT_Prospect", 1 if client_data["est_prospect"] else 0 ) 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 client_data.get(key) is not None: try_set_attribute(client, attr, safe_int(client_data[key])) logger.info("[ETAPE 10] 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 client_data.get(key) is not None: try_set_attribute(client, attr, safe_int(client_data[key])) if client_data.get("commentaire"): try_set_attribute( client, "CT_Commentaire", clean_str(client_data["commentaire"], 35), ) logger.info("[ETAPE 12] ANALYTIQUE") if client_data.get("section_analytique"): try_set_attribute( client, "CA_Num", clean_str(client_data["section_analytique"], 13), ) logger.info("[ETAPE 13] ORGANISATION") if client_data.get("mode_reglement_code") is not None: mr_no = safe_int(client_data["mode_reglement_code"]) if not try_set_attribute(client, "MR_No", mr_no): 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 logger.debug(f" ModeRegl (objet) = {mr_no} [OK]") except Exception as e: logger.debug(f" ModeRegl echec: {e}") if client_data.get("surveillance_active") is not None: surveillance = 1 if client_data["surveillance_active"] else 0 try: client.CT_Surveillance = surveillance logger.info(f" CT_Surveillance = {surveillance} [OK]") except Exception as e: logger.warning(f" CT_Surveillance [ECHEC: {e}]") if client_data.get("coface"): coface = clean_str(client_data["coface"], 25) try: client.CT_Coface = coface logger.info(f" CT_Coface = {coface} [OK]") except Exception as e: logger.warning(f" CT_Coface [ECHEC: {e}]") if client_data.get("forme_juridique"): try_set_attribute( client, "CT_SvFormeJuri", clean_str(client_data["forme_juridique"], 33), ) if client_data.get("effectif"): try_set_attribute( client, "CT_SvEffectif", clean_str(client_data["effectif"], 11) ) if client_data.get("sv_regularite"): try_set_attribute( client, "CT_SvRegul", clean_str(client_data["sv_regularite"], 3) ) if client_data.get("sv_cotation"): try_set_attribute( client, "CT_SvCotation", clean_str(client_data["sv_cotation"], 5), ) if client_data.get("sv_objet_maj"): try_set_attribute( client, "CT_SvObjetMaj", clean_str(client_data["sv_objet_maj"], 61), ) ca = client_data.get("ca_annuel") or client_data.get( "sv_chiffre_affaires" ) if ca: try_set_attribute(client, "CT_SvCA", safe_float(ca)) if client_data.get("sv_resultat"): try_set_attribute( client, "CT_SvResultat", safe_float(client_data["sv_resultat"]) ) logger.info("=" * 80) logger.info("[DIAGNOSTIC PRE-WRITE]") champs_diagnostic = [ "CT_Num", "CT_Intitule", "CT_Type", "CT_Qualite", "CT_Facture", "CT_Lettrage", "CT_Sommeil", ] for champ in champs_diagnostic: try: valeur = getattr(client, champ, "ATTRIBUT_INEXISTANT") logger.info(f" {champ}: {valeur}") except Exception as e: logger.info(f" {champ}: ERREUR ({str(e)[:50]})") try: compte_obj = client.CompteGPrinc if compte_obj: logger.info(f" CompteGPrinc.CG_Num: {compte_obj.CG_Num}") logger.info(f" CompteGPrinc.CG_Type: {compte_obj.CG_Type}") else: logger.error(" CompteGPrinc: NULL !!!") except Exception as e: logger.error(f" CompteGPrinc: ERREUR - {e}") logger.info("=" * 80) logger.info("[WRITE]") try: client.Write() client.Read() logger.info("[OK] 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})" ) except Exception: pass logger.error(f"[ERREUR] {error_detail}") raise RuntimeError(f"Echec Write(): {error_detail}") num_final = getattr(client, "CT_Num", numero) logger.info("=" * 80) logger.info(f"[SUCCES] CLIENT CREE: {num_final}") logger.info("=" * 80) return { "numero": num_final, "intitule": intitule, "type_tiers": type_tiers, "qualite": qualite, "compte_general": compte, "date_creation": datetime.now().isoformat(), } except ValueError as e: logger.error(f"[ERREUR VALIDATION] {e}") raise except Exception as e: logger.error(f"[ERREUR] {e}", exc_info=True) raise RuntimeError(f"Erreur technique: {e}") def modifier_client(self, code: str, client_data: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: logger.info("=" * 80) logger.info(f"[MODIFICATION CLIENT SAGE - {code}]") logger.info("=" * 80) 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 = [] logger.info("[ETAPE 1] CHARGEMENT CLIENT") factory_client = self.cial.CptaApplication.FactoryClient persist = factory_client.ReadNumero(code) if not persist: raise ValueError(f"Client {code} introuvable") client = win32com.client.CastTo(persist, "IBOClient3") client.Read() logger.info(f" Client chargé: {getattr(client, 'CT_Intitule', '')}") logger.info("[ETAPE 2] IDENTIFICATION") if "intitule" in client_data: intitule = clean_str(client_data["intitule"], 69) client.CT_Intitule = intitule 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() 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 Exception: 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") 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: if "contact" in client_data: contact_nom = clean_str(client_data["contact"], 35) 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}]") 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}]") adresse_obj = client.Adresse if "adresse" in client_data: 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: adresse_obj.CodePostal = clean_str( client_data["code_postal"], 9 ) champs_modifies.append("code_postal") if "ville" in client_data: 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: 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.error(f" Adresse erreur: {e}") 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 "telephone" in client_data: 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.error(f" Telecom erreur: {e}") 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: 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" CompteGPrinc erreur: {e}") 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}") 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) 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) 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}") 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) 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) 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") 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") 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") 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}") 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}]") 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}]") 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") if not champs_modifies: logger.warning("Aucun champ à modifier") return _extraire_client(client) 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() 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})" ) except Exception: pass logger.error(f"[ERREUR] {error_detail}") raise RuntimeError(f"Echec Write(): {error_detail}") logger.info("=" * 80) logger.info( f"[SUCCES] CLIENT MODIFIÉ: {code} ({len(champs_modifies)} champs)" ) logger.info("=" * 80) return _extraire_client(client) except ValueError as e: logger.error(f"[ERREUR VALIDATION] {e}") raise except Exception as e: logger.error(f"[ERREUR] {e}", exc_info=True) raise RuntimeError(f"Erreur technique: {e}") def creer_commande_enrichi(self, commande_data: dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") logger.info( f" Début création commande pour client {commande_data['client']['code']}" ) try: with self._com_context(), self._lock_com: transaction_active = False try: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug(" Transaction Sage démarrée") except Exception: pass try: process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_BON_COMMANDE ) doc = process.Document try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") except Exception: pass logger.info(" Document commande créé") doc.DO_Date = pywintypes.Time( normaliser_date(commande_data.get("date_commande")) ) if ( "date_livraison" in commande_data and commande_data["date_livraison"] ): doc.DO_DateLivr = pywintypes.Time( normaliser_date(commande_data["date_livraison"]) ) logger.info( f" Date livraison: {commande_data['date_livraison']}" ) factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( commande_data["client"]["code"] ) if not persist_client: raise ValueError( f"Client {commande_data['client']['code']} introuvable" ) client_obj = _cast_client(persist_client) if not client_obj: raise ValueError("Impossible de charger le client") doc.SetDefaultClient(client_obj) doc.Write() logger.info(f" Client {commande_data['client']['code']} associé") if commande_data.get("reference"): try: doc.DO_Ref = commande_data["reference"] logger.info(f" Référence: {commande_data['reference']}") except Exception: pass try: factory_lignes = doc.FactoryDocumentLigne except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle logger.info(f" Ajout de {len(commande_data['lignes'])} lignes...") for idx, ligne_data in enumerate(commande_data["lignes"], 1): logger.info( f"--- Ligne {idx}: {ligne_data['article_code']} ---" ) persist_article = factory_article.ReadReference( ligne_data["article_code"] ) if not persist_article: raise ValueError( f" Article {ligne_data['article_code']} introuvable dans Sage" ) article_obj = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_obj.Read() prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") logger.info(f" Prix Sage: {prix_sage}€") if prix_sage == 0: logger.warning( f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" ) ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) logger.info( " Article associé via SetDefaultArticleReference" ) except Exception as e: logger.warning( f"SetDefaultArticleReference échoué: {e}, tentative avec objet" ) try: ligne_obj.SetDefaultArticle(article_obj, quantite) logger.info(" Article associé via SetDefaultArticle") except Exception: logger.error(" Toutes les méthodes ont échoué") ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") ) ligne_obj.DL_Qte = quantite logger.warning("Configuration manuelle appliquée") prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) logger.info(f" Prix auto chargé: {prix_auto}€") prix_a_utiliser = ligne_data.get("prix_unitaire_ht") if prix_a_utiliser is not None and prix_a_utiliser > 0: ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) logger.info(f" Prix personnalisé: {prix_a_utiliser}€") elif prix_auto == 0 and prix_sage > 0: ligne_obj.DL_PrixUnitaire = float(prix_sage) logger.info(f" Prix Sage forcé: {prix_sage}€") elif prix_auto > 0: logger.info(f" Prix auto conservé: {prix_auto}€") prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) montant_ligne = quantite * prix_final logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") remise = ligne_data.get("remise_pourcentage", 0) if remise > 0: try: ligne_obj.DL_Remise01REM_Valeur = float(remise) ligne_obj.DL_Remise01REM_Type = 0 montant_apres_remise = montant_ligne * ( 1 - remise / 100 ) logger.info( f" Remise {remise}% → {montant_apres_remise}€" ) except Exception as e: logger.warning(f"Remise non appliquée: {e}") ligne_obj.Write() logger.info(f" Ligne {idx} écrite") try: ligne_obj.Read() prix_enregistre = float( getattr(ligne_obj, "DL_PrixUnitaire", 0.0) ) montant_enregistre = float( getattr(ligne_obj, "DL_MontantHT", 0.0) ) logger.info( f" Vérif: Prix={prix_enregistre}€, Montant HT={montant_enregistre}€" ) except Exception as e: logger.warning(f"Impossible de vérifier: {e}") doc.Write() process.Process() if transaction_active: self.cial.CptaApplication.CommitTrans() time.sleep(2) numero_commande = None try: doc_result = process.DocumentResult if doc_result: doc_result = win32com.client.CastTo( doc_result, "IBODocumentVente3" ) doc_result.Read() numero_commande = getattr(doc_result, "DO_Piece", "") except Exception: pass if not numero_commande: numero_commande = getattr(doc, "DO_Piece", "") factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece( settings.SAGE_TYPE_BON_COMMANDE, numero_commande ) 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)) reference_finale = 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 Exception: pass else: total_ht = 0.0 total_ttc = 0.0 reference_finale = commande_data.get("reference", "") date_livraison_final = commande_data.get("date_livraison") logger.info( f" COMMANDE CRÉÉE: {numero_commande} - {total_ttc}€ TTC " ) if date_livraison_final: logger.info(f" Date livraison: {date_livraison_final}") return { "numero_commande": numero_commande, "total_ht": total_ht, "total_ttc": total_ttc, "nb_lignes": len(commande_data["lignes"]), "client_code": commande_data["client"]["code"], "date_commande": str( normaliser_date(commande_data.get("date_commande")) ), "date_livraison": date_livraison_final, "reference": reference_finale, } except Exception: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() except Exception: pass raise except Exception as e: logger.error(f" Erreur création commande: {e}", exc_info=True) raise RuntimeError(f"Échec création commande: {str(e)}") def modifier_commande(self, numero: str, commande_data: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: logger.info(f" === MODIFICATION COMMANDE {numero} ===") logger.info(" Chargement document...") factory = self.cial.FactoryDocumentVente persist = None for type_test in [10, 3, settings.SAGE_TYPE_BON_COMMANDE]: try: persist_test = factory.ReadPiece(type_test, numero) if persist_test: persist = persist_test logger.info(f" Document trouvé (type={type_test})") break except Exception: continue if not persist: raise ValueError(f" Commande {numero} INTROUVABLE") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() statut_actuel = getattr(doc, "DO_Statut", 0) type_reel = getattr(doc, "DO_Type", -1) logger.info(f" Type={type_reel}, Statut={statut_actuel}") client_code_initial = "" try: client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_code_initial = getattr(client_obj, "CT_Num", "").strip() logger.info(f" Client initial: {client_code_initial}") else: logger.error(" Objet Client NULL à l'état initial !") except Exception as e: logger.error(f" Erreur lecture client initial: {e}") if not client_code_initial: raise ValueError(" Client introuvable dans le document") nb_lignes_initial = 0 try: factory_lignes = getattr( doc, "FactoryDocumentLigne", None ) or getattr(doc, "FactoryDocumentVenteLigne", None) index = 1 while index <= 100: try: ligne_p = factory_lignes.List(index) if ligne_p is None: break nb_lignes_initial += 1 index += 1 except Exception: break logger.info(f" Lignes initiales: {nb_lignes_initial}") except Exception as e: logger.warning(f" Erreur comptage lignes: {e}") champs_modifies = [] modif_date = "date_commande" in commande_data modif_date_livraison = "date_livraison" in commande_data modif_statut = "statut" in commande_data modif_ref = "reference" in commande_data modif_lignes = ( "lignes" in commande_data and commande_data["lignes"] is not None ) logger.info("Modifications demandées:") logger.info(f" Date commande: {modif_date}") logger.info(f" Date livraison: {modif_date_livraison}") logger.info(f" Statut: {modif_statut}") logger.info(f" Référence: {modif_ref}") logger.info(f" Lignes: {modif_lignes}") commande_data_temp = commande_data.copy() reference_a_modifier = None statut_a_modifier = None if modif_lignes: if modif_ref: reference_a_modifier = commande_data_temp.pop("reference") logger.info( " Modification de la référence reportée après les lignes" ) modif_ref = False if modif_statut: statut_a_modifier = commande_data_temp.pop("statut") logger.info(" Modification du statut reportée après les lignes") modif_statut = False logger.info(" Test Write() basique (sans modification)...") try: doc.Write() logger.info(" Write() basique OK") doc.Read() client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") if client_apres == client_code_initial: logger.info(f" Client préservé: {client_apres}") else: logger.error( f" Client a changé: {client_code_initial} → {client_apres}" ) else: logger.error(" Client devenu NULL après Write() basique") except Exception as e: logger.error(f" Write() basique ÉCHOUE: {e}") logger.error(" ABANDON: Le document est VERROUILLÉ") raise ValueError( f"Document verrouillé, impossible de modifier: {e}" ) if not modif_lignes and ( modif_date or modif_date_livraison or modif_statut or modif_ref ): logger.info(" Modifications simples (sans lignes)...") if modif_date: logger.info(" Modification date commande...") doc.DO_Date = pywintypes.Time( normaliser_date(commande_data_temp.get("date_commande")) ) champs_modifies.append("date") if modif_date_livraison: logger.info(" Modification date livraison...") doc.DO_DateLivr = pywintypes.Time( normaliser_date(commande_data_temp["date_livraison"]) ) logger.info( f" Date livraison: {commande_data_temp['date_livraison']}" ) champs_modifies.append("date_livraison") if modif_statut: logger.info(" Modification statut...") nouveau_statut = commande_data_temp["statut"] doc.DO_Statut = nouveau_statut champs_modifies.append("statut") logger.info(f" Statut défini: {nouveau_statut}") if modif_ref: logger.info(" Modification référence...") try: doc.DO_Ref = commande_data_temp["reference"] champs_modifies.append("reference") logger.info( f" Référence définie: {commande_data_temp['reference']}" ) except Exception as e: logger.warning(f" Référence non définie: {e}") logger.info(" Write() sans réassociation client...") try: doc.Write() logger.info(" Write() réussi") doc.Read() client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") if client_apres == client_code_initial: logger.info(f" Client préservé: {client_apres}") else: logger.error( f" Client perdu: {client_code_initial} → {client_apres}" ) except Exception as e: error_msg = str(e) try: sage_error = self.cial.CptaApplication.LastError if sage_error: error_msg = f"{sage_error.Description} (Code: {sage_error.Number})" except Exception: pass logger.error(f" Write() échoue: {error_msg}") raise ValueError(f"Sage refuse: {error_msg}") elif modif_lignes: logger.info(" REMPLACEMENT COMPLET DES LIGNES...") if modif_date: doc.DO_Date = pywintypes.Time( normaliser_date(commande_data_temp.get("date_commande")) ) champs_modifies.append("date") logger.info(" Date commande modifiée") if modif_date_livraison: doc.DO_DateLivr = pywintypes.Time( normaliser_date(commande_data_temp["date_livraison"]) ) logger.info(" Date livraison modifiée") champs_modifies.append("date_livraison") nouvelles_lignes = commande_data["lignes"] nb_nouvelles = len(nouvelles_lignes) logger.info( f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" ) try: factory_lignes = doc.FactoryDocumentLigne except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle if nb_lignes_initial > 0: logger.info( f" Suppression de {nb_lignes_initial} lignes existantes..." ) for idx in range(nb_lignes_initial, 0, -1): try: ligne_p = factory_lignes.List(idx) if ligne_p: ligne = win32com.client.CastTo( ligne_p, "IBODocumentLigne3" ) ligne.Read() ligne.Remove() logger.debug(f" Ligne {idx} supprimée") except Exception as e: logger.warning( f" Impossible de supprimer ligne {idx}: {e}" ) logger.info(" Toutes les lignes existantes supprimées") logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...") for idx, ligne_data in enumerate(nouvelles_lignes, 1): logger.info( f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---" ) persist_article = factory_article.ReadReference( ligne_data["article_code"] ) if not persist_article: raise ValueError( f"Article {ligne_data['article_code']} introuvable" ) article_obj = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_obj.Read() ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) except Exception: try: ligne_obj.SetDefaultArticle(article_obj, quantite) except Exception: ligne_obj.DL_Design = ligne_data.get("designation", "") ligne_obj.DL_Qte = quantite if ligne_data.get("prix_unitaire_ht"): ligne_obj.DL_PrixUnitaire = float( ligne_data["prix_unitaire_ht"] ) if ligne_data.get("remise_pourcentage", 0) > 0: try: ligne_obj.DL_Remise01REM_Valeur = float( ligne_data["remise_pourcentage"] ) ligne_obj.DL_Remise01REM_Type = 0 except Exception: pass ligne_obj.Write() logger.info(f" Ligne {idx} ajoutée") logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées") logger.info(" Write() document après remplacement lignes...") doc.Write() logger.info(" Document écrit") import time time.sleep(0.5) doc.Read() client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") logger.info(f" Client après remplacement: {client_apres}") else: logger.error(" Client NULL après remplacement") champs_modifies.append("lignes") if reference_a_modifier is not None: try: ancienne_reference = getattr(doc, "DO_Ref", "") nouvelle_reference = ( str(reference_a_modifier) if reference_a_modifier else "" ) doc.DO_Ref = nouvelle_reference doc.Write() import time time.sleep(0.5) doc.Read() champs_modifies.append("reference") logger.info( f" Référence modifiée: '{ancienne_reference}' → '{nouvelle_reference}'" ) except Exception as e: logger.warning(f"Impossible de modifier la référence: {e}") if statut_a_modifier is not None: try: statut_actuel = getattr(doc, "DO_Statut", 0) nouveau_statut = int(statut_a_modifier) if nouveau_statut != statut_actuel: doc.DO_Statut = nouveau_statut doc.Write() import time time.sleep(0.5) doc.Read() champs_modifies.append("statut") logger.info( f" Statut modifié: {statut_actuel} → {nouveau_statut}" ) except Exception as e: logger.warning(f"Impossible de modifier le statut: {e}") logger.info(" Relecture finale...") import time time.sleep(0.5) doc.Read() client_obj_final = getattr(doc, "Client", None) if client_obj_final: client_obj_final.Read() client_final = getattr(client_obj_final, "CT_Num", "") else: client_final = "" total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) reference_finale = getattr(doc, "DO_Ref", "") date_livraison_final = None try: date_livr = getattr(doc, "DO_DateLivr", None) if date_livr: date_livraison_final = date_livr.strftime("%Y-%m-%d") except Exception: pass logger.info(f" SUCCÈS: {numero} modifiée ") logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC") logger.info(f" Client final: {client_final}") logger.info(f" Référence: {reference_finale}") if date_livraison_final: logger.info(f" Date livraison: {date_livraison_final}") logger.info(f" Champs modifiés: {champs_modifies}") return { "numero": numero, "total_ht": total_ht, "total_ttc": total_ttc, "reference": reference_finale, "date_livraison": date_livraison_final, "champs_modifies": champs_modifies, "statut": getattr(doc, "DO_Statut", 0), "client_code": client_final, } except ValueError as e: logger.error(f" ERREUR MÉTIER: {e}") raise except Exception as e: logger.error(f" ERREUR TECHNIQUE: {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} (Code: {err.Number})" ) except Exception: pass raise RuntimeError(f"Erreur Sage: {error_message}") def creer_livraison_enrichi(self, livraison_data: dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") logger.info( f" Début création livraison pour client {livraison_data['client']['code']}" ) try: with self._com_context(), self._lock_com: transaction_active = False try: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug(" Transaction Sage démarrée") except Exception: pass try: process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_BON_LIVRAISON ) doc = process.Document try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") except Exception: pass logger.info(" Document livraison créé") doc.DO_Date = pywintypes.Time( normaliser_date(livraison_data.get("date_livraison")) ) if ( "date_livraison_prevue" in livraison_data and livraison_data["date_livraison_prevue"] ): doc.DO_DateLivr = pywintypes.Time( normaliser_date(livraison_data["date_livraison_prevue"]) ) logger.info( f" Date livraison prévue: {livraison_data['date_livraison_prevue']}" ) factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( livraison_data["client"]["code"] ) if not persist_client: raise ValueError( f"Client {livraison_data['client']['code']} introuvable" ) client_obj = _cast_client(persist_client) if not client_obj: raise ValueError("Impossible de charger le client") doc.SetDefaultClient(client_obj) doc.Write() logger.info(f" Client {livraison_data['client']['code']} associé") if livraison_data.get("reference"): try: doc.DO_Ref = livraison_data["reference"] logger.info(f" Référence: {livraison_data['reference']}") except Exception: pass try: factory_lignes = doc.FactoryDocumentLigne except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle logger.info(f" Ajout de {len(livraison_data['lignes'])} lignes...") for idx, ligne_data in enumerate(livraison_data["lignes"], 1): logger.info( f"--- Ligne {idx}: {ligne_data['article_code']} ---" ) persist_article = factory_article.ReadReference( ligne_data["article_code"] ) if not persist_article: raise ValueError( f" Article {ligne_data['article_code']} introuvable dans Sage" ) article_obj = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_obj.Read() prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") logger.info(f" Prix Sage: {prix_sage}€") if prix_sage == 0: logger.warning( f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" ) ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) logger.info( " Article associé via SetDefaultArticleReference" ) except Exception as e: logger.warning( f"SetDefaultArticleReference échoué: {e}, tentative avec objet" ) try: ligne_obj.SetDefaultArticle(article_obj, quantite) logger.info(" Article associé via SetDefaultArticle") except Exception: logger.error(" Toutes les méthodes ont échoué") ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") ) ligne_obj.DL_Qte = quantite logger.warning("Configuration manuelle appliquée") prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) logger.info(f" Prix auto chargé: {prix_auto}€") prix_a_utiliser = ligne_data.get("prix_unitaire_ht") if prix_a_utiliser is not None and prix_a_utiliser > 0: ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) logger.info(f" Prix personnalisé: {prix_a_utiliser}€") elif prix_auto == 0 and prix_sage > 0: ligne_obj.DL_PrixUnitaire = float(prix_sage) logger.info(f" Prix Sage forcé: {prix_sage}€") elif prix_auto > 0: logger.info(f" Prix auto conservé: {prix_auto}€") prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) montant_ligne = quantite * prix_final logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") remise = ligne_data.get("remise_pourcentage", 0) if remise > 0: try: ligne_obj.DL_Remise01REM_Valeur = float(remise) ligne_obj.DL_Remise01REM_Type = 0 montant_apres_remise = montant_ligne * ( 1 - remise / 100 ) logger.info( f" Remise {remise}% → {montant_apres_remise}€" ) except Exception as e: logger.warning(f"Remise non appliquée: {e}") ligne_obj.Write() logger.info(f" Ligne {idx} écrite") doc.Write() process.Process() if transaction_active: self.cial.CptaApplication.CommitTrans() time.sleep(2) numero_livraison = None try: doc_result = process.DocumentResult if doc_result: doc_result = win32com.client.CastTo( doc_result, "IBODocumentVente3" ) doc_result.Read() numero_livraison = getattr(doc_result, "DO_Piece", "") except Exception: pass if not numero_livraison: numero_livraison = getattr(doc, "DO_Piece", "") factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece( settings.SAGE_TYPE_BON_LIVRAISON, numero_livraison ) 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)) reference_finale = getattr(doc_final, "DO_Ref", "") date_livraison_prevue_final = None try: date_livr = getattr(doc_final, "DO_DateLivr", None) if date_livr: date_livraison_prevue_final = date_livr.strftime( "%Y-%m-%d" ) except Exception: pass else: total_ht = 0.0 total_ttc = 0.0 reference_finale = livraison_data.get("reference", "") date_livraison_prevue_final = livraison_data.get( "date_livraison_prevue" ) logger.info( f" LIVRAISON CRÉÉE: {numero_livraison} - {total_ttc}€ TTC " ) if date_livraison_prevue_final: logger.info( f" Date livraison prévue: {date_livraison_prevue_final}" ) return { "numero_livraison": numero_livraison, "total_ht": total_ht, "total_ttc": total_ttc, "nb_lignes": len(livraison_data["lignes"]), "client_code": livraison_data["client"]["code"], "date_livraison": str( normaliser_date(livraison_data.get("date_livraison")) ), "date_livraison_prevue": date_livraison_prevue_final, "reference": reference_finale, } except Exception: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() except Exception: pass raise except Exception as e: logger.error(f" Erreur création livraison: {e}", exc_info=True) raise RuntimeError(f"Échec création livraison: {str(e)}") def modifier_livraison(self, numero: str, livraison_data: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: logger.info(f" === MODIFICATION LIVRAISON {numero} ===") logger.info(" Chargement document...") factory = self.cial.FactoryDocumentVente persist = None for type_test in [30, settings.SAGE_TYPE_BON_LIVRAISON]: try: persist_test = factory.ReadPiece(type_test, numero) if persist_test: persist = persist_test logger.info(f" Document trouvé (type={type_test})") break except Exception: continue if not persist: raise ValueError(f" Livraison {numero} INTROUVABLE") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() statut_actuel = getattr(doc, "DO_Statut", 0) logger.info(f" Statut={statut_actuel}") if statut_actuel == 5: raise ValueError(f"La livraison {numero} a déjà été transformée") if statut_actuel == 6: raise ValueError(f"La livraison {numero} est annulée") nb_lignes_initial = 0 try: factory_lignes = getattr( doc, "FactoryDocumentLigne", None ) or getattr(doc, "FactoryDocumentVenteLigne", None) index = 1 while index <= 100: try: ligne_p = factory_lignes.List(index) if ligne_p is None: break nb_lignes_initial += 1 index += 1 except Exception: break logger.info(f" Lignes initiales: {nb_lignes_initial}") except Exception as e: logger.warning(f" Erreur comptage lignes: {e}") champs_modifies = [] modif_date = "date_livraison" in livraison_data modif_date_livraison_prevue = "date_livraison_prevue" in livraison_data modif_statut = "statut" in livraison_data modif_ref = "reference" in livraison_data modif_lignes = ( "lignes" in livraison_data and livraison_data["lignes"] is not None ) logger.info("Modifications demandées:") logger.info(f" Date livraison: {modif_date}") logger.info(f" Date livraison prévue: {modif_date_livraison_prevue}") logger.info(f" Statut: {modif_statut}") logger.info(f" Référence: {modif_ref}") logger.info(f" Lignes: {modif_lignes}") livraison_data_temp = livraison_data.copy() reference_a_modifier = None statut_a_modifier = None if modif_lignes: if modif_ref: reference_a_modifier = livraison_data_temp.pop("reference") logger.info( " Modification de la référence reportée après les lignes" ) modif_ref = False if modif_statut: statut_a_modifier = livraison_data_temp.pop("statut") logger.info(" Modification du statut reportée après les lignes") modif_statut = False if not modif_lignes and ( modif_date or modif_date_livraison_prevue or modif_statut or modif_ref ): logger.info(" Modifications simples (sans lignes)...") if modif_date: logger.info(" Modification date livraison...") doc.DO_Date = pywintypes.Time( normaliser_date(livraison_data_temp.get("date_livraison")) ) champs_modifies.append("date") if modif_date_livraison_prevue: logger.info(" Modification date livraison prévue...") doc.DO_DateLivr = pywintypes.Time( normaliser_date( livraison_data_temp["date_livraison_prevue"] ) ) logger.info( f" Date livraison prévue: {livraison_data_temp['date_livraison_prevue']}" ) champs_modifies.append("date_livraison_prevue") if modif_statut: logger.info(" Modification statut...") nouveau_statut = livraison_data_temp["statut"] doc.DO_Statut = nouveau_statut champs_modifies.append("statut") logger.info(f" Statut défini: {nouveau_statut}") if modif_ref: logger.info(" Modification référence...") try: doc.DO_Ref = livraison_data_temp["reference"] champs_modifies.append("reference") logger.info( f" Référence définie: {livraison_data_temp['reference']}" ) except Exception as e: logger.warning(f" Référence non définie: {e}") logger.info(" Write()...") doc.Write() logger.info(" Write() réussi") elif modif_lignes: logger.info(" REMPLACEMENT COMPLET DES LIGNES...") if modif_date: doc.DO_Date = pywintypes.Time( normaliser_date(livraison_data_temp.get("date_livraison")) ) champs_modifies.append("date") logger.info(" Date livraison modifiée") if modif_date_livraison_prevue: doc.DO_DateLivr = pywintypes.Time( normaliser_date( livraison_data_temp["date_livraison_prevue"] ) ) logger.info(" Date livraison prévue modifiée") champs_modifies.append("date_livraison_prevue") nouvelles_lignes = livraison_data["lignes"] nb_nouvelles = len(nouvelles_lignes) logger.info( f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles" ) try: factory_lignes = doc.FactoryDocumentLigne except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle if nb_lignes_initial > 0: logger.info(f" Suppression de {nb_lignes_initial} lignes...") for idx in range(nb_lignes_initial, 0, -1): try: ligne_p = factory_lignes.List(idx) if ligne_p: ligne = win32com.client.CastTo( ligne_p, "IBODocumentLigne3" ) ligne.Read() ligne.Remove() logger.debug(f" Ligne {idx} supprimée") except Exception as e: logger.warning( f" Erreur suppression ligne {idx}: {e}" ) logger.info(" Toutes les lignes supprimées") logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...") for idx, ligne_data in enumerate(nouvelles_lignes, 1): logger.info( f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---" ) persist_article = factory_article.ReadReference( ligne_data["article_code"] ) if not persist_article: raise ValueError( f"Article {ligne_data['article_code']} introuvable" ) article_obj = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_obj.Read() ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) except Exception: try: ligne_obj.SetDefaultArticle(article_obj, quantite) except Exception: ligne_obj.DL_Design = ligne_data.get("designation", "") ligne_obj.DL_Qte = quantite if ligne_data.get("prix_unitaire_ht"): ligne_obj.DL_PrixUnitaire = float( ligne_data["prix_unitaire_ht"] ) if ligne_data.get("remise_pourcentage", 0) > 0: try: ligne_obj.DL_Remise01REM_Valeur = float( ligne_data["remise_pourcentage"] ) ligne_obj.DL_Remise01REM_Type = 0 except Exception: pass ligne_obj.Write() logger.info(f" Ligne {idx} ajoutée") logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées") logger.info(" Write() document après remplacement lignes...") doc.Write() logger.info(" Document écrit") import time time.sleep(0.5) doc.Read() champs_modifies.append("lignes") if reference_a_modifier is not None: try: ancienne_reference = getattr(doc, "DO_Ref", "") nouvelle_reference = ( str(reference_a_modifier) if reference_a_modifier else "" ) logger.info( f" Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'" ) doc.DO_Ref = nouvelle_reference doc.Write() import time time.sleep(0.5) doc.Read() champs_modifies.append("reference") logger.info(" Référence modifiée avec succès") except Exception as e: logger.warning(f"Impossible de modifier la référence: {e}") if statut_a_modifier is not None: try: statut_actuel = getattr(doc, "DO_Statut", 0) nouveau_statut = int(statut_a_modifier) if nouveau_statut != statut_actuel: logger.info( f" Modification statut: {statut_actuel} → {nouveau_statut}" ) doc.DO_Statut = nouveau_statut doc.Write() import time time.sleep(0.5) doc.Read() champs_modifies.append("statut") logger.info(" Statut modifié avec succès") except Exception as e: logger.warning(f"Impossible de modifier le statut: {e}") logger.info(" Relecture finale...") import time time.sleep(0.5) doc.Read() total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) reference_finale = getattr(doc, "DO_Ref", "") statut_final = getattr(doc, "DO_Statut", 0) date_livraison_prevue_final = None try: date_livr = getattr(doc, "DO_DateLivr", None) if date_livr: date_livraison_prevue_final = date_livr.strftime("%Y-%m-%d") except Exception: pass logger.info(f" SUCCÈS: {numero} modifiée ") logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC") logger.info(f" Référence: {reference_finale}") logger.info(f" Statut: {statut_final}") if date_livraison_prevue_final: logger.info( f" Date livraison prévue: {date_livraison_prevue_final}" ) logger.info(f" Champs modifiés: {champs_modifies}") return { "numero": numero, "total_ht": total_ht, "total_ttc": total_ttc, "reference": reference_finale, "date_livraison_prevue": date_livraison_prevue_final, "champs_modifies": champs_modifies, "statut": statut_final, } except ValueError as e: logger.error(f" ERREUR MÉTIER: {e}") raise except Exception as e: logger.error(f" ERREUR TECHNIQUE: {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} (Code: {err.Number})" ) except Exception: pass raise RuntimeError(f"Erreur Sage: {error_message}") def creer_avoir_enrichi(self, avoir_data: dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") logger.info(f" Début création avoir pour client {avoir_data['client']['code']}") try: with self._com_context(), self._lock_com: transaction_active = False try: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug(" Transaction Sage démarrée") except Exception: pass try: process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_BON_AVOIR ) doc = process.Document try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") except Exception: pass logger.info(" Document avoir créé") doc.DO_Date = pywintypes.Time( normaliser_date(avoir_data.get("date_avoir")) ) if "date_livraison" in avoir_data and avoir_data["date_livraison"]: doc.DO_DateLivr = pywintypes.Time( normaliser_date(avoir_data["date_livraison"]) ) logger.info(f" Date livraison: {avoir_data['date_livraison']}") factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( avoir_data["client"]["code"] ) if not persist_client: raise ValueError( f"Client {avoir_data['client']['code']} introuvable" ) client_obj = _cast_client(persist_client) if not client_obj: raise ValueError("Impossible de charger le client") doc.SetDefaultClient(client_obj) doc.Write() logger.info(f" Client {avoir_data['client']['code']} associé") if avoir_data.get("reference"): try: doc.DO_Ref = avoir_data["reference"] logger.info(f" Référence: {avoir_data['reference']}") except Exception: pass try: factory_lignes = doc.FactoryDocumentLigne except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle logger.info(f" Ajout de {len(avoir_data['lignes'])} lignes...") for idx, ligne_data in enumerate(avoir_data["lignes"], 1): logger.info( f"--- Ligne {idx}: {ligne_data['article_code']} ---" ) persist_article = factory_article.ReadReference( ligne_data["article_code"] ) if not persist_article: raise ValueError( f" Article {ligne_data['article_code']} introuvable dans Sage" ) article_obj = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_obj.Read() prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") logger.info(f" Prix Sage: {prix_sage}€") if prix_sage == 0: logger.warning( f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" ) ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) logger.info( " Article associé via SetDefaultArticleReference" ) except Exception as e: logger.warning( f"SetDefaultArticleReference échoué: {e}, tentative avec objet" ) try: ligne_obj.SetDefaultArticle(article_obj, quantite) logger.info(" Article associé via SetDefaultArticle") except Exception: logger.error(" Toutes les méthodes ont échoué") ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") ) ligne_obj.DL_Qte = quantite logger.warning("Configuration manuelle appliquée") prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) logger.info(f" Prix auto chargé: {prix_auto}€") prix_a_utiliser = ligne_data.get("prix_unitaire_ht") if prix_a_utiliser is not None and prix_a_utiliser > 0: ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) logger.info(f" Prix personnalisé: {prix_a_utiliser}€") elif prix_auto == 0 and prix_sage > 0: ligne_obj.DL_PrixUnitaire = float(prix_sage) logger.info(f" Prix Sage forcé: {prix_sage}€") elif prix_auto > 0: logger.info(f" Prix auto conservé: {prix_auto}€") prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) montant_ligne = quantite * prix_final logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") remise = ligne_data.get("remise_pourcentage", 0) if remise > 0: try: ligne_obj.DL_Remise01REM_Valeur = float(remise) ligne_obj.DL_Remise01REM_Type = 0 montant_apres_remise = montant_ligne * ( 1 - remise / 100 ) logger.info( f" Remise {remise}% → {montant_apres_remise}€" ) except Exception as e: logger.warning(f"Remise non appliquée: {e}") ligne_obj.Write() logger.info(f" Ligne {idx} écrite") doc.Write() process.Process() if transaction_active: self.cial.CptaApplication.CommitTrans() time.sleep(2) numero_avoir = None try: doc_result = process.DocumentResult if doc_result: doc_result = win32com.client.CastTo( doc_result, "IBODocumentVente3" ) doc_result.Read() numero_avoir = getattr(doc_result, "DO_Piece", "") except Exception: pass if not numero_avoir: numero_avoir = getattr(doc, "DO_Piece", "") factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece( settings.SAGE_TYPE_BON_AVOIR, numero_avoir ) 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)) reference_finale = 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 Exception: pass else: total_ht = 0.0 total_ttc = 0.0 reference_finale = avoir_data.get("reference", "") date_livraison_final = avoir_data.get("date_livraison") logger.info(f" AVOIR CRÉÉ: {numero_avoir} - {total_ttc}€ TTC ") if date_livraison_final: logger.info(f" Date livraison: {date_livraison_final}") return { "numero_avoir": numero_avoir, "total_ht": total_ht, "total_ttc": total_ttc, "nb_lignes": len(avoir_data["lignes"]), "client_code": avoir_data["client"]["code"], "date_avoir": str( normaliser_date(avoir_data.get("date_avoir")) ), "date_livraison": date_livraison_final, "reference": reference_finale, } except Exception: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() except Exception: pass raise except Exception as e: logger.error(f" Erreur création avoir: {e}", exc_info=True) raise RuntimeError(f"Échec création avoir: {str(e)}") def modifier_avoir(self, numero: str, avoir_data: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: logger.info(f" === MODIFICATION AVOIR {numero} ===") logger.info(" Chargement document...") factory = self.cial.FactoryDocumentVente persist = None for type_test in [50, settings.SAGE_TYPE_BON_AVOIR]: try: persist_test = factory.ReadPiece(type_test, numero) if persist_test: persist = persist_test logger.info(f" Document trouvé (type={type_test})") break except Exception: continue if not persist: raise ValueError(f" Avoir {numero} INTROUVABLE") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() statut_actuel = getattr(doc, "DO_Statut", 0) type_reel = getattr(doc, "DO_Type", -1) logger.info(f" Type={type_reel}, Statut={statut_actuel}") if statut_actuel == 5: raise ValueError(f"L'avoir {numero} a déjà été transformé") if statut_actuel == 6: raise ValueError(f"L'avoir {numero} est annulé") client_code_initial = "" try: client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_code_initial = getattr(client_obj, "CT_Num", "").strip() logger.info(f" Client initial: {client_code_initial}") else: logger.error(" Objet Client NULL à l'état initial !") except Exception as e: logger.error(f" Erreur lecture client initial: {e}") if not client_code_initial: raise ValueError(" Client introuvable dans le document") nb_lignes_initial = 0 try: factory_lignes = getattr( doc, "FactoryDocumentLigne", None ) or getattr(doc, "FactoryDocumentVenteLigne", None) index = 1 while index <= 100: try: ligne_p = factory_lignes.List(index) if ligne_p is None: break nb_lignes_initial += 1 index += 1 except Exception: break logger.info(f" Lignes initiales: {nb_lignes_initial}") except Exception as e: logger.warning(f" Erreur comptage lignes: {e}") champs_modifies = [] modif_date = "date_avoir" in avoir_data modif_date_livraison = "date_livraison" in avoir_data modif_statut = "statut" in avoir_data modif_ref = "reference" in avoir_data modif_lignes = ( "lignes" in avoir_data and avoir_data["lignes"] is not None ) logger.info("Modifications demandées:") logger.info(f" Date avoir: {modif_date}") logger.info(f" Date livraison: {modif_date_livraison}") logger.info(f" Statut: {modif_statut}") logger.info(f" Référence: {modif_ref}") logger.info(f" Lignes: {modif_lignes}") avoir_data_temp = avoir_data.copy() reference_a_modifier = None statut_a_modifier = None if modif_lignes: if modif_ref: reference_a_modifier = avoir_data_temp.pop("reference") logger.info( " Modification de la référence reportée après les lignes" ) modif_ref = False if modif_statut: statut_a_modifier = avoir_data_temp.pop("statut") logger.info(" Modification du statut reportée après les lignes") modif_statut = False logger.info(" Test Write() basique (sans modification)...") try: doc.Write() logger.info(" Write() basique OK") doc.Read() client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") if client_apres == client_code_initial: logger.info(f" Client préservé: {client_apres}") else: logger.error( f" Client a changé: {client_code_initial} → {client_apres}" ) else: logger.error(" Client devenu NULL après Write() basique") except Exception as e: logger.error(f" Write() basique ÉCHOUE: {e}") logger.error(" ABANDON: Le document est VERROUILLÉ") raise ValueError( f"Document verrouillé, impossible de modifier: {e}" ) if not modif_lignes and ( modif_date or modif_date_livraison or modif_statut or modif_ref ): logger.info(" Modifications simples (sans lignes)...") if modif_date: logger.info(" Modification date avoir...") doc.DO_Date = pywintypes.Time( normaliser_date(avoir_data_temp.get("date_avoir")) ) champs_modifies.append("date") if modif_date_livraison: logger.info(" Modification date livraison...") doc.DO_DateLivr = pywintypes.Time( normaliser_date(avoir_data_temp["date_livraison"]) ) logger.info( f" Date livraison: {avoir_data_temp['date_livraison']}" ) champs_modifies.append("date_livraison") if modif_statut: logger.info(" Modification statut...") nouveau_statut = avoir_data_temp["statut"] doc.DO_Statut = nouveau_statut champs_modifies.append("statut") logger.info(f" Statut défini: {nouveau_statut}") if modif_ref: logger.info(" Modification référence...") try: doc.DO_Ref = avoir_data_temp["reference"] champs_modifies.append("reference") logger.info( f" Référence définie: {avoir_data_temp['reference']}" ) except Exception as e: logger.warning(f" Référence non définie: {e}") logger.info(" Write() sans réassociation client...") try: doc.Write() logger.info(" Write() réussi") doc.Read() client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") if client_apres == client_code_initial: logger.info(f" Client préservé: {client_apres}") else: logger.error( f" Client perdu: {client_code_initial} → {client_apres}" ) except Exception as e: error_msg = str(e) try: sage_error = self.cial.CptaApplication.LastError if sage_error: error_msg = f"{sage_error.Description} (Code: {sage_error.Number})" except Exception: pass logger.error(f" Write() échoue: {error_msg}") raise ValueError(f"Sage refuse: {error_msg}") elif modif_lignes: logger.info(" REMPLACEMENT COMPLET DES LIGNES...") if modif_date: doc.DO_Date = pywintypes.Time( normaliser_date(avoir_data_temp.get("date_avoir")) ) champs_modifies.append("date") logger.info(" Date avoir modifiée") if modif_date_livraison: doc.DO_DateLivr = pywintypes.Time( normaliser_date(avoir_data_temp["date_livraison"]) ) logger.info(" Date livraison modifiée") champs_modifies.append("date_livraison") nouvelles_lignes = avoir_data["lignes"] nb_nouvelles = len(nouvelles_lignes) logger.info( f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" ) try: factory_lignes = doc.FactoryDocumentLigne except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle if nb_lignes_initial > 0: logger.info( f" Suppression de {nb_lignes_initial} lignes existantes..." ) for idx in range(nb_lignes_initial, 0, -1): try: ligne_p = factory_lignes.List(idx) if ligne_p: ligne = win32com.client.CastTo( ligne_p, "IBODocumentLigne3" ) ligne.Read() ligne.Remove() logger.debug(f" Ligne {idx} supprimée") except Exception as e: logger.warning( f" Impossible de supprimer ligne {idx}: {e}" ) logger.info(" Toutes les lignes existantes supprimées") logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...") for idx, ligne_data in enumerate(nouvelles_lignes, 1): logger.info( f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---" ) persist_article = factory_article.ReadReference( ligne_data["article_code"] ) if not persist_article: raise ValueError( f"Article {ligne_data['article_code']} introuvable" ) article_obj = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_obj.Read() ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) except Exception: try: ligne_obj.SetDefaultArticle(article_obj, quantite) except Exception: ligne_obj.DL_Design = ligne_data.get("designation", "") ligne_obj.DL_Qte = quantite if ligne_data.get("prix_unitaire_ht"): ligne_obj.DL_PrixUnitaire = float( ligne_data["prix_unitaire_ht"] ) if ligne_data.get("remise_pourcentage", 0) > 0: try: ligne_obj.DL_Remise01REM_Valeur = float( ligne_data["remise_pourcentage"] ) ligne_obj.DL_Remise01REM_Type = 0 except Exception: pass ligne_obj.Write() logger.info(f" Ligne {idx} ajoutée") logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées") logger.info(" Write() document après remplacement lignes...") doc.Write() logger.info(" Document écrit") import time time.sleep(0.5) doc.Read() client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") logger.info(f" Client après remplacement: {client_apres}") else: logger.error(" Client NULL après remplacement") champs_modifies.append("lignes") if reference_a_modifier is not None: try: ancienne_reference = getattr(doc, "DO_Ref", "") nouvelle_reference = ( str(reference_a_modifier) if reference_a_modifier else "" ) logger.info( f" Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'" ) doc.DO_Ref = nouvelle_reference doc.Write() import time time.sleep(0.5) doc.Read() champs_modifies.append("reference") logger.info(" Référence modifiée avec succès") except Exception as e: logger.warning(f"Impossible de modifier la référence: {e}") if statut_a_modifier is not None: try: statut_actuel = getattr(doc, "DO_Statut", 0) nouveau_statut = int(statut_a_modifier) if nouveau_statut != statut_actuel: logger.info( f" Modification statut: {statut_actuel} → {nouveau_statut}" ) doc.DO_Statut = nouveau_statut doc.Write() import time time.sleep(0.5) doc.Read() champs_modifies.append("statut") logger.info(" Statut modifié avec succès") except Exception as e: logger.warning(f"Impossible de modifier le statut: {e}") logger.info(" Relecture finale...") import time time.sleep(0.5) doc.Read() client_obj_final = getattr(doc, "Client", None) if client_obj_final: client_obj_final.Read() client_final = getattr(client_obj_final, "CT_Num", "") else: client_final = "" total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) reference_finale = getattr(doc, "DO_Ref", "") statut_final = getattr(doc, "DO_Statut", 0) date_livraison_final = None try: date_livr = getattr(doc, "DO_DateLivr", None) if date_livr: date_livraison_final = date_livr.strftime("%Y-%m-%d") except Exception: pass logger.info(f" SUCCÈS: {numero} modifié ") logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC") logger.info(f" Client final: {client_final}") logger.info(f" Référence: {reference_finale}") logger.info(f" Statut: {statut_final}") if date_livraison_final: logger.info(f" Date livraison: {date_livraison_final}") logger.info(f" Champs modifiés: {champs_modifies}") return { "numero": numero, "total_ht": total_ht, "total_ttc": total_ttc, "reference": reference_finale, "date_livraison": date_livraison_final, "champs_modifies": champs_modifies, "statut": statut_final, "client_code": client_final, } except ValueError as e: logger.error(f" ERREUR MÉTIER: {e}") raise except Exception as e: logger.error(f" ERREUR TECHNIQUE: {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} (Code: {err.Number})" ) except Exception: pass raise RuntimeError(f"Erreur Sage: {error_message}") def creer_facture_enrichi(self, facture_data: dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") logger.info( f" Début création facture pour client {facture_data['client']['code']}" ) try: with self._com_context(), self._lock_com: transaction_active = False try: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug(" Transaction Sage démarrée") except Exception: pass try: process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_FACTURE ) doc = process.Document try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") except Exception: pass logger.info(" Document facture créé") doc.DO_Date = pywintypes.Time( normaliser_date(facture_data.get("date_facture")) ) if ( "date_livraison" in facture_data and facture_data["date_livraison"] ): doc.DO_DateLivr = pywintypes.Time( normaliser_date(facture_data["date_livraison"]) ) logger.info( f" Date livraison: {facture_data['date_livraison']}" ) factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( facture_data["client"]["code"] ) if not persist_client: raise ValueError( f"Client {facture_data['client']['code']} introuvable" ) client_obj = _cast_client(persist_client) if not client_obj: raise ValueError("Impossible de charger le client") doc.SetDefaultClient(client_obj) doc.Write() logger.info(f" Client {facture_data['client']['code']} associé") if facture_data.get("reference"): try: doc.DO_Ref = facture_data["reference"] logger.info(f" Référence: {facture_data['reference']}") except Exception: pass logger.info(" Configuration champs spécifiques factures...") try: if hasattr(doc, "DO_CodeJournal"): try: param_societe = ( self.cial.CptaApplication.ParametreSociete ) journal_defaut = getattr( param_societe, "P_CodeJournalVte", "VTE" ) doc.DO_CodeJournal = journal_defaut logger.info(f" Code journal: {journal_defaut}") except Exception: doc.DO_CodeJournal = "VTE" logger.info(" Code journal: VTE (défaut)") except Exception as e: logger.debug(f" Code journal: {e}") try: if hasattr(doc, "DO_Souche"): doc.DO_Souche = 0 logger.debug(" Souche: 0 (défaut)") except Exception: pass try: if hasattr(doc, "DO_Regime"): doc.DO_Regime = 0 logger.debug(" Régime: 0 (défaut)") except Exception: pass try: factory_lignes = doc.FactoryDocumentLigne except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle logger.info(f" Ajout de {len(facture_data['lignes'])} lignes...") for idx, ligne_data in enumerate(facture_data["lignes"], 1): logger.info( f"--- Ligne {idx}: {ligne_data['article_code']} ---" ) persist_article = factory_article.ReadReference( ligne_data["article_code"] ) if not persist_article: raise ValueError( f" Article {ligne_data['article_code']} introuvable dans Sage" ) article_obj = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_obj.Read() prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) designation_sage = getattr(article_obj, "AR_Design", "") logger.info(f" Prix Sage: {prix_sage}€") if prix_sage == 0: logger.warning( f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" ) ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) logger.info( " Article associé via SetDefaultArticleReference" ) except Exception as e: logger.warning( f"SetDefaultArticleReference échoué: {e}, tentative avec objet" ) try: ligne_obj.SetDefaultArticle(article_obj, quantite) logger.info(" Article associé via SetDefaultArticle") except Exception: logger.error(" Toutes les méthodes ont échoué") ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") ) ligne_obj.DL_Qte = quantite logger.warning("Configuration manuelle appliquée") prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) logger.info(f" Prix auto chargé: {prix_auto}€") prix_a_utiliser = ligne_data.get("prix_unitaire_ht") if prix_a_utiliser is not None and prix_a_utiliser > 0: ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) logger.info(f" Prix personnalisé: {prix_a_utiliser}€") elif prix_auto == 0 and prix_sage > 0: ligne_obj.DL_PrixUnitaire = float(prix_sage) logger.info(f" Prix Sage forcé: {prix_sage}€") elif prix_auto > 0: logger.info(f" Prix auto conservé: {prix_auto}€") prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) montant_ligne = quantite * prix_final logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") remise = ligne_data.get("remise_pourcentage", 0) if remise > 0: try: ligne_obj.DL_Remise01REM_Valeur = float(remise) ligne_obj.DL_Remise01REM_Type = 0 montant_apres_remise = montant_ligne * ( 1 - remise / 100 ) logger.info( f" Remise {remise}% → {montant_apres_remise}€" ) except Exception as e: logger.warning(f"Remise non appliquée: {e}") ligne_obj.Write() logger.info(f" Ligne {idx} écrite") logger.info(" Validation facture...") try: doc.SetClient(client_obj) logger.debug(" Client réassocié avant validation") except Exception: try: doc.SetDefaultClient(client_obj) except Exception: pass doc.Write() logger.info(" Process()...") process.Process() if transaction_active: self.cial.CptaApplication.CommitTrans() logger.info(" Transaction committée") time.sleep(2) numero_facture = None try: doc_result = process.DocumentResult if doc_result: doc_result = win32com.client.CastTo( doc_result, "IBODocumentVente3" ) doc_result.Read() numero_facture = getattr(doc_result, "DO_Piece", "") except Exception: pass if not numero_facture: numero_facture = getattr(doc, "DO_Piece", "") if not numero_facture: raise RuntimeError("Numéro facture vide après création") logger.info(f" Numéro facture: {numero_facture}") factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece( settings.SAGE_TYPE_FACTURE, numero_facture ) 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)) reference_finale = 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 Exception: pass else: total_ht = 0.0 total_ttc = 0.0 reference_finale = facture_data.get("reference", "") date_livraison_final = facture_data.get("date_livraison") logger.info(f" FACTURE CRÉÉE: {numero_facture} - {total_ttc}€ TTC ") if date_livraison_final: logger.info(f" Date livraison: {date_livraison_final}") return { "numero_facture": numero_facture, "total_ht": total_ht, "total_ttc": total_ttc, "nb_lignes": len(facture_data["lignes"]), "client_code": facture_data["client"]["code"], "date_facture": str( normaliser_date(facture_data.get("date_facture")) ), "date_livraison": date_livraison_final, "reference": reference_finale, } except Exception: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() logger.error(" Transaction annulée (rollback)") except Exception: pass raise except Exception as e: logger.error(f" Erreur création facture: {e}", exc_info=True) raise RuntimeError(f"Échec création facture: {str(e)}") def modifier_facture(self, numero: str, facture_data: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: logger.info(f" === MODIFICATION FACTURE {numero} ===") logger.info(" Chargement document...") factory = self.cial.FactoryDocumentVente persist = None for type_test in [60, 5, settings.SAGE_TYPE_FACTURE]: try: persist_test = factory.ReadPiece(type_test, numero) if persist_test: persist = persist_test logger.info(f" Document trouvé (type={type_test})") break except Exception: continue if not persist: raise ValueError(f" Facture {numero} INTROUVABLE") doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() statut_actuel = getattr(doc, "DO_Statut", 0) type_reel = getattr(doc, "DO_Type", -1) logger.info(f" Type={type_reel}, Statut={statut_actuel}") if statut_actuel == 5: raise ValueError(f"La facture {numero} a déjà été transformée") if statut_actuel == 6: raise ValueError(f"La facture {numero} est annulée") client_code_initial = "" try: client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_code_initial = getattr(client_obj, "CT_Num", "").strip() logger.info(f" Client initial: {client_code_initial}") else: logger.error(" Objet Client NULL à l'état initial !") except Exception as e: logger.error(f" Erreur lecture client initial: {e}") if not client_code_initial: raise ValueError(" Client introuvable dans le document") nb_lignes_initial = 0 try: factory_lignes = getattr( doc, "FactoryDocumentLigne", None ) or getattr(doc, "FactoryDocumentVenteLigne", None) index = 1 while index <= 100: try: ligne_p = factory_lignes.List(index) if ligne_p is None: break nb_lignes_initial += 1 index += 1 except Exception: break logger.info(f" Lignes initiales: {nb_lignes_initial}") except Exception as e: logger.warning(f" Erreur comptage lignes: {e}") champs_modifies = [] modif_date = "date_facture" in facture_data modif_date_livraison = "date_livraison" in facture_data modif_statut = "statut" in facture_data modif_ref = "reference" in facture_data modif_lignes = ( "lignes" in facture_data and facture_data["lignes"] is not None ) logger.info("Modifications demandées:") logger.info(f" Date facture: {modif_date}") logger.info(f" Date livraison: {modif_date_livraison}") logger.info(f" Statut: {modif_statut}") logger.info(f" Référence: {modif_ref}") logger.info(f" Lignes: {modif_lignes}") facture_data_temp = facture_data.copy() reference_a_modifier = None statut_a_modifier = None if modif_lignes: if modif_ref: reference_a_modifier = facture_data_temp.pop("reference") logger.info( " Modification de la référence reportée après les lignes" ) modif_ref = False if modif_statut: statut_a_modifier = facture_data_temp.pop("statut") logger.info(" Modification du statut reportée après les lignes") modif_statut = False logger.info(" Test Write() basique (sans modification)...") try: doc.Write() logger.info(" Write() basique OK") doc.Read() client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") if client_apres == client_code_initial: logger.info(f" Client préservé: {client_apres}") else: logger.error( f" Client a changé: {client_code_initial} → {client_apres}" ) else: logger.error(" Client devenu NULL après Write() basique") except Exception as e: logger.error(f" Write() basique ÉCHOUE: {e}") logger.error(" ABANDON: Le document est VERROUILLÉ") raise ValueError( f"Document verrouillé, impossible de modifier: {e}" ) if not modif_lignes and ( modif_date or modif_date_livraison or modif_statut or modif_ref ): logger.info(" Modifications simples (sans lignes)...") if modif_date: logger.info(" Modification date facture...") doc.DO_Date = pywintypes.Time( normaliser_date(facture_data_temp.get("date_facture")) ) champs_modifies.append("date") if modif_date_livraison: logger.info(" Modification date livraison...") doc.DO_DateLivr = pywintypes.Time( normaliser_date(facture_data_temp["date_livraison"]) ) logger.info( f" Date livraison: {facture_data_temp['date_livraison']}" ) champs_modifies.append("date_livraison") if modif_statut: logger.info(" Modification statut...") nouveau_statut = facture_data_temp["statut"] doc.DO_Statut = nouveau_statut champs_modifies.append("statut") logger.info(f" Statut défini: {nouveau_statut}") if modif_ref: logger.info(" Modification référence...") try: doc.DO_Ref = facture_data_temp["reference"] champs_modifies.append("reference") logger.info( f" Référence définie: {facture_data_temp['reference']}" ) except Exception as e: logger.warning(f" Référence non définie: {e}") logger.info(" Write() sans réassociation client...") try: doc.Write() logger.info(" Write() réussi") doc.Read() client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") if client_apres == client_code_initial: logger.info(f" Client préservé: {client_apres}") else: logger.error( f" Client perdu: {client_code_initial} → {client_apres}" ) except Exception as e: error_msg = str(e) try: sage_error = self.cial.CptaApplication.LastError if sage_error: error_msg = f"{sage_error.Description} (Code: {sage_error.Number})" except Exception: pass logger.error(f" Write() échoue: {error_msg}") raise ValueError(f"Sage refuse: {error_msg}") elif modif_lignes: logger.info(" REMPLACEMENT COMPLET DES LIGNES...") if modif_date: doc.DO_Date = pywintypes.Time( normaliser_date(facture_data_temp.get("date_facture")) ) champs_modifies.append("date") logger.info(" Date facture modifiée") if modif_date_livraison: doc.DO_DateLivr = pywintypes.Time( normaliser_date(facture_data_temp["date_livraison"]) ) logger.info(" Date livraison modifiée") champs_modifies.append("date_livraison") nouvelles_lignes = facture_data["lignes"] nb_nouvelles = len(nouvelles_lignes) logger.info( f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes" ) try: factory_lignes = doc.FactoryDocumentLigne except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle if nb_lignes_initial > 0: logger.info( f" Suppression de {nb_lignes_initial} lignes existantes..." ) for idx in range(nb_lignes_initial, 0, -1): try: ligne_p = factory_lignes.List(idx) if ligne_p: ligne = win32com.client.CastTo( ligne_p, "IBODocumentLigne3" ) ligne.Read() ligne.Remove() logger.debug(f" Ligne {idx} supprimée") except Exception as e: logger.warning( f" Impossible de supprimer ligne {idx}: {e}" ) logger.info(" Toutes les lignes existantes supprimées") logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...") for idx, ligne_data in enumerate(nouvelles_lignes, 1): logger.info( f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---" ) persist_article = factory_article.ReadReference( ligne_data["article_code"] ) if not persist_article: raise ValueError( f"Article {ligne_data['article_code']} introuvable" ) article_obj = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_obj.Read() ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) except Exception: try: ligne_obj.SetDefaultArticle(article_obj, quantite) except Exception: ligne_obj.DL_Design = ligne_data.get("designation", "") ligne_obj.DL_Qte = quantite if ligne_data.get("prix_unitaire_ht"): ligne_obj.DL_PrixUnitaire = float( ligne_data["prix_unitaire_ht"] ) if ligne_data.get("remise_pourcentage", 0) > 0: try: ligne_obj.DL_Remise01REM_Valeur = float( ligne_data["remise_pourcentage"] ) ligne_obj.DL_Remise01REM_Type = 0 except Exception: pass ligne_obj.Write() logger.info(f" Ligne {idx} ajoutée") logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées") logger.info(" Write() document après remplacement lignes...") doc.Write() logger.info(" Document écrit") import time time.sleep(0.5) doc.Read() client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_apres = getattr(client_obj, "CT_Num", "") logger.info(f" Client après remplacement: {client_apres}") else: logger.error(" Client NULL après remplacement") champs_modifies.append("lignes") if reference_a_modifier is not None: try: ancienne_reference = getattr(doc, "DO_Ref", "") nouvelle_reference = ( str(reference_a_modifier) if reference_a_modifier else "" ) logger.info( f" Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'" ) doc.DO_Ref = nouvelle_reference doc.Write() import time time.sleep(0.5) doc.Read() champs_modifies.append("reference") logger.info(" Référence modifiée avec succès") except Exception as e: logger.warning(f"Impossible de modifier la référence: {e}") if statut_a_modifier is not None: try: statut_actuel = getattr(doc, "DO_Statut", 0) nouveau_statut = int(statut_a_modifier) if nouveau_statut != statut_actuel: logger.info( f" Modification statut: {statut_actuel} → {nouveau_statut}" ) doc.DO_Statut = nouveau_statut doc.Write() import time time.sleep(0.5) doc.Read() champs_modifies.append("statut") logger.info(" Statut modifié avec succès") except Exception as e: logger.warning(f"Impossible de modifier le statut: {e}") logger.info(" Relecture finale...") import time time.sleep(0.5) doc.Read() client_obj_final = getattr(doc, "Client", None) if client_obj_final: client_obj_final.Read() client_final = getattr(client_obj_final, "CT_Num", "") else: client_final = "" total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) reference_finale = getattr(doc, "DO_Ref", "") statut_final = getattr(doc, "DO_Statut", 0) date_livraison_final = None try: date_livr = getattr(doc, "DO_DateLivr", None) if date_livr: date_livraison_final = date_livr.strftime("%Y-%m-%d") except Exception: pass logger.info(f" SUCCÈS: {numero} modifiée ") logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC") logger.info(f" Client final: {client_final}") logger.info(f" Référence: {reference_finale}") logger.info(f" Statut: {statut_final}") if date_livraison_final: logger.info(f" Date livraison: {date_livraison_final}") logger.info(f" Champs modifiés: {champs_modifies}") return { "numero": numero, "total_ht": total_ht, "total_ttc": total_ttc, "reference": reference_finale, "date_livraison": date_livraison_final, "champs_modifies": champs_modifies, "statut": statut_final, "client_code": client_final, } except ValueError as e: logger.error(f" ERREUR MÉTIER: {e}") raise except Exception as e: logger.error(f" ERREUR TECHNIQUE: {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} (Code: {err.Number})" ) except Exception: pass raise RuntimeError(f"Erreur Sage: {error_message}") def creer_article(self, article_data: dict) -> dict: """Crée un article dans Sage 100 avec COM uniquement""" with self._com_context(), self._lock_com: try: logger.info("[ARTICLE] === CREATION ARTICLE ===") transaction_active = False try: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug("Transaction Sage démarrée") except Exception as e: logger.debug(f"BeginTrans non disponible : {e}") try: # === Découverte dépôts === depots_disponibles = [] depot_a_utiliser = None depot_code_demande = article_data.get("depot_code") try: factory_depot = self.cial.FactoryDepot index = 1 while index <= 100: try: persist = factory_depot.List(index) if persist is None: break depot_obj = win32com.client.CastTo(persist, "IBODepot3") depot_obj.Read() code = getattr(depot_obj, "DE_Code", "").strip() if not code: index += 1 continue numero = int(getattr(depot_obj, "Compteur", 0)) intitule = getattr( depot_obj, "DE_Intitule", f"Depot {code}" ) depot_info = { "code": code, "numero": numero, "intitule": intitule, "objet": depot_obj, } depots_disponibles.append(depot_info) if depot_code_demande and code == depot_code_demande: depot_a_utiliser = depot_info elif not depot_code_demande and not depot_a_utiliser: depot_a_utiliser = depot_info index += 1 except Exception as e: if "Acces refuse" in str(e): break index += 1 except Exception as e: logger.warning(f"[DEPOT] Erreur découverte dépôts : {e}") if not depots_disponibles: raise ValueError( "Aucun dépôt trouvé dans Sage. Créez d'abord un dépôt." ) if not depot_a_utiliser: depot_a_utiliser = depots_disponibles[0] logger.info( f"[DEPOT] Utilisation : '{depot_a_utiliser['code']}' ({depot_a_utiliser['intitule']})" ) # === Validation données === reference = article_data.get("reference", "").upper().strip() if not reference: raise ValueError("La référence est obligatoire") if len(reference) > 18: raise ValueError( "La référence ne peut pas dépasser 18 caractères" ) designation = article_data.get("designation", "").strip() if not designation: raise ValueError("La désignation est obligatoire") if len(designation) > 69: designation = designation[:69] stock_reel = article_data.get("stock_reel", 0.0) stock_mini = article_data.get("stock_mini", 0.0) stock_maxi = article_data.get("stock_maxi", 0.0) logger.info(f"[ARTICLE] Référence : {reference}") logger.info(f"[ARTICLE] Désignation : {designation}") # === Vérifier si article existe === factory = self.cial.FactoryArticle try: article_existant = factory.ReadReference(reference) if article_existant: raise ValueError(f"L'article {reference} existe déjà") except Exception as e: error_msg = str(e) if ( "Enregistrement non trouve" not in error_msg and "-2607" not in error_msg ): raise # === Créer l'article === persist = factory.Create() article = win32com.client.CastTo(persist, "IBOArticle3") article.SetDefault() article.AR_Ref = reference article.AR_Design = designation # === Recherche article modèle === logger.info("[MODELE] Recherche article modèle...") article_modele_ref = None article_modele = None try: with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute(""" SELECT TOP 1 AR_Ref FROM F_ARTICLE WHERE AR_Sommeil = 0 ORDER BY AR_Ref """) row = cursor.fetchone() if row: article_modele_ref = _safe_strip(row.AR_Ref) logger.info( f" [SQL] Article modèle : {article_modele_ref}" ) except Exception as e: logger.warning(f" [SQL] Erreur recherche : {e}") if article_modele_ref: try: persist_modele = factory.ReadReference(article_modele_ref) if persist_modele: article_modele = win32com.client.CastTo( persist_modele, "IBOArticle3" ) article_modele.Read() logger.info( f" [OK] Modèle chargé : {article_modele_ref}" ) except Exception as e: logger.warning(f" [WARN] Erreur chargement : {e}") article_modele = None if not article_modele: raise ValueError( "Aucun article modèle trouvé. Créez au moins un article dans Sage." ) # === Copie Unite depuis modèle === logger.info("[UNITE] Copie Unite depuis modèle...") unite_trouvee = False try: unite_obj = getattr(article_modele, "Unite", None) if unite_obj: article.Unite = unite_obj logger.info(" [OK] Unite copiée") unite_trouvee = True except Exception as e: logger.debug(f" Unite non copiable : {e}") if not unite_trouvee: raise ValueError( "Impossible de copier l'unité depuis le modèle" ) # === Gestion famille === famille_trouvee = False famille_code_personnalise = article_data.get("famille") famille_obj = None if famille_code_personnalise: logger.info( f" [FAMILLE] Code demandé : {famille_code_personnalise}" ) try: famille_code_exact = None with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( """ SELECT FA_CodeFamille, FA_Type FROM F_FAMILLE WHERE UPPER(FA_CodeFamille) = ? """, (famille_code_personnalise.upper(),), ) row = cursor.fetchone() if row: famille_code_exact = _safe_strip(row.FA_CodeFamille) if row.FA_Type == 1: raise ValueError( f"Famille '{famille_code_personnalise}' est de type Total" ) logger.info( f" [SQL] Famille trouvée : {famille_code_exact}" ) else: raise ValueError( f"Famille '{famille_code_personnalise}' introuvable" ) if famille_code_exact: factory_famille = self.cial.FactoryFamille index = 1 while index <= 1000: try: persist_test = factory_famille.List(index) if persist_test is None: break fam_test = win32com.client.CastTo( persist_test, "IBOFamille3" ) fam_test.Read() code_test = ( getattr(fam_test, "FA_CodeFamille", "") .strip() .upper() ) if code_test == famille_code_exact.upper(): famille_obj = fam_test famille_trouvee = True logger.info( f" [OK] Famille trouvée à index {index}" ) break index += 1 except Exception: break if famille_obj: famille_obj.Read() article.Famille = famille_obj else: raise ValueError( f"Famille '{famille_code_personnalise}' inaccessible via COM" ) except Exception as e: logger.error(f" [ERREUR] Famille : {e}") raise if not famille_trouvee: try: famille_obj = getattr(article_modele, "Famille", None) if famille_obj: article.Famille = famille_obj logger.info(" [OK] Famille copiée depuis modèle") famille_trouvee = True except Exception as e: logger.debug(f" Famille non copiable : {e}") # === Champs obligatoires === logger.info("[CHAMPS] Copie champs obligatoires...") article.AR_Type = int(getattr(article_modele, "AR_Type", 0)) article.AR_Nature = int(getattr(article_modele, "AR_Nature", 0)) article.AR_Nomencl = int(getattr(article_modele, "AR_Nomencl", 0)) article.AR_SuiviStock = 2 logger.info(" [OK] AR_SuiviStock=2 (FIFO/LIFO)") # === Application des champs fournis === logger.info("[CHAMPS] Application champs fournis...") champs_appliques = [] # Prix de vente if "prix_vente" in article_data: try: article.AR_PrixVen = float(article_data["prix_vente"]) champs_appliques.append("prix_vente") logger.info( f" ✓ prix_vente = {article_data['prix_vente']}" ) except Exception as e: logger.warning(f" ⚠ prix_vente : {e}") # Prix d'achat (AR_PrixAchat avec un 't') if "prix_achat" in article_data: try: article.AR_PrixAchat = float(article_data["prix_achat"]) champs_appliques.append("prix_achat") logger.info( f" ✓ prix_achat = {article_data['prix_achat']}" ) except Exception as e: logger.warning(f" ⚠ prix_achat : {e}") # Coefficient if "coef" in article_data: try: article.AR_Coef = float(article_data["coef"]) champs_appliques.append("coef") logger.info(f" ✓ coef = {article_data['coef']}") except Exception as e: logger.warning(f" ⚠ coef : {e}") # Code EAN if "code_ean" in article_data: try: article.AR_CodeBarre = str(article_data["code_ean"]) champs_appliques.append("code_ean") logger.info(f" ✓ code_ean = {article_data['code_ean']}") except Exception as e: logger.warning(f" ⚠ code_ean : {e}") # Description (AR_Langue1) if "description" in article_data: try: article.AR_Langue1 = str(article_data["description"])[:255] champs_appliques.append("description") logger.info(" ✓ description définie") except Exception as e: logger.warning(f" ⚠ description : {e}") # Pays if "pays" in article_data: try: article.AR_Pays = str(article_data["pays"])[:3].upper() champs_appliques.append("pays") logger.info(f" ✓ pays = {article_data['pays']}") except Exception as e: logger.warning(f" ⚠ pays : {e}") # Garantie if "garantie" in article_data: try: article.AR_Garantie = int(article_data["garantie"]) champs_appliques.append("garantie") logger.info(f" ✓ garantie = {article_data['garantie']}") except Exception as e: logger.warning(f" ⚠ garantie : {e}") # Délai if "delai" in article_data: try: article.AR_Delai = int(article_data["delai"]) champs_appliques.append("delai") logger.info(f" ✓ delai = {article_data['delai']}") except Exception as e: logger.warning(f" ⚠ delai : {e}") # Poids net if "poids_net" in article_data: try: article.AR_PoidsNet = float(article_data["poids_net"]) champs_appliques.append("poids_net") logger.info(f" ✓ poids_net = {article_data['poids_net']}") except Exception as e: logger.warning(f" ⚠ poids_net : {e}") # Poids brut if "poids_brut" in article_data: try: article.AR_PoidsBrut = float(article_data["poids_brut"]) champs_appliques.append("poids_brut") logger.info( f" ✓ poids_brut = {article_data['poids_brut']}" ) except Exception as e: logger.warning(f" ⚠ poids_brut : {e}") # Code fiscal if "code_fiscal" in article_data: try: article.AR_CodeFiscal = str(article_data["code_fiscal"])[ :10 ] champs_appliques.append("code_fiscal") logger.info( f" ✓ code_fiscal = {article_data['code_fiscal']}" ) except Exception as e: logger.warning(f" ⚠ code_fiscal : {e}") # Soumis escompte if "soumis_escompte" in article_data: try: article.AR_Escompte = ( 1 if article_data["soumis_escompte"] else 0 ) champs_appliques.append("soumis_escompte") logger.info( f" ✓ soumis_escompte = {article_data['soumis_escompte']}" ) except Exception as e: logger.warning(f" ⚠ soumis_escompte : {e}") # Publié if "publie" in article_data: try: article.AR_Publie = 1 if article_data["publie"] else 0 champs_appliques.append("publie") logger.info(f" ✓ publie = {article_data['publie']}") except Exception as e: logger.warning(f" ⚠ publie : {e}") # En sommeil if "en_sommeil" in article_data: try: article.AR_Sommeil = 1 if article_data["en_sommeil"] else 0 champs_appliques.append("en_sommeil") logger.info( f" ✓ en_sommeil = {article_data['en_sommeil']}" ) except Exception as e: logger.warning(f" ⚠ en_sommeil : {e}") logger.info(f"[CHAMPS] {len(champs_appliques)} champs appliqués") # === Écriture article === logger.info("[ARTICLE] Écriture dans Sage...") try: article.Write() 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})" except Exception: pass logger.error(f" [ERREUR] Write() : {error_detail}") raise RuntimeError(f"Échec création : {error_detail}") # === Statistiques (méthode AR_Stat après Write) === stats_a_definir = [] for i in range(1, 6): stat_key = f"stat_0{i}" if stat_key in article_data: stats_a_definir.append( (i - 1, str(article_data[stat_key])[:20]) ) if stats_a_definir: logger.info("[STATS] Définition statistiques...") try: for index, value in stats_a_definir: article.AR_Stat(index, value) logger.info(f" ✓ stat_0{index + 1} = {value}") article.Write() logger.info(" [OK] Statistiques sauvegardées") except Exception as e: logger.warning(f" ⚠ Statistiques : {e}") # === Gestion stocks === stock_defini = False has_stock_values = stock_reel or stock_mini or stock_maxi if has_stock_values: logger.info( f"[STOCK] Définition stock (dépôt '{depot_a_utiliser['code']}')..." ) try: depot_obj = depot_a_utiliser["objet"] factory_stock = None for factory_name in [ "FactoryArticleStock", "FactoryDepotStock", ]: try: factory_stock = getattr( depot_obj, factory_name, None ) if factory_stock: logger.info(f" Factory : {factory_name}") break except Exception: continue if not factory_stock: raise RuntimeError("Factory de stock introuvable") stock_persist = factory_stock.Create() stock_obj = win32com.client.CastTo( stock_persist, "IBODepotStock3" ) stock_obj.SetDefault() stock_obj.AR_Ref = reference if stock_reel: stock_obj.AS_QteSto = float(stock_reel) logger.info(f" AS_QteSto = {stock_reel}") if stock_mini: try: stock_obj.AS_QteMini = float(stock_mini) logger.info(f" AS_QteMini = {stock_mini}") except Exception as e: logger.warning(f" AS_QteMini : {e}") if stock_maxi: try: stock_obj.AS_QteMaxi = float(stock_maxi) logger.info(f" AS_QteMaxi = {stock_maxi}") except Exception as e: logger.warning(f" AS_QteMaxi : {e}") stock_obj.Write() stock_defini = True logger.info( f" [OK] Stock défini : réel={stock_reel}, min={stock_mini}, max={stock_maxi}" ) except Exception as e: logger.error(f" [ERREUR] Stock : {e}", exc_info=True) # === Commit transaction === if transaction_active: try: self.cial.CptaApplication.CommitTrans() logger.info("[COMMIT] Transaction committée") except Exception as e: logger.warning(f"[COMMIT] Erreur : {e}") # === Relecture et construction réponse === logger.info("[VERIF] Relecture article...") article_cree_persist = factory.ReadReference(reference) if not article_cree_persist: raise RuntimeError("Article créé mais introuvable") article_cree = win32com.client.CastTo( article_cree_persist, "IBOArticle3" ) article_cree.Read() # Vérification stocks SQL stocks_par_depot = [] stock_total = 0.0 try: with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( """ SELECT d.DE_Code, s.AS_QteSto, s.AS_QteMini, s.AS_QteMaxi FROM F_ARTSTOCK s LEFT JOIN F_DEPOT d ON s.DE_No = d.DE_No WHERE s.AR_Ref = ? """, (reference.upper(),), ) for row in cursor.fetchall(): qte = float(row[1]) if row[1] else 0.0 stock_total += qte stocks_par_depot.append( { "depot_code": _safe_strip(row[0]), "quantite": qte, "qte_mini": float(row[2]) if row[2] else 0.0, "qte_maxi": float(row[3]) if row[3] else 0.0, } ) logger.info( f"[VERIF] Stock : {stock_total} dans {len(stocks_par_depot)} dépôt(s)" ) except Exception as e: logger.warning(f"[VERIF] Erreur stock : {e}") logger.info(f"[OK] ARTICLE CREE : {reference}") # Construction réponse resultat = _extraire_article(article_cree) if not resultat: resultat = {"reference": reference, "designation": designation} resultat["stock_reel"] = stock_total resultat["stock_disponible"] = stock_total resultat["stock_reserve"] = 0.0 resultat["stock_commande"] = 0.0 if stock_mini: resultat["stock_mini"] = float(stock_mini) if stock_maxi: resultat["stock_maxi"] = float(stock_maxi) if stocks_par_depot: resultat["stocks_par_depot"] = stocks_par_depot return resultat except ValueError: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() except Exception: pass raise except Exception as e: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() except Exception: pass logger.error(f"Erreur création : {e}", exc_info=True) raise RuntimeError(f"Erreur création : {str(e)}") except Exception as e: logger.error(f"Erreur globale : {e}", exc_info=True) raise def modifier_article(self, reference: str, article_data: dict) -> dict: """Modifie un article existant dans Sage 100""" if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: logger.info(f"[ARTICLE] === MODIFICATION {reference} ===") factory_article = self.cial.FactoryArticle persist = factory_article.ReadReference(reference.upper()) if not persist: raise ValueError(f"Article {reference} introuvable") article = win32com.client.CastTo(persist, "IBOArticle3") article.Read() logger.info(f"[ARTICLE] Trouvé : {reference}") champs_modifies = [] # === Gestion famille === if "famille" in article_data and article_data["famille"]: famille_code_demande = article_data["famille"].upper().strip() logger.info(f"[FAMILLE] Changement : {famille_code_demande}") try: famille_code_exact = None with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( """ SELECT FA_CodeFamille, FA_Type FROM F_FAMILLE WHERE UPPER(FA_CodeFamille) = ? """, (famille_code_demande,), ) row = cursor.fetchone() if row: famille_code_exact = _safe_strip(row.FA_CodeFamille) if row.FA_Type == 1: raise ValueError( f"Famille '{famille_code_demande}' est de type Total" ) logger.info( f" [SQL] Famille trouvée : {famille_code_exact}" ) else: raise ValueError( f"Famille '{famille_code_demande}' introuvable" ) if famille_code_exact: factory_famille = self.cial.FactoryFamille famille_obj = None index = 1 while index <= 1000: try: persist_test = factory_famille.List(index) if persist_test is None: break fam_test = win32com.client.CastTo( persist_test, "IBOFamille3" ) fam_test.Read() code_test = ( getattr(fam_test, "FA_CodeFamille", "") .strip() .upper() ) if code_test == famille_code_exact.upper(): famille_obj = fam_test logger.info(f" [OK] Famille à index {index}") break index += 1 except Exception: break if famille_obj: famille_obj.Read() article.Famille = famille_obj champs_modifies.append("famille") logger.info( f" [OK] Famille changée : {famille_code_exact}" ) else: raise ValueError( f"Famille '{famille_code_demande}' inaccessible via COM" ) except Exception as e: logger.error(f" [ERREUR] Famille : {e}") raise # === Traitement des champs === if "designation" in article_data: try: article.AR_Design = str(article_data["designation"])[ :69 ].strip() champs_modifies.append("designation") logger.info(" ✓ designation") except Exception as e: logger.warning(f" ⚠ designation : {e}") if "prix_vente" in article_data: try: article.AR_PrixVen = float(article_data["prix_vente"]) champs_modifies.append("prix_vente") logger.info(f" ✓ prix_vente = {article_data['prix_vente']}") except Exception as e: logger.warning(f" ⚠ prix_vente : {e}") if "prix_achat" in article_data: try: article.AR_PrixAchat = float(article_data["prix_achat"]) champs_modifies.append("prix_achat") logger.info(f" ✓ prix_achat = {article_data['prix_achat']}") except Exception as e: logger.warning(f" ⚠ prix_achat : {e}") if "coef" in article_data: try: article.AR_Coef = float(article_data["coef"]) champs_modifies.append("coef") logger.info(f" ✓ coef = {article_data['coef']}") except Exception as e: logger.warning(f" ⚠ coef : {e}") if "code_ean" in article_data: try: article.AR_CodeBarre = str(article_data["code_ean"])[ :13 ].strip() champs_modifies.append("code_ean") logger.info(" ✓ code_ean") except Exception as e: logger.warning(f" ⚠ code_ean : {e}") if "description" in article_data: try: article.AR_Langue1 = str(article_data["description"])[ :255 ].strip() champs_modifies.append("description") logger.info(" ✓ description") except Exception as e: logger.warning(f" ⚠ description : {e}") if "pays" in article_data: try: article.AR_Pays = str(article_data["pays"])[:3].upper() champs_modifies.append("pays") logger.info(f" ✓ pays = {article_data['pays']}") except Exception as e: logger.warning(f" ⚠ pays : {e}") if "garantie" in article_data: try: article.AR_Garantie = int(article_data["garantie"]) champs_modifies.append("garantie") logger.info(f" ✓ garantie = {article_data['garantie']}") except Exception as e: logger.warning(f" ⚠ garantie : {e}") if "delai" in article_data: try: article.AR_Delai = int(article_data["delai"]) champs_modifies.append("delai") logger.info(f" ✓ delai = {article_data['delai']}") except Exception as e: logger.warning(f" ⚠ delai : {e}") if "poids_net" in article_data: try: article.AR_PoidsNet = float(article_data["poids_net"]) champs_modifies.append("poids_net") logger.info(f" ✓ poids_net = {article_data['poids_net']}") except Exception as e: logger.warning(f" ⚠ poids_net : {e}") if "poids_brut" in article_data: try: article.AR_PoidsBrut = float(article_data["poids_brut"]) champs_modifies.append("poids_brut") logger.info(f" ✓ poids_brut = {article_data['poids_brut']}") except Exception as e: logger.warning(f" ⚠ poids_brut : {e}") if "code_fiscal" in article_data: try: article.AR_CodeFiscal = str(article_data["code_fiscal"])[:10] champs_modifies.append("code_fiscal") logger.info(f" ✓ code_fiscal = {article_data['code_fiscal']}") except Exception as e: logger.warning(f" ⚠ code_fiscal : {e}") if "soumis_escompte" in article_data: try: article.AR_Escompte = ( 1 if article_data["soumis_escompte"] else 0 ) champs_modifies.append("soumis_escompte") logger.info( f" ✓ soumis_escompte = {article_data['soumis_escompte']}" ) except Exception as e: logger.warning(f" ⚠ soumis_escompte : {e}") if "publie" in article_data: try: article.AR_Publie = 1 if article_data["publie"] else 0 champs_modifies.append("publie") logger.info(f" ✓ publie = {article_data['publie']}") except Exception as e: logger.warning(f" ⚠ publie : {e}") if "en_sommeil" in article_data: try: article.AR_Sommeil = 1 if article_data["en_sommeil"] else 0 champs_modifies.append("en_sommeil") logger.info(f" ✓ en_sommeil = {article_data['en_sommeil']}") except Exception as e: logger.warning(f" ⚠ en_sommeil : {e}") if not champs_modifies: logger.warning("[ARTICLE] Aucun champ modifié") return _extraire_article(article) logger.info(f"[ARTICLE] {len(champs_modifies)} champs à modifier") # === Écriture === logger.info("[ARTICLE] Écriture...") try: article.Write() 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})" ) except Exception: pass logger.error(f" [ERREUR] Write() : {error_detail}") raise RuntimeError(f"Échec modification : {error_detail}") # === Statistiques (méthode AR_Stat après Write) === stats_a_definir = [] for i in range(1, 6): stat_key = f"stat_0{i}" if stat_key in article_data: stats_a_definir.append( (i - 1, str(article_data[stat_key])[:20]) ) if stats_a_definir: logger.info("[STATS] Définition statistiques...") try: for index, value in stats_a_definir: article.AR_Stat(index, value) champs_modifies.append(f"stat_0{index + 1}") logger.info(f" ✓ stat_0{index + 1} = {value}") article.Write() logger.info(" [OK] Statistiques sauvegardées") except Exception as e: logger.warning(f" ⚠ Statistiques : {e}") # === Gestion stocks mini/maxi via SQL === if "stock_mini" in article_data or "stock_maxi" in article_data: try: with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( "SELECT TOP 1 DE_No FROM F_DEPOT ORDER BY DE_No" ) row = cursor.fetchone() if row: depot_no = row.DE_No update_parts = [] params = [] if "stock_mini" in article_data: update_parts.append("AS_QteMini = ?") params.append(float(article_data["stock_mini"])) champs_modifies.append("stock_mini") if "stock_maxi" in article_data: update_parts.append("AS_QteMaxi = ?") params.append(float(article_data["stock_maxi"])) champs_modifies.append("stock_maxi") if update_parts: params.extend([reference.upper(), depot_no]) cursor.execute( f""" UPDATE F_ARTSTOCK SET {", ".join(update_parts)} WHERE AR_Ref = ? AND DE_No = ? """, params, ) conn.commit() logger.info(" [SQL] Stocks mini/maxi mis à jour") except Exception as e: logger.error(f" [ERREUR] Stocks : {e}") article.Read() logger.info( f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)" ) resultat = _extraire_article(article) if not resultat: resultat = {"reference": reference} return resultat except ValueError as e: logger.error(f"[ARTICLE] Erreur métier : {e}") raise except Exception as e: logger.error(f"[ARTICLE] Erreur technique : {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 Exception: pass raise RuntimeError(f"Erreur technique Sage : {error_message}") def creer_famille(self, famille_data: dict) -> dict: with self._com_context(), self._lock_com: try: logger.info("[FAMILLE] === CRÉATION FAMILLE (TYPE DÉTAIL) ===") code = famille_data.get("code", "").upper().strip() if not code: raise ValueError("Le code famille est obligatoire") if len(code) > 18: raise ValueError( "Le code famille ne peut pas dépasser 18 caractères" ) intitule = famille_data.get("intitule", "").strip() if not intitule: raise ValueError("L'intitulé est obligatoire") if len(intitule) > 69: intitule = intitule[:69] logger.info(f"[FAMILLE] Code : {code}") logger.info(f"[FAMILLE] Intitulé : {intitule}") type_demande = famille_data.get("type", 0) if type_demande == 1: logger.warning( "[FAMILLE] Type 'Total' demandé mais IGNORÉ - Création en type Détail uniquement" ) factory_famille = self.cial.FactoryFamille try: index = 1 while index <= 1000: try: persist_test = factory_famille.List(index) if persist_test is None: break fam_test = win32com.client.CastTo( persist_test, "IBOFamille3" ) fam_test.Read() code_existant = ( getattr(fam_test, "FA_CodeFamille", "").strip().upper() ) if code_existant == code: raise ValueError(f"La famille {code} existe déjà") index += 1 except ValueError: raise except Exception: index += 1 except ValueError: raise persist = factory_famille.Create() famille = win32com.client.CastTo(persist, "IBOFamille3") famille.SetDefault() famille.FA_CodeFamille = code famille.FA_Intitule = intitule try: famille.FA_Type = 0 logger.info("[FAMILLE] Type : 0 (Détail)") except Exception as e: logger.warning(f"[FAMILLE] FA_Type non défini : {e}") compte_achat = famille_data.get("compte_achat") if compte_achat: try: factory_compte = self.cial.CptaApplication.FactoryCompteG persist_compte = factory_compte.ReadNumero(compte_achat) if persist_compte: compte_obj = win32com.client.CastTo( persist_compte, "IBOCompteG3" ) compte_obj.Read() famille.CompteGAchat = compte_obj logger.info(f"[FAMILLE] Compte achat : {compte_achat}") except Exception as e: logger.warning(f"[FAMILLE] Compte achat non défini : {e}") compte_vente = famille_data.get("compte_vente") if compte_vente: try: factory_compte = self.cial.CptaApplication.FactoryCompteG persist_compte = factory_compte.ReadNumero(compte_vente) if persist_compte: compte_obj = win32com.client.CastTo( persist_compte, "IBOCompteG3" ) compte_obj.Read() famille.CompteGVente = compte_obj logger.info(f"[FAMILLE] Compte vente : {compte_vente}") except Exception as e: logger.warning(f"[FAMILLE] Compte vente non défini : {e}") logger.info("[FAMILLE] Écriture dans Sage...") try: famille.Write() logger.info("[FAMILLE] 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})" ) except Exception: pass logger.error(f"[FAMILLE] Erreur Write() : {error_detail}") raise RuntimeError(f"Échec création famille : {error_detail}") famille.Read() resultat = { "code": getattr(famille, "FA_CodeFamille", "").strip(), "intitule": getattr(famille, "FA_Intitule", "").strip(), "type": 0, "type_libelle": "Détail", } logger.info(f"[FAMILLE] FAMILLE CRÉÉE : {code} - {intitule} (Détail)") return resultat except ValueError as e: logger.error(f"[FAMILLE] Erreur métier : {e}") raise except Exception as e: logger.error(f"[FAMILLE] Erreur technique : {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 Exception: pass 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() 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 -- 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 = [] 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}%"]) 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 """ cursor.execute(query, params) rows = cursor.fetchall() 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_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_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_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) familles = [] 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 += 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], "cb_creation": row[idx + 3], "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], "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: with self._get_sql_connection() as conn: cursor = conn.cursor() logger.info(f"[SQL] Lecture famille : {code}") cursor.execute("SELECT TOP 1 * FROM F_FAMILLE") colonnes_disponibles = [column[0] for column in cursor.description] logger.info(f"[SQL] Colonnes trouvées : {len(colonnes_disponibles)}") colonnes_souhaitees = [ "FA_CodeFamille", "FA_Intitule", "FA_Type", "FA_UniteVen", "FA_Coef", "FA_SuiviStock", "FA_Garantie", "FA_UnitePoids", "FA_Delai", "FA_NbColis", "CG_NumAch", "CG_NumVte", "FA_CodeFiscal", "FA_Escompte", "FA_Central", "FA_Nature", "CL_No1", "CL_No2", "CL_No3", "CL_No4", "FA_Stat01", "FA_Stat02", "FA_Stat03", "FA_Stat04", "FA_Stat05", "FA_HorsStat", "FA_Pays", "FA_VteDebit", "FA_NotImp", "FA_Contremarque", "FA_FactPoids", "FA_FactForfait", "FA_Publie", "FA_RacineRef", "FA_RacineCB", "FA_Raccourci", "FA_SousTraitance", "FA_Fictif", "FA_Criticite", ] colonnes_a_lire = [ col for col in colonnes_souhaitees if col in colonnes_disponibles ] if not colonnes_a_lire: colonnes_a_lire = colonnes_disponibles logger.info(f"[SQL] Colonnes sélectionnées : {len(colonnes_a_lire)}") colonnes_str = ", ".join([f"f.{col}" for col in colonnes_a_lire]) query = f""" SELECT {colonnes_str}, ISNULL(COUNT(a.AR_Ref), 0) as nb_articles FROM F_FAMILLE f LEFT JOIN F_ARTICLE a ON f.FA_CodeFamille = a.FA_CodeFamille WHERE UPPER(f.FA_CodeFamille) = ? GROUP BY {colonnes_str} """ cursor.execute(query, (code.upper().strip(),)) row = cursor.fetchone() if not row: raise ValueError(f"Famille '{code}' introuvable dans Sage") famille = {} for idx, colonne in enumerate(colonnes_a_lire): valeur = row[idx] if isinstance(valeur, str): valeur = valeur.strip() famille[colonne] = valeur famille["nb_articles"] = row[-1] if "FA_CodeFamille" in famille: famille["code"] = famille["FA_CodeFamille"] if "FA_Intitule" in famille: famille["intitule"] = famille["FA_Intitule"] if "FA_Type" in famille: type_val = famille["FA_Type"] famille["type"] = type_val famille["type_libelle"] = "Total" if type_val == 1 else "Détail" famille["est_total"] = type_val == 1 else: famille["type"] = 0 famille["type_libelle"] = "Détail" famille["est_total"] = False famille["unite_vente"] = str(famille.get("FA_UniteVen", "")) famille["unite_poids"] = str(famille.get("FA_UnitePoids", "")) famille["coef"] = ( float(famille.get("FA_Coef", 0.0)) if famille.get("FA_Coef") is not None else 0.0 ) famille["suivi_stock"] = bool(famille.get("FA_SuiviStock", 0)) famille["garantie"] = int(famille.get("FA_Garantie", 0)) famille["delai"] = int(famille.get("FA_Delai", 0)) famille["nb_colis"] = int(famille.get("FA_NbColis", 0)) famille["compte_achat"] = famille.get("CG_NumAch", "") famille["compte_vente"] = famille.get("CG_NumVte", "") famille["code_fiscal"] = famille.get("FA_CodeFiscal", "") famille["escompte"] = bool(famille.get("FA_Escompte", 0)) famille["est_centrale"] = bool(famille.get("FA_Central", 0)) famille["nature"] = famille.get("FA_Nature", 0) famille["pays"] = famille.get("FA_Pays", "") famille["categorie_1"] = famille.get("CL_No1", 0) famille["categorie_2"] = famille.get("CL_No2", 0) famille["categorie_3"] = famille.get("CL_No3", 0) famille["categorie_4"] = famille.get("CL_No4", 0) famille["stat_01"] = famille.get("FA_Stat01", "") famille["stat_02"] = famille.get("FA_Stat02", "") famille["stat_03"] = famille.get("FA_Stat03", "") famille["stat_04"] = famille.get("FA_Stat04", "") famille["stat_05"] = famille.get("FA_Stat05", "") famille["hors_statistique"] = bool(famille.get("FA_HorsStat", 0)) famille["vente_debit"] = bool(famille.get("FA_VteDebit", 0)) famille["non_imprimable"] = bool(famille.get("FA_NotImp", 0)) famille["contremarque"] = bool(famille.get("FA_Contremarque", 0)) famille["fact_poids"] = bool(famille.get("FA_FactPoids", 0)) famille["fact_forfait"] = bool(famille.get("FA_FactForfait", 0)) famille["publie"] = bool(famille.get("FA_Publie", 0)) famille["racine_reference"] = famille.get("FA_RacineRef", "") famille["racine_code_barre"] = famille.get("FA_RacineCB", "") famille["raccourci"] = famille.get("FA_Raccourci", "") famille["sous_traitance"] = bool(famille.get("FA_SousTraitance", 0)) famille["fictif"] = bool(famille.get("FA_Fictif", 0)) famille["criticite"] = int(famille.get("FA_Criticite", 0)) logger.info( f"SQL: Famille '{famille['code']}' chargée ({famille['nb_articles']} articles)" ) return famille except ValueError as e: logger.error(f"Erreur famille: {e}") raise except Exception as e: logger.error(f"Erreur SQL famille: {e}", exc_info=True) raise RuntimeError(f"Erreur lecture famille: {str(e)}") def creer_entree_stock(self, entree_data: Dict) -> Dict: try: with self._com_context(), self._lock_com: logger.info("[STOCK] === CRÉATION ENTRÉE STOCK (COM pur) ===") transaction_active = False try: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug("Transaction démarrée") except Exception: pass try: factory_doc = self.cial.FactoryDocumentStock persist_doc = factory_doc.CreateType(180) doc = win32com.client.CastTo(persist_doc, "IBODocumentStock3") doc.SetDefault() date_mouv = entree_data.get("date_mouvement") if isinstance(date_mouv, date): doc.DO_Date = pywintypes.Time( datetime.combine(date_mouv, datetime.min.time()) ) else: doc.DO_Date = pywintypes.Time(datetime.now()) if entree_data.get("reference"): doc.DO_Ref = entree_data["reference"] doc.Write() logger.info("[STOCK] Document créé") factory_article = self.cial.FactoryArticle factory_depot = self.cial.FactoryDepot stocks_mis_a_jour = [] depot_principal = None try: persist_depot = factory_depot.List(1) if persist_depot: depot_principal = win32com.client.CastTo( persist_depot, "IBODepot3" ) depot_principal.Read() logger.info( f"Dépôt principal: {getattr(depot_principal, 'DE_Code', '?')}" ) except Exception as e: logger.warning(f"Erreur chargement dépôt: {e}") try: factory_lignes = doc.FactoryDocumentLigne except Exception: factory_lignes = doc.FactoryDocumentStockLigne for idx, ligne_data in enumerate(entree_data["lignes"], 1): article_ref = ligne_data["article_ref"].upper() quantite = ligne_data["quantite"] stock_mini = ligne_data.get("stock_mini") stock_maxi = ligne_data.get("stock_maxi") logger.info(f"[STOCK] Ligne {idx}: {article_ref} x {quantite}") persist_article = factory_article.ReadReference(article_ref) if not persist_article: raise ValueError(f"Article {article_ref} introuvable") article_obj = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_obj.Read() ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentStockLigne3" ) ligne_obj.SetDefault() try: ligne_obj.SetDefaultArticleReference( article_ref, float(quantite) ) except Exception: try: ligne_obj.SetDefaultArticle( article_obj, float(quantite) ) except Exception: raise ValueError( f"Impossible de lier l'article {article_ref}" ) prix = ligne_data.get("prix_unitaire") if prix: try: ligne_obj.DL_PrixUnitaire = float(prix) except Exception: pass ligne_obj.Write() if stock_mini is not None or stock_maxi is not None: logger.info( f"[STOCK] Ajustement stock pour {article_ref}..." ) try: logger.info( " [COM] Méthode A : Article.FactoryArticleStock" ) factory_article = self.cial.FactoryArticle persist_article_full = factory_article.ReadReference( article_ref ) article_full = win32com.client.CastTo( persist_article_full, "IBOArticle3" ) article_full.Read() factory_article_stock = None try: factory_article_stock = ( article_full.FactoryArticleStock ) logger.info(" FactoryArticleStock trouvée") except AttributeError: logger.warning( " FactoryArticleStock non disponible" ) if factory_article_stock: stock_trouve = None index_stock = 1 while index_stock <= 100: try: stock_persist = factory_article_stock.List( index_stock ) if stock_persist is None: break stock_obj = win32com.client.CastTo( stock_persist, "IBOArticleStock3" ) stock_obj.Read() depot_stock = None try: depot_stock = getattr( stock_obj, "Depot", None ) if depot_stock: depot_stock.Read() depot_code = getattr( depot_stock, "DE_Code", "" ).strip() logger.debug( f" Dépôt {index_stock}: {depot_code}" ) if ( not stock_trouve or depot_code == getattr( depot_principal, "DE_Code", "", ) ): stock_trouve = stock_obj logger.info( f" Stock trouvé pour dépôt {depot_code}" ) except Exception: pass index_stock += 1 except Exception as e: logger.debug( f" Erreur stock {index_stock}: {e}" ) index_stock += 1 if not stock_trouve: try: stock_persist = ( factory_article_stock.Create() ) stock_trouve = win32com.client.CastTo( stock_persist, "IBOArticleStock3" ) stock_trouve.SetDefault() if depot_principal: try: stock_trouve.Depot = depot_principal logger.info( " Dépôt principal lié" ) except Exception: pass logger.info(" Nouvel ArticleStock créé") except Exception as e: logger.error( f" Impossible de créer ArticleStock: {e}" ) raise if stock_trouve: try: stock_trouve.Read() except Exception: pass if stock_mini is not None: try: for prop_name in [ "AS_QteMini", "AS_Mini", "AR_StockMini", "StockMini", ]: try: setattr( stock_trouve, prop_name, float(stock_mini), ) logger.info( f" Stock mini défini via {prop_name}: {stock_mini}" ) break except AttributeError: continue except Exception as e: logger.debug( f" {prop_name} échoué: {e}" ) continue except Exception as e: logger.warning( f" Stock mini non défini: {e}" ) if stock_maxi is not None: try: for prop_name in [ "AS_QteMaxi", "AS_Maxi", "AR_StockMaxi", "StockMaxi", ]: try: setattr( stock_trouve, prop_name, float(stock_maxi), ) logger.info( f" Stock maxi défini via {prop_name}: {stock_maxi}" ) break except AttributeError: continue except Exception as e: logger.debug( f" {prop_name} échoué: {e}" ) continue except Exception as e: logger.warning( f" Stock maxi non défini: {e}" ) try: stock_trouve.Write() logger.info(" ArticleStock sauvegardé") except Exception as e: logger.error( f" Erreur Write() ArticleStock: {e}" ) raise if depot_principal and ( stock_mini is not None or stock_maxi is not None ): logger.info( " [COM] Méthode B : Depot.FactoryDepotStock (alternative)" ) try: factory_depot_stock = None for factory_name in [ "FactoryDepotStock", "FactoryArticleStock", ]: try: factory_depot_stock = getattr( depot_principal, factory_name, None ) if factory_depot_stock: logger.info( f" Factory trouvée: {factory_name}" ) break except Exception: continue if factory_depot_stock: stock_depot_trouve = None index_ds = 1 while index_ds <= 100: try: stock_ds_persist = ( factory_depot_stock.List( index_ds ) ) if stock_ds_persist is None: break stock_ds = win32com.client.CastTo( stock_ds_persist, "IBODepotStock3", ) stock_ds.Read() ar_ref_ds = ( getattr(stock_ds, "AR_Ref", "") .strip() .upper() ) if ar_ref_ds == article_ref: stock_depot_trouve = stock_ds break index_ds += 1 except Exception: index_ds += 1 if not stock_depot_trouve: try: stock_ds_persist = ( factory_depot_stock.Create() ) stock_depot_trouve = ( win32com.client.CastTo( stock_ds_persist, "IBODepotStock3", ) ) stock_depot_trouve.SetDefault() stock_depot_trouve.AR_Ref = ( article_ref ) logger.info( " Nouveau DepotStock créé" ) except Exception as e: logger.error( f" Impossible de créer DepotStock: {e}" ) if stock_depot_trouve: if stock_mini is not None: try: stock_depot_trouve.AS_QteMini = float( stock_mini ) logger.info( f" DepotStock.AS_QteMini = {stock_mini}" ) except Exception as e: logger.warning( f" DepotStock mini échoué: {e}" ) if stock_maxi is not None: try: stock_depot_trouve.AS_QteMaxi = float( stock_maxi ) logger.info( f" DepotStock.AS_QteMaxi = {stock_maxi}" ) except Exception as e: logger.warning( f" DepotStock maxi échoué: {e}" ) try: stock_depot_trouve.Write() logger.info( " DepotStock sauvegardé" ) except Exception as e: logger.error( f" DepotStock Write() échoué: {e}" ) except Exception as e: logger.warning(f" Méthode B échouée: {e}") except Exception as e: logger.error( f"[STOCK] Erreur ajustement stock: {e}", exc_info=True, ) stocks_mis_a_jour.append( { "article_ref": article_ref, "quantite_ajoutee": quantite, "stock_mini_defini": stock_mini, "stock_maxi_defini": stock_maxi, } ) doc.Write() doc.Read() numero = getattr(doc, "DO_Piece", "") logger.info(f"[STOCK] Document finalisé: {numero}") logger.info("[STOCK] Vérification finale via COM...") for stock_info in stocks_mis_a_jour: article_ref = stock_info["article_ref"] try: persist_article = factory_article.ReadReference(article_ref) article_verif = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_verif.Read() stock_total = 0.0 stock_mini_lu = 0.0 stock_maxi_lu = 0.0 for attr in ["AR_Stock", "AS_QteSto", "Stock"]: try: val = getattr(article_verif, attr, None) if val is not None: stock_total = float(val) break except Exception: pass for attr in ["AR_StockMini", "AS_QteMini", "StockMini"]: try: val = getattr(article_verif, attr, None) if val is not None: stock_mini_lu = float(val) break except Exception: pass for attr in ["AR_StockMaxi", "AS_QteMaxi", "StockMaxi"]: try: val = getattr(article_verif, attr, None) if val is not None: stock_maxi_lu = float(val) break except Exception: pass logger.info( f"[VERIF] {article_ref}: " f"Total={stock_total}, " f"Mini={stock_mini_lu}, " f"Maxi={stock_maxi_lu}" ) stock_info["stock_total_verifie"] = stock_total stock_info["stock_mini_verifie"] = stock_mini_lu stock_info["stock_maxi_verifie"] = stock_maxi_lu except Exception as e: logger.warning( f"[VERIF] Erreur vérification {article_ref}: {e}" ) if transaction_active: try: self.cial.CptaApplication.CommitTrans() logger.info("[STOCK] Transaction committée") except Exception: logger.info("[STOCK] Changements sauvegardés") return { "article_ref": article_ref, "numero": numero, "type": 180, "type_libelle": "Entrée en stock", "date": str(getattr(doc, "DO_Date", "")), "nb_lignes": len(stocks_mis_a_jour), "stocks_mis_a_jour": stocks_mis_a_jour, } except Exception as e: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() logger.info("[STOCK] Transaction annulée") except Exception: pass logger.error(f"[STOCK] ERREUR : {e}", exc_info=True) raise ValueError(f"Erreur création entrée stock : {str(e)}") except Exception as e: logger.error(f"[STOCK] ERREUR GLOBALE : {e}", exc_info=True) raise ValueError(f"Erreur création entrée stock : {str(e)}") def creer_sortie_stock(self, sortie_data: Dict) -> Dict: try: with ( self._com_context(), self._lock_com, self._get_sql_connection() as conn, ): cursor = conn.cursor() logger.info("[STOCK] === CRÉATION SORTIE STOCK ===") logger.info(f"[STOCK] {len(sortie_data.get('lignes', []))} ligne(s)") try: self.cial.CptaApplication.BeginTrans() except Exception: pass try: factory = self.cial.FactoryDocumentStock persist = factory.CreateType(181) doc = win32com.client.CastTo(persist, "IBODocumentStock3") doc.SetDefault() date_mouv = sortie_data.get("date_mouvement") if isinstance(date_mouv, date): doc.DO_Date = pywintypes.Time( datetime.combine(date_mouv, datetime.min.time()) ) else: doc.DO_Date = pywintypes.Time(datetime.now()) if sortie_data.get("reference"): doc.DO_Ref = sortie_data["reference"] doc.Write() logger.info( f"[STOCK] Document créé : {getattr(doc, 'DO_Piece', '?')}" ) try: factory_lignes = doc.FactoryDocumentLigne except Exception: factory_lignes = doc.FactoryDocumentStockLigne factory_article = self.cial.FactoryArticle stocks_mis_a_jour = [] for idx, ligne_data in enumerate(sortie_data["lignes"], 1): article_ref = ligne_data["article_ref"].upper() quantite = ligne_data["quantite"] logger.info( f"[STOCK] ======== LIGNE {idx} : {article_ref} x {quantite} ========" ) persist_article = factory_article.ReadReference(article_ref) if not persist_article: raise ValueError(f"Article {article_ref} introuvable") article_obj = win32com.client.CastTo( persist_article, "IBOArticle3" ) article_obj.Read() ar_suivi = getattr(article_obj, "AR_SuiviStock", 0) ar_design = getattr(article_obj, "AR_Design", article_ref) logger.info(f"[STOCK] Article : {ar_design}") logger.info(f"[STOCK] AR_SuiviStock : {ar_suivi}") stock_dispo = verifier_stock_suffisant( article_ref, quantite, cursor, None ) if not stock_dispo["suffisant"]: raise ValueError( f"Stock insuffisant pour {article_ref} : " f"disponible={stock_dispo['stock_disponible']}, " f"demandé={quantite}" ) logger.info( f"[STOCK] Stock disponible : {stock_dispo['stock_disponible']}" ) numero_lot = ligne_data.get("numero_lot") if ar_suivi == 1: if numero_lot: logger.warning("[STOCK] CMUP : Suppression du lot") numero_lot = None elif ar_suivi == 2: if not numero_lot: import uuid numero_lot = f"AUTO-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6].upper()}" logger.info( f"[STOCK] FIFO/LIFO : Lot auto-généré '{numero_lot}'" ) ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentStockLigne3" ) ligne_obj.SetDefault() article_lie = False try: ligne_obj.SetDefaultArticleReference( article_ref, float(quantite) ) article_lie = True logger.info("[STOCK] SetDefaultArticleReference()") except Exception: try: ligne_obj.SetDefaultArticle( article_obj, float(quantite) ) article_lie = True logger.info("[STOCK] SetDefaultArticle()") except Exception: pass if not article_lie: raise ValueError( f"Impossible de lier l'article {article_ref}" ) if numero_lot and ar_suivi == 2: try: ligne_obj.SetDefaultLot(numero_lot) logger.info("[STOCK] Lot défini") except Exception: try: ligne_obj.LS_NoSerie = numero_lot logger.info("[STOCK] Lot via LS_NoSerie") except Exception: pass prix = ligne_data.get("prix_unitaire") if prix: try: ligne_obj.DL_PrixUnitaire = float(prix) except Exception: pass ligne_obj.Write() logger.info("[STOCK] Write() réussi") ligne_obj.Read() ref_verifiee = article_ref try: article_lie_obj = getattr(ligne_obj, "Article", None) if article_lie_obj: article_lie_obj.Read() ref_verifiee = ( getattr(article_lie_obj, "AR_Ref", "").strip() or article_ref ) except Exception: pass logger.info(f"[STOCK] LIGNE {idx} COMPLÈTE") stocks_mis_a_jour.append( { "article_ref": article_ref, "quantite_retiree": quantite, "reference_verifiee": ref_verifiee, "stock_avant": stock_dispo["stock_disponible"], "stock_apres": stock_dispo["stock_apres"], "numero_lot": numero_lot if ar_suivi == 2 else None, } ) doc.Write() doc.Read() numero = getattr(doc, "DO_Piece", "") logger.info(f"[STOCK] Document finalisé : {numero}") try: self.cial.CptaApplication.CommitTrans() logger.info("[STOCK] Transaction committée") except Exception: pass return { "numero": numero, "type": 1, "date": str(getattr(doc, "DO_Date", "")), "nb_lignes": len(stocks_mis_a_jour), "reference": sortie_data.get("reference"), "stocks_mis_a_jour": stocks_mis_a_jour, } except Exception as e: logger.error(f"[STOCK] ERREUR : {e}", exc_info=True) try: self.cial.CptaApplication.RollbackTrans() except Exception: pass raise ValueError(f"Erreur création sortie stock : {str(e)}") except Exception as e: logger.error(f"[STOCK] ERREUR GLOBALE : {e}", exc_info=True) raise ValueError(f"Erreur création sortie stock : {str(e)}") def lire_mouvement_stock(self, numero: str) -> Dict: try: with self._com_context(), self._lock_com: factory = self.cial.FactoryDocumentStock persist = None index = 1 logger.info(f"[MOUVEMENT] Recherche de {numero}...") while index < 10000: try: persist_test = factory.List(index) if persist_test is None: break doc_test = win32com.client.CastTo( persist_test, "IBODocumentStock3" ) doc_test.Read() if getattr(doc_test, "DO_Piece", "") == numero: persist = persist_test logger.info(f"[MOUVEMENT] Trouvé à l'index {index}") break index += 1 except Exception: index += 1 if not persist: raise ValueError(f"Mouvement {numero} introuvable") doc = win32com.client.CastTo(persist, "IBODocumentStock3") doc.Read() do_type = getattr(doc, "DO_Type", -1) types_mouvements = { 180: "Entrée", 181: "Sortie", 182: "Transfert", 183: "Inventaire", } mouvement = { "numero": numero, "type": do_type, "type_libelle": types_mouvements.get(do_type, f"Type {do_type}"), "date": str(getattr(doc, "DO_Date", "")), "reference": getattr(doc, "DO_Ref", ""), "lignes": [], } try: factory_lignes = getattr( doc, "FactoryDocumentLigne", None ) or getattr(doc, "FactoryDocumentStockLigne", None) if factory_lignes: idx = 1 while idx <= 100: try: ligne_p = factory_lignes.List(idx) if ligne_p is None: break try: ligne = win32com.client.CastTo( ligne_p, "IBODocumentLigne3" ) except Exception: ligne = win32com.client.CastTo( ligne_p, "IBODocumentStockLigne3" ) ligne.Read() article_ref = "" try: article_obj = getattr(ligne, "Article", None) if article_obj: article_obj.Read() article_ref = getattr( article_obj, "AR_Ref", "" ).strip() except Exception: pass ligne_info = { "article_ref": article_ref, "designation": getattr(ligne, "DL_Design", ""), "quantite": float(getattr(ligne, "DL_Qte", 0.0)), "prix_unitaire": float( getattr(ligne, "DL_PrixUnitaire", 0.0) ), "montant_ht": float( getattr(ligne, "DL_MontantHT", 0.0) ), "numero_lot": getattr(ligne, "LS_NoSerie", ""), } mouvement["lignes"].append(ligne_info) idx += 1 except Exception: break except Exception as e: logger.warning(f"[MOUVEMENT] Erreur lecture lignes : {e}") mouvement["nb_lignes"] = len(mouvement["lignes"]) logger.info( f"[MOUVEMENT] Lu : {numero} - {mouvement['nb_lignes']} ligne(s)" ) return mouvement except ValueError: raise except Exception as e: logger.error(f"[MOUVEMENT] Erreur : {e}", exc_info=True) raise ValueError(f"Erreur lecture mouvement : {str(e)}") def lister_tous_tiers( self, type_tiers: Optional[str] = None, filtre: str = "" ) -> List[Dict]: try: with self._get_sql_connection() as conn: cursor = conn.cursor() query = _build_tiers_select_query() query += " FROM F_COMPTET WHERE 1=1" params = [] if type_tiers and type_tiers != "all": if type_tiers == "prospect": query += " AND CT_Type = 0 AND CT_Prospect = 1" elif type_tiers == "client": query += " AND CT_Type = 0 AND CT_Prospect = 0" elif type_tiers == "fournisseur": query += " AND CT_Type = 1" if filtre: query += " AND (CT_Num LIKE ? OR CT_Intitule LIKE ?)" params.extend([f"%{filtre}%", f"%{filtre}%"]) query += " ORDER BY CT_Intitule" cursor.execute(query, params) rows = cursor.fetchall() tiers_list = [] for row in rows: tiers = _row_to_tiers_dict(row) tiers["contacts"] = _get_contacts_client(row.CT_Num, conn) tiers_list.append(tiers) logger.info( f" SQL: {len(tiers_list)} tiers retournés (type={type_tiers}, filtre={filtre})" ) return tiers_list except Exception as e: logger.error(f" Erreur SQL tiers: {e}") raise RuntimeError(f"Erreur lecture tiers: {str(e)}") def lire_tiers(self, code: str) -> Optional[Dict]: """Lit un tiers (client/fournisseur/prospect) par code""" try: with self._get_sql_connection() as conn: cursor = conn.cursor() query = _build_tiers_select_query() query += " FROM F_COMPTET WHERE CT_Num = ?" cursor.execute(query, (code.upper(),)) row = cursor.fetchone() if not row: return None tiers = _row_to_tiers_dict(row) tiers["contacts"] = _get_contacts_client(row.CT_Num, conn) logger.info(f" SQL: Tiers {code} lu avec succès") return tiers except Exception as e: logger.error(f" Erreur SQL tiers {code}: {e}") return None