diff --git a/main.py b/main.py index f8cb908..11872de 100644 --- a/main.py +++ b/main.py @@ -840,30 +840,44 @@ def modifier_facture_endpoint(req: FactureUpdate): @app.post("/sage/articles/create", dependencies=[Depends(verify_token)]) def create_article_endpoint(req: ArticleCreate): try: + logger.info(f"[ENDPOINT] Création article : {req.reference}") + logger.debug(f"[ENDPOINT] Données reçues : {req.dict()}") + resultat = sage.creer_article(req.dict()) + + logger.info(f"[ENDPOINT] Article créé : {resultat.get('reference')}") return {"success": True, "data": resultat} except ValueError as e: - logger.warning(f"Erreur métier création article: {e}") + logger.warning(f"[ENDPOINT] Erreur métier création article: {e}") raise HTTPException(400, str(e)) except Exception as e: - logger.error(f"Erreur technique création article: {e}") + logger.error( + f"[ENDPOINT] Erreur technique création article: {e}", exc_info=True + ) raise HTTPException(500, str(e)) @app.post("/sage/articles/update", dependencies=[Depends(verify_token)]) def modifier_article_endpoint(req: ArticleUpdate): try: + logger.info(f"[ENDPOINT] Modification article : {req.reference}") + logger.debug(f"[ENDPOINT] Champs à modifier : {list(req.article_data.keys())}") + resultat = sage.modifier_article(req.reference, req.article_data) + + logger.info(f"[ENDPOINT] Article modifié : {req.reference}") return {"success": True, "data": resultat} except ValueError as e: - logger.warning(f"Erreur métier modification article: {e}") + logger.warning(f"[ENDPOINT] Erreur métier modification article: {e}") raise HTTPException(404, str(e)) except Exception as e: - logger.error(f"Erreur technique modification article: {e}") + logger.error( + f"[ENDPOINT] Erreur technique modification article: {e}", exc_info=True + ) raise HTTPException(500, str(e)) diff --git a/sage_connector.py b/sage_connector.py index 7b28ad3..d17167a 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -85,6 +85,12 @@ from utils import ( valider_donnees_creation, mapper_champ_api_vers_sage, CHAMPS_STOCK_INITIAL, + CHAMPS_ASSIGNABLES_CREATION, + CHAMPS_ASSIGNABLES_MODIFICATION, + CHAMPS_OBJETS_SPECIAUX, + valider_champ, + valider_donnees_modification, + obtenir_champs_assignables, ) logger = logging.getLogger(__name__) @@ -1438,6 +1444,141 @@ class SageConnector: logger.error(f" Erreur SQL articles: {e}", exc_info=True) raise RuntimeError(f"Erreur lecture articles: {str(e)}") + def lire_article(self, reference): + 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(),), + ) + + row = cursor.fetchone() + + if not row: + 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, + } + + article["tva_taux"] = 20.0 + + logger.info(f" Lecture stock depuis F_ARTSTOCK pour {reference}...") + + 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(),), + ) + + stock_row = cursor.fetchone() + + 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 + + except Exception as e: + logger.error(f" Erreur SQL article {reference}: {e}") + return None + def obtenir_contact(self, numero: str, contact_numero: int) -> Optional[Dict]: """ Récupère un contact spécifique par son CT_No @@ -8184,6 +8325,7 @@ 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""" with self._com_context(), self._lock_com: try: logger.info("[ARTICLE] === CREATION ARTICLE ===") @@ -8203,7 +8345,6 @@ class SageConnector: try: reference = article_data.get("reference", "").upper().strip() designation = article_data.get("designation", "").strip() - if len(designation) > 69: designation = designation[:69] @@ -8213,11 +8354,8 @@ class SageConnector: logger.info(f"[ARTICLE] Référence : {reference}") logger.info(f"[ARTICLE] Désignation : {designation}") - if stock_reel or stock_mini or stock_maxi: - logger.info( - f"[ARTICLE] Stocks demandés : réel={stock_reel}, mini={stock_mini}, maxi={stock_maxi}" - ) + # Vérifier si article existe factory = self.cial.FactoryArticle try: article_existant = factory.ReadReference(reference) @@ -8226,14 +8364,12 @@ class SageConnector: 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 + "Enregistrement non trouve" not in error_msg + and "-2607" not in error_msg ): - logger.debug(f"[ARTICLE] {reference} n'existe pas encore") - else: raise + # Créer l'article persist = factory.Create() article = win32com.client.CastTo(persist, "IBOArticle3") article.SetDefault() @@ -8241,29 +8377,28 @@ class SageConnector: article.AR_Ref = reference article.AR_Design = designation - logger.info("[MODELE] Recherche article modèle via SQL...") + # === Recherche article modèle === + logger.info("[MODELE] Recherche article modèle...") article_modele_ref = None article_modele = None try: with self._get_sql_connection() as conn: cursor = conn.cursor() - cursor.execute( - """ + cursor.execute(""" SELECT TOP 1 AR_Ref FROM F_ARTICLE WHERE AR_Sommeil = 0 ORDER BY AR_Ref - """ - ) + """) row = cursor.fetchone() if row: article_modele_ref = _safe_strip(row.AR_Ref) logger.info( - f" [SQL] Article modèle trouvé : {article_modele_ref}" + f" [SQL] Article modèle : {article_modele_ref}" ) except Exception as e: - logger.warning(f" [SQL] Erreur recherche article : {e}") + logger.warning(f" [SQL] Erreur recherche : {e}") if article_modele_ref: try: @@ -8274,192 +8409,302 @@ class SageConnector: ) article_modele.Read() logger.info( - f" [OK] Article modèle chargé : {article_modele_ref}" + f" [OK] Modèle chargé : {article_modele_ref}" ) except Exception as e: - logger.warning(f" [WARN] Erreur chargement modèle : {e}") + logger.warning(f" [WARN] Erreur chargement : {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." + "Aucun article modèle trouvé. Créez au moins un article dans Sage." ) - logger.info("[OBJETS] Copie Unite + Famille depuis modèle...") - + # === Copie Unite depuis modèle === + logger.info("[UNITE] Copie Unite depuis modèle...") unite_trouvee = False try: unite_obj = getattr(article_modele, "Unite", None) if unite_obj: article.Unite = unite_obj - logger.info( - f" [OK] Objet Unite copié depuis {article_modele_ref}" - ) + logger.info(" [OK] Unite copiée") unite_trouvee = True except Exception as e: - logger.debug(f" Unite non copiable : {str(e)[:80]}") + logger.warning(f" Unite non copiable : {e}") if not unite_trouvee: raise ValueError( - "Impossible de copier l'unité de vente depuis le modèle" + "Impossible de copier l'unité depuis le modèle" ) + # === Gestion famille === famille_trouvee = False famille_code_personnalise = article_data.get("famille") - famille_obj = None if famille_code_personnalise: logger.info( - f" [FAMILLE] Code personnalisé demandé : {famille_code_personnalise}" + f" [FAMILLE] Code 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(),), + with self._get_sql_connection() as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT FA_CodeFamille, FA_Type + FROM F_FAMILLE + WHERE UPPER(FA_CodeFamille) = ? + """, + (famille_code_personnalise.upper(),), + ) + row = cursor.fetchone() + if row: + famille_code_exact = _safe_strip(row.FA_CodeFamille) + if row.FA_Type == 1: + raise ValueError( + f"Famille '{famille_code_personnalise}' est de type Total" + ) + logger.info( + f" [SQL] Famille trouvée : {famille_code_exact}" + ) + else: + raise ValueError( + f"Famille '{famille_code_personnalise}' introuvable" ) - row = cursor.fetchone() - if row: - famille_code_exact = _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: + if 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" + famille_obj = None + index = 1 + while index <= 1000: + try: + persist_test = factory_famille.List(index) + if persist_test is None: + break + fam_test = win32com.client.CastTo( + persist_test, "IBOFamille3" ) - else: - raise ValueError( - f"Famille '{famille_code_personnalise}' inaccessible via COM" + fam_test.Read() + code_test = ( + getattr(fam_test, "FA_CodeFamille", "") + .strip() + .upper() ) - except Exception as e: - logger.warning(f" [COM] Erreur scanner : {e}") - raise - except ValueError: - raise + if code_test == famille_code_exact.upper(): + famille_obj = fam_test + famille_trouvee = True + logger.info( + f" [OK] Famille trouvée à index {index}" + ) + break + index += 1 + except Exception: + break + + if famille_obj: + famille_obj.Read() + article.Famille = famille_obj + else: + raise ValueError( + f"Famille '{famille_code_personnalise}' inaccessible via COM" + ) except Exception as e: - logger.warning( - f" [WARN] Famille personnalisée non chargée : {str(e)[:100]}" - ) + logger.error(f" [ERREUR] Famille : {e}") + raise if not famille_trouvee: try: famille_obj = getattr(article_modele, "Famille", None) if famille_obj: article.Famille = famille_obj - logger.info( - f" [OK] Objet Famille copié depuis {article_modele_ref}" - ) + logger.info(" [OK] Famille copiée depuis modèle") 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...") + logger.debug(f" Famille non copiable : {e}") + # === 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") - logger.info("[CHAMPS] Application des champs fournis...") - - for champ_api, valeur in article_data.items(): - if ( - champ_api in ["reference", "designation", "famille"] - or champ_api in CHAMPS_STOCK_INITIAL - ): - continue - - champ_sage = mapper_champ_api_vers_sage(champ_api) + # === Application des champs fournis === + logger.info("[CHAMPS] Application champs fournis...") + champs_appliques = [] + champs_echoues = [] + # Prix de vente + if "prix_vente" in article_data: try: - if champ_sage == "AR_PrixVen": - article.AR_PrixVen = float(valeur) - logger.info(f" {champ_sage} = {valeur}") - elif champ_sage == "AR_PrixAch": - try: - article.AR_PrixAch = float(valeur) - except Exception: - article.AR_PrixAchat = float(valeur) - logger.info(f" {champ_sage} = {valeur}") - elif champ_sage == "AR_CodeBarre": - article.AR_CodeBarre = str(valeur) - logger.info(f" {champ_sage} = {valeur}") - elif champ_sage == "AR_Commentaire": - article.AR_Commentaire = str(valeur) - logger.info(f" {champ_sage} défini") - elif hasattr(article, champ_sage): - setattr(article, champ_sage, valeur) - logger.info(f" {champ_sage} = {valeur}") - except Exception as e: - logger.warning( - f" {champ_sage} non assignable : {str(e)[:100]}" + article.AR_PrixVen = float(article_data["prix_vente"]) + champs_appliques.append("prix_vente") + logger.info( + f" ✓ prix_vente = {article_data['prix_vente']}" ) + except Exception as e: + champs_echoues.append(f"prix_vente: {e}") + # Prix d'achat + if "prix_achat" in article_data: + try: + article.AR_PrixAch = 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}") + + # Coefficient + if "coef" in article_data: + try: + article.AR_Coef = float(article_data["coef"]) + champs_appliques.append("coef") + logger.info(f" ✓ coef = {article_data['coef']}") + except Exception as e: + champs_echoues.append(f"coef: {e}") + + # Code EAN + if "code_ean" in article_data: + try: + article.AR_CodeBarre = str(article_data["code_ean"]) + champs_appliques.append("code_ean") + logger.info(f" ✓ code_ean = {article_data['code_ean']}") + except Exception as e: + champs_echoues.append(f"code_ean: {e}") + + # Description -> Utiliser AR_Langue1 ou AR_Langue2 (pas de AR_Commentaire) + 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)") + except Exception as e: + champs_echoues.append(f"description: {e}") + + # Pays + if "pays" in article_data: + try: + article.AR_Pays = str(article_data["pays"])[:3].upper() + champs_appliques.append("pays") + logger.info(f" ✓ pays = {article_data['pays']}") + except Exception as e: + champs_echoues.append(f"pays: {e}") + + # Garantie + if "garantie" in article_data: + try: + article.AR_Garantie = int(article_data["garantie"]) + champs_appliques.append("garantie") + logger.info(f" ✓ garantie = {article_data['garantie']}") + except Exception as e: + champs_echoues.append(f"garantie: {e}") + + # Délai + if "delai" in article_data: + try: + article.AR_Delai = int(article_data["delai"]) + champs_appliques.append("delai") + logger.info(f" ✓ delai = {article_data['delai']}") + except Exception as e: + champs_echoues.append(f"delai: {e}") + + # Poids net + if "poids_net" in article_data: + try: + article.AR_PoidsNet = float(article_data["poids_net"]) + champs_appliques.append("poids_net") + logger.info(f" ✓ poids_net = {article_data['poids_net']}") + except Exception as e: + champs_echoues.append(f"poids_net: {e}") + + # Poids brut + if "poids_brut" in article_data: + try: + article.AR_PoidsBrut = float(article_data["poids_brut"]) + champs_appliques.append("poids_brut") + logger.info( + f" ✓ poids_brut = {article_data['poids_brut']}" + ) + except Exception as e: + champs_echoues.append(f"poids_brut: {e}") + + # Code fiscal + if "code_fiscal" in article_data: + try: + article.AR_CodeFiscal = str(article_data["code_fiscal"])[ + :10 + ] + champs_appliques.append("code_fiscal") + logger.info( + f" ✓ code_fiscal = {article_data['code_fiscal']}" + ) + except Exception as e: + 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}") + + # Soumis escompte + if "soumis_escompte" in article_data: + try: + article.AR_Escompte = ( + 1 if article_data["soumis_escompte"] else 0 + ) + champs_appliques.append("soumis_escompte") + logger.info( + f" ✓ soumis_escompte = {article_data['soumis_escompte']}" + ) + except Exception as e: + champs_echoues.append(f"soumis_escompte: {e}") + + # Publié + if "publie" in article_data: + try: + article.AR_Publie = 1 if article_data["publie"] else 0 + champs_appliques.append("publie") + logger.info(f" ✓ publie = {article_data['publie']}") + except Exception as e: + champs_echoues.append(f"publie: {e}") + + # En sommeil + if "en_sommeil" in article_data: + try: + article.AR_Sommeil = 1 if article_data["en_sommeil"] else 0 + champs_appliques.append("en_sommeil") + logger.info( + f" ✓ en_sommeil = {article_data['en_sommeil']}" + ) + except Exception as e: + champs_echoues.append(f"en_sommeil: {e}") + + if champs_echoues: + logger.warning( + f"[WARN] Champs échoués : {', '.join(champs_echoues)}" + ) + + 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() logger.info(" [OK] Write() réussi") @@ -8471,126 +8716,294 @@ class SageConnector: error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" except Exception: pass - logger.error(f" [ERREUR] Write() échoué : {error_detail}") - raise RuntimeError(f"Échec création article : {error_detail}") + logger.error(f" [ERREUR] Write() : {error_detail}") + raise RuntimeError(f"Échec création : {error_detail}") if transaction_active: try: self.cial.CptaApplication.CommitTrans() logger.info("[COMMIT] Transaction committée") except Exception as e: - logger.warning(f"[COMMIT] Erreur commit : {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 via creer_entree_stock...") - try: - lignes = [ - { - "article_ref": reference, - "quantite": stock_reel if stock_reel else 0.0, - "stock_mini": stock_mini, - "stock_maxi": stock_maxi, + 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}") - entree_stock_data = { - "date_mouvement": datetime.now().date(), - "reference": f"INIT-{reference}", - "lignes": lignes, - } + # 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), + ) - 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 initialisation stock : {e}", - exc_info=True, + 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(),), ) - logger.info("[VERIF] Relecture article créé...") + 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...") article_cree_persist = factory.ReadReference(reference) if not article_cree_persist: - raise RuntimeError( - "Article créé mais introuvable à la relecture" - ) + raise RuntimeError("Article créé mais introuvable") 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": _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 total : {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 : {e}") - - logger.info( - f"[OK] ARTICLE CREE : {reference} - Stock : {stock_total}" - ) - resultat = _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 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 @@ -8607,23 +9020,19 @@ class SageConnector: self.cial.CptaApplication.RollbackTrans() except Exception: pass - logger.error(f"Erreur creation article : {e}", exc_info=True) - raise RuntimeError(f"Erreur creation article : {str(e)}") + logger.error(f"Erreur création : {e}", exc_info=True) + raise RuntimeError(f"Erreur création : {str(e)}") except Exception as e: logger.error(f"Erreur globale : {e}", exc_info=True) raise def modifier_article(self, reference: str, article_data: Dict) -> Dict: + """Modifie un article existant dans Sage 100""" if not self.cial: raise RuntimeError("Connexion Sage non établie") try: - from utils.article_fields import ( - valider_donnees_modification, - mapper_champ_api_vers_sage, - CHAMPS_ASSIGNABLES_MODIFICATION, - ) valide, erreur = valider_donnees_modification(article_data) if not valide: @@ -8641,219 +9050,242 @@ class SageConnector: 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...") + logger.info(f"[ARTICLE] Trouvé : {reference}") champs_modifies = [] + champs_echoues = [] + # === Gestion famille === 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}" - ) + logger.info(f"[FAMILLE] Changement : {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 = _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: + with self._get_sql_connection() as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT FA_CodeFamille, FA_Type + FROM F_FAMILLE + WHERE UPPER(FA_CodeFamille) = ? + """, + (famille_code_demande,), + ) + row = cursor.fetchone() + if row: + famille_code_exact = _safe_strip(row.FA_CodeFamille) + if row.FA_Type == 1: raise ValueError( - f"Famille '{famille_code_demande}' introuvable dans Sage" + f"Famille '{famille_code_demande}' est de type Total" ) - except ValueError: - raise - except Exception as e: - logger.warning(f" [SQL] Erreur : {e}") - raise ValueError(f"Impossible de vérifier la famille : {e}") + logger.info( + f" [SQL] Famille trouvée : {famille_code_exact}" + ) + else: + raise ValueError( + f"Famille '{famille_code_demande}' introuvable" + ) - if famille_existe_sql and famille_code_exact: - logger.info(" [COM] Recherche via scanner...") + if famille_code_exact: 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]}" - ) + index = 1 + while index <= 1000: + try: + persist_test = factory_famille.List(index) + if persist_test is None: + break + fam_test = win32com.client.CastTo( + persist_test, "IBOFamille3" + ) + fam_test.Read() + code_test = ( + getattr(fam_test, "FA_CodeFamille", "") + .strip() + .upper() + ) + if code_test == famille_code_exact.upper(): + famille_obj = fam_test + logger.info(f" [OK] Famille à index {index}") + break + index += 1 + except Exception: + break if famille_obj: famille_obj.Read() article.Famille = famille_obj - champs_modifies.append(f"famille={famille_code_exact}") + champs_modifies.append("famille") 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"Famille '{famille_code_demande}' inaccessible via COM" ) - except ValueError: - raise except Exception as e: - logger.error(f" [ERREUR] Changement famille : {e}") - raise ValueError(f"Impossible de changer la famille : {str(e)}") - - for champ_api, valeur in article_data.items(): - if champ_api == "famille": - continue - - champ_sage = mapper_champ_api_vers_sage(champ_api) - - if ( - champ_sage not in CHAMPS_ASSIGNABLES_MODIFICATION - and champ_sage - not in [ - "AR_Stock", - "AR_StockMini", - "AR_StockMaxi", - ] - ): - logger.debug( - f" [SKIP] Champ {champ_api} non dans la whitelist" - ) - continue + logger.error(f" [ERREUR] Famille : {e}") + champs_echoues.append(f"famille: {e}") + # === Traitement explicite des champs === + if "designation" in article_data: try: - if champ_sage == "AR_Design": - designation = str(valeur)[:69].strip() - article.AR_Design = designation - champs_modifies.append("designation") - logger.info(f" [OK] {champ_sage} = {designation}") - - elif champ_sage == "AR_PrixVen": - prix_vente = float(valeur) - article.AR_PrixVen = prix_vente - champs_modifies.append("prix_vente") - logger.info(f" [OK] {champ_sage} = {prix_vente}") - - elif champ_sage == "AR_PrixAch": - prix_achat = float(valeur) - try: - article.AR_PrixAch = prix_achat - except Exception: - article.AR_PrixAchat = prix_achat - champs_modifies.append("prix_achat") - logger.info(f" [OK] {champ_sage} = {prix_achat}") - - elif champ_sage == "AR_Stock": - stock_reel = float(valeur) - 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}" - ) - - elif champ_sage == "AR_StockMini": - stock_mini = float(valeur) - article.AR_StockMini = stock_mini - champs_modifies.append("stock_mini") - logger.info(f" [OK] {champ_sage} = {stock_mini}") - - elif champ_sage == "AR_StockMaxi": - stock_maxi = float(valeur) - article.AR_StockMaxi = stock_maxi - champs_modifies.append("stock_maxi") - logger.info(f" [OK] {champ_sage} = {stock_maxi}") - - elif champ_sage == "AR_CodeBarre": - code_ean = str(valeur)[:13].strip() - article.AR_CodeBarre = code_ean - champs_modifies.append("code_ean") - logger.info(f" [OK] {champ_sage} = {code_ean}") - - elif champ_sage == "AR_Commentaire": - description = str(valeur)[:255].strip() - article.AR_Commentaire = description - champs_modifies.append("description") - logger.info(" [OK] Description définie") - - elif hasattr(article, champ_sage): - setattr(article, champ_sage, valeur) - champs_modifies.append(champ_api) - logger.info(f" [OK] {champ_sage} = {valeur}") - + designation = str(article_data["designation"])[:69].strip() + article.AR_Design = designation + champs_modifies.append("designation") + logger.info(f" ✓ designation = {designation}") except Exception as e: - logger.warning(f" [WARN] {champ_sage} : {e}") + champs_echoues.append(f"designation: {e}") + + if "prix_vente" in article_data: + try: + article.AR_PrixVen = float(article_data["prix_vente"]) + champs_modifies.append("prix_vente") + logger.info(f" ✓ prix_vente = {article_data['prix_vente']}") + except Exception as e: + champs_echoues.append(f"prix_vente: {e}") + + if "prix_achat" in article_data: + try: + article.AR_PrixAch = 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}") + + if "coef" in article_data: + try: + article.AR_Coef = float(article_data["coef"]) + champs_modifies.append("coef") + logger.info(f" ✓ coef = {article_data['coef']}") + except Exception as e: + champs_echoues.append(f"coef: {e}") + + if "code_ean" in article_data: + try: + article.AR_CodeBarre = str(article_data["code_ean"])[ + :13 + ].strip() + champs_modifies.append("code_ean") + logger.info(f" ✓ code_ean = {article_data['code_ean']}") + except Exception as e: + champs_echoues.append(f"code_ean: {e}") + + if "description" in article_data: + try: + article.AR_Langue1 = str(article_data["description"])[ + :255 + ].strip() + champs_modifies.append("description") + logger.info(" ✓ description définie (AR_Langue1)") + except Exception as e: + champs_echoues.append(f"description: {e}") + + if "pays" in article_data: + try: + article.AR_Pays = str(article_data["pays"])[:3].upper() + champs_modifies.append("pays") + logger.info(f" ✓ pays = {article_data['pays']}") + except Exception as e: + champs_echoues.append(f"pays: {e}") + + if "garantie" in article_data: + try: + article.AR_Garantie = int(article_data["garantie"]) + champs_modifies.append("garantie") + logger.info(f" ✓ garantie = {article_data['garantie']}") + except Exception as e: + champs_echoues.append(f"garantie: {e}") + + if "delai" in article_data: + try: + article.AR_Delai = int(article_data["delai"]) + champs_modifies.append("delai") + logger.info(f" ✓ delai = {article_data['delai']}") + except Exception as e: + champs_echoues.append(f"delai: {e}") + + if "poids_net" in article_data: + try: + article.AR_PoidsNet = float(article_data["poids_net"]) + champs_modifies.append("poids_net") + logger.info(f" ✓ poids_net = {article_data['poids_net']}") + except Exception as e: + champs_echoues.append(f"poids_net: {e}") + + if "poids_brut" in article_data: + try: + article.AR_PoidsBrut = float(article_data["poids_brut"]) + champs_modifies.append("poids_brut") + logger.info(f" ✓ poids_brut = {article_data['poids_brut']}") + except Exception as e: + champs_echoues.append(f"poids_brut: {e}") + + if "code_fiscal" in article_data: + try: + article.AR_CodeFiscal = str(article_data["code_fiscal"])[:10] + champs_modifies.append("code_fiscal") + logger.info(f" ✓ code_fiscal = {article_data['code_fiscal']}") + except Exception as e: + 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}") + + if "soumis_escompte" in article_data: + try: + article.AR_Escompte = ( + 1 if article_data["soumis_escompte"] else 0 + ) + champs_modifies.append("soumis_escompte") + logger.info( + f" ✓ soumis_escompte = {article_data['soumis_escompte']}" + ) + except Exception as e: + champs_echoues.append(f"soumis_escompte: {e}") + + if "publie" in article_data: + try: + article.AR_Publie = 1 if article_data["publie"] else 0 + champs_modifies.append("publie") + logger.info(f" ✓ publie = {article_data['publie']}") + except Exception as e: + champs_echoues.append(f"publie: {e}") + + if "en_sommeil" in article_data: + try: + article.AR_Sommeil = 1 if article_data["en_sommeil"] else 0 + champs_modifies.append("en_sommeil") + logger.info(f" ✓ en_sommeil = {article_data['en_sommeil']}") + except Exception as e: + 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 à modifier") + logger.warning("[ARTICLE] Aucun champ modifié") return _extraire_article(article) - logger.info( - f"[ARTICLE] Champs à modifier : {', '.join(champs_modifies)}" - ) - logger.info("[ARTICLE] Écriture des modifications...") + logger.info(f"[ARTICLE] Champs modifiés : {', '.join(champs_modifies)}") + logger.info("[ARTICLE] Écriture...") + # === Écriture COM === try: article.Write() logger.info("[ARTICLE] Write() réussi") @@ -8870,31 +9302,182 @@ class SageConnector: logger.error(f"[ARTICLE] Erreur Write() : {error_detail}") raise RuntimeError(f"Échec modification : {error_detail}") - article.Read() + # === Gestion stocks mini/maxi via SQL === + if "stock_mini" in article_data or "stock_maxi" in article_data: + try: + with self._get_sql_connection() as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT TOP 1 DE_No FROM F_DEPOT ORDER BY DE_No" + ) + row = cursor.fetchone() + if row: + depot_no = row.DE_No + update_parts = [] + params = [] + if "stock_mini" in article_data: + update_parts.append("AS_QteMini = ?") + params.append(float(article_data["stock_mini"])) + champs_modifies.append("stock_mini") + + if "stock_maxi" in article_data: + update_parts.append("AS_QteMaxi = ?") + params.append(float(article_data["stock_maxi"])) + champs_modifies.append("stock_maxi") + + if update_parts: + params.extend([reference.upper(), depot_no]) + cursor.execute( + f""" + UPDATE F_ARTSTOCK + SET {", ".join(update_parts)} + WHERE AR_Ref = ? AND DE_No = ? + """, + params, + ) + conn.commit() + logger.info(" [SQL] Stocks mini/maxi mis à jour") + except Exception as e: + logger.error(f"[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"[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)" ) resultat = _extraire_article(article) - if not resultat: - resultat = { - "reference": reference, - "designation": getattr(article, "AR_Design", ""), - } - - for champ_api in article_data.keys(): - if champ_api in [ - "prix_vente", - "prix_achat", - "stock_reel", - "stock_mini", - "stock_maxi", - ]: - resultat[champ_api] = article_data[champ_api] - elif champ_api in ["code_ean", "description"]: - resultat[champ_api] = article_data[champ_api] + resultat = {"reference": reference} return resultat diff --git a/utils/__init__.py b/utils/__init__.py index 3907f87..749f680 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -16,6 +16,12 @@ from .article_fields import ( valider_donnees_creation, mapper_champ_api_vers_sage, CHAMPS_STOCK_INITIAL, + CHAMPS_ASSIGNABLES_CREATION, + CHAMPS_ASSIGNABLES_MODIFICATION, + CHAMPS_OBJETS_SPECIAUX, + valider_champ, + valider_donnees_modification, + obtenir_champs_assignables ) __all__ = [ @@ -33,4 +39,10 @@ __all__ = [ "valider_donnees_creation", "mapper_champ_api_vers_sage", "CHAMPS_STOCK_INITIAL", + "CHAMPS_ASSIGNABLES_MODIFICATION", + "CHAMPS_OBJETS_SPECIAUX", + "CHAMPS_ASSIGNABLES_CREATION", + "valider_champ", + "valider_donnees_modification", + "obtenir_champs_assignables" ]