From ea344b2669f5bd281c5685c0bf325307f702d022 Mon Sep 17 00:00:00 2001 From: fanilo Date: Sat, 3 Jan 2026 18:48:31 +0100 Subject: [PATCH] article's management functionnal --- sage_connector.py | 714 +++++++++++++++++++++++++++++++++------------- 1 file changed, 515 insertions(+), 199 deletions(-) diff --git a/sage_connector.py b/sage_connector.py index 402a118..c202abd 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -1446,19 +1446,19 @@ class SageConnector: def lire_article(self, reference): """ - Lit un article complet depuis SQL avec enrichissements - Version alignée sur lister_tous_articles + Lit un article complet depuis SQL avec tous les enrichissements + Version fusionnée complète """ try: with self._get_sql_connection() as conn: cursor = conn.cursor() - # === DÉTECTION DES COLONNES (identique à lister_tous_articles) === + # === DÉTECTION DES COLONNES === 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) + # Configuration du mapping complet colonnes_config = { "AR_Ref": "reference", "AR_Design": "designation", @@ -1578,10 +1578,10 @@ class SageConnector: valeur = valeur.strip() row_data[col_sql] = valeur - # Mapping de l'article (fonction partagée) + # Mapping de l'article article = _mapper_article_depuis_row(row_data, colonnes_config) - # Enrichissements (dans le même ordre que lister_tous_articles) + # Enrichissements articles = [ article ] # Liste d'un seul article pour les fonctions d'enrichissement @@ -1590,7 +1590,6 @@ class SageConnector: articles = _enrichir_familles_articles(articles, cursor) articles = _enrichir_fournisseurs_articles(articles, cursor) articles = _enrichir_tva_articles(articles, cursor) - articles = _enrichir_stock_emplacements(articles, cursor) articles = _enrichir_gammes_articles(articles, cursor) articles = _enrichir_tarifs_clients(articles, cursor) @@ -1605,11 +1604,11 @@ class SageConnector: articles = _enrichir_prix_gammes(articles, cursor) articles = _enrichir_conditionnements(articles, cursor) - logger.info(f"✓ Article {reference} lu avec succès") + logger.info(f"✓ Article {reference} lu avec tous les enrichissements") return articles[0] except Exception as e: - logger.error(f" Erreur SQL article {reference}: {e}", exc_info=True) + logger.error(f"✗ Erreur SQL article {reference}: {e}", exc_info=True) return None def obtenir_contact(self, numero: str, contact_numero: int) -> Optional[Dict]: @@ -8358,11 +8357,16 @@ 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 COM uniquement""" + """Crée un article dans Sage 100 - Version fusionnée complète""" with self._com_context(), self._lock_com: try: logger.info("[ARTICLE] === CREATION ARTICLE ===") + # === Validation données === + valide, erreur = valider_donnees_creation(article_data) + if not valide: + raise ValueError(erreur) + transaction_active = False try: self.cial.CptaApplication.BeginTrans() @@ -8436,18 +8440,9 @@ class SageConnector: f"[DEPOT] Utilisation : '{depot_a_utiliser['code']}' ({depot_a_utiliser['intitule']})" ) - # === Validation données === + # === Extraction et validation des 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] @@ -8533,7 +8528,7 @@ class SageConnector: logger.info(" [OK] Unite copiée") unite_trouvee = True except Exception as e: - logger.debug(f" Unite non copiable : {e}") + logger.warning(f" Unite non copiable : {e}") if not unite_trouvee: raise ValueError( @@ -8543,7 +8538,6 @@ class SageConnector: # === Gestion famille === famille_trouvee = False famille_code_personnalise = article_data.get("famille") - famille_obj = None if famille_code_personnalise: logger.info( @@ -8578,6 +8572,7 @@ class SageConnector: if famille_code_exact: factory_famille = self.cial.FactoryFamille + famille_obj = None index = 1 while index <= 1000: try: @@ -8625,17 +8620,18 @@ class SageConnector: except Exception as e: logger.debug(f" Famille non copiable : {e}") - # === Champs obligatoires === + # === Champs obligatoires depuis modèle === logger.info("[CHAMPS] Copie champs obligatoires...") article.AR_Type = int(getattr(article_modele, "AR_Type", 0)) article.AR_Nature = int(getattr(article_modele, "AR_Nature", 0)) article.AR_Nomencl = int(getattr(article_modele, "AR_Nomencl", 0)) article.AR_SuiviStock = 2 - logger.info(" [OK] AR_SuiviStock=2 (FIFO/LIFO)") + logger.info(" [OK] Champs de base copiés (AR_SuiviStock=2)") # === Application des champs fournis === logger.info("[CHAMPS] Application champs fournis...") champs_appliques = [] + champs_echoues = [] # Prix de vente if "prix_vente" in article_data: @@ -8646,9 +8642,9 @@ class SageConnector: f" ✓ prix_vente = {article_data['prix_vente']}" ) except Exception as e: - logger.warning(f" ⚠ prix_vente : {e}") + champs_echoues.append(f"prix_vente: {e}") - # Prix d'achat (AR_PrixAchat avec un 't') + # Prix d'achat if "prix_achat" in article_data: try: article.AR_PrixAchat = float(article_data["prix_achat"]) @@ -8657,7 +8653,7 @@ class SageConnector: f" ✓ prix_achat = {article_data['prix_achat']}" ) except Exception as e: - logger.warning(f" ⚠ prix_achat : {e}") + champs_echoues.append(f"prix_achat: {e}") # Coefficient if "coef" in article_data: @@ -8666,7 +8662,7 @@ class SageConnector: champs_appliques.append("coef") logger.info(f" ✓ coef = {article_data['coef']}") except Exception as e: - logger.warning(f" ⚠ coef : {e}") + champs_echoues.append(f"coef: {e}") # Code EAN if "code_ean" in article_data: @@ -8675,16 +8671,16 @@ class SageConnector: champs_appliques.append("code_ean") logger.info(f" ✓ code_ean = {article_data['code_ean']}") except Exception as e: - logger.warning(f" ⚠ code_ean : {e}") + champs_echoues.append(f"code_ean: {e}") - # Description (AR_Langue1) + # 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") + logger.info(" ✓ description définie (AR_Langue1)") except Exception as e: - logger.warning(f" ⚠ description : {e}") + champs_echoues.append(f"description: {e}") # Pays if "pays" in article_data: @@ -8693,7 +8689,7 @@ class SageConnector: champs_appliques.append("pays") logger.info(f" ✓ pays = {article_data['pays']}") except Exception as e: - logger.warning(f" ⚠ pays : {e}") + champs_echoues.append(f"pays: {e}") # Garantie if "garantie" in article_data: @@ -8702,7 +8698,7 @@ class SageConnector: champs_appliques.append("garantie") logger.info(f" ✓ garantie = {article_data['garantie']}") except Exception as e: - logger.warning(f" ⚠ garantie : {e}") + champs_echoues.append(f"garantie: {e}") # Délai if "delai" in article_data: @@ -8711,7 +8707,7 @@ class SageConnector: champs_appliques.append("delai") logger.info(f" ✓ delai = {article_data['delai']}") except Exception as e: - logger.warning(f" ⚠ delai : {e}") + champs_echoues.append(f"delai: {e}") # Poids net if "poids_net" in article_data: @@ -8720,7 +8716,7 @@ class SageConnector: champs_appliques.append("poids_net") logger.info(f" ✓ poids_net = {article_data['poids_net']}") except Exception as e: - logger.warning(f" ⚠ poids_net : {e}") + champs_echoues.append(f"poids_net: {e}") # Poids brut if "poids_brut" in article_data: @@ -8731,7 +8727,7 @@ class SageConnector: f" ✓ poids_brut = {article_data['poids_brut']}" ) except Exception as e: - logger.warning(f" ⚠ poids_brut : {e}") + champs_echoues.append(f"poids_brut: {e}") # Code fiscal if "code_fiscal" in article_data: @@ -8744,7 +8740,7 @@ class SageConnector: f" ✓ code_fiscal = {article_data['code_fiscal']}" ) except Exception as e: - logger.warning(f" ⚠ code_fiscal : {e}") + champs_echoues.append(f"code_fiscal: {e}") # Soumis escompte if "soumis_escompte" in article_data: @@ -8757,7 +8753,7 @@ class SageConnector: f" ✓ soumis_escompte = {article_data['soumis_escompte']}" ) except Exception as e: - logger.warning(f" ⚠ soumis_escompte : {e}") + champs_echoues.append(f"soumis_escompte: {e}") # Publié if "publie" in article_data: @@ -8766,7 +8762,7 @@ class SageConnector: champs_appliques.append("publie") logger.info(f" ✓ publie = {article_data['publie']}") except Exception as e: - logger.warning(f" ⚠ publie : {e}") + champs_echoues.append(f"publie: {e}") # En sommeil if "en_sommeil" in article_data: @@ -8777,11 +8773,18 @@ class SageConnector: f" ✓ en_sommeil = {article_data['en_sommeil']}" ) except Exception as e: - logger.warning(f" ⚠ en_sommeil : {e}") + champs_echoues.append(f"en_sommeil: {e}") - logger.info(f"[CHAMPS] {len(champs_appliques)} champs appliqués") + if champs_echoues: + logger.warning( + f"[WARN] Champs échoués : {', '.join(champs_echoues)}" + ) - # === Écriture article === + logger.info( + f"[CHAMPS] Appliqués : {len(champs_appliques)} / Échoués : {len(champs_echoues)}" + ) + + # === Écriture dans Sage === logger.info("[ARTICLE] Écriture dans Sage...") try: article.Write() @@ -8797,90 +8800,28 @@ 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) === + # === Statistiques (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: + if stat_key in article_data and article_data[stat_key]: stats_a_definir.append( (i - 1, str(article_data[stat_key])[:20]) ) if stats_a_definir: - logger.info("[STATS] Définition statistiques...") + logger.info("[STATS] Définition statistiques via AR_Stat()...") try: for index, value in stats_a_definir: article.AR_Stat(index, value) logger.info(f" ✓ stat_0{index + 1} = {value}") + champs_appliques.append(f"stat_0{index + 1}") 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: @@ -8889,8 +8830,274 @@ class SageConnector: except Exception as e: logger.warning(f"[COMMIT] Erreur : {e}") - # === Relecture et construction réponse === - logger.info("[VERIF] Relecture article...") + # === 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']}')..." + ) + + # Méthode 1 : Créer via COM + if stock_reel: + 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 factory_stock: + stock_persist = factory_stock.Create() + stock_obj = win32com.client.CastTo( + stock_persist, "IBODepotStock3" + ) + stock_obj.SetDefault() + stock_obj.AR_Ref = reference + stock_obj.AS_QteSto = float(stock_reel) + + if stock_mini: + try: + stock_obj.AS_QteMini = float(stock_mini) + except Exception as e: + logger.warning(f" AS_QteMini : {e}") + + if stock_maxi: + try: + stock_obj.AS_QteMaxi = float(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 via COM : réel={stock_reel}, min={stock_mini}, max={stock_maxi}" + ) + except Exception as e: + logger.warning(f" [WARN] Stock COM : {e}") + + # Méthode 2 : Mise à jour SQL si COM échoue ou pour mini/maxi seulement + if (stock_mini or stock_maxi) and not stock_defini: + 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" + ) + else: + cursor.execute( + """ + INSERT INTO F_ARTSTOCK (AR_Ref, DE_No, AS_QteSto, AS_QteMini, AS_QteMaxi) + VALUES (?, ?, ?, ?, ?) + """, + ( + reference.upper(), + depot_no, + float(stock_reel) + if stock_reel + else 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") + except Exception as e: + logger.error(f"[STOCK] Erreur SQL : {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"[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...") article_cree_persist = factory.ReadReference(reference) if not article_cree_persist: raise RuntimeError("Article créé mais introuvable") @@ -8900,59 +9107,29 @@ 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} - 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 + # 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] return resultat @@ -8976,12 +9153,16 @@ class SageConnector: logger.error(f"Erreur globale : {e}", exc_info=True) raise - def modifier_article(self, reference: str, article_data: dict) -> dict: - """Modifie un article existant dans Sage 100""" + def modifier_article(self, reference: str, article_data: Dict) -> Dict: + """Modifie un article existant dans Sage 100 - Version fusionnée""" 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} ===") @@ -8997,6 +9178,7 @@ class SageConnector: logger.info(f"[ARTICLE] Trouvé : {reference}") champs_modifies = [] + champs_echoues = [] # === Gestion famille === if "famille" in article_data and article_data["famille"]: @@ -9069,18 +9251,17 @@ class SageConnector: ) except Exception as e: logger.error(f" [ERREUR] Famille : {e}") - raise + champs_echoues.append(f"famille: {e}") - # === Traitement des champs === + # === Traitement explicite des champs === if "designation" in article_data: try: - article.AR_Design = str(article_data["designation"])[ - :69 - ].strip() + designation = str(article_data["designation"])[:69].strip() + article.AR_Design = designation champs_modifies.append("designation") - logger.info(" ✓ designation") + logger.info(f" ✓ designation = {designation}") except Exception as e: - logger.warning(f" ⚠ designation : {e}") + champs_echoues.append(f"designation: {e}") if "prix_vente" in article_data: try: @@ -9088,7 +9269,7 @@ class SageConnector: champs_modifies.append("prix_vente") logger.info(f" ✓ prix_vente = {article_data['prix_vente']}") except Exception as e: - logger.warning(f" ⚠ prix_vente : {e}") + champs_echoues.append(f"prix_vente: {e}") if "prix_achat" in article_data: try: @@ -9096,7 +9277,7 @@ class SageConnector: champs_modifies.append("prix_achat") logger.info(f" ✓ prix_achat = {article_data['prix_achat']}") except Exception as e: - logger.warning(f" ⚠ prix_achat : {e}") + champs_echoues.append(f"prix_achat: {e}") if "coef" in article_data: try: @@ -9104,7 +9285,7 @@ class SageConnector: champs_modifies.append("coef") logger.info(f" ✓ coef = {article_data['coef']}") except Exception as e: - logger.warning(f" ⚠ coef : {e}") + champs_echoues.append(f"coef: {e}") if "code_ean" in article_data: try: @@ -9112,9 +9293,9 @@ class SageConnector: :13 ].strip() champs_modifies.append("code_ean") - logger.info(" ✓ code_ean") + logger.info(f" ✓ code_ean = {article_data['code_ean']}") except Exception as e: - logger.warning(f" ⚠ code_ean : {e}") + champs_echoues.append(f"code_ean: {e}") if "description" in article_data: try: @@ -9122,9 +9303,9 @@ class SageConnector: :255 ].strip() champs_modifies.append("description") - logger.info(" ✓ description") + logger.info(" ✓ description définie (AR_Langue1)") except Exception as e: - logger.warning(f" ⚠ description : {e}") + champs_echoues.append(f"description: {e}") if "pays" in article_data: try: @@ -9132,7 +9313,7 @@ class SageConnector: champs_modifies.append("pays") logger.info(f" ✓ pays = {article_data['pays']}") except Exception as e: - logger.warning(f" ⚠ pays : {e}") + champs_echoues.append(f"pays: {e}") if "garantie" in article_data: try: @@ -9140,7 +9321,7 @@ class SageConnector: champs_modifies.append("garantie") logger.info(f" ✓ garantie = {article_data['garantie']}") except Exception as e: - logger.warning(f" ⚠ garantie : {e}") + champs_echoues.append(f"garantie: {e}") if "delai" in article_data: try: @@ -9148,7 +9329,7 @@ class SageConnector: champs_modifies.append("delai") logger.info(f" ✓ delai = {article_data['delai']}") except Exception as e: - logger.warning(f" ⚠ delai : {e}") + champs_echoues.append(f"delai: {e}") if "poids_net" in article_data: try: @@ -9156,7 +9337,7 @@ class SageConnector: champs_modifies.append("poids_net") logger.info(f" ✓ poids_net = {article_data['poids_net']}") except Exception as e: - logger.warning(f" ⚠ poids_net : {e}") + champs_echoues.append(f"poids_net: {e}") if "poids_brut" in article_data: try: @@ -9164,7 +9345,7 @@ class SageConnector: champs_modifies.append("poids_brut") logger.info(f" ✓ poids_brut = {article_data['poids_brut']}") except Exception as e: - logger.warning(f" ⚠ poids_brut : {e}") + champs_echoues.append(f"poids_brut: {e}") if "code_fiscal" in article_data: try: @@ -9172,7 +9353,7 @@ class SageConnector: champs_modifies.append("code_fiscal") logger.info(f" ✓ code_fiscal = {article_data['code_fiscal']}") except Exception as e: - logger.warning(f" ⚠ code_fiscal : {e}") + champs_echoues.append(f"code_fiscal: {e}") if "soumis_escompte" in article_data: try: @@ -9184,7 +9365,7 @@ class SageConnector: f" ✓ soumis_escompte = {article_data['soumis_escompte']}" ) except Exception as e: - logger.warning(f" ⚠ soumis_escompte : {e}") + champs_echoues.append(f"soumis_escompte: {e}") if "publie" in article_data: try: @@ -9192,7 +9373,7 @@ class SageConnector: champs_modifies.append("publie") logger.info(f" ✓ publie = {article_data['publie']}") except Exception as e: - logger.warning(f" ⚠ publie : {e}") + champs_echoues.append(f"publie: {e}") if "en_sommeil" in article_data: try: @@ -9200,19 +9381,24 @@ class SageConnector: champs_modifies.append("en_sommeil") logger.info(f" ✓ en_sommeil = {article_data['en_sommeil']}") except Exception as e: - logger.warning(f" ⚠ en_sommeil : {e}") + champs_echoues.append(f"en_sommeil: {e}") + + if champs_echoues: + logger.warning( + f"[WARN] Champs échoués : {', '.join(champs_echoues)}" + ) if not champs_modifies: logger.warning("[ARTICLE] Aucun champ modifié") return _extraire_article(article) - logger.info(f"[ARTICLE] {len(champs_modifies)} champs à modifier") - - # === Écriture === + logger.info(f"[ARTICLE] Champs modifiés : {', '.join(champs_modifies)}") logger.info("[ARTICLE] Écriture...") + + # === Écriture COM === try: article.Write() - logger.info(" [OK] Write() réussi") + logger.info("[ARTICLE] Write() réussi") except Exception as e: error_detail = str(e) try: @@ -9223,25 +9409,30 @@ class SageConnector: ) except Exception: pass - logger.error(f" [ERREUR] Write() : {error_detail}") + logger.error(f"[ARTICLE] Erreur Write() : {error_detail}") raise RuntimeError(f"Échec modification : {error_detail}") - # === Statistiques (méthode AR_Stat après Write) === - stats_a_definir = [] + # === Statistiques (AR_Stat après Write) === + stats_a_modifier = [] 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]) + stats_a_modifier.append( + ( + i - 1, + str(article_data[stat_key])[:20] + if article_data[stat_key] + else "", + ) ) - if stats_a_definir: - logger.info("[STATS] Définition statistiques...") + if stats_a_modifier: + logger.info("[STATS] Modification statistiques via AR_Stat()...") try: - for index, value in stats_a_definir: + for index, value in stats_a_modifier: article.AR_Stat(index, value) - champs_modifies.append(f"stat_0{index + 1}") logger.info(f" ✓ stat_0{index + 1} = {value}") + champs_modifies.append(f"stat_0{index + 1}") article.Write() logger.info(" [OK] Statistiques sauvegardées") @@ -9285,8 +9476,133 @@ class SageConnector: conn.commit() logger.info(" [SQL] Stocks mini/maxi mis à jour") except Exception as e: - logger.error(f" [ERREUR] Stocks : {e}") + logger.error(f"[STOCK] Erreur SQL : {e}") + champs_echoues.append(f"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"[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)"