From e56159268f89dab2602268fdabc3758c857f0715 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 11 Dec 2025 12:01:54 +0300 Subject: [PATCH] feat(articles): enhance article endpoints with async support and validation --- api.py | 201 ++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 150 insertions(+), 51 deletions(-) diff --git a/api.py b/api.py index 4bfea6b..02e90d9 100644 --- a/api.py +++ b/api.py @@ -1086,108 +1086,207 @@ async def rechercher_articles(query: Optional[str] = Query(None)): logger.error(f"Erreur recherche articles: {e}") raise HTTPException(500, str(e)) -@app.post("/articles", status_code=status.HTTP_201_CREATED, tags=["Articles"]) -def creer_article(article: ArticleCreateRequest): +@app.post( + "/articles", + response_model=ArticleResponse, + status_code=status.HTTP_201_CREATED, + tags=["Articles"] +) +async def creer_article(article: ArticleCreateRequest): """ - ➕ Création d'un article dans Sage + ➕ Création d'un nouvel article dans Sage - **Usage**: Créer un article avec stock pour éviter l'erreur 2881 + **Usage typique:** Créer un article avec stock pour éviter l'erreur 2881 - **Erreurs possibles**: + **Champs obligatoires:** + - `reference` (max 18 caractères) : Référence unique de l'article + - `designation` (max 69 caractères) : Désignation de l'article + + **Champs optionnels mais recommandés:** + - `stock_reel` : Stock initial (important pour éviter erreurs de transformation) + - `prix_vente` : Prix de vente HT + - `unite_vente` : Unité de vente (défaut: "UN") + + **Erreurs possibles:** - 400: Article existe déjà ou données invalides - - 500: Erreur Sage + - 500: Erreur Sage (problème de connexion, champs mal formatés, etc.) + + **Exemple:** + ```json + { + "reference": "ART001", + "designation": "Article de test", + "prix_vente": 10.50, + "stock_reel": 100.0, + "stock_mini": 10.0, + "unite_vente": "UN", + "tva_code": "C20" + } + ``` """ try: - resultat = sage_client.creer_article(article.dict(exclude_unset=True)) + # Validation des données + if not article.reference or not article.designation: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Les champs 'reference' et 'designation' sont obligatoires" + ) - logger.info(f"✅ Article créé: {resultat.get('reference')}") + # ⚠️ CORRECTION: Ne pas utiliser exclude_none=True car on veut garder + # les valeurs par défaut (comme unite_vente="UN") + article_data = article.dict(exclude_unset=True) - return { - "message": "Article créé avec succès", - "article": resultat - } + logger.info(f"📝 Création article: {article.reference} - {article.designation}") + + # Appel à la gateway Windows + resultat = sage_client.creer_article(article_data) + + logger.info(f"✅ Article créé: {resultat.get('reference')} (stock: {resultat.get('stock_reel', 0)})") + + return ArticleResponse(**resultat) except ValueError as e: - logger.warning(f"Erreur métier création article: {e}") - raise HTTPException(status.HTTP_400_BAD_REQUEST, str(e)) + # Erreur métier (ex: article existe déjà) + logger.warning(f"⚠️ Erreur métier création article: {e}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + except HTTPException: + raise except Exception as e: - logger.error(f"Erreur création article: {e}") - raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, str(e)) + # Erreur technique Sage + logger.error(f"❌ Erreur technique création article: {e}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Erreur lors de la création de l'article: {str(e)}" + ) -@app.put("/articles/{reference}", tags=["Articles"]) -def modifier_article(reference: str, article: ArticleUpdateRequest): +@app.put("/articles/{reference}", response_model=ArticleResponse, tags=["Articles"]) +async def modifier_article( + reference: str = Path(..., description="Référence de l'article à modifier"), + article: ArticleUpdateRequest = Body(...) +): """ - ✏️ Modification d'un article dans Sage + ✏️ Modification complète d'un article existant - **Usage critique**: Augmenter le stock pour résoudre l'erreur 2881 + **Usage critique:** Augmenter le stock pour résoudre l'erreur 2881 - **Example** - Résoudre l'erreur "L'état du stock ne permet pas de créer la ligne": -```bash - curl -X PUT "http://api.example.com/api/articles/ART001" \ - -H "Content-Type: application/json" \ - -d '{"stock_reel": 100.0}' -``` + **Erreur 2881 - "L'état du stock ne permet pas de créer la ligne"** - **Erreurs possibles**: + Cette erreur survient lors de la transformation de documents (devis → commande → facture) + lorsque le stock de l'article est insuffisant. + + **Solution:** Augmenter le `stock_reel` de l'article + + **Exemple - Résoudre l'erreur 2881:** + ```json + { + "stock_reel": 100.0 + } + ``` + + **Autres modifications possibles:** + - Prix de vente/achat + - Stock minimum + - Code EAN + - Description + + **Erreurs possibles:** - 404: Article introuvable - - 400: Données invalides + - 400: Aucun champ à modifier ou données invalides - 500: Erreur Sage + + **Note:** Seuls les champs fournis seront modifiés, les autres restent inchangés """ try: - # Filtrer les champs None + # ✅ CORRECTION: Utiliser exclude_unset=True au lieu de exclude_none=True + # Cela permet de distinguer entre: + # - Champ non fourni (exclu) + # - Champ fourni avec valeur None (inclus pour reset) article_data = article.dict(exclude_unset=True) if not article_data: raise HTTPException( - status.HTTP_400_BAD_REQUEST, - "Aucun champ à modifier" + status_code=status.HTTP_400_BAD_REQUEST, + detail="Aucun champ à modifier. Fournissez au moins un champ à mettre à jour." ) + logger.info(f"📝 Modification article {reference}: {list(article_data.keys())}") + + # Appel à la gateway Windows resultat = sage_client.modifier_article(reference, article_data) - logger.info(f"✅ Article {reference} modifié: {list(article_data.keys())}") + # Log spécial pour modification de stock (important pour erreur 2881) + if "stock_reel" in article_data: + logger.info( + f"📦 Stock {reference} modifié: {article_data['stock_reel']} " + f"(peut résoudre erreur 2881)" + ) - return { - "message": f"Article {reference} modifié avec succès", - "article": resultat, - "champs_modifies": list(article_data.keys()) - } + logger.info(f"✅ Article {reference} modifié ({len(article_data)} champs)") + + return ArticleResponse(**resultat) except ValueError as e: - logger.warning(f"Erreur métier modification article: {e}") - raise HTTPException(status.HTTP_404_NOT_FOUND, str(e)) + # Erreur métier (ex: article introuvable) + logger.warning(f"⚠️ Erreur métier modification article: {e}") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + + except HTTPException: + raise except Exception as e: - logger.error(f"Erreur modification article: {e}") - raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, str(e)) + # Erreur technique Sage + logger.error(f"❌ Erreur technique modification article: {e}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Erreur lors de la modification de l'article: {str(e)}" + ) -@app.get("/articles/{reference}", tags=["Articles"]) -def lire_article(reference: str): +@app.get("/articles/{reference}", response_model=ArticleResponse, tags=["Articles"]) +async def lire_article(reference: str = Path(..., description="Référence de l'article")): """ - 📄 Lecture d'un article par référence + 📄 Lecture d'un article spécifique par référence - Retourne toutes les informations incluant le stock actuel + **Retourne:** + - Toutes les informations de l'article + - Stock actuel (réel, réservé, disponible) + - Prix de vente et d'achat + - Famille, fournisseur principal + - Caractéristiques physiques (poids, volume) + + **Source:** Cache mémoire (instantané) """ try: article = sage_client.lire_article(reference) if not article: + logger.warning(f"⚠️ Article {reference} introuvable") raise HTTPException( - status.HTTP_404_NOT_FOUND, - f"Article {reference} introuvable" + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Article {reference} introuvable" ) - return {"article": article} + logger.info(f"✅ Article {reference} lu: {article.get('designation', '')}") + + return ArticleResponse(**article) except HTTPException: raise except Exception as e: - logger.error(f"Erreur lecture article: {e}") - raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, str(e)) - + logger.error(f"❌ Erreur lecture article {reference}: {e}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Erreur lors de la lecture de l'article: {str(e)}" + ) @app.get("/articles/all") def lister_articles(filtre: str = ""):