feat(articles): enhance article endpoints with async support and validation
This commit is contained in:
parent
963118641b
commit
e56159268f
1 changed files with 150 additions and 51 deletions
201
api.py
201
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 = ""):
|
||||
|
|
|
|||
Loading…
Reference in a new issue