import win32com.client import pythoncom # AJOUT CRITIQUE from datetime import datetime, timedelta, date from typing import Dict, List, Optional, Any import threading import time import logging from config import settings, validate_settings import pyodbc from contextlib import contextmanager import pywintypes import os import glob import tempfile import logging from dataclasses import dataclass, field import zlib import struct 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 _safe_strip(self, value): """Strip sécurisé pour valeurs SQL""" if value is None: return None if isinstance(value, str): return value.strip() return value 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: 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: cursor = 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: pass def lister_tous_fournisseurs(self, filtre=""): """ Liste tous les fournisseurs avec TOUS les champs Symétrie complète avec lister_tous_clients """ 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": self._safe_strip(row.CT_Num), "intitule": self._safe_strip(row.CT_Intitule), "type_tiers": row.CT_Type, "qualite": self._safe_strip(row.CT_Qualite), "classement": self._safe_strip(row.CT_Classement), "raccourci": self._safe_strip(row.CT_Raccourci), "siret": self._safe_strip(row.CT_Siret), "tva_intra": self._safe_strip(row.CT_Identifiant), "code_naf": self._safe_strip(row.CT_Ape), "contact": self._safe_strip(row.CT_Contact), "adresse": self._safe_strip(row.CT_Adresse), "complement": self._safe_strip(row.CT_Complement), "code_postal": self._safe_strip(row.CT_CodePostal), "ville": self._safe_strip(row.CT_Ville), "region": self._safe_strip(row.CT_CodeRegion), "pays": self._safe_strip(row.CT_Pays), "telephone": self._safe_strip(row.CT_Telephone), "telecopie": self._safe_strip(row.CT_Telecopie), "email": self._safe_strip(row.CT_EMail), "site_web": self._safe_strip(row.CT_Site), "facebook": self._safe_strip(row.CT_Facebook), "linkedin": self._safe_strip(row.CT_LinkedIn), "taux01": row.CT_Taux01, "taux02": row.CT_Taux02, "taux03": row.CT_Taux03, "taux04": row.CT_Taux04, "statistique01": self._safe_strip(row.CT_Statistique01), "statistique02": self._safe_strip(row.CT_Statistique02), "statistique03": self._safe_strip(row.CT_Statistique03), "statistique04": self._safe_strip(row.CT_Statistique04), "statistique05": self._safe_strip(row.CT_Statistique05), "statistique06": self._safe_strip(row.CT_Statistique06), "statistique07": self._safe_strip(row.CT_Statistique07), "statistique08": self._safe_strip(row.CT_Statistique08), "statistique09": self._safe_strip(row.CT_Statistique09), "statistique10": self._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": self._safe_strip(row.CT_Commentaire), "section_analytique": self._safe_strip(row.CA_Num), "mode_reglement_code": row.MR_No, "surveillance_active": (row.CT_Surveillance == 1), "coface": self._safe_strip(row.CT_Coface), "forme_juridique": self._safe_strip(row.CT_SvFormeJuri), "effectif": self._safe_strip(row.CT_SvEffectif), "sv_regularite": self._safe_strip(row.CT_SvRegul), "sv_cotation": self._safe_strip(row.CT_SvCotation), "sv_objet_maj": self._safe_strip(row.CT_SvObjetMaj), "sv_chiffre_affaires": row.CT_SvCA, "sv_resultat": row.CT_SvResultat, "compte_general": self._safe_strip(row.CG_NumPrinc), "categorie_tarif": row.N_CatTarif, "categorie_compta": row.N_CatCompta, } fournisseur["contacts"] = self._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): """ Lit un fournisseur avec TOUS les champs (identique à lister_tous_fournisseurs) Symétrie complète GET/POST """ 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": self._safe_strip(row.CT_Num), "intitule": self._safe_strip(row.CT_Intitule), "type_tiers": row.CT_Type, "qualite": self._safe_strip(row.CT_Qualite), "classement": self._safe_strip(row.CT_Classement), "raccourci": self._safe_strip(row.CT_Raccourci), "siret": self._safe_strip(row.CT_Siret), "tva_intra": self._safe_strip(row.CT_Identifiant), "code_naf": self._safe_strip(row.CT_Ape), "contact": self._safe_strip(row.CT_Contact), "adresse": self._safe_strip(row.CT_Adresse), "complement": self._safe_strip(row.CT_Complement), "code_postal": self._safe_strip(row.CT_CodePostal), "ville": self._safe_strip(row.CT_Ville), "region": self._safe_strip(row.CT_CodeRegion), "pays": self._safe_strip(row.CT_Pays), "telephone": self._safe_strip(row.CT_Telephone), "telecopie": self._safe_strip(row.CT_Telecopie), "email": self._safe_strip(row.CT_EMail), "site_web": self._safe_strip(row.CT_Site), "facebook": self._safe_strip(row.CT_Facebook), "linkedin": self._safe_strip(row.CT_LinkedIn), "taux01": row.CT_Taux01, "taux02": row.CT_Taux02, "taux03": row.CT_Taux03, "taux04": row.CT_Taux04, "statistique01": self._safe_strip(row.CT_Statistique01), "statistique02": self._safe_strip(row.CT_Statistique02), "statistique03": self._safe_strip(row.CT_Statistique03), "statistique04": self._safe_strip(row.CT_Statistique04), "statistique05": self._safe_strip(row.CT_Statistique05), "statistique06": self._safe_strip(row.CT_Statistique06), "statistique07": self._safe_strip(row.CT_Statistique07), "statistique08": self._safe_strip(row.CT_Statistique08), "statistique09": self._safe_strip(row.CT_Statistique09), "statistique10": self._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": self._safe_strip(row.CT_Commentaire), "section_analytique": self._safe_strip(row.CA_Num), "mode_reglement_code": row.MR_No, "surveillance_active": (row.CT_Surveillance == 1), "coface": self._safe_strip(row.CT_Coface), "forme_juridique": self._safe_strip(row.CT_SvFormeJuri), "effectif": self._safe_strip(row.CT_SvEffectif), "sv_regularite": self._safe_strip(row.CT_SvRegul), "sv_cotation": self._safe_strip(row.CT_SvCotation), "sv_objet_maj": self._safe_strip(row.CT_SvObjetMaj), "sv_chiffre_affaires": row.CT_SvCA, "sv_resultat": row.CT_SvResultat, "compte_general": self._safe_strip(row.CG_NumPrinc), "categorie_tarif": row.N_CatTarif, "categorie_compta": row.N_CatCompta, } fournisseur["contacts"] = self._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 # 1 = Fournisseur logger.debug(" CT_Type: 1 (Fournisseur)") except: logger.debug(" CT_Type non défini (géré par FactoryFournisseur)") try: fournisseur.CT_Qualite = "FOU" logger.debug(" CT_Qualite: 'FOU'") except: 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: 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, # Fournisseur "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: 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 = self._cast_client(persist) # Réutiliser _cast_client 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: 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: 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: 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: pass raise RuntimeError(f"Erreur technique Sage: {error_message}") def _get_contacts_client(self, ct_num: str, conn) -> list: """ Récupère tous les contacts d'un client avec TOUS les champs """ try: cursor = conn.cursor() query = """ SELECT -- IDENTIFICATION CT_Num, CT_No, N_Contact, -- IDENTITÉ CT_Civilite, CT_Nom, CT_Prenom, CT_Fonction, -- ORGANISATION N_Service, -- COORDONNÉES CT_Telephone, CT_TelPortable, CT_Telecopie, CT_EMail, -- RÉSEAUX SOCIAUX CT_Facebook, CT_LinkedIn, CT_Skype FROM F_CONTACTT WHERE CT_Num = ? ORDER BY N_Contact, CT_Nom, CT_Prenom """ cursor.execute(query, [ct_num]) rows = cursor.fetchall() contacts = [] for row in rows: contact = { "ct_num": self._safe_strip(row.CT_Num), "ct_no": row.CT_No, "n_contact": row.N_Contact, "civilite": self._safe_strip(row.CT_Civilite), "nom": self._safe_strip(row.CT_Nom), "prenom": self._safe_strip(row.CT_Prenom), "fonction": self._safe_strip(row.CT_Fonction), "service_code": row.N_Service, "telephone": self._safe_strip(row.CT_Telephone), "portable": self._safe_strip(row.CT_TelPortable), "telecopie": self._safe_strip(row.CT_Telecopie), "email": self._safe_strip(row.CT_EMail), "facebook": self._safe_strip(row.CT_Facebook), "linkedin": self._safe_strip(row.CT_LinkedIn), "skype": self._safe_strip(row.CT_Skype) } contacts.append(contact) return contacts except Exception as e: logger.warning(f"⚠️ Impossible de récupérer contacts pour {ct_num}: {e}") return [] 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": self._safe_strip(row.CT_Num), "intitule": self._safe_strip(row.CT_Intitule), "type_tiers": row.CT_Type, "qualite": self._safe_strip(row.CT_Qualite), "classement": self._safe_strip(row.CT_Classement), "raccourci": self._safe_strip(row.CT_Raccourci), "siret": self._safe_strip(row.CT_Siret), "tva_intra": self._safe_strip(row.CT_Identifiant), "code_naf": self._safe_strip(row.CT_Ape), "contact": self._safe_strip(row.CT_Contact), "adresse": self._safe_strip(row.CT_Adresse), "complement": self._safe_strip(row.CT_Complement), "code_postal": self._safe_strip(row.CT_CodePostal), "ville": self._safe_strip(row.CT_Ville), "region": self._safe_strip(row.CT_CodeRegion), "pays": self._safe_strip(row.CT_Pays), "telephone": self._safe_strip(row.CT_Telephone), "telecopie": self._safe_strip(row.CT_Telecopie), "email": self._safe_strip(row.CT_EMail), "site_web": self._safe_strip(row.CT_Site), "facebook": self._safe_strip(row.CT_Facebook), "linkedin": self._safe_strip(row.CT_LinkedIn), "taux01": row.CT_Taux01, "taux02": row.CT_Taux02, "taux03": row.CT_Taux03, "taux04": row.CT_Taux04, "statistique01": self._safe_strip(row.CT_Statistique01), "statistique02": self._safe_strip(row.CT_Statistique02), "statistique03": self._safe_strip(row.CT_Statistique03), "statistique04": self._safe_strip(row.CT_Statistique04), "statistique05": self._safe_strip(row.CT_Statistique05), "statistique06": self._safe_strip(row.CT_Statistique06), "statistique07": self._safe_strip(row.CT_Statistique07), "statistique08": self._safe_strip(row.CT_Statistique08), "statistique09": self._safe_strip(row.CT_Statistique09), "statistique10": self._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": self._safe_strip(row.CT_Commentaire), "section_analytique": self._safe_strip(row.CA_Num), "mode_reglement_code": row.MR_No, "surveillance_active": (row.CT_Surveillance == 1), "coface": self._safe_strip(row.CT_Coface), "forme_juridique": self._safe_strip(row.CT_SvFormeJuri), "effectif": self._safe_strip(row.CT_SvEffectif), "sv_regularite": self._safe_strip(row.CT_SvRegul), "sv_cotation": self._safe_strip(row.CT_SvCotation), "sv_objet_maj": self._safe_strip(row.CT_SvObjetMaj), "sv_chiffre_affaires": row.CT_SvCA, "sv_resultat": row.CT_SvResultat, "compte_general": self._safe_strip(row.CG_NumPrinc), "categorie_tarif": row.N_CatTarif, "categorie_compta": row.N_CatCompta, } client["contacts"] = self._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": self._safe_strip(row.CT_Num), "intitule": self._safe_strip(row.CT_Intitule), "type_tiers": row.CT_Type, "qualite": self._safe_strip(row.CT_Qualite), "classement": self._safe_strip(row.CT_Classement), "raccourci": self._safe_strip(row.CT_Raccourci), "siret": self._safe_strip(row.CT_Siret), "tva_intra": self._safe_strip(row.CT_Identifiant), "code_naf": self._safe_strip(row.CT_Ape), "contact": self._safe_strip(row.CT_Contact), "adresse": self._safe_strip(row.CT_Adresse), "complement": self._safe_strip(row.CT_Complement), "code_postal": self._safe_strip(row.CT_CodePostal), "ville": self._safe_strip(row.CT_Ville), "region": self._safe_strip(row.CT_CodeRegion), "pays": self._safe_strip(row.CT_Pays), "telephone": self._safe_strip(row.CT_Telephone), "telecopie": self._safe_strip(row.CT_Telecopie), "email": self._safe_strip(row.CT_EMail), "site_web": self._safe_strip(row.CT_Site), "facebook": self._safe_strip(row.CT_Facebook), "linkedin": self._safe_strip(row.CT_LinkedIn), "taux01": row.CT_Taux01, "taux02": row.CT_Taux02, "taux03": row.CT_Taux03, "taux04": row.CT_Taux04, "statistique01": self._safe_strip(row.CT_Statistique01), "statistique02": self._safe_strip(row.CT_Statistique02), "statistique03": self._safe_strip(row.CT_Statistique03), "statistique04": self._safe_strip(row.CT_Statistique04), "statistique05": self._safe_strip(row.CT_Statistique05), "statistique06": self._safe_strip(row.CT_Statistique06), "statistique07": self._safe_strip(row.CT_Statistique07), "statistique08": self._safe_strip(row.CT_Statistique08), "statistique09": self._safe_strip(row.CT_Statistique09), "statistique10": self._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": self._safe_strip(row.CT_Commentaire), "section_analytique": self._safe_strip(row.CA_Num), "mode_reglement_code": row.MR_No, "surveillance_active": (row.CT_Surveillance == 1), "coface": self._safe_strip(row.CT_Coface), "forme_juridique": self._safe_strip(row.CT_SvFormeJuri), "effectif": self._safe_strip(row.CT_SvEffectif), "sv_regularite": self._safe_strip(row.CT_SvRegul), "sv_cotation": self._safe_strip(row.CT_SvCotation), "sv_objet_maj": self._safe_strip(row.CT_SvObjetMaj), "sv_chiffre_affaires": row.CT_SvCA, "sv_resultat": row.CT_SvResultat, "compte_general": self._safe_strip(row.CG_NumPrinc), "categorie_tarif": row.N_CatTarif, "categorie_compta": row.N_CatCompta, } client["contacts"] = self._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 = self._mapper_article_depuis_row(row_data, colonnes_config) articles.append(article_data) articles = self._enrichir_stocks_articles(articles, cursor) articles = self._enrichir_familles_articles(articles, cursor) articles = self._enrichir_fournisseurs_articles(articles, cursor) articles = self._enrichir_tva_articles(articles, cursor) articles = self._enrichir_stock_emplacements(articles, cursor) articles = self._enrichir_gammes_articles(articles, cursor) articles = self._enrichir_tarifs_clients(articles, cursor) articles = self._enrichir_nomenclature(articles, cursor) articles = self._enrichir_compta_articles(articles, cursor) articles = self._enrichir_fournisseurs_multiples(articles, cursor) articles = self._enrichir_depots_details(articles, cursor) articles = self._enrichir_emplacements_details(articles, cursor) articles = self._enrichir_gammes_enumeres(articles, cursor) articles = self._enrichir_references_enumerees(articles, cursor) articles = self._enrichir_medias_articles(articles, cursor) articles = self._enrichir_prix_gammes(articles, cursor) articles = self._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 _enrichir_stock_emplacements(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit avec le détail du stock par emplacement Structure: articles[i]["emplacements"] = [{"depot": "01", "emplacement": "A1", "qte": 10}, ...] """ try: logger.info(f" → Enrichissement stock emplacements...") references = [a["reference"] for a in articles if a["reference"]] if not references: return articles placeholders = ",".join(["?"] * len(references)) query = f""" SELECT AR_Ref, DE_No, DP_No, AE_QteSto, AE_QtePrepa, AE_QteAControler, cbCreation, cbModification FROM F_ARTSTOCKEMPL WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, DE_No, DP_No """ cursor.execute(query, references) rows = cursor.fetchall() emplacements_map = {} for row in rows: ref = self._safe_strip(row[0]) if not ref: continue if ref not in emplacements_map: emplacements_map[ref] = [] emplacements_map[ref].append({ "depot": self._safe_strip(row[1]), "emplacement": self._safe_strip(row[2]), "qte_stockee": float(row[3]) if row[3] else 0.0, "qte_preparee": float(row[4]) if row[4] else 0.0, "qte_a_controler": float(row[5]) if row[5] else 0.0, "date_creation": row[6], "date_modification": row[7], }) for article in articles: article["emplacements"] = emplacements_map.get(article["reference"], []) article["nb_emplacements"] = len(article["emplacements"]) logger.info(f" ✓ {len(emplacements_map)} articles avec emplacements") return articles except Exception as e: logger.error(f" ✗ Erreur stock emplacements: {e}") for article in articles: article["emplacements"] = [] article["nb_emplacements"] = 0 return articles def _enrichir_gammes_articles(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit avec les gammes (taille, couleur, etc.) Structure: articles[i]["gammes"] = [{"numero": 1, "enumere": "001", "type": 0}, ...] """ try: logger.info(f" → Enrichissement gammes articles...") references = [a["reference"] for a in articles if a["reference"]] if not references: return articles placeholders = ",".join(["?"] * len(references)) query = f""" SELECT AR_Ref, AG_No, EG_Enumere, AG_Type, cbCreation, cbModification FROM F_ARTGAMME WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, AG_No, EG_Enumere """ cursor.execute(query, references) rows = cursor.fetchall() gammes_map = {} for row in rows: ref = self._safe_strip(row[0]) if not ref: continue if ref not in gammes_map: gammes_map[ref] = [] gammes_map[ref].append({ "numero_gamme": int(row[1]) if row[1] else 0, "enumere": self._safe_strip(row[2]), "type_gamme": int(row[3]) if row[3] else 0, "date_creation": row[4], "date_modification": row[5], }) for article in articles: article["gammes"] = gammes_map.get(article["reference"], []) article["nb_gammes"] = len(article["gammes"]) logger.info(f" ✓ {len(gammes_map)} articles avec gammes") return articles except Exception as e: logger.error(f" ✗ Erreur gammes: {e}") for article in articles: article["gammes"] = [] article["nb_gammes"] = 0 return articles def _enrichir_tarifs_clients(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit avec les tarifs spécifiques par client/catégorie tarifaire Structure: articles[i]["tarifs_clients"] = [{"client": "CLI001", "prix": 125.5}, ...] """ try: logger.info(f" → Enrichissement tarifs clients...") references = [a["reference"] for a in articles if a["reference"]] if not references: return articles placeholders = ",".join(["?"] * len(references)) query = f""" SELECT AR_Ref, AC_Categorie, CT_Num, AC_PrixVen, AC_Coef, AC_PrixTTC, AC_Arrondi, AC_QteMont, EG_Champ, AC_PrixDev, AC_Devise, AC_Remise, AC_Calcul, AC_TypeRem, AC_RefClient, AC_CoefNouv, AC_PrixVenNouv, AC_PrixDevNouv, AC_RemiseNouv, AC_DateApplication, cbCreation, cbModification FROM F_ARTCLIENT WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, AC_Categorie, CT_Num """ cursor.execute(query, references) rows = cursor.fetchall() tarifs_map = {} for row in rows: ref = self._safe_strip(row[0]) if not ref: continue if ref not in tarifs_map: tarifs_map[ref] = [] tarifs_map[ref].append({ "categorie": int(row[1]) if row[1] else 0, "client_num": self._safe_strip(row[2]), "prix_vente": float(row[3]) if row[3] else 0.0, "coefficient": float(row[4]) if row[4] else 0.0, "prix_ttc": float(row[5]) if row[5] else 0.0, "arrondi": float(row[6]) if row[6] else 0.0, "qte_montant": float(row[7]) if row[7] else 0.0, "enumere_gamme": int(row[8]) if row[8] else 0, "prix_devise": float(row[9]) if row[9] else 0.0, "devise": int(row[10]) if row[10] else 0, "remise": float(row[11]) if row[11] else 0.0, "mode_calcul": int(row[12]) if row[12] else 0, "type_remise": int(row[13]) if row[13] else 0, "ref_client": self._safe_strip(row[14]), "coef_nouveau": float(row[15]) if row[15] else 0.0, "prix_vente_nouveau": float(row[16]) if row[16] else 0.0, "prix_devise_nouveau": float(row[17]) if row[17] else 0.0, "remise_nouvelle": float(row[18]) if row[18] else 0.0, "date_application": row[19], "date_creation": row[20], "date_modification": row[21], }) for article in articles: article["tarifs_clients"] = tarifs_map.get(article["reference"], []) article["nb_tarifs_clients"] = len(article["tarifs_clients"]) logger.info(f" ✓ {len(tarifs_map)} articles avec tarifs clients") return articles except Exception as e: logger.error(f" ✗ Erreur tarifs clients: {e}") for article in articles: article["tarifs_clients"] = [] article["nb_tarifs_clients"] = 0 return articles def _enrichir_nomenclature(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit avec la nomenclature de production (composants, opérations) Structure: articles[i]["composants"] = [{"operation": "OP10", "ressource": "RES01"}, ...] """ try: logger.info(f" → Enrichissement nomenclature...") references = [a["reference"] for a in articles if a["reference"]] if not references: return articles placeholders = ",".join(["?"] * len(references)) query = f""" SELECT AR_Ref, AT_Operation, RP_Code, AT_Temps, AT_Type, AT_Description, AT_Ordre, AG_No1Comp, AG_No2Comp, AT_TypeRessource, AT_Chevauche, AT_Demarre, AT_OperationChevauche, AT_ValeurChevauche, AT_TypeChevauche, cbCreation, cbModification FROM F_ARTCOMPO WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, AT_Ordre, AT_Operation """ cursor.execute(query, references) rows = cursor.fetchall() composants_map = {} for row in rows: ref = self._safe_strip(row[0]) if not ref: continue if ref not in composants_map: composants_map[ref] = [] composants_map[ref].append({ "operation": self._safe_strip(row[1]), "code_ressource": self._safe_strip(row[2]), "temps": float(row[3]) if row[3] else 0.0, "type": int(row[4]) if row[4] else 0, "description": self._safe_strip(row[5]), "ordre": int(row[6]) if row[6] else 0, "gamme_1_comp": int(row[7]) if row[7] else 0, "gamme_2_comp": int(row[8]) if row[8] else 0, "type_ressource": int(row[9]) if row[9] else 0, "chevauche": int(row[10]) if row[10] else 0, "demarre": int(row[11]) if row[11] else 0, "operation_chevauche": self._safe_strip(row[12]), "valeur_chevauche": float(row[13]) if row[13] else 0.0, "type_chevauche": int(row[14]) if row[14] else 0, "date_creation": row[15], "date_modification": row[16], }) for article in articles: article["composants"] = composants_map.get(article["reference"], []) article["nb_composants"] = len(article["composants"]) logger.info(f" ✓ {len(composants_map)} articles avec nomenclature") return articles except Exception as e: logger.error(f" ✗ Erreur nomenclature: {e}") for article in articles: article["composants"] = [] article["nb_composants"] = 0 return articles def _enrichir_compta_articles(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit avec les comptes comptables spécifiques par article Structure: articles[i]["compta_vente/achat/stock"] = {...} """ try: logger.info(f" → Enrichissement comptabilité articles...") references = [a["reference"] for a in articles if a["reference"]] if not references: return articles placeholders = ",".join(["?"] * len(references)) query = f""" SELECT AR_Ref, ACP_Type, ACP_Champ, ACP_ComptaCPT_CompteG, ACP_ComptaCPT_CompteA, ACP_ComptaCPT_Taxe1, ACP_ComptaCPT_Taxe2, ACP_ComptaCPT_Taxe3, ACP_ComptaCPT_Date1, ACP_ComptaCPT_Date2, ACP_ComptaCPT_Date3, ACP_ComptaCPT_TaxeAnc1, ACP_ComptaCPT_TaxeAnc2, ACP_ComptaCPT_TaxeAnc3, ACP_TypeFacture, cbCreation, cbModification FROM F_ARTCOMPTA WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, ACP_Type, ACP_Champ """ cursor.execute(query, references) rows = cursor.fetchall() compta_map = {} for row in rows: ref = self._safe_strip(row[0]) if not ref: continue if ref not in compta_map: compta_map[ref] = {"vente": [], "achat": [], "stock": []} type_compta = int(row[1]) if row[1] else 0 type_key = {0: "vente", 1: "achat", 2: "stock"}.get(type_compta, "autre") compta_entry = { "champ": int(row[2]) if row[2] else 0, "compte_general": self._safe_strip(row[3]), "compte_auxiliaire": self._safe_strip(row[4]), "taxe_1": self._safe_strip(row[5]), "taxe_2": self._safe_strip(row[6]), "taxe_3": self._safe_strip(row[7]), "taxe_date_1": row[8], "taxe_date_2": row[9], "taxe_date_3": row[10], "taxe_anc_1": self._safe_strip(row[11]), "taxe_anc_2": self._safe_strip(row[12]), "taxe_anc_3": self._safe_strip(row[13]), "type_facture": int(row[14]) if row[14] else 0, "date_creation": row[15], "date_modification": row[16], } if type_key in compta_map[ref]: compta_map[ref][type_key].append(compta_entry) for article in articles: compta = compta_map.get(article["reference"], {"vente": [], "achat": [], "stock": []}) article["compta_vente"] = compta["vente"] article["compta_achat"] = compta["achat"] article["compta_stock"] = compta["stock"] logger.info(f" ✓ {len(compta_map)} articles avec compta spécifique") return articles except Exception as e: logger.error(f" ✗ Erreur comptabilité articles: {e}") for article in articles: article["compta_vente"] = [] article["compta_achat"] = [] article["compta_stock"] = [] return articles def _enrichir_fournisseurs_multiples(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit avec TOUS les fournisseurs (pas seulement le principal) Structure: articles[i]["fournisseurs"] = [{"num": "F001", "ref": "REF123", "prix": 45.5}, ...] """ try: logger.info(f" → Enrichissement fournisseurs multiples...") references = [a["reference"] for a in articles if a["reference"]] if not references: return articles placeholders = ",".join(["?"] * len(references)) query = f""" SELECT AR_Ref, CT_Num, AF_RefFourniss, AF_PrixAch, AF_Unite, AF_Conversion, AF_DelaiAppro, AF_Garantie, AF_Colisage, AF_QteMini, AF_QteMont, EG_Champ, AF_Principal, AF_PrixDev, AF_Devise, AF_Remise, AF_ConvDiv, AF_TypeRem, AF_CodeBarre, AF_PrixAchNouv, AF_PrixDevNouv, AF_RemiseNouv, AF_DateApplication, cbCreation, cbModification FROM F_ARTFOURNISS WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, AF_Principal DESC, CT_Num """ cursor.execute(query, references) rows = cursor.fetchall() fournisseurs_map = {} for row in rows: ref = self._safe_strip(row[0]) if not ref: continue if ref not in fournisseurs_map: fournisseurs_map[ref] = [] fournisseurs_map[ref].append({ "fournisseur_num": self._safe_strip(row[1]), "ref_fournisseur": self._safe_strip(row[2]), "prix_achat": float(row[3]) if row[3] else 0.0, "unite": self._safe_strip(row[4]), "conversion": float(row[5]) if row[5] else 0.0, "delai_appro": int(row[6]) if row[6] else 0, "garantie": int(row[7]) if row[7] else 0, "colisage": int(row[8]) if row[8] else 0, "qte_mini": float(row[9]) if row[9] else 0.0, "qte_montant": float(row[10]) if row[10] else 0.0, "enumere_gamme": int(row[11]) if row[11] else 0, "est_principal": bool(row[12]), "prix_devise": float(row[13]) if row[13] else 0.0, "devise": int(row[14]) if row[14] else 0, "remise": float(row[15]) if row[15] else 0.0, "conversion_devise": float(row[16]) if row[16] else 0.0, "type_remise": int(row[17]) if row[17] else 0, "code_barre_fournisseur": self._safe_strip(row[18]), "prix_achat_nouveau": float(row[19]) if row[19] else 0.0, "prix_devise_nouveau": float(row[20]) if row[20] else 0.0, "remise_nouvelle": float(row[21]) if row[21] else 0.0, "date_application": row[22], "date_creation": row[23], "date_modification": row[24], }) for article in articles: article["fournisseurs"] = fournisseurs_map.get(article["reference"], []) article["nb_fournisseurs"] = len(article["fournisseurs"]) logger.info(f" ✓ {len(fournisseurs_map)} articles avec fournisseurs multiples") return articles except Exception as e: logger.error(f" ✗ Erreur fournisseurs multiples: {e}") for article in articles: article["fournisseurs"] = [] article["nb_fournisseurs"] = 0 return articles def _enrichir_depots_details(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit les stocks avec les informations détaillées des dépôts Ajoute le nom du dépôt à chaque ligne de stock """ try: logger.info(f" → Enrichissement détails dépôts...") query = """ SELECT DE_No, DE_Intitule, DE_Code, DE_Adresse, DE_Complement, DE_CodePostal, DE_Ville, DE_Contact, DE_Principal, DE_CatCompta, DE_Region, DE_Pays, DE_EMail, DE_Telephone, DE_Telecopie, DP_NoDefaut, DE_Exclure FROM F_DEPOT """ cursor.execute(query) rows = cursor.fetchall() depots_map = {} for row in rows: de_no = self._safe_strip(row[0]) if not de_no: continue depots_map[de_no] = { "depot_num": de_no, "depot_nom": self._safe_strip(row[1]), "depot_code": self._safe_strip(row[2]), "depot_adresse": self._safe_strip(row[3]), "depot_complement": self._safe_strip(row[4]), "depot_code_postal": self._safe_strip(row[5]), "depot_ville": self._safe_strip(row[6]), "depot_contact": self._safe_strip(row[7]), "depot_est_principal": bool(row[8]), "depot_categorie_compta": int(row[9]) if row[9] else 0, "depot_region": self._safe_strip(row[10]), "depot_pays": self._safe_strip(row[11]), "depot_email": self._safe_strip(row[12]), "depot_telephone": self._safe_strip(row[13]), "depot_fax": self._safe_strip(row[14]), "depot_emplacement_defaut": self._safe_strip(row[15]), "depot_exclu": bool(row[16]), } logger.info(f" → {len(depots_map)} dépôts chargés") for article in articles: for empl in article.get("emplacements", []): depot_num = empl.get("depot") if depot_num and depot_num in depots_map: empl.update(depots_map[depot_num]) logger.info(f" ✓ Emplacements enrichis avec détails dépôts") return articles except Exception as e: logger.error(f" ✗ Erreur détails dépôts: {e}") return articles def _enrichir_emplacements_details(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit les emplacements avec leurs détails (zone, type, etc.) """ try: logger.info(f" → Enrichissement détails emplacements...") query = """ SELECT DE_No, DP_No, DP_Code, DP_Intitule, DP_Zone, DP_Type FROM F_DEPOTEMPL """ cursor.execute(query) rows = cursor.fetchall() emplacements_map = {} for row in rows: de_no = self._safe_strip(row[0]) dp_no = self._safe_strip(row[1]) if not de_no or not dp_no: continue key = f"{de_no}_{dp_no}" emplacements_map[key] = { "emplacement_code": self._safe_strip(row[2]), "emplacement_libelle": self._safe_strip(row[3]), "emplacement_zone": self._safe_strip(row[4]), "emplacement_type": int(row[5]) if row[5] else 0, } logger.info(f" → {len(emplacements_map)} emplacements détaillés chargés") for article in articles: for empl in article.get("emplacements", []): depot = empl.get("depot") emplacement = empl.get("emplacement") if depot and emplacement: key = f"{depot}_{emplacement}" if key in emplacements_map: empl.update(emplacements_map[key]) logger.info(f" ✓ Emplacements enrichis avec détails") return articles except Exception as e: logger.error(f" ✗ Erreur détails emplacements: {e}") return articles def _enrichir_gammes_enumeres(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit les gammes avec leurs libellés depuis F_ENUMGAMME et P_GAMME """ try: logger.info(f" → Enrichissement énumérés gammes...") query_pgamme = "SELECT G_Intitule, G_Type FROM P_GAMME ORDER BY G_Type" cursor.execute(query_pgamme) pgamme_rows = cursor.fetchall() gammes_config = {} for idx, row in enumerate(pgamme_rows): gammes_config[idx + 1] = { "nom": self._safe_strip(row[0]), "type": int(row[1]) if row[1] else 0, } logger.info(f" → Configuration gammes: {gammes_config}") query_enum = """ SELECT EG_Champ, EG_Ligne, EG_Enumere, EG_BorneSup FROM F_ENUMGAMME ORDER BY EG_Champ, EG_Ligne """ cursor.execute(query_enum) enum_rows = cursor.fetchall() enumeres_map = {} for row in enum_rows: champ = int(row[0]) if row[0] else 0 enumere = self._safe_strip(row[2]) if not enumere: continue key = f"{champ}_{enumere}" enumeres_map[key] = { "ligne": int(row[1]) if row[1] else 0, "enumere": enumere, "borne_sup": float(row[3]) if row[3] else 0.0, "gamme_nom": gammes_config.get(champ, {}).get("nom", f"Gamme {champ}"), } logger.info(f" → {len(enumeres_map)} énumérés chargés") for article in articles: for gamme in article.get("gammes", []): num_gamme = gamme.get("numero_gamme") enumere = gamme.get("enumere") if num_gamme and enumere: key = f"{num_gamme}_{enumere}" if key in enumeres_map: gamme.update(enumeres_map[key]) logger.info(f" ✓ Gammes enrichies avec énumérés") return articles except Exception as e: logger.error(f" ✗ Erreur énumérés gammes: {e}") return articles def _enrichir_references_enumerees(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit avec les références énumérées (articles avec gammes) Structure: articles[i]["refs_enumerees"] = [{"gamme1": 1, "gamme2": 3, "ref": "ART-R-B"}, ...] """ try: logger.info(f" → Enrichissement références énumérées...") references = [a["reference"] for a in articles if a["reference"]] if not references: return articles placeholders = ",".join(["?"] * len(references)) query = f""" SELECT AR_Ref, AG_No1, AG_No2, AE_Ref, AE_PrixAch, AE_CodeBarre, AE_PrixAchNouv, AE_EdiCode, AE_Sommeil, cbCreation, cbModification FROM F_ARTENUMREF WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, AG_No1, AG_No2 """ cursor.execute(query, references) rows = cursor.fetchall() refs_enum_map = {} for row in rows: ref = self._safe_strip(row[0]) if not ref: continue if ref not in refs_enum_map: refs_enum_map[ref] = [] refs_enum_map[ref].append({ "gamme_1": int(row[1]) if row[1] else 0, "gamme_2": int(row[2]) if row[2] else 0, "reference_enumeree": self._safe_strip(row[3]), "prix_achat": float(row[4]) if row[4] else 0.0, "code_barre": self._safe_strip(row[5]), "prix_achat_nouveau": float(row[6]) if row[6] else 0.0, "edi_code": self._safe_strip(row[7]), "en_sommeil": bool(row[8]), "date_creation": row[9], "date_modification": row[10], }) for article in articles: article["refs_enumerees"] = refs_enum_map.get(article["reference"], []) article["nb_refs_enumerees"] = len(article["refs_enumerees"]) logger.info(f" ✓ {len(refs_enum_map)} articles avec références énumérées") return articles except Exception as e: logger.error(f" ✗ Erreur références énumérées: {e}") for article in articles: article["refs_enumerees"] = [] article["nb_refs_enumerees"] = 0 return articles def _enrichir_medias_articles(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit avec les médias attachés (photos, documents, etc.) Structure: articles[i]["medias"] = [{"fichier": "photo.jpg", "type": "image/jpeg"}, ...] """ try: logger.info(f" → Enrichissement médias articles...") references = [a["reference"] for a in articles if a["reference"]] if not references: return articles placeholders = ",".join(["?"] * len(references)) query = f""" SELECT AR_Ref, ME_Commentaire, ME_Fichier, ME_TypeMIME, ME_Origine, ME_GedId, cbCreation, cbModification FROM F_ARTICLEMEDIA WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, cbCreation """ cursor.execute(query, references) rows = cursor.fetchall() medias_map = {} for row in rows: ref = self._safe_strip(row[0]) if not ref: continue if ref not in medias_map: medias_map[ref] = [] medias_map[ref].append({ "commentaire": self._safe_strip(row[1]), "fichier": self._safe_strip(row[2]), "type_mime": self._safe_strip(row[3]), "origine": int(row[4]) if row[4] else 0, "ged_id": self._safe_strip(row[5]), "date_creation": row[6], "date_modification": row[7], }) for article in articles: article["medias"] = medias_map.get(article["reference"], []) article["nb_medias"] = len(article["medias"]) logger.info(f" ✓ {len(medias_map)} articles avec médias") return articles except Exception as e: logger.error(f" ✗ Erreur médias: {e}") for article in articles: article["medias"] = [] article["nb_medias"] = 0 return articles def _enrichir_prix_gammes(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit avec les prix spécifiques par combinaison de gammes Structure: articles[i]["prix_gammes"] = [{"gamme1": 1, "gamme2": 3, "prix_net": 125.5}, ...] """ try: logger.info(f" → Enrichissement prix par gammes...") references = [a["reference"] for a in articles if a["reference"]] if not references: return articles placeholders = ",".join(["?"] * len(references)) query = f""" SELECT AR_Ref, AG_No1, AG_No2, AR_PUNet, AR_CoutStd, cbCreation, cbModification FROM F_ARTPRIX WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, AG_No1, AG_No2 """ cursor.execute(query, references) rows = cursor.fetchall() prix_gammes_map = {} for row in rows: ref = self._safe_strip(row[0]) if not ref: continue if ref not in prix_gammes_map: prix_gammes_map[ref] = [] prix_gammes_map[ref].append({ "gamme_1": int(row[1]) if row[1] else 0, "gamme_2": int(row[2]) if row[2] else 0, "prix_net": float(row[3]) if row[3] else 0.0, "cout_standard": float(row[4]) if row[4] else 0.0, "date_creation": row[5], "date_modification": row[6], }) for article in articles: article["prix_gammes"] = prix_gammes_map.get(article["reference"], []) article["nb_prix_gammes"] = len(article["prix_gammes"]) logger.info(f" ✓ {len(prix_gammes_map)} articles avec prix par gammes") return articles except Exception as e: logger.error(f" ✗ Erreur prix gammes: {e}") for article in articles: article["prix_gammes"] = [] article["nb_prix_gammes"] = 0 return articles def _enrichir_conditionnements(self, articles: List[Dict], cursor) -> List[Dict]: """ Enrichit avec les conditionnements disponibles """ try: logger.info(f" → Enrichissement conditionnements...") query = """ SELECT EC_Champ, EC_Enumere, EC_Quantite, EC_EdiCode FROM F_ENUMCOND ORDER BY EC_Champ, EC_Enumere """ cursor.execute(query) rows = cursor.fetchall() cond_map = {} for row in rows: champ = int(row[0]) if row[0] else 0 enumere = self._safe_strip(row[1]) if not enumere: continue key = f"{champ}_{enumere}" cond_map[key] = { "champ": champ, "enumere": enumere, "quantite": float(row[2]) if row[2] else 0.0, "edi_code": self._safe_strip(row[3]), } logger.info(f" → {len(cond_map)} conditionnements chargés") for article in articles: conditionnement = article.get("conditionnement") if conditionnement: for key, cond_data in cond_map.items(): if cond_data["enumere"] == conditionnement: article["conditionnement_qte"] = cond_data["quantite"] article["conditionnement_edi"] = cond_data["edi_code"] break logger.info(f" ✓ Conditionnements enrichis") return articles except Exception as e: logger.error(f" ✗ Erreur conditionnements: {e}") return articles def _mapper_article_depuis_row(self, row_data: Dict, colonnes_config: Dict) -> Dict: """ Mappe une ligne SQL vers un dictionnaire article normalisé Args: row_data: Dictionnaire avec noms de colonnes SQL comme clés colonnes_config: Mapping SQL -> noms normalisés Returns: Dictionnaire article avec noms normalisés """ article = {} def get_val(sql_col, default=None, convert_type=None): val = row_data.get(sql_col, default) if val is None: return default if convert_type == float: return float(val) if val not in (None, "") else (default or 0.0) elif convert_type == int: return int(val) if val not in (None, "") else (default or 0) elif convert_type == bool: return bool(val) if val not in (None, "") else (default or False) elif convert_type == str: return self._safe_strip(val) return val article["reference"] = get_val("AR_Ref", convert_type=str) article["designation"] = get_val("AR_Design", convert_type=str) article["code_ean"] = get_val("AR_CodeBarre", convert_type=str) article["code_barre"] = get_val("AR_CodeBarre", convert_type=str) article["edi_code"] = get_val("AR_EdiCode", convert_type=str) article["raccourci"] = get_val("AR_Raccourci", convert_type=str) article["prix_vente"] = get_val("AR_PrixVen", 0.0, float) article["prix_achat"] = get_val("AR_PrixAch", 0.0, float) article["coef"] = get_val("AR_Coef", 0.0, float) article["prix_net"] = get_val("AR_PUNet", 0.0, float) article["prix_achat_nouveau"] = get_val("AR_PrixAchNouv", 0.0, float) article["coef_nouveau"] = get_val("AR_CoefNouv", 0.0, float) article["prix_vente_nouveau"] = get_val("AR_PrixVenNouv", 0.0, float) date_app = get_val("AR_DateApplication") article["date_application_prix"] = str(date_app) if date_app else None article["cout_standard"] = get_val("AR_CoutStd", 0.0, float) article["unite_vente"] = get_val("AR_UniteVen", convert_type=str) article["unite_poids"] = get_val("AR_UnitePoids", convert_type=str) article["poids_net"] = get_val("AR_PoidsNet", 0.0, float) article["poids_brut"] = get_val("AR_PoidsBrut", 0.0, float) article["gamme_1"] = get_val("AR_Gamme1", convert_type=str) article["gamme_2"] = get_val("AR_Gamme2", convert_type=str) type_val = get_val("AR_Type", 0, int) article["type_article"] = type_val article["type_article_libelle"] = self._get_type_article_libelle(type_val) article["famille_code"] = get_val("FA_CodeFamille", convert_type=str) article["nature"] = get_val("AR_Nature", 0, int) article["garantie"] = get_val("AR_Garantie", 0, int) article["code_fiscal"] = get_val("AR_CodeFiscal", convert_type=str) article["pays"] = get_val("AR_Pays", convert_type=str) article["fournisseur_principal"] = get_val("CO_No", 0, int) article["conditionnement"] = get_val("AR_Condition", convert_type=str) article["nb_colis"] = get_val("AR_NbColis", 0, int) article["prevision"] = get_val("AR_Prevision", False, bool) article["suivi_stock"] = get_val("AR_SuiviStock", False, bool) article["nomenclature"] = get_val("AR_Nomencl", False, bool) article["qte_composant"] = get_val("AR_QteComp", 0.0, float) article["qte_operatoire"] = get_val("AR_QteOperatoire", 0.0, float) sommeil = get_val("AR_Sommeil", 0, int) article["est_actif"] = (sommeil == 0) article["en_sommeil"] = (sommeil == 1) article["article_substitut"] = get_val("AR_Substitut", convert_type=str) article["soumis_escompte"] = get_val("AR_Escompte", False, bool) article["delai"] = get_val("AR_Delai", 0, int) article["stat_01"] = get_val("AR_Stat01", convert_type=str) article["stat_02"] = get_val("AR_Stat02", convert_type=str) article["stat_03"] = get_val("AR_Stat03", convert_type=str) article["stat_04"] = get_val("AR_Stat04", convert_type=str) article["stat_05"] = get_val("AR_Stat05", convert_type=str) article["hors_statistique"] = get_val("AR_HorsStat", False, bool) article["categorie_1"] = get_val("CL_No1", 0, int) article["categorie_2"] = get_val("CL_No2", 0, int) article["categorie_3"] = get_val("CL_No3", 0, int) article["categorie_4"] = get_val("CL_No4", 0, int) date_modif = get_val("AR_DateModif") article["date_modification"] = str(date_modif) if date_modif else None article["vente_debit"] = get_val("AR_VteDebit", False, bool) article["non_imprimable"] = get_val("AR_NotImp", False, bool) article["transfere"] = get_val("AR_Transfere", False, bool) article["publie"] = get_val("AR_Publie", False, bool) article["contremarque"] = get_val("AR_Contremarque", False, bool) article["fact_poids"] = get_val("AR_FactPoids", False, bool) article["fact_forfait"] = get_val("AR_FactForfait", False, bool) article["saisie_variable"] = get_val("AR_SaisieVar", False, bool) article["fictif"] = get_val("AR_Fictif", False, bool) article["sous_traitance"] = get_val("AR_SousTraitance", False, bool) article["criticite"] = get_val("AR_Criticite", 0, int) article["reprise_code_defaut"] = get_val("RP_CodeDefaut", convert_type=str) article["delai_fabrication"] = get_val("AR_DelaiFabrication", 0, int) article["delai_peremption"] = get_val("AR_DelaiPeremption", 0, int) article["delai_securite"] = get_val("AR_DelaiSecurite", 0, int) article["type_lancement"] = get_val("AR_TypeLancement", 0, int) article["cycle"] = get_val("AR_Cycle", 1, int) article["photo"] = get_val("AR_Photo", convert_type=str) article["langue_1"] = get_val("AR_Langue1", convert_type=str) article["langue_2"] = get_val("AR_Langue2", convert_type=str) article["frais_01_denomination"] = get_val("AR_Frais01FR_Denomination", convert_type=str) article["frais_02_denomination"] = get_val("AR_Frais02FR_Denomination", convert_type=str) article["frais_03_denomination"] = get_val("AR_Frais03FR_Denomination", convert_type=str) article["marque_commerciale"] = get_val("Marque commerciale", convert_type=str) objectif_val = get_val("Objectif / Qtés vendues") if objectif_val is not None: article["objectif_qtes_vendues"] = str(float(objectif_val)) if objectif_val not in ("", 0, 0.0) else None else: article["objectif_qtes_vendues"] = None pourcentage_val = get_val("Pourcentage teneur en or") if pourcentage_val is not None: article["pourcentage_or"] = str(float(pourcentage_val)) if pourcentage_val not in ("", 0, 0.0) else None else: article["pourcentage_or"] = None date_com = get_val("1ère commercialisation") article["premiere_commercialisation"] = str(date_com) if date_com else None article["interdire_commande"] = get_val("AR_InterdireCommande", False, bool) article["exclure"] = get_val("AR_Exclure", False, bool) article["stock_reel"] = 0.0 article["stock_mini"] = 0.0 article["stock_maxi"] = 0.0 article["stock_reserve"] = 0.0 article["stock_commande"] = 0.0 article["stock_disponible"] = 0.0 article["famille_libelle"] = None article["famille_type"] = None article["famille_unite_vente"] = None article["famille_coef"] = None article["famille_suivi_stock"] = None article["famille_garantie"] = None article["famille_unite_poids"] = None article["famille_delai"] = None article["famille_nb_colis"] = None article["famille_code_fiscal"] = None article["famille_escompte"] = None article["famille_centrale"] = None article["famille_nature"] = None article["famille_hors_stat"] = None article["famille_pays"] = None article["fournisseur_nom"] = None article["tva_code"] = None article["tva_taux"] = None return article def _enrichir_stocks_articles(self, articles: List[Dict], cursor) -> List[Dict]: """Enrichit les articles avec les données de stock depuis F_ARTSTOCK""" try: logger.info(f" → Enrichissement stocks pour {len(articles)} articles...") references = [a["reference"] for a in articles if a["reference"]] if not references: return articles placeholders = ",".join(["?"] * len(references)) stock_query = f""" SELECT AR_Ref, SUM(ISNULL(AS_QteSto, 0)) as Stock_Total, MIN(ISNULL(AS_QteMini, 0)) as Stock_Mini, MAX(ISNULL(AS_QteMaxi, 0)) as Stock_Maxi, SUM(ISNULL(AS_QteRes, 0)) as Stock_Reserve, SUM(ISNULL(AS_QteCom, 0)) as Stock_Commande FROM F_ARTSTOCK WHERE AR_Ref IN ({placeholders}) GROUP BY AR_Ref """ cursor.execute(stock_query, references) stock_rows = cursor.fetchall() stock_map = {} for stock_row in stock_rows: ref = self._safe_strip(stock_row[0]) if ref: stock_map[ref] = { "stock_reel": float(stock_row[1]) if stock_row[1] else 0.0, "stock_mini": float(stock_row[2]) if stock_row[2] else 0.0, "stock_maxi": float(stock_row[3]) if stock_row[3] else 0.0, "stock_reserve": float(stock_row[4]) if stock_row[4] else 0.0, "stock_commande": float(stock_row[5]) if stock_row[5] else 0.0, } logger.info(f" → {len(stock_map)} articles avec stock trouvés dans F_ARTSTOCK") for article in articles: if article["reference"] in stock_map: stock_data = stock_map[article["reference"]] article.update(stock_data) article["stock_disponible"] = ( article["stock_reel"] - article["stock_reserve"] ) return articles except Exception as e: logger.error(f" ✗ Erreur enrichissement stocks: {e}", exc_info=True) return articles def _enrichir_fournisseurs_articles(self, articles: List[Dict], cursor) -> List[Dict]: """Enrichit les articles avec le nom du fournisseur principal""" try: logger.info(f" → Enrichissement fournisseurs...") nums_fournisseurs = list(set([ a["fournisseur_principal"] for a in articles if a.get("fournisseur_principal") and a["fournisseur_principal"] > 0 ])) if not nums_fournisseurs: logger.warning(" ⚠ Aucun numéro de fournisseur trouvé dans les articles") for article in articles: article["fournisseur_nom"] = None return articles logger.info(f" → {len(nums_fournisseurs)} fournisseurs uniques à chercher") logger.info(f" → Exemples CO_No : {nums_fournisseurs[:5]}") placeholders = ",".join(["?"] * len(nums_fournisseurs)) fournisseur_query = f""" SELECT CT_Num, CT_Intitule, CT_Type FROM F_COMPTET WHERE CT_Num IN ({placeholders}) AND CT_Type = 1 """ cursor.execute(fournisseur_query, nums_fournisseurs) fournisseur_rows = cursor.fetchall() logger.info(f" → {len(fournisseur_rows)} fournisseurs trouvés dans F_COMPTET") if len(fournisseur_rows) == 0: logger.warning(f" ⚠ Aucun fournisseur trouvé pour CT_Type=1 et CT_Num IN {nums_fournisseurs[:5]}") cursor.execute(f"SELECT CT_Num, CT_Type FROM F_COMPTET WHERE CT_Num IN ({placeholders})", nums_fournisseurs) tous_types = cursor.fetchall() if tous_types: logger.info(f" → Trouvé {len(tous_types)} comptes (tous types) : {[(r[0], r[1]) for r in tous_types[:5]]}") fournisseur_map = {} for fourn_row in fournisseur_rows: num = int(fourn_row[0]) # CT_Num nom = self._safe_strip(fourn_row[1]) # CT_Intitule type_ct = int(fourn_row[2]) # CT_Type fournisseur_map[num] = nom logger.debug(f" → Fournisseur mappé : {num} = {nom} (Type={type_ct})") nb_enrichis = 0 for article in articles: num_fourn = article.get("fournisseur_principal") if num_fourn and num_fourn in fournisseur_map: article["fournisseur_nom"] = fournisseur_map[num_fourn] nb_enrichis += 1 else: article["fournisseur_nom"] = None logger.info(f" ✓ {nb_enrichis} articles enrichis avec nom fournisseur") return articles except Exception as e: logger.error(f" ✗ Erreur enrichissement fournisseurs: {e}", exc_info=True) for article in articles: article["fournisseur_nom"] = None return articles def _enrichir_familles_articles(self, articles: List[Dict], cursor) -> List[Dict]: """Enrichit les articles avec les informations de famille depuis F_FAMILLE""" try: logger.info(f" → Enrichissement familles pour {len(articles)} articles...") codes_familles_bruts = [ a.get("famille_code") for a in articles if a.get("famille_code") not in (None, "", " ") ] if codes_familles_bruts: logger.info(f" → Exemples de codes familles : {codes_familles_bruts[:5]}") codes_familles = list(set([ str(code).strip() for code in codes_familles_bruts if code ])) if not codes_familles: logger.warning(" ⚠ Aucun code famille trouvé dans les articles") for article in articles: self._init_champs_famille_vides(article) return articles logger.info(f" → {len(codes_familles)} codes famille uniques") cursor.execute("SELECT TOP 1 * FROM F_FAMILLE") colonnes_disponibles = [column[0] for column in cursor.description] colonnes_souhaitees = [ "FA_CodeFamille", "FA_Intitule", "FA_Type", "FA_UniteVen", "FA_Coef", "FA_SuiviStock", "FA_Garantie", "FA_UnitePoids", "FA_Delai", "FA_NbColis", "FA_CodeFiscal", "FA_Escompte", "FA_Central", "FA_Nature", "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 "FA_CodeFamille" not in colonnes_a_lire or "FA_Intitule" not in colonnes_a_lire: logger.error(" ✗ Colonnes essentielles manquantes !") return articles logger.info(f" → Colonnes disponibles : {len(colonnes_a_lire)}") colonnes_str = ", ".join(colonnes_a_lire) placeholders = ",".join(["?"] * len(codes_familles)) famille_query = f""" SELECT {colonnes_str} FROM F_FAMILLE WHERE FA_CodeFamille IN ({placeholders}) """ cursor.execute(famille_query, codes_familles) famille_rows = cursor.fetchall() logger.info(f" → {len(famille_rows)} familles trouvées") famille_map = {} for fam_row in famille_rows: famille_data = {} for idx, col in enumerate(colonnes_a_lire): famille_data[col] = fam_row[idx] code = self._safe_strip(famille_data.get("FA_CodeFamille")) if not code: continue famille_map[code] = { "famille_libelle": self._safe_strip(famille_data.get("FA_Intitule")), "famille_type": int(famille_data.get("FA_Type", 0) or 0), "famille_unite_vente": self._safe_strip(famille_data.get("FA_UniteVen")), "famille_coef": float(famille_data.get("FA_Coef", 0) or 0), "famille_suivi_stock": bool(famille_data.get("FA_SuiviStock", 0)), "famille_garantie": int(famille_data.get("FA_Garantie", 0) or 0), "famille_unite_poids": self._safe_strip(famille_data.get("FA_UnitePoids")), "famille_delai": int(famille_data.get("FA_Delai", 0) or 0), "famille_nb_colis": int(famille_data.get("FA_NbColis", 0) or 0), "famille_code_fiscal": self._safe_strip(famille_data.get("FA_CodeFiscal")), "famille_escompte": bool(famille_data.get("FA_Escompte", 0)), "famille_centrale": bool(famille_data.get("FA_Central", 0)), "famille_nature": int(famille_data.get("FA_Nature", 0) or 0), "famille_hors_stat": bool(famille_data.get("FA_HorsStat", 0)), "famille_pays": self._safe_strip(famille_data.get("FA_Pays")), } logger.info(f" → {len(famille_map)} familles mappées") nb_enrichis = 0 for article in articles: code_fam = str(article.get("famille_code", "")).strip() if code_fam and code_fam in famille_map: article.update(famille_map[code_fam]) nb_enrichis += 1 else: self._init_champs_famille_vides(article) logger.info(f" ✓ {nb_enrichis} articles enrichis avec infos famille") return articles except Exception as e: logger.error(f" Erreur enrichissement familles: {e}", exc_info=True) for article in articles: self._init_champs_famille_vides(article) return articles def _init_champs_famille_vides(self, article: Dict): """Initialise les champs famille à None/0""" article["famille_libelle"] = None article["famille_type"] = None article["famille_unite_vente"] = None article["famille_coef"] = None article["famille_suivi_stock"] = None article["famille_garantie"] = None article["famille_unite_poids"] = None article["famille_delai"] = None article["famille_nb_colis"] = None article["famille_code_fiscal"] = None article["famille_escompte"] = None article["famille_centrale"] = None article["famille_nature"] = None article["famille_hors_stat"] = None article["famille_pays"] = None def _enrichir_tva_articles(self, articles: List[Dict], cursor) -> List[Dict]: """Enrichit les articles avec le taux de TVA""" try: logger.info(f" → Enrichissement TVA...") codes_tva = list(set([ a["code_fiscal"] for a in articles if a.get("code_fiscal") ])) if not codes_tva: for article in articles: article["tva_code"] = None article["tva_taux"] = None return articles placeholders = ",".join(["?"] * len(codes_tva)) tva_query = f""" SELECT TA_Code, TA_Taux FROM F_TAXE WHERE TA_Code IN ({placeholders}) """ cursor.execute(tva_query, codes_tva) tva_rows = cursor.fetchall() tva_map = {} for tva_row in tva_rows: code = self._safe_strip(tva_row[0]) tva_map[code] = float(tva_row[1]) if tva_row[1] else 0.0 logger.info(f" → {len(tva_map)} codes TVA trouvés") for article in articles: code_tva = article.get("code_fiscal") if code_tva and code_tva in tva_map: article["tva_code"] = code_tva article["tva_taux"] = tva_map[code_tva] else: article["tva_code"] = code_tva article["tva_taux"] = None return articles except Exception as e: logger.error(f" ✗ Erreur enrichissement TVA: {e}", exc_info=True) for article in articles: article["tva_code"] = article.get("code_fiscal") article["tva_taux"] = None return articles def _get_type_article_libelle(self, type_val: int) -> str: """Retourne le libellé du type d'article""" types = { 0: "Article", 1: "Prestation", 2: "Divers / Frais", 3: "Nomenclature" } return types.get(type_val, f"Type {type_val}") def _safe_strip(self, value) -> Optional[str]: """Nettoie une valeur string en toute sécurité""" if value is None: return None if isinstance(value, str): stripped = value.strip() return stripped if stripped else None return str(value).strip() or None def _convertir_type_pour_sql(self, type_doc: int) -> int: """COM → SQL : 0, 10, 20, 30... → 0, 1, 2, 3...""" mapping = {0: 0, 10: 1, 20: 2, 30: 3, 40: 4, 50: 5, 60: 6} return mapping.get(type_doc, type_doc) def _convertir_type_depuis_sql(self, type_sql: int) -> int: """SQL → COM : 0, 1, 2, 3... → 0, 10, 20, 30...""" mapping = {0: 0, 1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 6: 60} return mapping.get(type_sql, type_sql) def _lire_document_sql(self, numero: str, type_doc: int): """ Lit un document spécifique par son numéro. PAS de filtre par préfixe car on cherche un document précis. """ try: with self._get_sql_connection() as conn: cursor = conn.cursor() query = """ SELECT d.DO_Piece, d.DO_Date, d.DO_Ref, d.DO_TotalHT, d.DO_TotalTTC, d.DO_Statut, d.DO_Tiers, d.DO_DateLivr, d.DO_DateExpedition, d.DO_Contact, d.DO_TotalHTNet, d.DO_NetAPayer, d.DO_MontantRegle, d.DO_Reliquat, d.DO_TxEscompte, d.DO_Escompte, d.DO_Taxe1, d.DO_Taxe2, d.DO_Taxe3, d.DO_CodeTaxe1, d.DO_CodeTaxe2, d.DO_CodeTaxe3, d.DO_EStatut, d.DO_Imprim, d.DO_Valide, d.DO_Cloture, d.DO_Transfere, d.DO_Souche, d.DO_PieceOrig, d.DO_GUID, d.CA_Num, d.CG_Num, d.DO_Expedit, d.DO_Condition, d.DO_Tarif, d.DO_TypeFrais, d.DO_ValFrais, d.DO_TypeFranco, d.DO_ValFranco, c.CT_Intitule, c.CT_Adresse, c.CT_CodePostal, c.CT_Ville, c.CT_Telephone, c.CT_EMail FROM F_DOCENTETE d LEFT JOIN F_COMPTET c ON d.DO_Tiers = c.CT_Num WHERE d.DO_Piece = ? AND d.DO_Type = ? """ logger.info(f"[SQL READ] Lecture directe de {numero} (type={type_doc})") cursor.execute(query, (numero, type_doc)) row = cursor.fetchone() if not row: logger.warning( f"[SQL READ] Document {numero} (type={type_doc}) INTROUVABLE dans F_DOCENTETE" ) return None numero_piece = self._safe_strip(row[0]) logger.info(f"[SQL READ] Document trouvé: {numero_piece}") doc = { "numero": numero_piece, "reference": self._safe_strip(row[2]), # DO_Ref "date": str(row[1]) if row[1] else "", # DO_Date "date_livraison": (str(row[7]) if row[7] else ""), # DO_DateLivr "date_expedition": ( str(row[8]) if row[8] else "" ), # DO_DateExpedition "client_code": self._safe_strip(row[6]), # DO_Tiers "client_intitule": self._safe_strip(row[39]), # CT_Intitule "client_adresse": self._safe_strip(row[40]), # CT_Adresse "client_code_postal": self._safe_strip(row[41]), # CT_CodePostal "client_ville": self._safe_strip(row[42]), # CT_Ville "client_telephone": self._safe_strip(row[43]), # CT_Telephone "client_email": self._safe_strip(row[44]), # CT_EMail "contact": self._safe_strip(row[9]), # DO_Contact "total_ht": float(row[3]) if row[3] else 0.0, # DO_TotalHT "total_ht_net": float(row[10]) if row[10] else 0.0, # DO_TotalHTNet "total_ttc": float(row[4]) if row[4] else 0.0, # DO_TotalTTC "net_a_payer": float(row[11]) if row[11] else 0.0, # DO_NetAPayer "montant_regle": ( float(row[12]) if row[12] else 0.0 ), # DO_MontantRegle "reliquat": float(row[13]) if row[13] else 0.0, # DO_Reliquat "taux_escompte": ( float(row[14]) if row[14] else 0.0 ), # DO_TxEscompte "escompte": float(row[15]) if row[15] else 0.0, # DO_Escompte "taxe1": float(row[16]) if row[16] else 0.0, # DO_Taxe1 "taxe2": float(row[17]) if row[17] else 0.0, # DO_Taxe2 "taxe3": float(row[18]) if row[18] else 0.0, # DO_Taxe3 "code_taxe1": self._safe_strip(row[19]), # DO_CodeTaxe1 "code_taxe2": self._safe_strip(row[20]), # DO_CodeTaxe2 "code_taxe3": self._safe_strip(row[21]), # DO_CodeTaxe3 "statut": int(row[5]) if row[5] is not None else 0, # DO_Statut "statut_estatut": ( int(row[22]) if row[22] is not None else 0 ), # DO_EStatut "imprime": int(row[23]) if row[23] is not None else 0, # DO_Imprim "valide": int(row[24]) if row[24] is not None else 0, # DO_Valide "cloture": int(row[25]) if row[25] is not None else 0, # DO_Cloture "transfere": ( int(row[26]) if row[26] is not None else 0 ), # DO_Transfere "souche": int(row[27]) if row[27] is not None else 0, # DO_Souche "piece_origine": self._safe_strip(row[28]), # DO_PieceOrig "guid": self._safe_strip(row[29]), # DO_GUID "ca_num": self._safe_strip(row[30]), # CA_Num "cg_num": self._safe_strip(row[31]), # CG_Num "expedition": ( int(row[32]) if row[32] is not None else 1 ), # DO_Expedit "condition": ( int(row[33]) if row[33] is not None else 1 ), # DO_Condition "tarif": int(row[34]) if row[34] is not None else 1, # DO_Tarif "type_frais": ( int(row[35]) if row[35] is not None else 0 ), # DO_TypeFrais "valeur_frais": float(row[36]) if row[36] else 0.0, # DO_ValFrais "type_franco": ( int(row[37]) if row[37] is not None else 0 ), # DO_TypeFranco "valeur_franco": float(row[38]) if row[38] else 0.0, # DO_ValFranco } cursor.execute( """ SELECT dl.*, a.AR_Design, a.FA_CodeFamille, a.AR_PrixTTC, a.AR_PrixVen, a.AR_PrixAch, a.AR_Gamme1, a.AR_Gamme2, a.AR_CodeBarre, a.AR_CoutStd, a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_UniteVen, a.AR_Type, a.AR_Nature, a.AR_Escompte, a.AR_Garantie FROM F_DOCLIGNE dl LEFT JOIN F_ARTICLE a ON dl.AR_Ref = a.AR_Ref WHERE dl.DO_Piece = ? AND dl.DO_Type = ? ORDER BY dl.DL_Ligne """, (numero, type_doc), ) lignes = [] for ligne_row in cursor.fetchall(): montant_ht = ( float(ligne_row.DL_MontantHT) if ligne_row.DL_MontantHT else 0.0 ) montant_net = ( float(ligne_row.DL_MontantNet) if hasattr(ligne_row, "DL_MontantNet") and ligne_row.DL_MontantNet else montant_ht ) taux_taxe1 = ( float(ligne_row.DL_Taxe1) if hasattr(ligne_row, "DL_Taxe1") and ligne_row.DL_Taxe1 else 0.0 ) taux_taxe2 = ( float(ligne_row.DL_Taxe2) if hasattr(ligne_row, "DL_Taxe2") and ligne_row.DL_Taxe2 else 0.0 ) taux_taxe3 = ( float(ligne_row.DL_Taxe3) if hasattr(ligne_row, "DL_Taxe3") and ligne_row.DL_Taxe3 else 0.0 ) total_taux_taxes = taux_taxe1 + taux_taxe2 + taux_taxe3 montant_ttc = montant_net * (1 + total_taux_taxes / 100) montant_taxe1 = montant_net * (taux_taxe1 / 100) montant_taxe2 = montant_net * (taux_taxe2 / 100) montant_taxe3 = montant_net * (taux_taxe3 / 100) ligne = { "numero_ligne": ( int(ligne_row.DL_Ligne) if ligne_row.DL_Ligne else 0 ), "article_code": self._safe_strip(ligne_row.AR_Ref), "designation": self._safe_strip(ligne_row.DL_Design), "designation_article": self._safe_strip(ligne_row.AR_Design), "quantite": ( float(ligne_row.DL_Qte) if ligne_row.DL_Qte else 0.0 ), "quantite_livree": ( float(ligne_row.DL_QteLiv) if hasattr(ligne_row, "DL_QteLiv") and ligne_row.DL_QteLiv else 0.0 ), "quantite_reservee": ( float(ligne_row.DL_QteRes) if hasattr(ligne_row, "DL_QteRes") and ligne_row.DL_QteRes else 0.0 ), "unite": ( self._safe_strip(ligne_row.DL_Unite) if hasattr(ligne_row, "DL_Unite") else "" ), "prix_unitaire_ht": ( float(ligne_row.DL_PrixUnitaire) if ligne_row.DL_PrixUnitaire else 0.0 ), "prix_unitaire_achat": ( float(ligne_row.AR_PrixAch) if ligne_row.AR_PrixAch else 0.0 ), "prix_unitaire_vente": ( float(ligne_row.AR_PrixVen) if ligne_row.AR_PrixVen else 0.0 ), "prix_unitaire_ttc": ( float(ligne_row.AR_PrixTTC) if ligne_row.AR_PrixTTC else 0.0 ), "montant_ligne_ht": montant_ht, "montant_ligne_net": montant_net, "montant_ligne_ttc": montant_ttc, "remise_valeur1": ( float(ligne_row.DL_Remise01REM_Valeur) if hasattr(ligne_row, "DL_Remise01REM_Valeur") and ligne_row.DL_Remise01REM_Valeur else 0.0 ), "remise_type1": ( int(ligne_row.DL_Remise01REM_Type) if hasattr(ligne_row, "DL_Remise01REM_Type") and ligne_row.DL_Remise01REM_Type else 0 ), "remise_valeur2": ( float(ligne_row.DL_Remise02REM_Valeur) if hasattr(ligne_row, "DL_Remise02REM_Valeur") and ligne_row.DL_Remise02REM_Valeur else 0.0 ), "remise_type2": ( int(ligne_row.DL_Remise02REM_Type) if hasattr(ligne_row, "DL_Remise02REM_Type") and ligne_row.DL_Remise02REM_Type else 0 ), "remise_article": ( float(ligne_row.AR_Escompte) if ligne_row.AR_Escompte else 0.0 ), "taux_taxe1": taux_taxe1, "montant_taxe1": montant_taxe1, "taux_taxe2": taux_taxe2, "montant_taxe2": montant_taxe2, "taux_taxe3": taux_taxe3, "montant_taxe3": montant_taxe3, "total_taxes": montant_taxe1 + montant_taxe2 + montant_taxe3, "famille_article": self._safe_strip(ligne_row.FA_CodeFamille), "gamme1": self._safe_strip(ligne_row.AR_Gamme1), "gamme2": self._safe_strip(ligne_row.AR_Gamme2), "code_barre": self._safe_strip(ligne_row.AR_CodeBarre), "type_article": self._safe_strip(ligne_row.AR_Type), "nature_article": self._safe_strip(ligne_row.AR_Nature), "garantie": self._safe_strip(ligne_row.AR_Garantie), "cout_standard": ( float(ligne_row.AR_CoutStd) if ligne_row.AR_CoutStd else 0.0 ), "poids_net": ( float(ligne_row.AR_PoidsNet) if ligne_row.AR_PoidsNet else 0.0 ), "poids_brut": ( float(ligne_row.AR_PoidsBrut) if ligne_row.AR_PoidsBrut else 0.0 ), "unite_vente": self._safe_strip(ligne_row.AR_UniteVen), "date_livraison_ligne": ( str(ligne_row.DL_DateLivr) if hasattr(ligne_row, "DL_DateLivr") and ligne_row.DL_DateLivr else "" ), "statut_ligne": ( int(ligne_row.DL_Statut) if hasattr(ligne_row, "DL_Statut") and ligne_row.DL_Statut is not None else 0 ), "depot": ( self._safe_strip(ligne_row.DE_No) if hasattr(ligne_row, "DE_No") else "" ), "numero_commande": ( self._safe_strip(ligne_row.DL_NoColis) if hasattr(ligne_row, "DL_NoColis") else "" ), "num_colis": ( self._safe_strip(ligne_row.DL_Colis) if hasattr(ligne_row, "DL_Colis") else "" ), } lignes.append(ligne) doc["lignes"] = lignes doc["nb_lignes"] = len(lignes) total_ht_calcule = sum(l.get("montant_ligne_ht", 0) for l in lignes) total_ttc_calcule = sum(l.get("montant_ligne_ttc", 0) for l in lignes) total_taxes_calcule = sum(l.get("total_taxes", 0) for l in lignes) doc["total_ht_calcule"] = total_ht_calcule doc["total_ttc_calcule"] = total_ttc_calcule doc["total_taxes_calcule"] = total_taxes_calcule return doc except Exception as e: logger.error(f" Erreur SQL lecture document {numero}: {e}", exc_info=True) return None def _lister_documents_avec_lignes_sql( self, type_doc: int, filtre: str = "", limit: int = None, inclure_liaisons: bool = False, calculer_transformations: bool = True, ): """Liste les documents avec leurs lignes.""" try: type_doc_sql = self._convertir_type_pour_sql(type_doc) logger.info(f"[SQL LIST] ═══ Type COM {type_doc} → SQL {type_doc_sql} ═══") with self._get_sql_connection() as conn: cursor = conn.cursor() query = """ SELECT DISTINCT d.DO_Piece, d.DO_Type, d.DO_Date, d.DO_Ref, d.DO_Tiers, d.DO_TotalHT, d.DO_TotalTTC, d.DO_NetAPayer, d.DO_Statut, d.DO_DateLivr, d.DO_DateExpedition, d.DO_Contact, d.DO_TotalHTNet, d.DO_MontantRegle, d.DO_Reliquat, d.DO_TxEscompte, d.DO_Escompte, d.DO_Taxe1, d.DO_Taxe2, d.DO_Taxe3, d.DO_CodeTaxe1, d.DO_CodeTaxe2, d.DO_CodeTaxe3, d.DO_EStatut, d.DO_Imprim, d.DO_Valide, d.DO_Cloture, d.DO_Transfere, d.DO_Souche, d.DO_PieceOrig, d.DO_GUID, d.CA_Num, d.CG_Num, d.DO_Expedit, d.DO_Condition, d.DO_Tarif, d.DO_TypeFrais, d.DO_ValFrais, d.DO_TypeFranco, d.DO_ValFranco, c.CT_Intitule, c.CT_Adresse, c.CT_CodePostal, c.CT_Ville, c.CT_Telephone, c.CT_EMail FROM F_DOCENTETE d LEFT JOIN F_COMPTET c ON d.DO_Tiers = c.CT_Num WHERE d.DO_Type = ? """ params = [type_doc_sql] if filtre: query += " AND (d.DO_Piece LIKE ? OR c.CT_Intitule LIKE ? OR d.DO_Ref LIKE ?)" params.extend([f"%{filtre}%", f"%{filtre}%", f"%{filtre}%"]) query += " ORDER BY d.DO_Date DESC" if limit: query = f"SELECT TOP ({limit}) * FROM ({query}) AS subquery" cursor.execute(query, params) entetes = cursor.fetchall() logger.info(f"[SQL LIST] {len(entetes)} documents SQL") documents = [] stats = { "total": len(entetes), "exclus_prefixe": 0, "erreur_construction": 0, "erreur_lignes": 0, "erreur_transformations": 0, "erreur_liaisons": 0, "succes": 0, } for idx, entete in enumerate(entetes): numero = self._safe_strip(entete.DO_Piece) logger.info( f"[SQL LIST] [{idx+1}/{len(entetes)}] Traitement {numero}..." ) try: prefixes_vente = { 0: ["DE"], 10: ["BC"], 30: ["BL"], 50: ["AV", "AR"], 60: ["FA", "FC"], } prefixes_acceptes = prefixes_vente.get(type_doc, []) if prefixes_acceptes: est_vente = any( numero.upper().startswith(p) for p in prefixes_acceptes ) if not est_vente: logger.info( f"[SQL LIST] {numero} : exclu (préfixe achat)" ) stats["exclus_prefixe"] += 1 continue logger.debug(f"[SQL LIST] {numero} : préfixe OK") try: type_doc_depuis_sql = self._convertir_type_depuis_sql( int(entete.DO_Type) ) doc = { "numero": numero, "type": type_doc_depuis_sql, "reference": self._safe_strip(entete.DO_Ref), "date": str(entete.DO_Date) if entete.DO_Date else "", "date_livraison": ( str(entete.DO_DateLivr) if entete.DO_DateLivr else "" ), "date_expedition": ( str(entete.DO_DateExpedition) if entete.DO_DateExpedition else "" ), "client_code": self._safe_strip(entete.DO_Tiers), "client_intitule": self._safe_strip(entete.CT_Intitule), "client_adresse": self._safe_strip(entete.CT_Adresse), "client_code_postal": self._safe_strip( entete.CT_CodePostal ), "client_ville": self._safe_strip(entete.CT_Ville), "client_telephone": self._safe_strip( entete.CT_Telephone ), "client_email": self._safe_strip(entete.CT_EMail), "contact": self._safe_strip(entete.DO_Contact), "total_ht": ( float(entete.DO_TotalHT) if entete.DO_TotalHT else 0.0 ), "total_ht_net": ( float(entete.DO_TotalHTNet) if entete.DO_TotalHTNet else 0.0 ), "total_ttc": ( float(entete.DO_TotalTTC) if entete.DO_TotalTTC else 0.0 ), "net_a_payer": ( float(entete.DO_NetAPayer) if entete.DO_NetAPayer else 0.0 ), "montant_regle": ( float(entete.DO_MontantRegle) if entete.DO_MontantRegle else 0.0 ), "reliquat": ( float(entete.DO_Reliquat) if entete.DO_Reliquat else 0.0 ), "taux_escompte": ( float(entete.DO_TxEscompte) if entete.DO_TxEscompte else 0.0 ), "escompte": ( float(entete.DO_Escompte) if entete.DO_Escompte else 0.0 ), "taxe1": ( float(entete.DO_Taxe1) if entete.DO_Taxe1 else 0.0 ), "taxe2": ( float(entete.DO_Taxe2) if entete.DO_Taxe2 else 0.0 ), "taxe3": ( float(entete.DO_Taxe3) if entete.DO_Taxe3 else 0.0 ), "code_taxe1": self._safe_strip(entete.DO_CodeTaxe1), "code_taxe2": self._safe_strip(entete.DO_CodeTaxe2), "code_taxe3": self._safe_strip(entete.DO_CodeTaxe3), "statut": ( int(entete.DO_Statut) if entete.DO_Statut is not None else 0 ), "statut_estatut": ( int(entete.DO_EStatut) if entete.DO_EStatut is not None else 0 ), "imprime": ( int(entete.DO_Imprim) if entete.DO_Imprim is not None else 0 ), "valide": ( int(entete.DO_Valide) if entete.DO_Valide is not None else 0 ), "cloture": ( int(entete.DO_Cloture) if entete.DO_Cloture is not None else 0 ), "transfere": ( int(entete.DO_Transfere) if entete.DO_Transfere is not None else 0 ), "souche": self._safe_strip(entete.DO_Souche), "piece_origine": self._safe_strip(entete.DO_PieceOrig), "guid": self._safe_strip(entete.DO_GUID), "ca_num": self._safe_strip(entete.CA_Num), "cg_num": self._safe_strip(entete.CG_Num), "expedition": self._safe_strip(entete.DO_Expedit), "condition": self._safe_strip(entete.DO_Condition), "tarif": self._safe_strip(entete.DO_Tarif), "type_frais": ( int(entete.DO_TypeFrais) if entete.DO_TypeFrais is not None else 0 ), "valeur_frais": ( float(entete.DO_ValFrais) if entete.DO_ValFrais else 0.0 ), "type_franco": ( int(entete.DO_TypeFranco) if entete.DO_TypeFranco is not None else 0 ), "valeur_franco": ( float(entete.DO_ValFranco) if entete.DO_ValFranco else 0.0 ), "lignes": [], } logger.debug( f"[SQL LIST] {numero} : document de base créé" ) except Exception as e: logger.error( f"[SQL LIST] {numero} : ERREUR construction base: {e}", exc_info=True, ) stats["erreur_construction"] += 1 continue try: cursor.execute( """ SELECT dl.*, a.AR_Design, a.FA_CodeFamille, a.AR_PrixTTC, a.AR_PrixVen, a.AR_PrixAch, a.AR_Gamme1, a.AR_Gamme2, a.AR_CodeBarre, a.AR_CoutStd, a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_UniteVen, a.AR_Type, a.AR_Nature, a.AR_Escompte, a.AR_Garantie FROM F_DOCLIGNE dl LEFT JOIN F_ARTICLE a ON dl.AR_Ref = a.AR_Ref WHERE dl.DO_Piece = ? AND dl.DO_Type = ? ORDER BY dl.DL_Ligne """, (numero, type_doc_sql), ) for ligne_row in cursor.fetchall(): montant_ht = ( float(ligne_row.DL_MontantHT) if ligne_row.DL_MontantHT else 0.0 ) montant_net = ( float(ligne_row.DL_MontantNet) if hasattr(ligne_row, "DL_MontantNet") and ligne_row.DL_MontantNet else montant_ht ) taux_taxe1 = ( float(ligne_row.DL_Taxe1) if hasattr(ligne_row, "DL_Taxe1") and ligne_row.DL_Taxe1 else 0.0 ) taux_taxe2 = ( float(ligne_row.DL_Taxe2) if hasattr(ligne_row, "DL_Taxe2") and ligne_row.DL_Taxe2 else 0.0 ) taux_taxe3 = ( float(ligne_row.DL_Taxe3) if hasattr(ligne_row, "DL_Taxe3") and ligne_row.DL_Taxe3 else 0.0 ) total_taux_taxes = taux_taxe1 + taux_taxe2 + taux_taxe3 montant_ttc = montant_net * (1 + total_taux_taxes / 100) montant_taxe1 = montant_net * (taux_taxe1 / 100) montant_taxe2 = montant_net * (taux_taxe2 / 100) montant_taxe3 = montant_net * (taux_taxe3 / 100) ligne = { "numero_ligne": ( int(ligne_row.DL_Ligne) if ligne_row.DL_Ligne else 0 ), "article_code": self._safe_strip(ligne_row.AR_Ref), "designation": self._safe_strip( ligne_row.DL_Design ), "designation_article": self._safe_strip( ligne_row.AR_Design ), "quantite": ( float(ligne_row.DL_Qte) if ligne_row.DL_Qte else 0.0 ), "quantite_livree": ( float(ligne_row.DL_QteLiv) if hasattr(ligne_row, "DL_QteLiv") and ligne_row.DL_QteLiv else 0.0 ), "quantite_reservee": ( float(ligne_row.DL_QteRes) if hasattr(ligne_row, "DL_QteRes") and ligne_row.DL_QteRes else 0.0 ), "unite": ( self._safe_strip(ligne_row.DL_Unite) if hasattr(ligne_row, "DL_Unite") else "" ), "prix_unitaire_ht": ( float(ligne_row.DL_PrixUnitaire) if ligne_row.DL_PrixUnitaire else 0.0 ), "prix_unitaire_achat": ( float(ligne_row.AR_PrixAch) if ligne_row.AR_PrixAch else 0.0 ), "prix_unitaire_vente": ( float(ligne_row.AR_PrixVen) if ligne_row.AR_PrixVen else 0.0 ), "prix_unitaire_ttc": ( float(ligne_row.AR_PrixTTC) if ligne_row.AR_PrixTTC else 0.0 ), "montant_ligne_ht": montant_ht, "montant_ligne_net": montant_net, "montant_ligne_ttc": montant_ttc, "remise_valeur1": ( float(ligne_row.DL_Remise01REM_Valeur) if hasattr(ligne_row, "DL_Remise01REM_Valeur") and ligne_row.DL_Remise01REM_Valeur else 0.0 ), "remise_type1": ( int(ligne_row.DL_Remise01REM_Type) if hasattr(ligne_row, "DL_Remise01REM_Type") and ligne_row.DL_Remise01REM_Type else 0 ), "remise_valeur2": ( float(ligne_row.DL_Remise02REM_Valeur) if hasattr(ligne_row, "DL_Remise02REM_Valeur") and ligne_row.DL_Remise02REM_Valeur else 0.0 ), "remise_type2": ( int(ligne_row.DL_Remise02REM_Type) if hasattr(ligne_row, "DL_Remise02REM_Type") and ligne_row.DL_Remise02REM_Type else 0 ), "remise_article": ( float(ligne_row.AR_Escompte) if ligne_row.AR_Escompte else 0.0 ), "taux_taxe1": taux_taxe1, "montant_taxe1": montant_taxe1, "taux_taxe2": taux_taxe2, "montant_taxe2": montant_taxe2, "taux_taxe3": taux_taxe3, "montant_taxe3": montant_taxe3, "total_taxes": montant_taxe1 + montant_taxe2 + montant_taxe3, "famille_article": self._safe_strip( ligne_row.FA_CodeFamille ), "gamme1": self._safe_strip(ligne_row.AR_Gamme1), "gamme2": self._safe_strip(ligne_row.AR_Gamme2), "code_barre": self._safe_strip( ligne_row.AR_CodeBarre ), "type_article": self._safe_strip(ligne_row.AR_Type), "nature_article": self._safe_strip( ligne_row.AR_Nature ), "garantie": self._safe_strip(ligne_row.AR_Garantie), "cout_standard": ( float(ligne_row.AR_CoutStd) if ligne_row.AR_CoutStd else 0.0 ), "poids_net": ( float(ligne_row.AR_PoidsNet) if ligne_row.AR_PoidsNet else 0.0 ), "poids_brut": ( float(ligne_row.AR_PoidsBrut) if ligne_row.AR_PoidsBrut else 0.0 ), "unite_vente": self._safe_strip( ligne_row.AR_UniteVen ), "date_livraison_ligne": ( str(ligne_row.DL_DateLivr) if hasattr(ligne_row, "DL_DateLivr") and ligne_row.DL_DateLivr else "" ), "statut_ligne": ( int(ligne_row.DL_Statut) if hasattr(ligne_row, "DL_Statut") and ligne_row.DL_Statut is not None else 0 ), "depot": ( self._safe_strip(ligne_row.DE_No) if hasattr(ligne_row, "DE_No") else "" ), "numero_commande": ( self._safe_strip(ligne_row.DL_NoColis) if hasattr(ligne_row, "DL_NoColis") else "" ), "num_colis": ( self._safe_strip(ligne_row.DL_Colis) if hasattr(ligne_row, "DL_Colis") else "" ), } doc["lignes"].append(ligne) doc["nb_lignes"] = len(doc["lignes"]) doc["total_ht_calcule"] = sum( l.get("montant_ligne_ht", 0) for l in doc["lignes"] ) doc["total_ttc_calcule"] = sum( l.get("montant_ligne_ttc", 0) for l in doc["lignes"] ) doc["total_taxes_calcule"] = sum( l.get("total_taxes", 0) for l in doc["lignes"] ) logger.debug( f"[SQL LIST] {numero} : {doc['nb_lignes']} lignes chargées" ) except Exception as e: logger.error( f"[SQL LIST] {numero} : ERREUR lignes: {e}", exc_info=True, ) stats["erreur_lignes"] += 1 documents.append(doc) stats["succes"] += 1 logger.info( f"[SQL LIST] {numero} : AJOUTÉ à la liste (total: {len(documents)})" ) except Exception as e: logger.error( f"[SQL LIST] {numero} : EXCEPTION GLOBALE - DOCUMENT EXCLU: {e}", exc_info=True, ) continue logger.info(f"[SQL LIST] ═══════════════════════════") logger.info(f"[SQL LIST] STATISTIQUES FINALES:") logger.info(f"[SQL LIST] Total SQL: {stats['total']}") logger.info(f"[SQL LIST] Exclus préfixe: {stats['exclus_prefixe']}") logger.info( f"[SQL LIST] Erreur construction: {stats['erreur_construction']}" ) logger.info(f"[SQL LIST] Erreur lignes: {stats['erreur_lignes']}") logger.info( f"[SQL LIST] Erreur transformations: {stats['erreur_transformations']}" ) logger.info(f"[SQL LIST] Erreur liaisons: {stats['erreur_liaisons']}") logger.info(f"[SQL LIST] SUCCÈS: {stats['succes']}") logger.info(f"[SQL LIST] Documents retournés: {len(documents)}") logger.info(f"[SQL LIST] ═══════════════════════════") return documents except Exception as e: logger.error(f" Erreur GLOBALE listage: {e}", exc_info=True) return [] def lister_tous_devis_cache(self, filtre=""): return self._lister_documents_avec_lignes_sql(type_doc=0, filtre=filtre) def lire_devis_cache(self, numero): return self._lire_document_sql(numero, type_doc=0) def lister_toutes_commandes_cache(self, filtre=""): return self._lister_documents_avec_lignes_sql(type_doc=1, filtre=filtre) def lire_commande_cache(self, numero): return self._lire_document_sql(numero, type_doc=1) def lister_toutes_factures_cache(self, filtre=""): return self._lister_documents_avec_lignes_sql(type_doc=6, filtre=filtre) def lire_facture_cache(self, numero): return self._lire_document_sql(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=""): return self._lister_documents_avec_lignes_sql(type_doc=3, filtre=filtre) def lire_livraison_cache(self, numero): return self._lire_document_sql(numero, type_doc=3) def lister_tous_avoirs_cache(self, filtre=""): return self._lister_documents_avec_lignes_sql(type_doc=5, filtre=filtre) def lire_avoir_cache(self, numero): return self._lire_document_sql(numero, type_doc=5) def _cast_client(self, persist_obj): try: obj = win32com.client.CastTo(persist_obj, "IBOClient3") obj.Read() return obj except Exception as e: logger.debug(f" _cast_client échoue: {e}") # AJOUTER CE LOG return None def _cast_article(self, persist_obj): try: obj = win32com.client.CastTo(persist_obj, "IBOArticle3") obj.Read() return obj except: return None def _extraire_client(self, client_obj): try: try: numero = getattr(client_obj, "CT_Num", "").strip() if not numero: logger.debug("Objet sans CT_Num, skip") return None except Exception as e: logger.debug(f" Erreur lecture CT_Num: {e}") return None try: intitule = getattr(client_obj, "CT_Intitule", "").strip() if not intitule: logger.debug(f"{numero} sans CT_Intitule") except Exception as e: logger.debug(f"Erreur CT_Intitule sur {numero}: {e}") intitule = "" data = { "numero": numero, "intitule": intitule, } try: qualite_code = getattr(client_obj, "CT_Type", None) qualite_map = { 0: "CLI", # Client 1: "FOU", # Fournisseur 2: "CLIFOU", # Client + Fournisseur 3: "SAL", # Salarié 4: "PRO", # Prospect } data["qualite"] = qualite_map.get(qualite_code, "CLI") data["est_fournisseur"] = qualite_code in [1, 2] except: data["qualite"] = "CLI" data["est_fournisseur"] = False try: data["est_prospect"] = getattr(client_obj, "CT_Prospect", 0) == 1 except: data["est_prospect"] = False if data["est_prospect"]: data["type_tiers"] = "prospect" elif data["est_fournisseur"] and data["qualite"] != "CLIFOU": data["type_tiers"] = "fournisseur" elif data["qualite"] == "CLIFOU": data["type_tiers"] = "client_fournisseur" else: data["type_tiers"] = "client" try: sommeil = getattr(client_obj, "CT_Sommeil", 0) data["est_actif"] = sommeil == 0 data["est_en_sommeil"] = sommeil == 1 except: data["est_actif"] = True data["est_en_sommeil"] = False try: forme_juridique = getattr(client_obj, "CT_FormeJuridique", "").strip() data["forme_juridique"] = forme_juridique data["est_entreprise"] = bool(forme_juridique) data["est_particulier"] = not bool(forme_juridique) except: data["forme_juridique"] = "" data["est_entreprise"] = False data["est_particulier"] = True try: data["civilite"] = getattr(client_obj, "CT_Civilite", "").strip() except: data["civilite"] = "" try: data["nom"] = getattr(client_obj, "CT_Nom", "").strip() except: data["nom"] = "" try: data["prenom"] = getattr(client_obj, "CT_Prenom", "").strip() except: data["prenom"] = "" if data.get("nom") or data.get("prenom"): parts = [] if data.get("civilite"): parts.append(data["civilite"]) if data.get("prenom"): parts.append(data["prenom"]) if data.get("nom"): parts.append(data["nom"]) data["nom_complet"] = " ".join(parts) else: data["nom_complet"] = "" try: data["contact"] = getattr(client_obj, "CT_Contact", "").strip() except: data["contact"] = "" try: adresse_obj = getattr(client_obj, "Adresse", None) if adresse_obj: try: data["adresse"] = getattr(adresse_obj, "Adresse", "").strip() except: data["adresse"] = "" try: data["complement"] = getattr( adresse_obj, "Complement", "" ).strip() except: data["complement"] = "" try: data["code_postal"] = getattr( adresse_obj, "CodePostal", "" ).strip() except: data["code_postal"] = "" try: data["ville"] = getattr(adresse_obj, "Ville", "").strip() except: data["ville"] = "" try: data["region"] = getattr(adresse_obj, "Region", "").strip() except: data["region"] = "" try: data["pays"] = getattr(adresse_obj, "Pays", "").strip() except: data["pays"] = "" else: data["adresse"] = "" data["complement"] = "" data["code_postal"] = "" data["ville"] = "" data["region"] = "" data["pays"] = "" except Exception as e: logger.debug(f"Erreur adresse sur {numero}: {e}") data["adresse"] = "" data["complement"] = "" data["code_postal"] = "" data["ville"] = "" data["region"] = "" data["pays"] = "" try: telecom = getattr(client_obj, "Telecom", None) if telecom: try: data["telephone"] = getattr(telecom, "Telephone", "").strip() except: data["telephone"] = "" try: data["portable"] = getattr(telecom, "Portable", "").strip() except: data["portable"] = "" try: data["telecopie"] = getattr(telecom, "Telecopie", "").strip() except: data["telecopie"] = "" try: data["email"] = getattr(telecom, "EMail", "").strip() except: data["email"] = "" try: site = ( getattr(telecom, "Site", None) or getattr(telecom, "Web", None) or getattr(telecom, "SiteWeb", "") ) data["site_web"] = str(site).strip() if site else "" except: data["site_web"] = "" else: data["telephone"] = "" data["portable"] = "" data["telecopie"] = "" data["email"] = "" data["site_web"] = "" except Exception as e: logger.debug(f"Erreur telecom sur {numero}: {e}") data["telephone"] = "" data["portable"] = "" data["telecopie"] = "" data["email"] = "" data["site_web"] = "" try: data["siret"] = getattr(client_obj, "CT_Siret", "").strip() except: data["siret"] = "" try: data["siren"] = getattr(client_obj, "CT_Siren", "").strip() except: data["siren"] = "" try: data["tva_intra"] = getattr(client_obj, "CT_Identifiant", "").strip() except: data["tva_intra"] = "" try: data["code_naf"] = ( getattr(client_obj, "CT_CodeNAF", "").strip() or getattr(client_obj, "CT_APE", "").strip() ) except: data["code_naf"] = "" try: data["secteur"] = getattr(client_obj, "CT_Secteur", "").strip() except: data["secteur"] = "" try: effectif = getattr(client_obj, "CT_Effectif", None) data["effectif"] = int(effectif) if effectif is not None else None except: data["effectif"] = None try: ca = getattr(client_obj, "CT_ChiffreAffaire", None) data["ca_annuel"] = float(ca) if ca is not None else None except: data["ca_annuel"] = None try: data["commercial_code"] = getattr(client_obj, "CO_No", "").strip() except: try: data["commercial_code"] = getattr( client_obj, "CT_Commercial", "" ).strip() except: data["commercial_code"] = "" if data.get("commercial_code"): try: commercial_obj = getattr(client_obj, "Commercial", None) if commercial_obj: commercial_obj.Read() data["commercial_nom"] = getattr( commercial_obj, "CO_Nom", "" ).strip() else: data["commercial_nom"] = "" except: data["commercial_nom"] = "" else: data["commercial_nom"] = "" try: data["categorie_tarifaire"] = getattr(client_obj, "N_CatTarif", None) except: data["categorie_tarifaire"] = None try: data["categorie_comptable"] = getattr(client_obj, "N_CatCompta", None) except: data["categorie_comptable"] = None try: data["encours_autorise"] = float(getattr(client_obj, "CT_Encours", 0.0)) except: data["encours_autorise"] = 0.0 try: data["assurance_credit"] = float( getattr(client_obj, "CT_Assurance", 0.0) ) except: data["assurance_credit"] = 0.0 try: data["compte_general"] = getattr(client_obj, "CG_Num", "").strip() except: data["compte_general"] = "" try: date_creation = getattr(client_obj, "CT_DateCreate", None) data["date_creation"] = str(date_creation) if date_creation else "" except: data["date_creation"] = "" try: date_modif = getattr(client_obj, "CT_DateModif", None) data["date_modification"] = str(date_modif) if date_modif else "" except: data["date_modification"] = "" return data except Exception as e: logger.error(f" ERREUR GLOBALE _extraire_client: {e}", exc_info=True) return None def _extraire_article(self, article_obj): try: data = { "reference": getattr(article_obj, "AR_Ref", "").strip(), "designation": getattr(article_obj, "AR_Design", "").strip(), } data["code_ean"] = "" data["code_barre"] = "" try: code_barre = getattr(article_obj, "AR_CodeBarre", "").strip() if code_barre: data["code_ean"] = code_barre data["code_barre"] = code_barre if not data["code_ean"]: code_barre1 = getattr(article_obj, "AR_CodeBarre1", "").strip() if code_barre1: data["code_ean"] = code_barre1 data["code_barre"] = code_barre1 except: pass try: data["prix_vente"] = float(getattr(article_obj, "AR_PrixVen", 0.0)) except: data["prix_vente"] = 0.0 try: data["prix_achat"] = float(getattr(article_obj, "AR_PrixAch", 0.0)) except: data["prix_achat"] = 0.0 try: data["prix_revient"] = float( getattr(article_obj, "AR_PrixRevient", 0.0) ) except: data["prix_revient"] = 0.0 try: data["stock_reel"] = float(getattr(article_obj, "AR_Stock", 0.0)) except: data["stock_reel"] = 0.0 try: data["stock_mini"] = float(getattr(article_obj, "AR_StockMini", 0.0)) except: data["stock_mini"] = 0.0 try: data["stock_maxi"] = float(getattr(article_obj, "AR_StockMaxi", 0.0)) except: data["stock_maxi"] = 0.0 try: data["stock_reserve"] = float(getattr(article_obj, "AR_QteCom", 0.0)) except: data["stock_reserve"] = 0.0 try: data["stock_commande"] = float( getattr(article_obj, "AR_QteComFou", 0.0) ) except: data["stock_commande"] = 0.0 try: data["stock_disponible"] = data["stock_reel"] - data["stock_reserve"] except: data["stock_disponible"] = data["stock_reel"] try: commentaire = getattr(article_obj, "AR_Commentaire", "").strip() data["description"] = commentaire except: data["description"] = "" try: design2 = getattr(article_obj, "AR_Design2", "").strip() data["designation_complementaire"] = design2 except: data["designation_complementaire"] = "" try: type_art = getattr(article_obj, "AR_Type", 0) data["type_article"] = type_art data["type_article_libelle"] = { 0: "Article", 1: "Prestation", 2: "Divers", }.get(type_art, "Inconnu") except: data["type_article"] = 0 data["type_article_libelle"] = "Article" try: famille_code = getattr(article_obj, "FA_CodeFamille", "").strip() data["famille_code"] = famille_code if famille_code: try: famille_obj = getattr(article_obj, "Famille", None) if famille_obj: famille_obj.Read() data["famille_libelle"] = getattr( famille_obj, "FA_Intitule", "" ).strip() else: data["famille_libelle"] = "" except: data["famille_libelle"] = "" else: data["famille_libelle"] = "" except: data["famille_code"] = "" data["famille_libelle"] = "" try: fournisseur_code = getattr(article_obj, "CT_Num", "").strip() data["fournisseur_principal"] = fournisseur_code if fournisseur_code: try: fourn_obj = getattr(article_obj, "Fournisseur", None) if fourn_obj: fourn_obj.Read() data["fournisseur_nom"] = getattr( fourn_obj, "CT_Intitule", "" ).strip() else: data["fournisseur_nom"] = "" except: data["fournisseur_nom"] = "" else: data["fournisseur_nom"] = "" except: data["fournisseur_principal"] = "" data["fournisseur_nom"] = "" try: data["unite_vente"] = getattr(article_obj, "AR_UniteVen", "").strip() except: data["unite_vente"] = "" try: data["unite_achat"] = getattr(article_obj, "AR_UniteAch", "").strip() except: data["unite_achat"] = "" try: data["poids"] = float(getattr(article_obj, "AR_Poids", 0.0)) except: data["poids"] = 0.0 try: data["volume"] = float(getattr(article_obj, "AR_Volume", 0.0)) except: data["volume"] = 0.0 try: sommeil = getattr(article_obj, "AR_Sommeil", 0) data["est_actif"] = sommeil == 0 data["en_sommeil"] = sommeil == 1 except: data["est_actif"] = True data["en_sommeil"] = False try: tva_code = getattr(article_obj, "TA_Code", "").strip() data["tva_code"] = tva_code try: tva_obj = getattr(article_obj, "Taxe1", None) if tva_obj: tva_obj.Read() data["tva_taux"] = float(getattr(tva_obj, "TA_Taux", 20.0)) else: data["tva_taux"] = 20.0 except: data["tva_taux"] = 20.0 except: data["tva_code"] = "" data["tva_taux"] = 20.0 try: date_creation = getattr(article_obj, "AR_DateCreate", None) data["date_creation"] = str(date_creation) if date_creation else "" except: data["date_creation"] = "" try: date_modif = getattr(article_obj, "AR_DateModif", None) data["date_modification"] = str(date_modif) if date_modif else "" except: data["date_modification"] = "" return data except Exception as e: logger.error(f" Erreur extraction article: {e}", exc_info=True) return { "reference": getattr(article_obj, "AR_Ref", "").strip(), "designation": getattr(article_obj, "AR_Design", "").strip(), "prix_vente": 0.0, "stock_reel": 0.0, "code_ean": "", "description": "", "designation_complementaire": "", "prix_achat": 0.0, "prix_revient": 0.0, "stock_mini": 0.0, "stock_maxi": 0.0, "stock_reserve": 0.0, "stock_commande": 0.0, "stock_disponible": 0.0, "code_barre": "", "type_article": 0, "type_article_libelle": "Article", "famille_code": "", "famille_libelle": "", "fournisseur_principal": "", "fournisseur_nom": "", "unite_vente": "", "unite_achat": "", "poids": 0.0, "volume": 0.0, "est_actif": True, "en_sommeil": False, "tva_code": "", "tva_taux": 20.0, "date_creation": "", "date_modification": "", } def _extraire_fournisseur_enrichi(self, fourn_obj): try: numero = getattr(fourn_obj, "CT_Num", "").strip() if not numero: return None intitule = getattr(fourn_obj, "CT_Intitule", "").strip() data = { "numero": numero, "intitule": intitule, "type": 1, # Fournisseur "est_fournisseur": True, } try: sommeil = getattr(fourn_obj, "CT_Sommeil", 0) data["est_actif"] = sommeil == 0 data["en_sommeil"] = sommeil == 1 except: data["est_actif"] = True data["en_sommeil"] = False try: adresse_obj = getattr(fourn_obj, "Adresse", None) if adresse_obj: data["adresse"] = getattr(adresse_obj, "Adresse", "").strip() data["complement"] = getattr(adresse_obj, "Complement", "").strip() data["code_postal"] = getattr(adresse_obj, "CodePostal", "").strip() data["ville"] = getattr(adresse_obj, "Ville", "").strip() data["region"] = getattr(adresse_obj, "Region", "").strip() data["pays"] = getattr(adresse_obj, "Pays", "").strip() parties_adresse = [] if data["adresse"]: parties_adresse.append(data["adresse"]) if data["complement"]: parties_adresse.append(data["complement"]) if data["code_postal"] or data["ville"]: ville_cp = f"{data['code_postal']} {data['ville']}".strip() if ville_cp: parties_adresse.append(ville_cp) if data["pays"]: parties_adresse.append(data["pays"]) data["adresse_complete"] = ", ".join(parties_adresse) else: data["adresse"] = "" data["complement"] = "" data["code_postal"] = "" data["ville"] = "" data["region"] = "" data["pays"] = "" data["adresse_complete"] = "" except Exception as e: logger.debug(f"Erreur adresse fournisseur {numero}: {e}") data["adresse"] = "" data["complement"] = "" data["code_postal"] = "" data["ville"] = "" data["region"] = "" data["pays"] = "" data["adresse_complete"] = "" try: telecom_obj = getattr(fourn_obj, "Telecom", None) if telecom_obj: data["telephone"] = getattr(telecom_obj, "Telephone", "").strip() data["portable"] = getattr(telecom_obj, "Portable", "").strip() data["telecopie"] = getattr(telecom_obj, "Telecopie", "").strip() data["email"] = getattr(telecom_obj, "EMail", "").strip() try: site = ( getattr(telecom_obj, "Site", None) or getattr(telecom_obj, "Web", None) or getattr(telecom_obj, "SiteWeb", "") ) data["site_web"] = str(site).strip() if site else "" except: data["site_web"] = "" else: data["telephone"] = "" data["portable"] = "" data["telecopie"] = "" data["email"] = "" data["site_web"] = "" except Exception as e: logger.debug(f"Erreur telecom fournisseur {numero}: {e}") data["telephone"] = "" data["portable"] = "" data["telecopie"] = "" data["email"] = "" data["site_web"] = "" try: data["siret"] = getattr(fourn_obj, "CT_Siret", "").strip() except: data["siret"] = "" try: if data["siret"] and len(data["siret"]) >= 9: data["siren"] = data["siret"][:9] else: data["siren"] = getattr(fourn_obj, "CT_Siren", "").strip() except: data["siren"] = "" try: data["tva_intra"] = getattr(fourn_obj, "CT_Identifiant", "").strip() except: data["tva_intra"] = "" try: data["code_naf"] = ( getattr(fourn_obj, "CT_CodeNAF", "").strip() or getattr(fourn_obj, "CT_APE", "").strip() ) except: data["code_naf"] = "" try: data["forme_juridique"] = getattr( fourn_obj, "CT_FormeJuridique", "" ).strip() except: data["forme_juridique"] = "" try: cat_tarif = getattr(fourn_obj, "N_CatTarif", None) data["categorie_tarifaire"] = ( int(cat_tarif) if cat_tarif is not None else None ) except: data["categorie_tarifaire"] = None try: cat_compta = getattr(fourn_obj, "N_CatCompta", None) data["categorie_comptable"] = ( int(cat_compta) if cat_compta is not None else None ) except: data["categorie_comptable"] = None try: cond_regl = getattr(fourn_obj, "CT_CondRegl", "").strip() data["conditions_reglement_code"] = cond_regl if cond_regl: try: cond_obj = getattr(fourn_obj, "ConditionReglement", None) if cond_obj: cond_obj.Read() data["conditions_reglement_libelle"] = getattr( cond_obj, "C_Intitule", "" ).strip() else: data["conditions_reglement_libelle"] = "" except: data["conditions_reglement_libelle"] = "" else: data["conditions_reglement_libelle"] = "" except: data["conditions_reglement_code"] = "" data["conditions_reglement_libelle"] = "" try: mode_regl = getattr(fourn_obj, "CT_ModeRegl", "").strip() data["mode_reglement_code"] = mode_regl if mode_regl: try: mode_obj = getattr(fourn_obj, "ModeReglement", None) if mode_obj: mode_obj.Read() data["mode_reglement_libelle"] = getattr( mode_obj, "M_Intitule", "" ).strip() else: data["mode_reglement_libelle"] = "" except: data["mode_reglement_libelle"] = "" else: data["mode_reglement_libelle"] = "" except: data["mode_reglement_code"] = "" data["mode_reglement_libelle"] = "" data["coordonnees_bancaires"] = [] try: factory_banque = getattr(fourn_obj, "FactoryBanque", None) if factory_banque: index = 1 while index <= 5: # Max 5 comptes bancaires try: banque_persist = factory_banque.List(index) if banque_persist is None: break banque = win32com.client.CastTo( banque_persist, "IBOBanque3" ) banque.Read() compte_bancaire = { "banque_nom": getattr( banque, "BI_Intitule", "" ).strip(), "iban": getattr(banque, "RIB_Iban", "").strip(), "bic": getattr(banque, "RIB_Bic", "").strip(), "code_banque": getattr( banque, "RIB_Banque", "" ).strip(), "code_guichet": getattr( banque, "RIB_Guichet", "" ).strip(), "numero_compte": getattr( banque, "RIB_Compte", "" ).strip(), "cle_rib": getattr(banque, "RIB_Cle", "").strip(), } if ( compte_bancaire["iban"] or compte_bancaire["numero_compte"] ): data["coordonnees_bancaires"].append(compte_bancaire) index += 1 except: break except Exception as e: logger.debug( f"Erreur coordonnées bancaires fournisseur {numero}: {e}" ) if data["coordonnees_bancaires"]: data["iban_principal"] = data["coordonnees_bancaires"][0].get( "iban", "" ) data["bic_principal"] = data["coordonnees_bancaires"][0].get("bic", "") else: data["iban_principal"] = "" data["bic_principal"] = "" data["contacts"] = [] try: factory_contact = getattr(fourn_obj, "FactoryContact", None) if factory_contact: index = 1 while index <= 20: # Max 20 contacts try: contact_persist = factory_contact.List(index) if contact_persist is None: break contact = win32com.client.CastTo( contact_persist, "IBOContact3" ) contact.Read() contact_data = { "nom": getattr(contact, "CO_Nom", "").strip(), "prenom": getattr(contact, "CO_Prenom", "").strip(), "fonction": getattr(contact, "CO_Fonction", "").strip(), "service": getattr(contact, "CO_Service", "").strip(), "telephone": getattr( contact, "CO_Telephone", "" ).strip(), "portable": getattr(contact, "CO_Portable", "").strip(), "email": getattr(contact, "CO_EMail", "").strip(), } nom_complet = f"{contact_data['prenom']} {contact_data['nom']}".strip() if nom_complet: contact_data["nom_complet"] = nom_complet else: contact_data["nom_complet"] = contact_data["nom"] if contact_data["nom"]: data["contacts"].append(contact_data) index += 1 except: break except Exception as e: logger.debug(f"Erreur contacts fournisseur {numero}: {e}") data["nb_contacts"] = len(data["contacts"]) if data["contacts"]: data["contact_principal"] = data["contacts"][0] else: data["contact_principal"] = None try: data["encours_autorise"] = float(getattr(fourn_obj, "CT_Encours", 0.0)) except: data["encours_autorise"] = 0.0 try: data["ca_annuel"] = float(getattr(fourn_obj, "CT_ChiffreAffaire", 0.0)) except: data["ca_annuel"] = 0.0 try: data["compte_general"] = getattr(fourn_obj, "CG_Num", "").strip() except: data["compte_general"] = "" try: date_creation = getattr(fourn_obj, "CT_DateCreate", None) data["date_creation"] = str(date_creation) if date_creation else "" except: data["date_creation"] = "" try: date_modif = getattr(fourn_obj, "CT_DateModif", None) data["date_modification"] = str(date_modif) if date_modif else "" except: data["date_modification"] = "" return data except Exception as e: logger.error(f" Erreur extraction fournisseur: {e}", exc_info=True) return { "numero": getattr(fourn_obj, "CT_Num", "").strip(), "intitule": getattr(fourn_obj, "CT_Intitule", "").strip(), "type": 1, "est_fournisseur": True, "est_actif": True, "en_sommeil": False, "adresse": "", "complement": "", "code_postal": "", "ville": "", "region": "", "pays": "", "adresse_complete": "", "telephone": "", "portable": "", "telecopie": "", "email": "", "site_web": "", "siret": "", "siren": "", "tva_intra": "", "code_naf": "", "forme_juridique": "", "categorie_tarifaire": None, "categorie_comptable": None, "conditions_reglement_code": "", "conditions_reglement_libelle": "", "mode_reglement_code": "", "mode_reglement_libelle": "", "iban_principal": "", "bic_principal": "", "coordonnees_bancaires": [], "contacts": [], "nb_contacts": 0, "contact_principal": None, "encours_autorise": 0.0, "ca_annuel": 0.0, "compte_general": "", "date_creation": "", "date_modification": "", } def normaliser_date(self, valeur): if isinstance(valeur, str): try: return datetime.fromisoformat(valeur) except ValueError: return datetime.now() elif isinstance(valeur, date): return datetime.combine(valeur, datetime.min.time()) elif isinstance(valeur, datetime): return valeur else: return datetime.now() def creer_devis_enrichi(self, devis_data: dict, forcer_brouillon: bool = False): if not self.cial: raise RuntimeError("Connexion Sage non établie") logger.info( f" Début création devis pour client {devis_data['client']['code']} " f"(brouillon={forcer_brouillon})" ) try: 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: pass logger.info(" Document devis créé") doc.DO_Date = pywintypes.Time( self.normaliser_date(devis_data.get("date_devis")) ) if "date_livraison" in devis_data and devis_data["date_livraison"]: doc.DO_DateLivr = pywintypes.Time( self.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 = self._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é") if forcer_brouillon: doc.DO_Statut = 0 logger.info(" Statut défini: 0 (Brouillon)") else: doc.DO_Statut = 2 logger.info(" Statut défini: 2 (Accepté)") doc.Write() try: factory_lignes = doc.FactoryDocumentLigne except: 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: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) except: try: ligne_obj.SetDefaultArticle(article_obj, quantite) except: 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() if not forcer_brouillon: logger.info(" Lancement Process()...") process.Process() else: try: process.Process() logger.info(" Process() appelé (brouillon)") except: logger.debug("Process() ignoré pour brouillon") numero_devis = self._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: 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, forcer_brouillon ) logger.info( f" DEVIS CRÉÉ: {numero_devis} - {doc_final_data['total_ttc']}€ TTC " ) return doc_final_data except Exception as e: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() logger.error(" Transaction annulée (rollback)") except: 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 _recuperer_numero_devis(self, process, doc): """Récupère le numéro du devis créé via plusieurs méthodes.""" numero_devis = None try: doc_result = process.DocumentResult if doc_result: doc_result = win32com.client.CastTo(doc_result, "IBODocumentVente3") doc_result.Read() numero_devis = getattr(doc_result, "DO_Piece", "") except: pass if not numero_devis: numero_devis = getattr(doc, "DO_Piece", "") if not numero_devis: try: doc.SetDefaultNumPiece() doc.Write() doc.Read() numero_devis = getattr(doc, "DO_Piece", "") except: pass return numero_devis def _relire_devis(self, numero_devis, devis_data, forcer_brouillon): """Relit le devis créé et extrait les informations finales.""" factory_doc = self.cial.FactoryDocumentVente persist_reread = factory_doc.ReadPiece(0, numero_devis) if not persist_reread: logger.debug("ReadPiece échoué, recherche dans List()...") persist_reread = self._rechercher_devis_dans_liste( numero_devis, factory_doc ) if persist_reread: doc_final = win32com.client.CastTo(persist_reread, "IBODocumentVente3") doc_final.Read() total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) statut_final = getattr(doc_final, "DO_Statut", 0) reference_final = getattr(doc_final, "DO_Ref", "") date_livraison_final = None try: date_livr = getattr(doc_final, "DO_DateLivr", None) if date_livr: date_livraison_final = date_livr.strftime("%Y-%m-%d") except: pass else: total_calcule = sum( l.get("montant_ligne_ht", 0) for l in devis_data["lignes"] ) total_ht = total_calcule total_ttc = round(total_calcule * 1.20, 2) statut_final = 0 if forcer_brouillon else 2 reference_final = devis_data.get("reference", "") date_livraison_final = devis_data.get("date_livraison") logger.info(f" Total HT: {total_ht}€") logger.info(f" Total TTC: {total_ttc}€") logger.info(f" Statut final: {statut_final}") if reference_final: logger.info(f" Référence: {reference_final}") if date_livraison_final: logger.info(f" Date livraison: {date_livraison_final}") return { "numero_devis": numero_devis, "total_ht": total_ht, "total_ttc": total_ttc, "nb_lignes": len(devis_data["lignes"]), "client_code": devis_data["client"]["code"], "date_devis": str(devis_data.get("date_devis", "")), "date_livraison": date_livraison_final, "reference": reference_final, "statut": statut_final, } def _rechercher_devis_dans_liste(self, numero_devis, factory_doc): """Recherche un devis dans les 100 premiers éléments de la liste.""" index = 1 while index < 100: try: persist_test = factory_doc.List(index) if persist_test is None: break doc_test = win32com.client.CastTo(persist_test, "IBODocumentVente3") doc_test.Read() if ( getattr(doc_test, "DO_Type", -1) == 0 and getattr(doc_test, "DO_Piece", "") == numero_devis ): logger.info(f" Document trouvé à l'index {index}") return persist_test index += 1 except: index += 1 return None def modifier_devis(self, numero: str, devis_data: Dict) -> Dict: """ Modifie un devis existant dans Sage - VERSION COMPLÈTE. Args: numero: Numéro du devis devis_data: dict contenant les champs à modifier: - date_devis: str ou date (optionnel) - date_livraison: str ou date (optionnel) - reference: str (optionnel) - statut: int (optionnel) - lignes: list[dict] (optionnel) """ 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: 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("") self._afficher_etat_document(doc, "📸 ÉTAT INITIAL") logger.info(" Vérification statut transformation...") self._verifier_devis_non_transforme(numero, doc) 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 = self._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 = self.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 = self.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 = self.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 = self.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: 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: 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(f" ✓ Article chargé") ligne_persist = factory_lignes.Create() try: ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3") except: ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3") try: ligne_obj.SetDefaultArticleReference(article_code, quantite) logger.info(f" ✓ Article associé via SetDefaultArticleReference") except: try: ligne_obj.SetDefaultArticle(article_obj, quantite) logger.info(f" ✓ Article associé via SetDefaultArticle") except: ligne_obj.DL_Design = ligne_data.get("designation", "") ligne_obj.DL_Qte = quantite logger.info(f" ✓ Article associé manuellement") if ligne_data.get("prix_unitaire_ht"): ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"]) logger.info(f" ✓ 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(f" ✓ Remise définie") except: logger.debug(f" 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(f" 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("") self._afficher_etat_document(doc, "📸 ÉTAT FINAL") logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 9] EXTRACTION RÉSULTAT") logger.info("=" * 80) resultat = self._extraire_infos_devis(doc, numero, champs_modifies) logger.info(f" 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 _afficher_etat_document(self, doc, titre: str): """Affiche l'état complet d'un document.""" logger.info("-" * 80) logger.info(titre) logger.info("-" * 80) try: logger.info(f" DO_Piece: {getattr(doc, 'DO_Piece', 'N/A')}") logger.info(f" DO_Ref: '{getattr(doc, 'DO_Ref', 'N/A')}'") logger.info(f" DO_Statut: {getattr(doc, 'DO_Statut', 'N/A')}") date_doc = getattr(doc, 'DO_Date', None) date_str = date_doc.strftime('%Y-%m-%d') if date_doc else 'None' logger.info(f" DO_Date: {date_str}") date_livr = getattr(doc, 'DO_DateLivr', None) date_livr_str = date_livr.strftime('%Y-%m-%d') if date_livr else 'None' logger.info(f" DO_DateLivr: {date_livr_str}") logger.info(f" DO_TotalHT: {getattr(doc, 'DO_TotalHT', 0)}€") logger.info(f" DO_TotalTTC: {getattr(doc, 'DO_TotalTTC', 0)}€") except Exception as e: logger.error(f" Erreur affichage état: {e}") logger.info("-" * 80) def _compter_lignes_document(self, doc) -> int: """Compte les lignes d'un document.""" try: try: factory_lignes = doc.FactoryDocumentLigne except: factory_lignes = doc.FactoryDocumentVenteLigne count = 0 index = 1 while index <= 100: try: ligne_p = factory_lignes.List(index) if ligne_p is None: break count += 1 index += 1 except: break return count except Exception as e: logger.warning(f" Erreur comptage lignes: {e}") return 0 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 = self._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 _rechercher_devis_par_numero(self, numero: str, factory): """Recherche un devis par numéro dans la liste.""" logger.info(f" Recherche de {numero} dans la liste...") index = 1 while index < 10000: try: persist_test = factory.List(index) if persist_test is None: break doc_test = win32com.client.CastTo(persist_test, "IBODocumentVente3") doc_test.Read() if ( getattr(doc_test, "DO_Type", -1) == 0 and getattr(doc_test, "DO_Piece", "") == numero ): logger.info(f" Trouvé à l'index {index}") return persist_test index += 1 except: index += 1 logger.error(f" Devis {numero} non trouvé dans la liste") return None def _verifier_devis_non_transforme(self, numero: str, doc): """Vérifie que le devis n'est pas transformé.""" verification = self.verifier_si_deja_transforme_sql(numero, 0) if verification["deja_transforme"]: docs_cibles = verification["documents_cibles"] nums = [d["numero"] for d in docs_cibles] raise ValueError( f" Devis {numero} déjà transformé en {len(docs_cibles)} document(s): {', '.join(nums)}" ) statut_actuel = getattr(doc, "DO_Statut", 0) if statut_actuel == 5: raise ValueError(f" Devis {numero} déjà transformé (statut=5)") def _extraire_infos_devis(self, doc, numero: str, champs_modifies: list) -> Dict: """Extrait les informations complètes du devis.""" total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) statut = getattr(doc, "DO_Statut", 0) reference = getattr(doc, "DO_Ref", "") date_devis = None try: date_doc = getattr(doc, "DO_Date", None) if date_doc: date_devis = date_doc.strftime("%Y-%m-%d") except: pass date_livraison = None try: date_livr = getattr(doc, "DO_DateLivr", None) if date_livr: date_livraison = date_livr.strftime("%Y-%m-%d") except: pass client_code = "" try: client_obj = getattr(doc, "Client", None) if client_obj: client_obj.Read() client_code = getattr(client_obj, "CT_Num", "") except: pass return { "numero": numero, "total_ht": total_ht, "total_ttc": total_ttc, "reference": reference, "date_devis": date_devis, "date_livraison": date_livraison, "champs_modifies": champs_modifies, "statut": statut, "client_code": client_code, } def lire_devis(self, numero_devis): try: devis = self._lire_document_sql(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): return self._lire_document_sql(numero, type_doc) def verifier_si_deja_transforme_sql(self, numero_source, type_source): """Version corrigée avec normalisation des types""" logger.info( f"[VERIF] Vérification transformations de {numero_source} (type {type_source})" ) logger.info( f"[VERIF] Vérification transformations de {numero_source} (type {type_source})" ) logger.info(f"[DEBUG] Type source brut: {type_source}") logger.info( f"[DEBUG] Type source après normalisation: {self._normaliser_type_document(type_source)}" ) logger.info( f"[DEBUG] Type source après normalisation SQL: {self._convertir_type_pour_sql(type_source)}" ) type_source = self._convertir_type_pour_sql(type_source) champ_liaison_mapping = { 0: "DL_PieceDE", 1: "DL_PieceBC", 3: "DL_PieceBL", } champ_liaison = champ_liaison_mapping.get(type_source) if not champ_liaison: logger.warning(f"[VERIF] Type source {type_source} non géré") return {"deja_transforme": False, "documents_cibles": []} try: with self._get_sql_connection() as conn: cursor = conn.cursor() query = f""" SELECT DISTINCT dc.DO_Piece, dc.DO_Type, dc.DO_Statut, (SELECT COUNT(*) FROM F_DOCLIGNE WHERE DO_Piece = dc.DO_Piece AND DO_Type = dc.DO_Type) as NbLignes FROM F_DOCENTETE dc INNER JOIN F_DOCLIGNE dl ON dc.DO_Piece = dl.DO_Piece AND dc.DO_Type = dl.DO_Type WHERE dl.{champ_liaison} = ? ORDER BY dc.DO_Type, dc.DO_Piece """ cursor.execute(query, (numero_source,)) resultats = cursor.fetchall() documents_cibles = [] for row in resultats: type_brut = int(row.DO_Type) type_normalise = self._convertir_type_depuis_sql(type_brut) doc = { "numero": row.DO_Piece.strip() if row.DO_Piece else "", "type": type_normalise, # ← TYPE NORMALISÉ "type_brut": type_brut, # Garder aussi le type original "type_libelle": self._get_type_libelle(type_brut), "statut": int(row.DO_Statut) if row.DO_Statut else 0, "nb_lignes": int(row.NbLignes) if row.NbLignes else 0, } documents_cibles.append(doc) logger.info( f"[VERIF] Trouvé: {doc['numero']} " f"(type {type_brut}→{type_normalise} - {doc['type_libelle']}) " f"- {doc['nb_lignes']} lignes" ) deja_transforme = len(documents_cibles) > 0 if deja_transforme: logger.info( f"[VERIF] Document {numero_source} a {len(documents_cibles)} transformation(s)" ) else: logger.info( f"[VERIF] Document {numero_source} pas encore transformé" ) return { "deja_transforme": deja_transforme, "documents_cibles": documents_cibles, } except Exception as e: logger.error(f"[VERIF] Erreur vérification: {e}") return {"deja_transforme": False, "documents_cibles": []} def peut_etre_transforme(self, numero_source, type_source, type_cible): """Version corrigée avec normalisation""" type_source = self._normaliser_type_document(type_source) type_cible = self._normaliser_type_document(type_cible) logger.info( f"[VERIF_TRANSFO] {numero_source} " f"(type {type_source}) → type {type_cible}" ) verif = self.verifier_si_deja_transforme_sql(numero_source, type_source) docs_meme_type = [ d for d in verif["documents_cibles"] if d["type"] == type_cible ] if docs_meme_type: nums = [d["numero"] for d in docs_meme_type] return { "possible": False, "raison": f"Document déjà transformé en {self._get_type_libelle(type_cible)}", "documents_existants": docs_meme_type, "message_detaille": f"Document(s) existant(s): {', '.join(nums)}", } return { "possible": True, "raison": "Transformation possible", "documents_existants": [], } def _get_type_libelle(self, type_doc: int) -> str: """ Retourne le libellé d'un type de document. Gère les 2 formats : valeur réelle Sage (0,10,20...) ET valeur affichée (0,1,2...) """ types_officiels = { 0: "Devis", 10: "Bon de commande", 20: "Préparation", 30: "Bon de livraison", 40: "Bon de retour", 50: "Bon d'avoir", 60: "Facture", } types_alternatifs = { 1: "Bon de commande", 2: "Préparation", 3: "Bon de livraison", 4: "Bon de retour", 5: "Bon d'avoir", 6: "Facture", } if type_doc in types_officiels: return types_officiels[type_doc] if type_doc in types_alternatifs: return types_alternatifs[type_doc] return f"Type {type_doc}" def _normaliser_type_document(self, type_doc: int) -> int: """ Normalise le type de document vers la valeur officielle Sage. Convertit 1→10, 2→20, etc. si nécessaire """ logger.info(f"[INFO] TYPE RECU{type_doc}") if type_doc in [0, 10, 20, 30, 40, 50, 60]: return type_doc mapping_normalisation = { 1: 10, # Commande 2: 20, # Préparation 3: 30, # BL 4: 40, # Retour 5: 50, # Avoir 6: 60, # Facture } return mapping_normalisation.get(type_doc, type_doc) def transformer_document( self, numero_source, type_source, type_cible, ignorer_controle_stock=False, 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"{self._get_type_libelle(type_source)} → {self._get_type_libelle(type_cible)}" ) module, methode = transformations_valides[(type_source, type_cible)] logger.info(f"[TRANSFORM] Méthode: Transformation.{module}.{methode}()") if verifier_doublons: logger.info("[TRANSFORM] Vérification des doublons...") verif = self.peut_etre_transforme(numero_source, type_source, type_cible) if not verif["possible"]: docs = [d["numero"] for d in verif.get("documents_existants", [])] raise ValueError( f"{verif['raison']}. Document(s) existant(s): {', '.join(docs)}" ) logger.info("[TRANSFORM] Aucun doublon détecté") try: with self._com_context(), self._lock_com: 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: 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: can_process = True if not can_process: erreurs = self.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: 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 = self.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: 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 as e: 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: 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 as e: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() logger.error("[TRANSFORM] Transaction annulée (rollback)") except: 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 lire_erreurs_sage(self, obj, nom_obj=""): """ Lit toutes les erreurs d'un objet Sage COM. Utilisé pour diagnostiquer les échecs de Process(). """ erreurs = [] try: if not hasattr(obj, "Errors") or obj.Errors is None: return erreurs nb_erreurs = 0 try: nb_erreurs = obj.Errors.Count except: return erreurs if nb_erreurs == 0: return erreurs for i in range(1, nb_erreurs + 1): try: err = None try: err = obj.Errors.Item(i) except: try: err = obj.Errors(i) except: try: err = obj.Errors.Item(i - 1) except: pass if err is not None: description = "" field = "" number = "" for attr in ["Description", "Descr", "Message", "Text"]: try: val = getattr(err, attr, None) if val: description = str(val) break except: pass for attr in ["Field", "FieldName", "Champ", "Property"]: try: val = getattr(err, attr, None) if val: field = str(val) break except: pass for attr in ["Number", "Code", "ErrorCode", "Numero"]: try: val = getattr(err, attr, None) if val is not None: number = str(val) break except: pass if description or field or number: erreurs.append( { "source": nom_obj, "index": i, "description": description or "Erreur inconnue", "field": field or "?", "number": number or "?", } ) except Exception as e: logger.debug(f"Erreur lecture erreur {i}: {e}") continue except Exception as e: logger.debug(f"Erreur globale lecture erreurs {nom_obj}: {e}") return erreurs def _find_document_in_list(self, numero, type_doc): """Cherche un document dans List() si ReadPiece échoue""" try: factory = self.cial.FactoryDocumentVente index = 1 while index < 10000: try: persist = factory.List(index) if persist is None: break doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() if ( getattr(doc, "DO_Type", -1) == type_doc and getattr(doc, "DO_Piece", "") == numero ): logger.info(f"[TRANSFORM] Document trouve a l'index {index}") return persist index += 1 except: index += 1 continue return None except Exception as e: logger.error(f"[TRANSFORM] Erreur recherche document: {e}") return None 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 self._cast_client(persist) except: 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 = self._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: pass try: contact_info["nom"] = ( getattr(client, "CT_Contact", "") or contact_info["client_intitule"] ) except: 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": self._safe_strip(row.CT_Num), "intitule": self._safe_strip(row.CT_Intitule), "adresse": self._safe_strip(row.CT_Adresse), "ville": self._safe_strip(row.CT_Ville), "code_postal": self._safe_strip(row.CT_CodePostal), "telephone": self._safe_strip(row.CT_Telephone), "email": self._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": self._safe_strip(row.CT_Num), "intitule": self._safe_strip(row.CT_Intitule), "type": 0, "qualite": self._safe_strip(row.CT_Qualite), "est_prospect": True, "adresse": self._safe_strip(row.CT_Adresse), "complement": self._safe_strip(row.CT_Complement), "ville": self._safe_strip(row.CT_Ville), "code_postal": self._safe_strip(row.CT_CodePostal), "pays": self._safe_strip(row.CT_Pays), "telephone": self._safe_strip(row.CT_Telephone), "portable": self._safe_strip(row.CT_Portable), "email": self._safe_strip(row.CT_EMail), "telecopie": self._safe_strip(row.CT_Telecopie), "siret": self._safe_strip(row.CT_Siret), "tva_intra": self._safe_strip(row.CT_Identifiant), "est_actif": (row.CT_Sommeil == 0), "contact": self._safe_strip(row.CT_Contact), "forme_juridique": self._safe_strip(row.CT_FormeJuridique), "secteur": self._safe_strip(row.CT_Secteur), } except Exception as e: logger.error(f" Erreur SQL prospect {code_prospect}: {e}") return None def lister_avoirs(self, limit=100, statut=None): try: with self._get_sql_connection() as conn: cursor = conn.cursor() query = f""" SELECT TOP ({limit}) d.DO_Piece, d.DO_Date, d.DO_Ref, d.DO_TotalHT, d.DO_TotalTTC, d.DO_Statut, d.CT_Num, c.CT_Intitule FROM F_DOCENTETE d LEFT JOIN F_COMPTET c ON d.CT_Num = c.CT_Num WHERE d.DO_Type = 50 """ params = [] if statut is not None: query += " AND d.DO_Statut = ?" params.append(statut) query += " ORDER BY d.DO_Date DESC" cursor.execute(query, params) rows = cursor.fetchall() avoirs = [] for row in rows: avoirs.append( { "numero": self._safe_strip(row.DO_Piece), "reference": self._safe_strip(row.DO_Ref), "date": str(row.DO_Date) if row.DO_Date else "", "client_code": self._safe_strip(row.CT_Num), "client_intitule": self._safe_strip(row.CT_Intitule), "total_ht": ( float(row.DO_TotalHT) if row.DO_TotalHT else 0.0 ), "total_ttc": ( float(row.DO_TotalTTC) if row.DO_TotalTTC else 0.0 ), "statut": row.DO_Statut if row.DO_Statut is not None else 0, } ) return avoirs except Exception as e: logger.error(f" Erreur SQL avoirs: {e}") return [] def lire_avoir(self, numero): return self._lire_document_sql(numero, type_doc=50) def lister_livraisons(self, limit=100, statut=None): """ Liste les livraisons via SQL (méthode legacy)""" try: with self._get_sql_connection() as conn: cursor = conn.cursor() query = f""" SELECT TOP ({limit}) d.DO_Piece, d.DO_Date, d.DO_Ref, d.DO_TotalHT, d.DO_TotalTTC, d.DO_Statut, d.CT_Num, c.CT_Intitule FROM F_DOCENTETE d LEFT JOIN F_COMPTET c ON d.CT_Num = c.CT_Num WHERE d.DO_Type = 30 """ params = [] if statut is not None: query += " AND d.DO_Statut = ?" params.append(statut) query += " ORDER BY d.DO_Date DESC" cursor.execute(query, params) rows = cursor.fetchall() livraisons = [] for row in rows: livraisons.append( { "numero": self._safe_strip(row.DO_Piece), "reference": self._safe_strip(row.DO_Ref), "date": str(row.DO_Date) if row.DO_Date else "", "client_code": self._safe_strip(row.CT_Num), "client_intitule": self._safe_strip(row.CT_Intitule), "total_ht": ( float(row.DO_TotalHT) if row.DO_TotalHT else 0.0 ), "total_ttc": ( float(row.DO_TotalTTC) if row.DO_TotalTTC else 0.0 ), "statut": row.DO_Statut if row.DO_Statut is not None else 0, } ) return livraisons except Exception as e: logger.error(f" Erreur SQL livraisons: {e}") return [] def lire_livraison(self, numero): """ Lit UNE livraison via SQL (avec lignes)""" return self._lire_document_sql(numero, type_doc=30) def creer_client(self, client_data: Dict) -> Dict: """ Creation client Sage - Version corrigée pour erreur cohérence """ if not self.cial: raise RuntimeError("Connexion Sage non etablie") 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 as e: 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: 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", # Clients "4010000", "401000", "401", # Fournisseurs ] 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: 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: 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) # URL ou @username 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) # URL ou profil 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: 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: """ Modification client Sage - Version complète alignée sur creer_client """ if not self.cial: raise RuntimeError("Connexion Sage non établie") try: with self._com_context(), self._lock_com: 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: 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_keys = ["statistique01", "secteur"] + [f"statistique{i:02d}" for i in range(2, 11)] stat_modifies = False stat01 = client_data.get("statistique01") or client_data.get("secteur") if stat01: if not stat_modifies: logger.info("[ETAPE 8] STATISTIQUES") stat_modifies = True if try_set_attribute(client, "CT_Statistique01", clean_str(stat01, 21)): champs_modifies.append("statistique01") for i in range(2, 11): key = f"statistique{i:02d}" if key in client_data: if not stat_modifies: logger.info("[ETAPE 8] STATISTIQUES") stat_modifies = True if try_set_attribute(client, f"CT_Statistique{i:02d}", clean_str(client_data[key], 21)): champs_modifies.append(key) 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 self._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: 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 self._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: """ Crée une commande dans Sage avec support des dates. Args: commande_data: dict contenant: - client: {code: str} - date_commande: str ou date - date_livraison: str ou date (optionnel) - reference: str (optionnel) - lignes: list[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: pass try: process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_BON_COMMANDE ) doc = process.Document try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") except: pass logger.info(" Document commande créé") doc.DO_Date = pywintypes.Time( self.normaliser_date(commande_data.get("date_commande")) ) if ("date_livraison" in commande_data and commande_data["date_livraison"]): doc.DO_DateLivr = pywintypes.Time( self.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 = self._cast_client(persist_client) if not client_obj: raise ValueError(f"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: pass try: factory_lignes = doc.FactoryDocumentLigne except: 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: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) logger.info( f" 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(f" Article associé via SetDefaultArticle") except Exception as e2: logger.error(f" 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: 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: 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( self.normaliser_date(commande_data.get("date_commande")) ), "date_livraison": date_livraison_final, "reference": reference_finale, } except Exception as e: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() except: 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: 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: 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(f"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(f" 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( self.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( self.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: 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( self.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( self.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: 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: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) except: try: ligne_obj.SetDefaultArticle(article_obj, quantite) except: 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: 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: 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: 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: pass try: process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_BON_LIVRAISON ) doc = process.Document try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") except: pass logger.info(" Document livraison créé") doc.DO_Date = pywintypes.Time( self.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( self.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 = self._cast_client(persist_client) if not client_obj: raise ValueError(f"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: pass try: factory_lignes = doc.FactoryDocumentLigne except: 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: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) logger.info( f" 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(f" Article associé via SetDefaultArticle") except Exception as e2: logger.error(f" 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: 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: 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( self.normaliser_date(livraison_data.get("date_livraison")) ), "date_livraison_prevue": date_livraison_prevue_final, "reference": reference_finale, } except Exception as e: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() except: 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: 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: 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(f"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( self.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( self.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( self.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( self.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: 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: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) except: try: ligne_obj.SetDefaultArticle(article_obj, quantite) except: 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: 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(f" 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(f" 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: 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: 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: pass try: process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_BON_AVOIR ) doc = process.Document try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") except: pass logger.info(" Document avoir créé") doc.DO_Date = pywintypes.Time( self.normaliser_date(avoir_data.get("date_avoir")) ) if "date_livraison" in avoir_data and avoir_data["date_livraison"]: doc.DO_DateLivr = pywintypes.Time( self.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 = self._cast_client(persist_client) if not client_obj: raise ValueError(f"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: pass try: factory_lignes = doc.FactoryDocumentLigne except: 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: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) logger.info( f" 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(f" Article associé via SetDefaultArticle") except Exception as e2: logger.error(f" 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: 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: 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( self.normaliser_date(avoir_data.get("date_avoir")) ), "date_livraison": date_livraison_final, "reference": reference_finale, } except Exception as e: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() except: 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: 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: 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(f"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(f" 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( self.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( self.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: 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( self.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( self.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: 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: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) except: try: ligne_obj.SetDefaultArticle(article_obj, quantite) except: 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: 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(f" 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(f" 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: 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: 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: pass try: process = self.cial.CreateProcess_Document( settings.SAGE_TYPE_FACTURE ) doc = process.Document try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") except: pass logger.info(" Document facture créé") doc.DO_Date = pywintypes.Time( self.normaliser_date(facture_data.get("date_facture")) ) if ( "date_livraison" in facture_data and facture_data["date_livraison"] ): doc.DO_DateLivr = pywintypes.Time( self.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 = self._cast_client(persist_client) if not client_obj: raise ValueError(f"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: 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: 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: pass try: if hasattr(doc, "DO_Regime"): doc.DO_Regime = 0 logger.debug(" Régime: 0 (défaut)") except: pass try: factory_lignes = doc.FactoryDocumentLigne except: 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: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) logger.info( f" 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(f" Article associé via SetDefaultArticle") except Exception as e2: logger.error(f" 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: try: doc.SetDefaultClient(client_obj) except: 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: 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: 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( self.normaliser_date(facture_data.get("date_facture")) ), "date_livraison": date_livraison_final, "reference": reference_finale, } except Exception as e: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() logger.error(" Transaction annulée (rollback)") except: 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: 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: 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(f"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(f" 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( self.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( self.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: 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( self.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( self.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: 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: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) quantite = float(ligne_data["quantite"]) try: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) except: try: ligne_obj.SetDefaultArticle(article_obj, quantite) except: 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: 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(f" 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(f" 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: 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: pass raise RuntimeError(f"Erreur Sage: {error_message}") def creer_article(self, article_data: dict) -> dict: 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: 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']})" ) 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}") logger.info(f"[ARTICLE] Stock réel demandé : {stock_reel}") logger.info(f"[ARTICLE] Stock mini demandé : {stock_mini}") logger.info(f"[ARTICLE] Stock maxi demandé : {stock_maxi}") 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" in error_msg or "non trouve" in error_msg or "-2607" in error_msg ): logger.debug( f"[ARTICLE] {reference} n'existe pas encore, création possible" ) else: logger.error(f"[ARTICLE] Erreur vérification : {e}") raise persist = factory.Create() article = win32com.client.CastTo(persist, "IBOArticle3") article.SetDefault() article.AR_Ref = reference article.AR_Design = designation logger.info("[MODELE] Recherche article modèle via SQL...") 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 = self._safe_strip(row.AR_Ref) logger.info( f" [SQL] Article modèle trouvé : {article_modele_ref}" ) except Exception as e: logger.warning(f" [SQL] Erreur recherche article : {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] Article modèle chargé : {article_modele_ref}" ) except Exception as e: logger.warning(f" [WARN] Erreur chargement modèle : {e}") article_modele = None if not article_modele: raise ValueError( "Aucun article modèle trouvé dans Sage.\n" "Créez au moins un article manuellement dans Sage pour servir de modèle." ) logger.info("[OBJETS] Copie Unite + Famille depuis modèle...") unite_trouvee = False try: unite_obj = getattr(article_modele, "Unite", None) if unite_obj: article.Unite = unite_obj logger.info( f" [OK] Objet Unite copié depuis {article_modele_ref}" ) unite_trouvee = True except Exception as e: logger.debug(f" Unite non copiable : {str(e)[:80]}") if not unite_trouvee: raise ValueError( "Impossible de copier l'unité de vente depuis le modèle" ) famille_trouvee = False famille_code_personnalise = article_data.get("famille") famille_obj = None if famille_code_personnalise: logger.info( f" [FAMILLE] Code personnalisé demandé : {famille_code_personnalise}" ) try: famille_existe_sql = False famille_code_exact = None try: 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 = self._safe_strip( row.FA_CodeFamille ) famille_existe_sql = True logger.info( f" [SQL] Famille trouvée : {famille_code_exact}" ) else: raise ValueError( f"Famille '{famille_code_personnalise}' introuvable" ) except ValueError: raise except Exception as e_sql: logger.warning(f" [SQL] Erreur : {e_sql}") if famille_existe_sql and famille_code_exact: factory_famille = self.cial.FactoryFamille try: index = 1 max_scan = 1000 while index <= max_scan: 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 à l'index {index}" ) break index += 1 except Exception as e: if "Accès refusé" in str(e): break index += 1 if famille_obj: famille_obj.Read() article.Famille = famille_obj logger.info( f" [OK] Famille '{famille_code_personnalise}' assignée" ) else: raise ValueError( f"Famille '{famille_code_personnalise}' inaccessible via COM" ) except Exception as e: logger.warning(f" [COM] Erreur scanner : {e}") raise except ValueError: raise except Exception as e: logger.warning( f" [WARN] Famille personnalisée non chargée : {str(e)[:100]}" ) if not famille_trouvee: try: famille_obj = getattr(article_modele, "Famille", None) if famille_obj: article.Famille = famille_obj logger.info( f" [OK] Objet Famille copié depuis {article_modele_ref}" ) famille_trouvee = True except Exception as e: logger.debug(f" Famille non copiable : {str(e)[:80]}") logger.info("[CHAMPS] Copie champs obligatoires depuis modèle...") 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(f" [OK] AR_SuiviStock=2 (FIFO/LIFO)") prix_vente = article_data.get("prix_vente") if prix_vente is not None: try: article.AR_PrixVen = float(prix_vente) logger.info(f" Prix vente : {prix_vente} EUR") except Exception as e: logger.warning(f" Prix vente erreur : {str(e)[:100]}") prix_achat = article_data.get("prix_achat") if prix_achat is not None: try: try: article.AR_PrixAch = float(prix_achat) logger.info( f" Prix achat (AR_PrixAch) : {prix_achat} EUR" ) except: article.AR_PrixAchat = float(prix_achat) logger.info( f" Prix achat (AR_PrixAchat) : {prix_achat} EUR" ) except Exception as e: logger.warning(f" Prix achat erreur : {str(e)[:100]}") code_ean = article_data.get("code_ean") if code_ean: article.AR_CodeBarre = str(code_ean) logger.info(f" Code EAN/Barre : {code_ean}") description = article_data.get("description") if description: try: article.AR_Commentaire = description logger.info(f" Description définie") except: pass 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: pass logger.error(f" [ERREUR] Write() échoué : {error_detail}") raise RuntimeError(f"Échec création article : {error_detail}") stock_defini = False stock_erreur = None has_stock_values = stock_reel or stock_mini or stock_maxi if has_stock_values: logger.info( f"[STOCK] Définition stock dans F_ARTSTOCK (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 trouvée : {factory_name}" ) break except: continue if not factory_stock: raise RuntimeError( "Factory de stock introuvable sur le dépôt" ) 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 non défini : {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 non défini : {e}") stock_obj.Write() stock_defini = True logger.info( f" [OK] Stock défini dans F_ARTSTOCK : réel={stock_reel}, min={stock_mini}, max={stock_maxi}" ) except Exception as e: stock_erreur = str(e) logger.error( f" [ERREUR] Stock non défini dans F_ARTSTOCK : {e}", exc_info=True, ) if transaction_active: try: self.cial.CptaApplication.CommitTrans() logger.info( "[COMMIT] Transaction committée - Article persiste dans Sage" ) except Exception as e: logger.warning(f"[COMMIT] Erreur commit : {e}") logger.info("[VERIF] Relecture article créé...") article_cree_persist = factory.ReadReference(reference) if not article_cree_persist: raise RuntimeError( "Article créé mais introuvable à la relecture" ) article_cree = win32com.client.CastTo( article_cree_persist, "IBOArticle3" ) article_cree.Read() 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(),), ) depot_rows = cursor.fetchall() for depot_row in depot_rows: if len(depot_row) >= 4: qte = float(depot_row[1]) if depot_row[1] else 0.0 stock_total += qte stocks_par_depot.append( { "depot_code": self._safe_strip( depot_row[0] ), "quantite": qte, "qte_mini": ( float(depot_row[2]) if depot_row[2] else 0.0 ), "qte_maxi": ( float(depot_row[3]) if depot_row[3] else 0.0 ), } ) logger.info( f"[VERIF] Stock dans F_ARTSTOCK : {stock_total} unités dans {len(stocks_par_depot)} dépôt(s)" ) except Exception as e: logger.warning( f"[VERIF] Impossible de vérifier le stock dans F_ARTSTOCK : {e}" ) logger.info( f"[OK] ARTICLE CREE : {reference} - Stock total : {stock_total}" ) logger.info("[EXTRACTION] Extraction complète de l'article créé...") resultat = self._extraire_article(article_cree) if not resultat: resultat = { "reference": reference, "designation": designation, } resultat["stock_reel"] = stock_total if stock_mini: resultat["stock_mini"] = float(stock_mini) if stock_maxi: resultat["stock_maxi"] = float(stock_maxi) resultat["stock_disponible"] = stock_total resultat["stock_reserve"] = 0.0 resultat["stock_commande"] = 0.0 if prix_vente is not None: resultat["prix_vente"] = float(prix_vente) if prix_achat is not None: resultat["prix_achat"] = float(prix_achat) if description: resultat["description"] = description if code_ean: resultat["code_ean"] = str(code_ean) resultat["code_barre"] = str(code_ean) if famille_code_personnalise and famille_trouvee: resultat["famille_code"] = famille_code_personnalise try: if famille_obj: famille_obj.Read() resultat["famille_libelle"] = getattr( famille_obj, "FA_Intitule", "" ) except: pass if stocks_par_depot: resultat["stocks_par_depot"] = stocks_par_depot resultat["depot_principal"] = { "code": depot_a_utiliser["code"], "intitule": depot_a_utiliser["intitule"], } resultat["suivi_stock_active"] = stock_defini if has_stock_values and not stock_defini and stock_erreur: resultat["avertissement"] = ( f"Stock demandé mais non défini dans F_ARTSTOCK : {stock_erreur}" ) logger.info( f"[EXTRACTION] Article extrait et enrichi avec {len(resultat)} champs" ) return resultat except ValueError: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() except: pass raise except Exception as e: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() except: pass logger.error(f"Erreur creation article : {e}", exc_info=True) raise RuntimeError(f"Erreur creation article : {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: 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() designation_actuelle = getattr(article, "AR_Design", "") logger.info(f"[ARTICLE] Trouvé : {reference} - {designation_actuelle}") logger.info("[ARTICLE] Mise à jour des champs...") champs_modifies = [] if "famille" in article_data and article_data["famille"]: famille_code_demande = article_data["famille"].upper().strip() logger.info( f"[FAMILLE] Changement demandé : {famille_code_demande}" ) try: famille_existe_sql = False famille_code_exact = None famille_type = None try: 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 = self._safe_strip( row.FA_CodeFamille ) famille_type = row.FA_Type if len(row) > 1 else 0 famille_existe_sql = True if famille_type == 1: raise ValueError( f"La famille '{famille_code_demande}' est de type 'Total' " f"et ne peut pas contenir d'articles. " f"Utilisez une famille de type Détail." ) logger.info( f" [SQL] Famille trouvée : {famille_code_exact} (type={famille_type})" ) else: raise ValueError( f"Famille '{famille_code_demande}' introuvable dans Sage" ) except ValueError: raise except Exception as e: logger.warning(f" [SQL] Erreur : {e}") raise ValueError(f"Impossible de vérifier la famille : {e}") if famille_existe_sql and famille_code_exact: logger.info(f" [COM] Recherche via scanner...") factory_famille = self.cial.FactoryFamille famille_obj = None try: index = 1 max_scan = 1000 while index <= max_scan: 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 trouvée à l'index {index}" ) break index += 1 except Exception as e: if "Accès refusé" in str(e) or "Access" in str( e ): break index += 1 except Exception as e: logger.warning( f" [COM] Scanner échoué : {str(e)[:200]}" ) if famille_obj: famille_obj.Read() article.Famille = famille_obj champs_modifies.append(f"famille={famille_code_exact}") logger.info( f" [OK] Famille changée : {famille_code_exact}" ) else: raise ValueError( f"Famille '{famille_code_demande}' trouvée en SQL mais inaccessible via COM. " f"Essayez avec une autre famille." ) except ValueError: raise except Exception as e: logger.error(f" [ERREUR] Changement famille : {e}") raise ValueError(f"Impossible de changer la famille : {str(e)}") if "designation" in article_data: designation = str(article_data["designation"])[:69].strip() article.AR_Design = designation champs_modifies.append(f"designation") logger.info(f" [OK] Désignation : {designation}") if "prix_vente" in article_data: try: prix_vente = float(article_data["prix_vente"]) article.AR_PrixVen = prix_vente champs_modifies.append("prix_vente") logger.info(f" [OK] Prix vente : {prix_vente} EUR") except Exception as e: logger.warning(f" [WARN] Prix vente : {e}") if "prix_achat" in article_data: try: prix_achat = float(article_data["prix_achat"]) try: article.AR_PrixAch = prix_achat champs_modifies.append("prix_achat") logger.info( f" [OK] Prix achat (AR_PrixAch) : {prix_achat} EUR" ) except: article.AR_PrixAchat = prix_achat champs_modifies.append("prix_achat") logger.info( f" [OK] Prix achat (AR_PrixAchat) : {prix_achat} EUR" ) except Exception as e: logger.warning(f" [WARN] Prix achat : {e}") if "stock_reel" in article_data: try: stock_reel = float(article_data["stock_reel"]) ancien_stock = float(getattr(article, "AR_Stock", 0.0)) article.AR_Stock = stock_reel champs_modifies.append("stock_reel") logger.info(f" [OK] Stock : {ancien_stock} -> {stock_reel}") if stock_reel > ancien_stock: logger.info( f" [+] Stock augmenté de {stock_reel - ancien_stock}" ) except Exception as e: logger.error(f" [ERREUR] Stock : {e}") raise ValueError(f"Impossible de modifier le stock: {e}") if "stock_mini" in article_data: try: stock_mini = float(article_data["stock_mini"]) article.AR_StockMini = stock_mini champs_modifies.append("stock_mini") logger.info(f" [OK] Stock mini : {stock_mini}") except Exception as e: logger.warning(f" [WARN] Stock mini : {e}") if "stock_maxi" in article_data: try: stock_maxi = float(article_data["stock_maxi"]) article.AR_StockMaxi = stock_maxi champs_modifies.append("stock_maxi") logger.info(f" [OK] Stock maxi : {stock_maxi}") except Exception as e: logger.warning(f" [WARN] Stock maxi : {e}") if "code_ean" in article_data: try: code_ean = str(article_data["code_ean"])[:13].strip() article.AR_CodeBarre = code_ean champs_modifies.append("code_ean") logger.info(f" [OK] Code EAN : {code_ean}") except Exception as e: logger.warning(f" [WARN] Code EAN : {e}") if "description" in article_data: try: description = str(article_data["description"])[:255].strip() article.AR_Commentaire = description champs_modifies.append("description") logger.info(f" [OK] Description définie") except Exception as e: logger.warning(f" [WARN] Description : {e}") if not champs_modifies: logger.warning("[ARTICLE] Aucun champ à modifier") return self._extraire_article(article) logger.info( f"[ARTICLE] Champs à modifier : {', '.join(champs_modifies)}" ) logger.info("[ARTICLE] Écriture des modifications...") try: article.Write() logger.info("[ARTICLE] 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: pass logger.error(f"[ARTICLE] Erreur Write() : {error_detail}") raise RuntimeError(f"Échec modification : {error_detail}") article.Read() logger.info( f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)" ) resultat = self._extraire_article(article) if not resultat: resultat = { "reference": reference, "designation": getattr(article, "AR_Design", ""), } if "prix_vente" in article_data: resultat["prix_vente"] = float(article_data["prix_vente"]) if "prix_achat" in article_data: resultat["prix_achat"] = float(article_data["prix_achat"]) if "stock_reel" in article_data: resultat["stock_reel"] = float(article_data["stock_reel"]) if "stock_mini" in article_data: resultat["stock_mini"] = float(article_data["stock_mini"]) if "stock_maxi" in article_data: resultat["stock_maxi"] = float(article_data["stock_maxi"]) if "code_ean" in article_data: resultat["code_ean"] = str(article_data["code_ean"]) resultat["code_barre"] = str(article_data["code_ean"]) if "description" in article_data: resultat["description"] = str(article_data["description"]) if "famille" in article_data: resultat["famille_code"] = ( famille_code_exact if "famille_code_exact" in locals() else "" ) 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: 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 # Re-raise si c'est notre erreur except: 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 # Toujours Détail logger.info(f"[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: 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, # Toujours Détail "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: 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], # datetime - garder tel quel "cb_creation": row[idx+3], # datetime - garder tel quel "cb_creation_user": to_str(row[idx+4]), }) idx += 5 famille.update({ "compte_vente": to_str(row[idx]), "compte_auxiliaire_vente": to_str(row[idx+1]), "tva_vente_1": to_str(row[idx+2]), "tva_vente_2": to_str(row[idx+3]), "tva_vente_3": to_str(row[idx+4]), "tva_vente_date_1": row[idx+5], # datetime "tva_vente_date_2": row[idx+6], "tva_vente_date_3": row[idx+7], "type_facture_vente": to_int(row[idx+8]), }) idx += 9 famille.update({ "compte_achat": to_str(row[idx]), "compte_auxiliaire_achat": to_str(row[idx+1]), "tva_achat_1": to_str(row[idx+2]), "tva_achat_2": to_str(row[idx+3]), "tva_achat_3": to_str(row[idx+4]), "tva_achat_date_1": row[idx+5], "tva_achat_date_2": row[idx+6], "tva_achat_date_3": row[idx+7], "type_facture_achat": to_int(row[idx+8]), }) idx += 9 famille.update({ "compte_stock": to_str(row[idx]), "compte_auxiliaire_stock": to_str(row[idx+1]), }) idx += 2 famille.update({ "fournisseur_principal": to_str(row[idx]), "fournisseur_unite": to_str(row[idx+1]), "fournisseur_conversion": to_float(row[idx+2]), "fournisseur_delai_appro": to_int(row[idx+3]), "fournisseur_garantie": to_int(row[idx+4]), "fournisseur_colisage": to_int(row[idx+5]), "fournisseur_qte_mini": to_float(row[idx+6]), "fournisseur_qte_mont": to_float(row[idx+7]), "fournisseur_enumere_gamme": to_int(row[idx+8]), "fournisseur_devise": to_int(row[idx+9]), "fournisseur_remise": to_float(row[idx+10]), "fournisseur_conv_div": to_float(row[idx+11]), "fournisseur_type_remise": to_int(row[idx+12]), }) idx += 13 famille["nb_articles"] = to_int(row[idx]) famille["type_libelle"] = "Total" if famille["type"] == 1 else "Détail" famille["est_total"] = famille["type"] == 1 famille["est_detail"] = famille["type"] == 0 famille["FA_CodeFamille"] = famille["code"] famille["FA_Intitule"] = famille["intitule"] famille["FA_Type"] = famille["type"] famille["CG_NumVte"] = famille["compte_vente"] famille["CG_NumAch"] = famille["compte_achat"] familles.append(famille) type_msg = "DÉTAIL uniquement" if not inclure_totaux else "TOUS types" logger.info(f"✓ {len(familles)} familles chargées ({type_msg})") return familles except Exception as e: logger.error(f"Erreur SQL familles: {e}", exc_info=True) raise RuntimeError(f"Erreur lecture familles: {str(e)}") def lire_famille(self, code: str) -> Dict: """ Lit une seule famille - même structure que lister_toutes_familles Args: code: Code de la famille à lire Returns: Dict avec la structure identique à lister_toutes_familles """ 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(f"[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: pass try: factory_doc = self.cial.FactoryDocumentStock persist_doc = factory_doc.CreateType(180) # 180 = Entrée 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(f"[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: 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: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentStockLigne3" ) ligne_obj.SetDefault() try: ligne_obj.SetDefaultArticleReference( article_ref, float(quantite) ) except: try: ligne_obj.SetDefaultArticle( article_obj, float(quantite) ) except: 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: 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( f" [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: 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: 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: 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( f" 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( f" [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: 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: 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(f"[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: 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: 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: 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(f"[STOCK] Transaction committée") except: logger.info(f"[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(f"[STOCK] Transaction annulée") except: 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 lire_stock_article(self, reference: str, calcul_complet: bool = False) -> Dict: try: with self._com_context(), self._lock_com: logger.info(f"[STOCK] Lecture stock : {reference}") factory_article = self.cial.FactoryArticle persist_article = factory_article.ReadReference(reference.upper()) if not persist_article: raise ValueError(f"Article {reference} introuvable") article = win32com.client.CastTo(persist_article, "IBOArticle3") article.Read() ar_suivi = getattr(article, "AR_SuiviStock", 0) ar_design = getattr(article, "AR_Design", reference) stock_info = { "article": reference.upper(), "designation": ar_design, "stock_total": 0.0, "suivi_stock": ar_suivi, "suivi_libelle": { 0: "Aucun suivi", 1: "CMUP (sans lot)", 2: "FIFO/LIFO (avec lot)", }.get(ar_suivi, f"Code {ar_suivi}"), "depots": [], "methode_lecture": None, } logger.info("[STOCK] Tentative Méthode 1 : Depot.FactoryDepotStock...") try: factory_depot = self.cial.FactoryDepot index_depot = 1 stocks_trouves = [] while index_depot <= 20: try: persist_depot = factory_depot.List(index_depot) if persist_depot is None: break depot = win32com.client.CastTo(persist_depot, "IBODepot3") depot.Read() depot_code = "" depot_intitule = "" try: depot_code = getattr(depot, "DE_Code", "").strip() depot_intitule = getattr( depot, "DE_Intitule", f"Dépôt {depot_code}" ) except: pass if not depot_code: index_depot += 1 continue factory_depot_stock = None for factory_name in [ "FactoryDepotStock", "FactoryArticleStock", ]: try: factory_depot_stock = getattr( depot, factory_name, None ) if factory_depot_stock: break except: pass if factory_depot_stock: index_stock = 1 while index_stock <= 1000: try: stock_persist = factory_depot_stock.List( index_stock ) if stock_persist is None: break stock = win32com.client.CastTo( stock_persist, "IBODepotStock3" ) stock.Read() article_ref_stock = "" for attr_ref in [ "AR_Ref", "AS_Article", "Article_Ref", ]: try: val = getattr(stock, attr_ref, None) if val: article_ref_stock = ( str(val).strip().upper() ) break except: pass if not article_ref_stock: try: article_obj = getattr( stock, "Article", None ) if article_obj: article_obj.Read() article_ref_stock = ( getattr( article_obj, "AR_Ref", "" ) .strip() .upper() ) except: pass if article_ref_stock == reference.upper(): quantite = 0.0 qte_mini = 0.0 qte_maxi = 0.0 for attr_qte in [ "AS_QteSto", "AS_Qte", "QteSto", "Quantite", ]: try: val = getattr(stock, attr_qte, None) if val is not None: quantite = float(val) break except: pass try: qte_mini = float( getattr(stock, "AS_QteMini", 0.0) ) except: pass try: qte_maxi = float( getattr(stock, "AS_QteMaxi", 0.0) ) except: pass stocks_trouves.append( { "code": depot_code, "intitule": depot_intitule, "quantite": quantite, "qte_mini": qte_mini, "qte_maxi": qte_maxi, } ) logger.info( f"[STOCK] Trouvé dépôt {depot_code} : {quantite} unités" ) break index_stock += 1 except Exception as e: if "Accès refusé" in str(e): break index_stock += 1 index_depot += 1 except Exception as e: if "Accès refusé" in str(e): break index_depot += 1 if stocks_trouves: stock_info["depots"] = stocks_trouves stock_info["stock_total"] = sum( d["quantite"] for d in stocks_trouves ) stock_info["methode_lecture"] = ( "Depot.FactoryDepotStock (RAPIDE)" ) logger.info( f"[STOCK] Méthode 1 réussie : {stock_info['stock_total']} unités" ) return stock_info except Exception as e: logger.warning(f"[STOCK] Méthode 1 échouée : {e}") logger.info("[STOCK] Tentative Méthode 2 : Attributs Article...") try: stock_trouve = False for attr_stock in ["AR_Stock", "AR_QteSto", "AR_QteStock"]: try: val = getattr(article, attr_stock, None) if val is not None: stock_info["stock_total"] = float(val) stock_info["methode_lecture"] = ( f"Article.{attr_stock} (RAPIDE)" ) stock_trouve = True logger.info( f"[STOCK] Méthode 2 réussie via {attr_stock}" ) break except: pass if stock_trouve: return stock_info except Exception as e: logger.warning(f"[STOCK] Méthode 2 échouée : {e}") if not calcul_complet: logger.warning( f"[STOCK] Méthodes rapides échouées pour {reference}" ) stock_info["methode_lecture"] = "ÉCHEC - Méthodes rapides échouées" stock_info["stock_total"] = 0.0 stock_info["note"] = ( "Les méthodes rapides de lecture de stock ont échoué. " "Options disponibles :\n" "1. Utiliser GET /sage/diagnostic/stock-rapide-test/{reference} pour un diagnostic détaillé\n" "2. Appeler lire_stock_article(reference, calcul_complet=True) pour un calcul depuis mouvements (LENT - 20+ min)\n" "3. Vérifier que l'article a bien un suivi de stock activé (AR_SuiviStock)" ) return stock_info logger.warning( "[STOCK] CALCUL DEPUIS MOUVEMENTS ACTIVÉ - PEUT PRENDRE 20+ MINUTES" ) except ValueError: raise except Exception as e: logger.error(f"[STOCK] Erreur lecture : {e}", exc_info=True) raise ValueError(f"Erreur lecture stock : {str(e)}") def creer_sortie_stock(self, sortie_data: Dict) -> Dict: try: with self._com_context(), self._lock_com: logger.info(f"[STOCK] === CRÉATION SORTIE STOCK ===") logger.info(f"[STOCK] {len(sortie_data.get('lignes', []))} ligne(s)") try: self.cial.CptaApplication.BeginTrans() except: pass try: factory = self.cial.FactoryDocumentStock persist = factory.CreateType(181) # 181 = Sortie 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: 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 = self.verifier_stock_suffisant( article_ref, quantite, 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: # CMUP if numero_lot: logger.warning(f"[STOCK] CMUP : Suppression du lot") numero_lot = None elif ar_suivi == 2: # FIFO/LIFO 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: 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(f"[STOCK] SetDefaultArticleReference()") except: try: ligne_obj.SetDefaultArticle( article_obj, float(quantite) ) article_lie = True logger.info(f"[STOCK] SetDefaultArticle()") except: 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(f"[STOCK] Lot défini") except: try: ligne_obj.LS_NoSerie = numero_lot logger.info(f"[STOCK] Lot via LS_NoSerie") except: pass prix = ligne_data.get("prix_unitaire") if prix: try: ligne_obj.DL_PrixUnitaire = float(prix) except: pass ligne_obj.Write() logger.info(f"[STOCK] Write() réussi") ligne_obj.Read() ref_verifiee = article_ref # Supposer OK si Write() réussi 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: 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(f"[STOCK] Transaction committée") except: 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: 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: 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: 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: 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: 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 verifier_stock_suffisant(self, article_ref, quantite, depot=None): """Version thread-safe avec lock SQL""" try: with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE") cursor.execute("BEGIN TRANSACTION") try: cursor.execute( """ SELECT SUM(AS_QteSto) FROM F_ARTSTOCK WITH (UPDLOCK, ROWLOCK) WHERE AR_Ref = ? """, (article_ref.upper(),), ) row = cursor.fetchone() stock_dispo = float(row[0]) if row and row[0] else 0.0 suffisant = stock_dispo >= quantite cursor.execute("COMMIT") return { "suffisant": suffisant, "stock_disponible": stock_dispo, "quantite_demandee": quantite, } except: cursor.execute("ROLLBACK") raise except Exception as e: logger.error(f"Erreur vérification stock: {e}") raise