From 3fa98a168c8c56ae52550f6de0b16223cf6e7231 Mon Sep 17 00:00:00 2001 From: fanilo Date: Sat, 3 Jan 2026 17:01:57 +0100 Subject: [PATCH] lire_article and prix_achat functionnal --- sage_connector.py | 1100 ++++++++++++++++++--------------------------- 1 file changed, 438 insertions(+), 662 deletions(-) diff --git a/sage_connector.py b/sage_connector.py index 6b8387a..402a118 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -1445,138 +1445,171 @@ class SageConnector: raise RuntimeError(f"Erreur lecture articles: {str(e)}") def lire_article(self, reference): + """ + Lit un article complet depuis SQL avec enrichissements + Version alignée sur lister_tous_articles + """ try: with self._get_sql_connection() as conn: cursor = conn.cursor() - cursor.execute( - """ - SELECT - AR_Ref, AR_Design, AR_PrixVen, AR_PrixAch, - AR_UniteVen, FA_CodeFamille, AR_Sommeil, - AR_CodeBarre, AR_Type - FROM F_ARTICLE - WHERE AR_Ref = ? - """, - (reference.upper(),), - ) + # === DÉTECTION DES COLONNES (identique à lister_tous_articles) === + logger.info(f"[SQL] Lecture article {reference}...") + cursor.execute("SELECT TOP 1 * FROM F_ARTICLE") + colonnes_disponibles = [column[0] for column in cursor.description] + # Configuration du mapping (identique à lister_tous_articles) + colonnes_config = { + "AR_Ref": "reference", + "AR_Design": "designation", + "AR_CodeBarre": "code_barre", + "AR_EdiCode": "edi_code", + "AR_Raccourci": "raccourci", + "AR_PrixVen": "prix_vente", + "AR_PrixAch": "prix_achat", + "AR_Coef": "coef", + "AR_PUNet": "prix_net", + "AR_PrixAchNouv": "prix_achat_nouveau", + "AR_CoefNouv": "coef_nouveau", + "AR_PrixVenNouv": "prix_vente_nouveau", + "AR_DateApplication": "date_application_prix", + "AR_CoutStd": "cout_standard", + "AR_UniteVen": "unite_vente", + "AR_UnitePoids": "unite_poids", + "AR_PoidsNet": "poids_net", + "AR_PoidsBrut": "poids_brut", + "AR_Gamme1": "gamme_1", + "AR_Gamme2": "gamme_2", + "FA_CodeFamille": "famille_code", + "AR_Type": "type_article", + "AR_Nature": "nature", + "AR_Garantie": "garantie", + "AR_CodeFiscal": "code_fiscal", + "AR_Pays": "pays", + "CO_No": "fournisseur_principal", + "AR_Condition": "conditionnement", + "AR_NbColis": "nb_colis", + "AR_Prevision": "prevision", + "AR_SuiviStock": "suivi_stock", + "AR_Nomencl": "nomenclature", + "AR_QteComp": "qte_composant", + "AR_QteOperatoire": "qte_operatoire", + "AR_Sommeil": "sommeil", + "AR_Substitut": "article_substitut", + "AR_Escompte": "soumis_escompte", + "AR_Delai": "delai", + "AR_Stat01": "stat_01", + "AR_Stat02": "stat_02", + "AR_Stat03": "stat_03", + "AR_Stat04": "stat_04", + "AR_Stat05": "stat_05", + "AR_HorsStat": "hors_statistique", + "CL_No1": "categorie_1", + "CL_No2": "categorie_2", + "CL_No3": "categorie_3", + "CL_No4": "categorie_4", + "AR_DateModif": "date_modification", + "AR_VteDebit": "vente_debit", + "AR_NotImp": "non_imprimable", + "AR_Transfere": "transfere", + "AR_Publie": "publie", + "AR_Contremarque": "contremarque", + "AR_FactPoids": "fact_poids", + "AR_FactForfait": "fact_forfait", + "AR_SaisieVar": "saisie_variable", + "AR_Fictif": "fictif", + "AR_SousTraitance": "sous_traitance", + "AR_Criticite": "criticite", + "RP_CodeDefaut": "reprise_code_defaut", + "AR_DelaiFabrication": "delai_fabrication", + "AR_DelaiPeremption": "delai_peremption", + "AR_DelaiSecurite": "delai_securite", + "AR_TypeLancement": "type_lancement", + "AR_Cycle": "cycle", + "AR_Photo": "photo", + "AR_Langue1": "langue_1", + "AR_Langue2": "langue_2", + "AR_Frais01FR_Denomination": "frais_01_denomination", + "AR_Frais02FR_Denomination": "frais_02_denomination", + "AR_Frais03FR_Denomination": "frais_03_denomination", + "Marque commerciale": "marque_commerciale", + "Objectif / Qtés vendues": "objectif_qtes_vendues", + "Pourcentage teneur en or": "pourcentage_or", + "1ère commercialisation": "premiere_commercialisation", + "AR_InterdireCommande": "interdire_commande", + "AR_Exclure": "exclure", + } + + # Sélection des colonnes disponibles + colonnes_a_lire = [ + col_sql + for col_sql in colonnes_config.keys() + if col_sql in colonnes_disponibles + ] + + if not colonnes_a_lire: + logger.error("[SQL] Aucune colonne mappée trouvée !") + return None + + # Construction de la requête SQL avec échappement des noms de colonnes + colonnes_sql = [] + for col in colonnes_a_lire: + if " " in col or "/" in col or "è" in col: + colonnes_sql.append(f"[{col}]") + else: + colonnes_sql.append(col) + + colonnes_str = ", ".join(colonnes_sql) + query = f"SELECT {colonnes_str} FROM F_ARTICLE WHERE AR_Ref = ?" + + logger.debug(f"[SQL] Requête : {query[:200]}...") + cursor.execute(query, (reference.upper(),)) row = cursor.fetchone() if not row: + logger.info(f"[SQL] Article {reference} non trouvé") return None - article = { - "reference": _safe_strip(row[0]), - "designation": _safe_strip(row[1]), - "prix_vente": float(row[2]) if row[2] is not None else 0.0, - "prix_achat": float(row[3]) if row[3] is not None else 0.0, - "unite_vente": str(row[4]).strip() if row[4] is not None else "", - "famille_code": _safe_strip(row[5]), - "est_actif": (row[6] == 0), - "code_ean": _safe_strip(row[7]), - "code_barre": _safe_strip(row[7]), - "type_article": row[8] if row[8] is not None else 0, - "type_article_libelle": { - 0: "Article", - 1: "Prestation", - 2: "Divers", - }.get(row[8] if row[8] is not None else 0, "Article"), - "description": "", - "designation_complementaire": "", - "poids": 0.0, - "volume": 0.0, - "tva_code": "", - "date_creation": "", - "date_modification": "", - "stock_reel": 0.0, - "stock_mini": 0.0, - "stock_maxi": 0.0, - "stock_reserve": 0.0, - "stock_commande": 0.0, - "stock_disponible": 0.0, - } + # Construction du dictionnaire row_data + row_data = {} + for idx, col_sql in enumerate(colonnes_a_lire): + valeur = row[idx] + if isinstance(valeur, str): + valeur = valeur.strip() + row_data[col_sql] = valeur - article["tva_taux"] = 20.0 + # Mapping de l'article (fonction partagée) + article = _mapper_article_depuis_row(row_data, colonnes_config) - logger.info(f" Lecture stock depuis F_ARTSTOCK pour {reference}...") + # Enrichissements (dans le même ordre que lister_tous_articles) + articles = [ + article + ] # Liste d'un seul article pour les fonctions d'enrichissement - try: - cursor.execute( - """ - SELECT - 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 = ? - GROUP BY AR_Ref - """, - (reference.upper(),), - ) + articles = _enrichir_stocks_articles(articles, cursor) + articles = _enrichir_familles_articles(articles, cursor) + articles = _enrichir_fournisseurs_articles(articles, cursor) + articles = _enrichir_tva_articles(articles, cursor) - stock_row = cursor.fetchone() + articles = _enrichir_stock_emplacements(articles, cursor) + articles = _enrichir_gammes_articles(articles, cursor) + articles = _enrichir_tarifs_clients(articles, cursor) + articles = _enrichir_nomenclature(articles, cursor) + articles = _enrichir_compta_articles(articles, cursor) + articles = _enrichir_fournisseurs_multiples(articles, cursor) + articles = _enrichir_depots_details(articles, cursor) + articles = _enrichir_emplacements_details(articles, cursor) + articles = _enrichir_gammes_enumeres(articles, cursor) + articles = _enrichir_references_enumerees(articles, cursor) + articles = _enrichir_medias_articles(articles, cursor) + articles = _enrichir_prix_gammes(articles, cursor) + articles = _enrichir_conditionnements(articles, cursor) - if stock_row: - article["stock_reel"] = ( - float(stock_row[0]) if stock_row[0] else 0.0 - ) - article["stock_mini"] = ( - float(stock_row[1]) if stock_row[1] else 0.0 - ) - article["stock_maxi"] = ( - float(stock_row[2]) if stock_row[2] else 0.0 - ) - - stock_reserve_artstock = ( - float(stock_row[3]) if stock_row[3] else 0.0 - ) - stock_commande_artstock = ( - float(stock_row[4]) if stock_row[4] else 0.0 - ) - - if stock_reserve_artstock > 0: - article["stock_reserve"] = stock_reserve_artstock - if stock_commande_artstock > 0: - article["stock_commande"] = stock_commande_artstock - - article["stock_disponible"] = ( - article["stock_reel"] - article["stock_reserve"] - ) - - logger.info( - f" Stock trouvé dans F_ARTSTOCK: {article['stock_reel']} unités" - ) - else: - logger.info( - f"Aucun stock trouvé dans F_ARTSTOCK pour {reference}" - ) - - except Exception as e: - logger.error(f" Erreur lecture F_ARTSTOCK pour {reference}: {e}") - - if article["famille_code"]: - try: - cursor.execute( - "SELECT FA_Intitule FROM F_FAMILLE WHERE FA_CodeFamille = ?", - (article["famille_code"],), - ) - famille_row = cursor.fetchone() - if famille_row: - article["famille_libelle"] = _safe_strip(famille_row[0]) - else: - article["famille_libelle"] = "" - except Exception: - article["famille_libelle"] = "" - else: - article["famille_libelle"] = "" - - return article + logger.info(f"✓ Article {reference} lu avec succès") + return articles[0] except Exception as e: - logger.error(f" Erreur SQL article {reference}: {e}") + logger.error(f" Erreur SQL article {reference}: {e}", exc_info=True) return None def obtenir_contact(self, numero: str, contact_numero: int) -> Optional[Dict]: @@ -8325,15 +8358,11 @@ class SageConnector: raise RuntimeError(f"Erreur Sage: {error_message}") def creer_article(self, article_data: dict) -> dict: - """Crée un article dans Sage 100 avec gestion complète des champs""" + """Crée un article dans Sage 100 avec COM uniquement""" with self._com_context(), self._lock_com: try: logger.info("[ARTICLE] === CREATION ARTICLE ===") - valide, erreur = valider_donnees_creation(article_data) - if not valide: - raise ValueError(erreur) - transaction_active = False try: self.cial.CptaApplication.BeginTrans() @@ -8343,8 +8372,82 @@ class SageConnector: logger.debug(f"BeginTrans non disponible : {e}") try: + # === Découverte dépôts === + depots_disponibles = [] + depot_a_utiliser = None + depot_code_demande = article_data.get("depot_code") + + try: + factory_depot = self.cial.FactoryDepot + index = 1 + + while index <= 100: + try: + persist = factory_depot.List(index) + if persist is None: + break + + depot_obj = win32com.client.CastTo(persist, "IBODepot3") + depot_obj.Read() + + code = getattr(depot_obj, "DE_Code", "").strip() + if not code: + index += 1 + continue + + numero = int(getattr(depot_obj, "Compteur", 0)) + intitule = getattr( + depot_obj, "DE_Intitule", f"Depot {code}" + ) + + depot_info = { + "code": code, + "numero": numero, + "intitule": intitule, + "objet": depot_obj, + } + + depots_disponibles.append(depot_info) + + if depot_code_demande and code == depot_code_demande: + depot_a_utiliser = depot_info + elif not depot_code_demande and not depot_a_utiliser: + depot_a_utiliser = depot_info + + index += 1 + + except Exception as e: + if "Acces refuse" in str(e): + break + index += 1 + + except Exception as e: + logger.warning(f"[DEPOT] Erreur découverte dépôts : {e}") + + if not depots_disponibles: + raise ValueError( + "Aucun dépôt trouvé dans Sage. Créez d'abord un dépôt." + ) + + if not depot_a_utiliser: + depot_a_utiliser = depots_disponibles[0] + + logger.info( + f"[DEPOT] Utilisation : '{depot_a_utiliser['code']}' ({depot_a_utiliser['intitule']})" + ) + + # === Validation données === reference = article_data.get("reference", "").upper().strip() + if not reference: + raise ValueError("La référence est obligatoire") + if len(reference) > 18: + raise ValueError( + "La référence ne peut pas dépasser 18 caractères" + ) + designation = article_data.get("designation", "").strip() + if not designation: + raise ValueError("La désignation est obligatoire") if len(designation) > 69: designation = designation[:69] @@ -8355,7 +8458,7 @@ class SageConnector: logger.info(f"[ARTICLE] Référence : {reference}") logger.info(f"[ARTICLE] Désignation : {designation}") - # Vérifier si article existe + # === Vérifier si article existe === factory = self.cial.FactoryArticle try: article_existant = factory.ReadReference(reference) @@ -8369,7 +8472,7 @@ class SageConnector: ): raise - # Créer l'article + # === Créer l'article === persist = factory.Create() article = win32com.client.CastTo(persist, "IBOArticle3") article.SetDefault() @@ -8430,7 +8533,7 @@ class SageConnector: logger.info(" [OK] Unite copiée") unite_trouvee = True except Exception as e: - logger.warning(f" Unite non copiable : {e}") + logger.debug(f" Unite non copiable : {e}") if not unite_trouvee: raise ValueError( @@ -8440,6 +8543,7 @@ class SageConnector: # === Gestion famille === famille_trouvee = False famille_code_personnalise = article_data.get("famille") + famille_obj = None if famille_code_personnalise: logger.info( @@ -8474,7 +8578,6 @@ class SageConnector: if famille_code_exact: factory_famille = self.cial.FactoryFamille - famille_obj = None index = 1 while index <= 1000: try: @@ -8522,18 +8625,17 @@ class SageConnector: except Exception as e: logger.debug(f" Famille non copiable : {e}") - # === Champs obligatoires depuis modèle === + # === Champs obligatoires === logger.info("[CHAMPS] Copie champs obligatoires...") article.AR_Type = int(getattr(article_modele, "AR_Type", 0)) article.AR_Nature = int(getattr(article_modele, "AR_Nature", 0)) article.AR_Nomencl = int(getattr(article_modele, "AR_Nomencl", 0)) article.AR_SuiviStock = 2 - logger.info(" [OK] Champs de base copiés") + logger.info(" [OK] AR_SuiviStock=2 (FIFO/LIFO)") # === Application des champs fournis === logger.info("[CHAMPS] Application champs fournis...") champs_appliques = [] - champs_echoues = [] # Prix de vente if "prix_vente" in article_data: @@ -8544,18 +8646,18 @@ class SageConnector: f" ✓ prix_vente = {article_data['prix_vente']}" ) except Exception as e: - champs_echoues.append(f"prix_vente: {e}") + logger.warning(f" ⚠ prix_vente : {e}") - # Prix d'achat + # Prix d'achat (AR_PrixAchat avec un 't') if "prix_achat" in article_data: try: - article.AR_PrixAch = float(article_data["prix_achat"]) + article.AR_PrixAchat = float(article_data["prix_achat"]) champs_appliques.append("prix_achat") logger.info( f" ✓ prix_achat = {article_data['prix_achat']}" ) except Exception as e: - champs_echoues.append(f"prix_achat: {e}") + logger.warning(f" ⚠ prix_achat : {e}") # Coefficient if "coef" in article_data: @@ -8564,7 +8666,7 @@ class SageConnector: champs_appliques.append("coef") logger.info(f" ✓ coef = {article_data['coef']}") except Exception as e: - champs_echoues.append(f"coef: {e}") + logger.warning(f" ⚠ coef : {e}") # Code EAN if "code_ean" in article_data: @@ -8573,16 +8675,16 @@ class SageConnector: champs_appliques.append("code_ean") logger.info(f" ✓ code_ean = {article_data['code_ean']}") except Exception as e: - champs_echoues.append(f"code_ean: {e}") + logger.warning(f" ⚠ code_ean : {e}") - # Description -> Utiliser AR_Langue1 ou AR_Langue2 (pas de AR_Commentaire) + # Description (AR_Langue1) if "description" in article_data: try: article.AR_Langue1 = str(article_data["description"])[:255] champs_appliques.append("description") - logger.info(" ✓ description définie (AR_Langue1)") + logger.info(" ✓ description définie") except Exception as e: - champs_echoues.append(f"description: {e}") + logger.warning(f" ⚠ description : {e}") # Pays if "pays" in article_data: @@ -8591,7 +8693,7 @@ class SageConnector: champs_appliques.append("pays") logger.info(f" ✓ pays = {article_data['pays']}") except Exception as e: - champs_echoues.append(f"pays: {e}") + logger.warning(f" ⚠ pays : {e}") # Garantie if "garantie" in article_data: @@ -8600,7 +8702,7 @@ class SageConnector: champs_appliques.append("garantie") logger.info(f" ✓ garantie = {article_data['garantie']}") except Exception as e: - champs_echoues.append(f"garantie: {e}") + logger.warning(f" ⚠ garantie : {e}") # Délai if "delai" in article_data: @@ -8609,7 +8711,7 @@ class SageConnector: champs_appliques.append("delai") logger.info(f" ✓ delai = {article_data['delai']}") except Exception as e: - champs_echoues.append(f"delai: {e}") + logger.warning(f" ⚠ delai : {e}") # Poids net if "poids_net" in article_data: @@ -8618,7 +8720,7 @@ class SageConnector: champs_appliques.append("poids_net") logger.info(f" ✓ poids_net = {article_data['poids_net']}") except Exception as e: - champs_echoues.append(f"poids_net: {e}") + logger.warning(f" ⚠ poids_net : {e}") # Poids brut if "poids_brut" in article_data: @@ -8629,7 +8731,7 @@ class SageConnector: f" ✓ poids_brut = {article_data['poids_brut']}" ) except Exception as e: - champs_echoues.append(f"poids_brut: {e}") + logger.warning(f" ⚠ poids_brut : {e}") # Code fiscal if "code_fiscal" in article_data: @@ -8642,24 +8744,7 @@ class SageConnector: f" ✓ code_fiscal = {article_data['code_fiscal']}" ) except Exception as e: - champs_echoues.append(f"code_fiscal: {e}") - - # Statistiques (AR_Stat01 à AR_Stat05 existent bien !) - for i in range(1, 6): - stat_key = f"stat_0{i}" - if stat_key in article_data: - try: - setattr( - article, - f"AR_Stat0{i}", - str(article_data[stat_key])[:20], - ) - champs_appliques.append(stat_key) - logger.info( - f" ✓ {stat_key} = {article_data[stat_key]}" - ) - except Exception as e: - champs_echoues.append(f"{stat_key}: {e}") + logger.warning(f" ⚠ code_fiscal : {e}") # Soumis escompte if "soumis_escompte" in article_data: @@ -8672,7 +8757,7 @@ class SageConnector: f" ✓ soumis_escompte = {article_data['soumis_escompte']}" ) except Exception as e: - champs_echoues.append(f"soumis_escompte: {e}") + logger.warning(f" ⚠ soumis_escompte : {e}") # Publié if "publie" in article_data: @@ -8681,7 +8766,7 @@ class SageConnector: champs_appliques.append("publie") logger.info(f" ✓ publie = {article_data['publie']}") except Exception as e: - champs_echoues.append(f"publie: {e}") + logger.warning(f" ⚠ publie : {e}") # En sommeil if "en_sommeil" in article_data: @@ -8692,43 +8777,15 @@ class SageConnector: f" ✓ en_sommeil = {article_data['en_sommeil']}" ) except Exception as e: - champs_echoues.append(f"en_sommeil: {e}") + logger.warning(f" ⚠ en_sommeil : {e}") - if champs_echoues: - logger.warning( - f"[WARN] Champs échoués : {', '.join(champs_echoues)}" - ) + logger.info(f"[CHAMPS] {len(champs_appliques)} champs appliqués") - logger.info( - f"[CHAMPS] Appliqués : {len(champs_appliques)} / Échoués : {len(champs_echoues)}" - ) - - # === Écriture dans Sage === + # === Écriture article === logger.info("[ARTICLE] Écriture dans Sage...") try: article.Write() logger.info(" [OK] Write() réussi") - - # Vérification immédiate SQL - try: - with self._get_sql_connection() as conn: - cursor = conn.cursor() - cursor.execute( - """ - SELECT AR_PrixAch, AR_Coef, AR_Stat01, AR_Stat02 - FROM F_ARTICLE - WHERE AR_Ref = ? - """, - (reference.upper(),), - ) - verif_row = cursor.fetchone() - if verif_row: - logger.info( - f" [VERIF SQL] prix_achat={verif_row[0]}, coef={verif_row[1]}, stat01={verif_row[2]}, stat02={verif_row[3]}" - ) - except Exception as e_verif: - logger.warning(f" [VERIF SQL] Impossible : {e_verif}") - except Exception as e: error_detail = str(e) try: @@ -8740,6 +8797,91 @@ class SageConnector: logger.error(f" [ERREUR] Write() : {error_detail}") raise RuntimeError(f"Échec création : {error_detail}") + # === Statistiques (méthode AR_Stat après Write) === + stats_a_definir = [] + for i in range(1, 6): + stat_key = f"stat_0{i}" + if stat_key in article_data: + stats_a_definir.append( + (i - 1, str(article_data[stat_key])[:20]) + ) + + if stats_a_definir: + logger.info("[STATS] Définition statistiques...") + try: + for index, value in stats_a_definir: + article.AR_Stat(index, value) + logger.info(f" ✓ stat_0{index + 1} = {value}") + + article.Write() + logger.info(" [OK] Statistiques sauvegardées") + except Exception as e: + logger.warning(f" ⚠ Statistiques : {e}") + + # === Gestion stocks === + stock_defini = False + has_stock_values = stock_reel or stock_mini or stock_maxi + + if has_stock_values: + logger.info( + f"[STOCK] Définition stock (dépôt '{depot_a_utiliser['code']}')..." + ) + try: + depot_obj = depot_a_utiliser["objet"] + + factory_stock = None + for factory_name in [ + "FactoryArticleStock", + "FactoryDepotStock", + ]: + try: + factory_stock = getattr( + depot_obj, factory_name, None + ) + if factory_stock: + logger.info(f" Factory : {factory_name}") + break + except Exception: + continue + + if not factory_stock: + raise RuntimeError("Factory de stock introuvable") + + stock_persist = factory_stock.Create() + stock_obj = win32com.client.CastTo( + stock_persist, "IBODepotStock3" + ) + stock_obj.SetDefault() + stock_obj.AR_Ref = reference + + if stock_reel: + stock_obj.AS_QteSto = float(stock_reel) + logger.info(f" AS_QteSto = {stock_reel}") + + if stock_mini: + try: + stock_obj.AS_QteMini = float(stock_mini) + logger.info(f" AS_QteMini = {stock_mini}") + except Exception as e: + logger.warning(f" AS_QteMini : {e}") + + if stock_maxi: + try: + stock_obj.AS_QteMaxi = float(stock_maxi) + logger.info(f" AS_QteMaxi = {stock_maxi}") + except Exception as e: + logger.warning(f" AS_QteMaxi : {e}") + + stock_obj.Write() + stock_defini = True + logger.info( + f" [OK] Stock défini : réel={stock_reel}, min={stock_mini}, max={stock_maxi}" + ) + + except Exception as e: + logger.error(f" [ERREUR] Stock : {e}", exc_info=True) + + # === Commit transaction === if transaction_active: try: self.cial.CptaApplication.CommitTrans() @@ -8747,252 +8889,8 @@ class SageConnector: except Exception as e: logger.warning(f"[COMMIT] Erreur : {e}") - # === Gestion des stocks === - has_stock_values = stock_reel or stock_mini or stock_maxi - - if has_stock_values: - logger.info("[STOCK] Initialisation stocks...") - - # Créer l'entrée de stock pour le stock_reel - if stock_reel: - try: - lignes = [ - { - "article_ref": reference, - "quantite": stock_reel, - "stock_mini": stock_mini if stock_mini else 0.0, - "stock_maxi": stock_maxi if stock_maxi else 0.0, - } - ] - entree_stock_data = { - "date_mouvement": datetime.now().date(), - "reference": f"INIT-{reference}", - "lignes": lignes, - } - resultat_stock = self.creer_entree_stock( - entree_stock_data - ) - logger.info( - f"[STOCK] Entrée créée : {resultat_stock.get('numero')}" - ) - except Exception as e: - logger.error(f"[STOCK] Erreur entrée : {e}") - - # Mise à jour stocks mini/maxi via SQL - if stock_mini or stock_maxi: - try: - with self._get_sql_connection() as conn: - cursor = conn.cursor() - cursor.execute( - "SELECT TOP 1 DE_No FROM F_DEPOT ORDER BY DE_No" - ) - row = cursor.fetchone() - if row: - depot_no = row.DE_No - cursor.execute( - """ - SELECT COUNT(*) - FROM F_ARTSTOCK - WHERE AR_Ref = ? AND DE_No = ? - """, - (reference.upper(), depot_no), - ) - - count = cursor.fetchone()[0] - - if count > 0: - update_parts = [] - params = [] - - if stock_mini: - update_parts.append("AS_QteMini = ?") - params.append(float(stock_mini)) - - if stock_maxi: - update_parts.append("AS_QteMaxi = ?") - params.append(float(stock_maxi)) - - if update_parts: - params.extend( - [reference.upper(), depot_no] - ) - cursor.execute( - f""" - UPDATE F_ARTSTOCK - SET {", ".join(update_parts)} - WHERE AR_Ref = ? AND DE_No = ? - """, - params, - ) - conn.commit() - logger.info( - f" [SQL] Stocks mini/maxi mis à jour (mini={stock_mini}, maxi={stock_maxi})" - ) - else: - cursor.execute( - """ - INSERT INTO F_ARTSTOCK (AR_Ref, DE_No, AS_QteSto, AS_QteMini, AS_QteMaxi) - VALUES (?, ?, ?, ?, ?) - """, - ( - reference.upper(), - depot_no, - 0.0, - float(stock_mini) - if stock_mini - else 0.0, - float(stock_maxi) - if stock_maxi - else 0.0, - ), - ) - conn.commit() - logger.info( - f" [SQL] Ligne stock créée (mini={stock_mini}, maxi={stock_maxi})" - ) - - except Exception as e: - logger.error(f"[STOCK] Erreur SQL mini/maxi : {e}") - - # === Construction réponse depuis SQL === - logger.info("[RESPONSE] Construction réponse depuis SQL...") - try: - with self._get_sql_connection() as conn: - cursor = conn.cursor() - - # Lecture complète article - cursor.execute( - """ - SELECT - a.AR_Ref, a.AR_Design, a.AR_PrixVen, a.AR_PrixAch, a.AR_Coef, - a.AR_CodeBarre, a.AR_CodeFiscal, a.AR_Pays, a.AR_Garantie, a.AR_Delai, - a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_Escompte, a.AR_Publie, a.AR_Sommeil, - a.FA_CodeFamille, f.FA_Intitule, - a.AR_Type, a.AR_UniteVen, a.AR_Langue1, - a.AR_Stat01, a.AR_Stat02, a.AR_Stat03, a.AR_Stat04, a.AR_Stat05 - FROM F_ARTICLE a - LEFT JOIN F_FAMILLE f ON a.FA_CodeFamille = f.FA_CodeFamille - WHERE a.AR_Ref = ? - """, - (reference.upper(),), - ) - - row = cursor.fetchone() - if row: - resultat = { - "reference": _safe_strip(row[0]), - "designation": _safe_strip(row[1]), - "prix_vente": float(row[2]) if row[2] else 0.0, - "prix_achat": float(row[3]) if row[3] else 0.0, - "coef": float(row[4]) if row[4] else None, - "code_ean": _safe_strip(row[5]), - "code_barre": _safe_strip(row[5]), - "code_fiscal": _safe_strip(row[6]), - "pays": _safe_strip(row[7]), - "garantie": int(row[8]) if row[8] else None, - "delai": int(row[9]) if row[9] else None, - "poids_net": float(row[10]) if row[10] else None, - "poids_brut": float(row[11]) if row[11] else None, - "soumis_escompte": bool(row[12]) - if row[12] is not None - else None, - "publie": bool(row[13]) - if row[13] is not None - else None, - "en_sommeil": bool(row[14]) if row[14] else False, - "est_actif": not bool(row[14]) - if row[14] is not None - else True, - "famille_code": _safe_strip(row[15]), - "famille_libelle": _safe_strip(row[16]) - if row[16] - else "", - "type_article": int(row[17]) - if row[17] is not None - else 0, - "type_article_libelle": "Article" - if not row[17] - else None, - "unite_vente": _safe_strip(row[18]) - if row[18] - else None, - "description": _safe_strip(row[19]) - if row[19] - else None, - "stat_01": _safe_strip(row[20]) - if row[20] - else None, - "stat_02": _safe_strip(row[21]) - if row[21] - else None, - "stat_03": _safe_strip(row[22]) - if row[22] - else None, - "stat_04": _safe_strip(row[23]) - if row[23] - else None, - "stat_05": _safe_strip(row[24]) - if row[24] - else None, - } - - # Lecture stocks - cursor.execute( - """ - SELECT s.AS_QteSto, s.AS_QteMini, s.AS_QteMaxi, s.AS_QteRes, s.AS_QteCom - FROM F_ARTSTOCK s - WHERE s.AR_Ref = ? - """, - (reference.upper(),), - ) - - stock_total = 0.0 - stock_mini_val = 0.0 - stock_maxi_val = 0.0 - stock_reserve_val = 0.0 - stock_commande_val = 0.0 - - stock_row = cursor.fetchone() - if stock_row: - stock_total = ( - float(stock_row[0]) if stock_row[0] else 0.0 - ) - stock_mini_val = ( - float(stock_row[1]) if stock_row[1] else 0.0 - ) - stock_maxi_val = ( - float(stock_row[2]) if stock_row[2] else 0.0 - ) - stock_reserve_val = ( - float(stock_row[3]) if stock_row[3] else 0.0 - ) - stock_commande_val = ( - float(stock_row[4]) if stock_row[4] else 0.0 - ) - - resultat["stock_reel"] = stock_total - resultat["stock_mini"] = stock_mini_val - resultat["stock_maxi"] = stock_maxi_val - resultat["stock_disponible"] = ( - stock_total - stock_reserve_val - ) - resultat["stock_reserve"] = stock_reserve_val - resultat["stock_commande"] = stock_commande_val - - logger.info( - f"[RESPONSE] Réponse SQL construite : prix_achat={resultat['prix_achat']}, coef={resultat['coef']}" - ) - logger.info( - f"[OK] ARTICLE CREE : {reference} - Stock : {stock_total}" - ) - - return resultat - - except Exception as e: - logger.warning(f"[RESPONSE] Erreur lecture SQL : {e}") - - # Fallback sur extraction COM si SQL échoue - logger.info("[FALLBACK] Extraction COM...") + # === Relecture et construction réponse === + logger.info("[VERIF] Relecture article...") article_cree_persist = factory.ReadReference(reference) if not article_cree_persist: raise RuntimeError("Article créé mais introuvable") @@ -9002,29 +8900,59 @@ class SageConnector: ) article_cree.Read() + # Vérification stocks SQL + stocks_par_depot = [] + stock_total = 0.0 + try: + with self._get_sql_connection() as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT d.DE_Code, s.AS_QteSto, s.AS_QteMini, s.AS_QteMaxi + FROM F_ARTSTOCK s + LEFT JOIN F_DEPOT d ON s.DE_No = d.DE_No + WHERE s.AR_Ref = ? + """, + (reference.upper(),), + ) + + for row in cursor.fetchall(): + qte = float(row[1]) if row[1] else 0.0 + stock_total += qte + stocks_par_depot.append( + { + "depot_code": _safe_strip(row[0]), + "quantite": qte, + "qte_mini": float(row[2]) if row[2] else 0.0, + "qte_maxi": float(row[3]) if row[3] else 0.0, + } + ) + + logger.info( + f"[VERIF] Stock : {stock_total} dans {len(stocks_par_depot)} dépôt(s)" + ) + except Exception as e: + logger.warning(f"[VERIF] Erreur stock : {e}") + + logger.info(f"[OK] ARTICLE CREE : {reference}") + + # Construction réponse resultat = _extraire_article(article_cree) if not resultat: resultat = {"reference": reference, "designation": designation} - # Forcer les valeurs connues - for key in [ - "prix_vente", - "prix_achat", - "coef", - "stock_mini", - "stock_maxi", - "code_ean", - "code_fiscal", - "pays", - "garantie", - "delai", - "poids_net", - "poids_brut", - "soumis_escompte", - "publie", - ]: - if key in article_data and article_data[key] is not None: - resultat[key] = article_data[key] + resultat["stock_reel"] = stock_total + resultat["stock_disponible"] = stock_total + resultat["stock_reserve"] = 0.0 + resultat["stock_commande"] = 0.0 + + if stock_mini: + resultat["stock_mini"] = float(stock_mini) + if stock_maxi: + resultat["stock_maxi"] = float(stock_maxi) + + if stocks_par_depot: + resultat["stocks_par_depot"] = stocks_par_depot return resultat @@ -9048,16 +8976,12 @@ class SageConnector: logger.error(f"Erreur globale : {e}", exc_info=True) raise - def modifier_article(self, reference: str, article_data: Dict) -> Dict: + def modifier_article(self, reference: str, article_data: dict) -> dict: """Modifie un article existant dans Sage 100""" if not self.cial: raise RuntimeError("Connexion Sage non établie") try: - valide, erreur = valider_donnees_modification(article_data) - if not valide: - raise ValueError(erreur) - with self._com_context(), self._lock_com: logger.info(f"[ARTICLE] === MODIFICATION {reference} ===") @@ -9073,7 +8997,6 @@ class SageConnector: logger.info(f"[ARTICLE] Trouvé : {reference}") champs_modifies = [] - champs_echoues = [] # === Gestion famille === if "famille" in article_data and article_data["famille"]: @@ -9146,17 +9069,18 @@ class SageConnector: ) except Exception as e: logger.error(f" [ERREUR] Famille : {e}") - champs_echoues.append(f"famille: {e}") + raise - # === Traitement explicite des champs === + # === Traitement des champs === if "designation" in article_data: try: - designation = str(article_data["designation"])[:69].strip() - article.AR_Design = designation + article.AR_Design = str(article_data["designation"])[ + :69 + ].strip() champs_modifies.append("designation") - logger.info(f" ✓ designation = {designation}") + logger.info(" ✓ designation") except Exception as e: - champs_echoues.append(f"designation: {e}") + logger.warning(f" ⚠ designation : {e}") if "prix_vente" in article_data: try: @@ -9164,15 +9088,15 @@ class SageConnector: champs_modifies.append("prix_vente") logger.info(f" ✓ prix_vente = {article_data['prix_vente']}") except Exception as e: - champs_echoues.append(f"prix_vente: {e}") + logger.warning(f" ⚠ prix_vente : {e}") if "prix_achat" in article_data: try: - article.AR_PrixAch = float(article_data["prix_achat"]) + article.AR_PrixAchat = float(article_data["prix_achat"]) champs_modifies.append("prix_achat") logger.info(f" ✓ prix_achat = {article_data['prix_achat']}") except Exception as e: - champs_echoues.append(f"prix_achat: {e}") + logger.warning(f" ⚠ prix_achat : {e}") if "coef" in article_data: try: @@ -9180,7 +9104,7 @@ class SageConnector: champs_modifies.append("coef") logger.info(f" ✓ coef = {article_data['coef']}") except Exception as e: - champs_echoues.append(f"coef: {e}") + logger.warning(f" ⚠ coef : {e}") if "code_ean" in article_data: try: @@ -9188,9 +9112,9 @@ class SageConnector: :13 ].strip() champs_modifies.append("code_ean") - logger.info(f" ✓ code_ean = {article_data['code_ean']}") + logger.info(" ✓ code_ean") except Exception as e: - champs_echoues.append(f"code_ean: {e}") + logger.warning(f" ⚠ code_ean : {e}") if "description" in article_data: try: @@ -9198,9 +9122,9 @@ class SageConnector: :255 ].strip() champs_modifies.append("description") - logger.info(" ✓ description définie (AR_Langue1)") + logger.info(" ✓ description") except Exception as e: - champs_echoues.append(f"description: {e}") + logger.warning(f" ⚠ description : {e}") if "pays" in article_data: try: @@ -9208,7 +9132,7 @@ class SageConnector: champs_modifies.append("pays") logger.info(f" ✓ pays = {article_data['pays']}") except Exception as e: - champs_echoues.append(f"pays: {e}") + logger.warning(f" ⚠ pays : {e}") if "garantie" in article_data: try: @@ -9216,7 +9140,7 @@ class SageConnector: champs_modifies.append("garantie") logger.info(f" ✓ garantie = {article_data['garantie']}") except Exception as e: - champs_echoues.append(f"garantie: {e}") + logger.warning(f" ⚠ garantie : {e}") if "delai" in article_data: try: @@ -9224,7 +9148,7 @@ class SageConnector: champs_modifies.append("delai") logger.info(f" ✓ delai = {article_data['delai']}") except Exception as e: - champs_echoues.append(f"delai: {e}") + logger.warning(f" ⚠ delai : {e}") if "poids_net" in article_data: try: @@ -9232,7 +9156,7 @@ class SageConnector: champs_modifies.append("poids_net") logger.info(f" ✓ poids_net = {article_data['poids_net']}") except Exception as e: - champs_echoues.append(f"poids_net: {e}") + logger.warning(f" ⚠ poids_net : {e}") if "poids_brut" in article_data: try: @@ -9240,7 +9164,7 @@ class SageConnector: champs_modifies.append("poids_brut") logger.info(f" ✓ poids_brut = {article_data['poids_brut']}") except Exception as e: - champs_echoues.append(f"poids_brut: {e}") + logger.warning(f" ⚠ poids_brut : {e}") if "code_fiscal" in article_data: try: @@ -9248,22 +9172,7 @@ class SageConnector: champs_modifies.append("code_fiscal") logger.info(f" ✓ code_fiscal = {article_data['code_fiscal']}") except Exception as e: - champs_echoues.append(f"code_fiscal: {e}") - - # Statistiques - for i in range(1, 6): - stat_key = f"stat_0{i}" - if stat_key in article_data: - try: - setattr( - article, - f"AR_Stat0{i}", - str(article_data[stat_key])[:20], - ) - champs_modifies.append(stat_key) - logger.info(f" ✓ {stat_key} = {article_data[stat_key]}") - except Exception as e: - champs_echoues.append(f"{stat_key}: {e}") + logger.warning(f" ⚠ code_fiscal : {e}") if "soumis_escompte" in article_data: try: @@ -9275,7 +9184,7 @@ class SageConnector: f" ✓ soumis_escompte = {article_data['soumis_escompte']}" ) except Exception as e: - champs_echoues.append(f"soumis_escompte: {e}") + logger.warning(f" ⚠ soumis_escompte : {e}") if "publie" in article_data: try: @@ -9283,7 +9192,7 @@ class SageConnector: champs_modifies.append("publie") logger.info(f" ✓ publie = {article_data['publie']}") except Exception as e: - champs_echoues.append(f"publie: {e}") + logger.warning(f" ⚠ publie : {e}") if "en_sommeil" in article_data: try: @@ -9291,45 +9200,19 @@ class SageConnector: champs_modifies.append("en_sommeil") logger.info(f" ✓ en_sommeil = {article_data['en_sommeil']}") except Exception as e: - champs_echoues.append(f"en_sommeil: {e}") - - if champs_echoues: - logger.warning( - f"[WARN] Champs échoués : {', '.join(champs_echoues)}" - ) + logger.warning(f" ⚠ en_sommeil : {e}") if not champs_modifies: logger.warning("[ARTICLE] Aucun champ modifié") return _extraire_article(article) - logger.info(f"[ARTICLE] Champs modifiés : {', '.join(champs_modifies)}") - logger.info("[ARTICLE] Écriture...") + logger.info(f"[ARTICLE] {len(champs_modifies)} champs à modifier") - # === Écriture COM === + # === Écriture === + logger.info("[ARTICLE] Écriture...") try: article.Write() - logger.info("[ARTICLE] Write() réussi") - - # Vérification immédiate SQL - try: - with self._get_sql_connection() as conn: - cursor = conn.cursor() - cursor.execute( - """ - SELECT AR_PrixAch, AR_Coef, AR_Stat01, AR_Stat02 - FROM F_ARTICLE - WHERE AR_Ref = ? - """, - (reference.upper(),), - ) - verif_row = cursor.fetchone() - if verif_row: - logger.info( - f" [VERIF SQL] prix_achat={verif_row[0]}, coef={verif_row[1]}, stat01={verif_row[2]}, stat02={verif_row[3]}" - ) - except Exception as e_verif: - logger.warning(f" [VERIF SQL] Impossible : {e_verif}") - + logger.info(" [OK] Write() réussi") except Exception as e: error_detail = str(e) try: @@ -9340,9 +9223,31 @@ class SageConnector: ) except Exception: pass - logger.error(f"[ARTICLE] Erreur Write() : {error_detail}") + logger.error(f" [ERREUR] Write() : {error_detail}") raise RuntimeError(f"Échec modification : {error_detail}") + # === Statistiques (méthode AR_Stat après Write) === + stats_a_definir = [] + for i in range(1, 6): + stat_key = f"stat_0{i}" + if stat_key in article_data: + stats_a_definir.append( + (i - 1, str(article_data[stat_key])[:20]) + ) + + if stats_a_definir: + logger.info("[STATS] Définition statistiques...") + try: + for index, value in stats_a_definir: + article.AR_Stat(index, value) + champs_modifies.append(f"stat_0{index + 1}") + logger.info(f" ✓ stat_0{index + 1} = {value}") + + article.Write() + logger.info(" [OK] Statistiques sauvegardées") + except Exception as e: + logger.warning(f" ⚠ Statistiques : {e}") + # === Gestion stocks mini/maxi via SQL === if "stock_mini" in article_data or "stock_maxi" in article_data: try: @@ -9380,137 +9285,8 @@ class SageConnector: conn.commit() logger.info(" [SQL] Stocks mini/maxi mis à jour") except Exception as e: - logger.error(f"[STOCK] Erreur SQL : {e}") - champs_echoues.append(f"stocks: {e}") + logger.error(f" [ERREUR] Stocks : {e}") - # === Construction réponse depuis SQL === - logger.info("[RESPONSE] Construction réponse depuis SQL...") - try: - with self._get_sql_connection() as conn: - cursor = conn.cursor() - - # Lecture complète article - cursor.execute( - """ - SELECT - a.AR_Ref, a.AR_Design, a.AR_PrixVen, a.AR_PrixAch, a.AR_Coef, - a.AR_CodeBarre, a.AR_CodeFiscal, a.AR_Pays, a.AR_Garantie, a.AR_Delai, - a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_Escompte, a.AR_Publie, a.AR_Sommeil, - a.FA_CodeFamille, f.FA_Intitule, - a.AR_Type, a.AR_UniteVen, a.AR_Langue1, - a.AR_Stat01, a.AR_Stat02, a.AR_Stat03, a.AR_Stat04, a.AR_Stat05 - FROM F_ARTICLE a - LEFT JOIN F_FAMILLE f ON a.FA_CodeFamille = f.FA_CodeFamille - WHERE a.AR_Ref = ? - """, - (reference.upper(),), - ) - - row = cursor.fetchone() - if row: - resultat = { - "reference": _safe_strip(row[0]), - "designation": _safe_strip(row[1]), - "prix_vente": float(row[2]) if row[2] else 0.0, - "prix_achat": float(row[3]) if row[3] else 0.0, - "coef": float(row[4]) if row[4] else None, - "code_ean": _safe_strip(row[5]), - "code_barre": _safe_strip(row[5]), - "code_fiscal": _safe_strip(row[6]), - "pays": _safe_strip(row[7]), - "garantie": int(row[8]) if row[8] else None, - "delai": int(row[9]) if row[9] else None, - "poids_net": float(row[10]) if row[10] else None, - "poids_brut": float(row[11]) if row[11] else None, - "soumis_escompte": bool(row[12]) - if row[12] is not None - else None, - "publie": bool(row[13]) - if row[13] is not None - else None, - "en_sommeil": bool(row[14]) if row[14] else False, - "est_actif": not bool(row[14]) - if row[14] is not None - else True, - "famille_code": _safe_strip(row[15]), - "famille_libelle": _safe_strip(row[16]) - if row[16] - else "", - "type_article": int(row[17]) - if row[17] is not None - else 0, - "type_article_libelle": "Article" - if not row[17] - else None, - "unite_vente": _safe_strip(row[18]) - if row[18] - else None, - "description": _safe_strip(row[19]) - if row[19] - else None, - "stat_01": _safe_strip(row[20]) if row[20] else None, - "stat_02": _safe_strip(row[21]) if row[21] else None, - "stat_03": _safe_strip(row[22]) if row[22] else None, - "stat_04": _safe_strip(row[23]) if row[23] else None, - "stat_05": _safe_strip(row[24]) if row[24] else None, - } - - # Lecture stocks - cursor.execute( - """ - SELECT s.AS_QteSto, s.AS_QteMini, s.AS_QteMaxi, s.AS_QteRes, s.AS_QteCom - FROM F_ARTSTOCK s - WHERE s.AR_Ref = ? - """, - (reference.upper(),), - ) - - stock_total = 0.0 - stock_mini_val = 0.0 - stock_maxi_val = 0.0 - stock_reserve_val = 0.0 - stock_commande_val = 0.0 - - stock_row = cursor.fetchone() - if stock_row: - stock_total = ( - float(stock_row[0]) if stock_row[0] else 0.0 - ) - stock_mini_val = ( - float(stock_row[1]) if stock_row[1] else 0.0 - ) - stock_maxi_val = ( - float(stock_row[2]) if stock_row[2] else 0.0 - ) - stock_reserve_val = ( - float(stock_row[3]) if stock_row[3] else 0.0 - ) - stock_commande_val = ( - float(stock_row[4]) if stock_row[4] else 0.0 - ) - - resultat["stock_reel"] = stock_total - resultat["stock_mini"] = stock_mini_val - resultat["stock_maxi"] = stock_maxi_val - resultat["stock_disponible"] = ( - stock_total - stock_reserve_val - ) - resultat["stock_reserve"] = stock_reserve_val - resultat["stock_commande"] = stock_commande_val - - logger.info( - f"[RESPONSE] Réponse SQL construite : prix_achat={resultat['prix_achat']}, coef={resultat['coef']}" - ) - logger.info( - f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)" - ) - - return resultat - - except Exception as e: - logger.warning(f"[RESPONSE] Erreur lecture SQL : {e}") - - # Fallback sur extraction COM article.Read() logger.info( f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)"