feat(stock): enhance stock movement models with lot tracking and min/max stock
This commit is contained in:
parent
421f4d24dc
commit
62e347969c
1 changed files with 107 additions and 47 deletions
114
api.py
114
api.py
|
|
@ -914,21 +914,74 @@ class FamilleResponse(BaseModel):
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class MouvementStockLigneRequest(BaseModel):
|
||||
"""Ligne de mouvement de stock"""
|
||||
article_ref: str = Field(..., description="Référence de l'article")
|
||||
quantite: float = Field(..., gt=0, description="Quantité (>0)")
|
||||
depot_code: Optional[str] = Field(None, description="Code du dépôt (ex: '01')")
|
||||
prix_unitaire: Optional[float] = Field(None, ge=0, description="Prix unitaire (optionnel)")
|
||||
prix_unitaire: Optional[float] = Field(
|
||||
None, ge=0, description="Prix unitaire (optionnel)"
|
||||
)
|
||||
commentaire: Optional[str] = Field(None, description="Commentaire ligne")
|
||||
numero_lot: Optional[str] = Field(
|
||||
None, description="Numéro de lot (pour FIFO/LIFO)"
|
||||
)
|
||||
stock_mini: Optional[float] = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="""Stock minimum à définir pour cet article.
|
||||
Si fourni, met à jour AS_QteMini dans F_ARTSTOCK.
|
||||
Laisser None pour ne pas modifier.""",
|
||||
)
|
||||
stock_maxi: Optional[float] = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="""Stock maximum à définir pour cet article.
|
||||
Doit être > stock_mini si les deux sont fournis.""",
|
||||
)
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"article_ref": "ARTS-001",
|
||||
"quantite": 50.0,
|
||||
"depot_code": "01",
|
||||
"prix_unitaire": 100.0,
|
||||
"commentaire": "Réapprovisionnement",
|
||||
"numero_lot": "LOT20241217",
|
||||
"stock_mini": 10.0,
|
||||
"stock_maxi": 200.0,
|
||||
}
|
||||
}
|
||||
|
||||
@validator("stock_maxi")
|
||||
def validate_stock_maxi(cls, v, values):
|
||||
"""Valide que stock_maxi > stock_mini si les deux sont fournis"""
|
||||
if (
|
||||
v is not None
|
||||
and "stock_mini" in values
|
||||
and values["stock_mini"] is not None
|
||||
):
|
||||
if v <= values["stock_mini"]:
|
||||
raise ValueError(
|
||||
"stock_maxi doit être strictement supérieur à stock_mini"
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class EntreeStockRequest(BaseModel):
|
||||
"""Création d'un bon d'entrée en stock"""
|
||||
date_entree: Optional[date] = Field(None, description="Date du mouvement (aujourd'hui par défaut)")
|
||||
|
||||
date_entree: Optional[date] = Field(
|
||||
None, description="Date du mouvement (aujourd'hui par défaut)"
|
||||
)
|
||||
reference: Optional[str] = Field(None, description="Référence externe")
|
||||
depot_code: Optional[str] = Field(None, description="Dépôt principal (si applicable)")
|
||||
lignes: List[MouvementStockLigneRequest] = Field(..., min_items=1, description="Lignes du mouvement")
|
||||
depot_code: Optional[str] = Field(
|
||||
None, description="Dépôt principal (si applicable)"
|
||||
)
|
||||
lignes: List[MouvementStockLigneRequest] = Field(
|
||||
..., min_items=1, description="Lignes du mouvement"
|
||||
)
|
||||
commentaire: Optional[str] = Field(None, description="Commentaire général")
|
||||
|
||||
class Config:
|
||||
|
|
@ -943,20 +996,27 @@ class EntreeStockRequest(BaseModel):
|
|||
"quantite": 50,
|
||||
"depot_code": "01",
|
||||
"prix_unitaire": 10.50,
|
||||
"commentaire": "Réception fournisseur"
|
||||
"commentaire": "Réception fournisseur",
|
||||
}
|
||||
],
|
||||
"commentaire": "Réception livraison fournisseur XYZ"
|
||||
"commentaire": "Réception livraison fournisseur XYZ",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SortieStockRequest(BaseModel):
|
||||
"""Création d'un bon de sortie de stock"""
|
||||
date_sortie: Optional[date] = Field(None, description="Date du mouvement (aujourd'hui par défaut)")
|
||||
|
||||
date_sortie: Optional[date] = Field(
|
||||
None, description="Date du mouvement (aujourd'hui par défaut)"
|
||||
)
|
||||
reference: Optional[str] = Field(None, description="Référence externe")
|
||||
depot_code: Optional[str] = Field(None, description="Dépôt principal (si applicable)")
|
||||
lignes: List[MouvementStockLigneRequest] = Field(..., min_items=1, description="Lignes du mouvement")
|
||||
depot_code: Optional[str] = Field(
|
||||
None, description="Dépôt principal (si applicable)"
|
||||
)
|
||||
lignes: List[MouvementStockLigneRequest] = Field(
|
||||
..., min_items=1, description="Lignes du mouvement"
|
||||
)
|
||||
commentaire: Optional[str] = Field(None, description="Commentaire général")
|
||||
|
||||
class Config:
|
||||
|
|
@ -970,16 +1030,17 @@ class SortieStockRequest(BaseModel):
|
|||
"article_ref": "ART001",
|
||||
"quantite": 10,
|
||||
"depot_code": "01",
|
||||
"commentaire": "Utilisation interne"
|
||||
"commentaire": "Utilisation interne",
|
||||
}
|
||||
],
|
||||
"commentaire": "Consommation atelier"
|
||||
"commentaire": "Consommation atelier",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MouvementStockResponse(BaseModel):
|
||||
"""Réponse pour un mouvement de stock"""
|
||||
|
||||
numero: str = Field(..., description="Numéro du mouvement")
|
||||
type: int = Field(..., description="Type (0=Entrée, 1=Sortie)")
|
||||
type_libelle: str = Field(..., description="Libellé du type")
|
||||
|
|
@ -1102,6 +1163,7 @@ async def universign_statut(transaction_id: str) -> Dict:
|
|||
logger.error(f"Erreur statut Universign: {e}")
|
||||
return {"statut": "ERREUR", "error": str(e)}
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Init base de données
|
||||
|
|
@ -1805,6 +1867,7 @@ async def changer_statut_devis(
|
|||
logger.error(f"Erreur changement statut devis {id}: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.get("/commandes/{id}", tags=["Commandes"])
|
||||
async def lire_commande(id: str):
|
||||
try:
|
||||
|
|
@ -2253,6 +2316,7 @@ async def envoyer_emails_lot(
|
|||
"details": resultats,
|
||||
}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/devis/valider-remise", response_model=BaremeRemiseResponse, tags=["Validation"]
|
||||
)
|
||||
|
|
@ -2638,6 +2702,7 @@ async def relancer_facture(
|
|||
logger.error(f"Erreur relance facture: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.get("/emails/logs", tags=["Emails"])
|
||||
async def journal_emails(
|
||||
statut: Optional[StatutEmail] = Query(None),
|
||||
|
|
@ -2733,6 +2798,7 @@ async def exporter_logs_csv(
|
|||
},
|
||||
)
|
||||
|
||||
|
||||
class TemplateEmail(BaseModel):
|
||||
id: Optional[str] = None
|
||||
nom: str
|
||||
|
|
@ -3616,7 +3682,7 @@ async def creer_famille(famille: FamilleCreateRequest):
|
|||
response_model=MouvementStockResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
tags=["Stock"],
|
||||
summary="ENTRÉE EN STOCK : Ajoute des articles dans le stock"
|
||||
summary="ENTRÉE EN STOCK : Ajoute des articles dans le stock",
|
||||
)
|
||||
async def creer_entree_stock(entree: EntreeStockRequest):
|
||||
try:
|
||||
|
|
@ -3636,16 +3702,13 @@ async def creer_entree_stock(entree: EntreeStockRequest):
|
|||
|
||||
except ValueError as e:
|
||||
logger.warning(f"⚠️ Erreur métier entrée stock: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Erreur technique entrée stock: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors de la création de l'entrée: {str(e)}"
|
||||
detail=f"Erreur lors de la création de l'entrée: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -3654,7 +3717,7 @@ async def creer_entree_stock(entree: EntreeStockRequest):
|
|||
response_model=MouvementStockResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
tags=["Stock"],
|
||||
summary="SORTIE DE STOCK : Retire des articles du stock"
|
||||
summary="SORTIE DE STOCK : Retire des articles du stock",
|
||||
)
|
||||
async def creer_sortie_stock(sortie: SortieStockRequest):
|
||||
try:
|
||||
|
|
@ -3674,16 +3737,13 @@ async def creer_sortie_stock(sortie: SortieStockRequest):
|
|||
|
||||
except ValueError as e:
|
||||
logger.warning(f"⚠️ Erreur métier sortie stock: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Erreur technique sortie stock: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors de la création de la sortie: {str(e)}"
|
||||
detail=f"Erreur lors de la création de la sortie: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -3691,7 +3751,7 @@ async def creer_sortie_stock(sortie: SortieStockRequest):
|
|||
"/stock/mouvement/{numero}",
|
||||
response_model=MouvementStockResponse,
|
||||
tags=["Stock"],
|
||||
summary="Lecture d'un mouvement de stock"
|
||||
summary="Lecture d'un mouvement de stock",
|
||||
)
|
||||
async def lire_mouvement_stock(
|
||||
numero: str = Path(..., description="Numéro du mouvement (ex: ME00123 ou MS00124)")
|
||||
|
|
@ -3703,7 +3763,7 @@ async def lire_mouvement_stock(
|
|||
logger.warning(f"⚠️ Mouvement {numero} introuvable")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Mouvement de stock {numero} introuvable"
|
||||
detail=f"Mouvement de stock {numero} introuvable",
|
||||
)
|
||||
|
||||
logger.info(f"✅ Mouvement {numero} lu")
|
||||
|
|
@ -3716,7 +3776,7 @@ async def lire_mouvement_stock(
|
|||
logger.error(f"❌ Erreur lecture mouvement {numero}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors de la lecture du mouvement: {str(e)}"
|
||||
detail=f"Erreur lors de la lecture du mouvement: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue