diff --git a/api.py b/api.py index a49060c..dff2e31 100644 --- a/api.py +++ b/api.py @@ -265,6 +265,87 @@ class FournisseurUpdateRequest(BaseModel): } } +class DevisUpdateRequest(BaseModel): + """Modèle pour modification d'un devis existant""" + date_devis: Optional[date] = None + lignes: Optional[List[LigneDevis]] = None + statut: Optional[int] = Field(None, ge=0, le=6) + + class Config: + json_schema_extra = { + "example": { + "date_devis": "2024-01-15", + "lignes": [ + { + "article_code": "ART001", + "quantite": 5.0, + "prix_unitaire_ht": 100.0, + "remise_pourcentage": 10.0 + } + ], + "statut": 2 + } + } + + +class LigneCommande(BaseModel): + """Ligne de commande""" + article_code: str + quantite: float + prix_unitaire_ht: Optional[float] = None + remise_pourcentage: Optional[float] = 0.0 + + @field_validator("article_code", mode="before") + def strip_insecables(cls, v): + return v.replace("\xa0", "").strip() + + +class CommandeCreateRequest(BaseModel): + """Création d'une commande""" + client_id: str + date_commande: Optional[date] = None + lignes: List[LigneCommande] + reference: Optional[str] = None # Référence externe + + class Config: + json_schema_extra = { + "example": { + "client_id": "CLI000001", + "date_commande": "2024-01-15", + "reference": "CMD-EXT-001", + "lignes": [ + { + "article_code": "ART001", + "quantite": 10.0, + "prix_unitaire_ht": 50.0, + "remise_pourcentage": 5.0 + } + ] + } + } + + +class CommandeUpdateRequest(BaseModel): + """Modification d'une commande existante""" + date_commande: Optional[date] = None + lignes: Optional[List[LigneCommande]] = None + statut: Optional[int] = Field(None, ge=0, le=6) + reference: Optional[str] = None + + class Config: + json_schema_extra = { + "example": { + "lignes": [ + { + "article_code": "ART001", + "quantite": 15.0, + "prix_unitaire_ht": 45.0 + } + ], + "statut": 2 + } + } + # ===================================================== # SERVICES EXTERNES (Universign) @@ -592,6 +673,256 @@ async def creer_devis(devis: DevisRequest): raise HTTPException(500, str(e)) +@app.put("/devis/{id}", tags=["US-A1"]) +async def modifier_devis( + id: str, + devis_update: DevisUpdateRequest, + session: AsyncSession = Depends(get_session) +): + """ + ✏️ Modification d'un devis existant + + **Champs modifiables:** + - `date_devis`: Nouvelle date du devis + - `lignes`: Nouvelles lignes (remplace toutes les lignes existantes) + - `statut`: Nouveau statut (0=Brouillon, 2=Accepté, 5=Transformé, 6=Annulé) + + **Note importante:** + - Si `lignes` est fourni, TOUTES les lignes existantes seront remplacées + - Pour ajouter/supprimer une ligne, il faut envoyer TOUTES les lignes + - Un devis transformé (statut=5) ne peut plus être modifié + + Args: + id: Numéro du devis à modifier + devis_update: Champs à mettre à jour + + Returns: + Devis modifié avec ses nouvelles valeurs + """ + try: + # Vérifier que le devis existe + devis_existant = sage_client.lire_devis(id) + if not devis_existant: + raise HTTPException(404, f"Devis {id} introuvable") + + # Vérifier qu'il n'est pas déjà transformé + if devis_existant.get("statut") == 5: + raise HTTPException( + 400, + f"Le devis {id} a déjà été transformé et ne peut plus être modifié" + ) + + # Construire les données de mise à jour + update_data = {} + + if devis_update.date_devis: + update_data["date_devis"] = devis_update.date_devis.isoformat() + + if devis_update.lignes is not None: + update_data["lignes"] = [ + { + "article_code": l.article_code, + "quantite": l.quantite, + "prix_unitaire_ht": l.prix_unitaire_ht, + "remise_pourcentage": l.remise_pourcentage, + } + for l in devis_update.lignes + ] + + if devis_update.statut is not None: + update_data["statut"] = devis_update.statut + + # Appel à la gateway Windows + resultat = sage_client.modifier_devis(id, update_data) + + logger.info(f"✅ Devis {id} modifié avec succès") + + return { + "success": True, + "message": f"Devis {id} modifié avec succès", + "devis": resultat + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur modification devis {id}: {e}") + raise HTTPException(500, str(e)) + + +@app.post("/commandes", status_code=201, tags=["US-A2"]) +async def creer_commande( + commande: CommandeCreateRequest, + session: AsyncSession = Depends(get_session) +): + """ + ➕ Création d'une nouvelle commande (Bon de commande) + + **Workflow typique:** + 1. Création d'un devis → transformation en commande (automatique) + 2. OU création directe d'une commande (cette route) + + **Champs obligatoires:** + - `client_id`: Code du client + - `lignes`: Liste des lignes (min 1) + + **Champs optionnels:** + - `date_commande`: Date de la commande (par défaut: aujourd'hui) + - `reference`: Référence externe (ex: numéro de commande client) + + Args: + commande: Données de la commande à créer + + Returns: + Commande créée avec son numéro et ses totaux + """ + try: + # Vérifier que le client existe + client = sage_client.lire_client(commande.client_id) + if not client: + raise HTTPException(404, f"Client {commande.client_id} introuvable") + + # Préparer les données pour la gateway + commande_data = { + "client_id": commande.client_id, + "date_commande": ( + commande.date_commande.isoformat() + if commande.date_commande + else None + ), + "reference": commande.reference, + "lignes": [ + { + "article_code": l.article_code, + "quantite": l.quantite, + "prix_unitaire_ht": l.prix_unitaire_ht, + "remise_pourcentage": l.remise_pourcentage, + } + for l in commande.lignes + ], + } + + # Appel à la gateway Windows + resultat = sage_client.creer_commande(commande_data) + + logger.info(f"✅ Commande créée: {resultat.get('numero_commande')}") + + return { + "success": True, + "message": "Commande créée avec succès", + "data": { + "numero_commande": resultat["numero_commande"], + "client_id": commande.client_id, + "date_commande": resultat["date_commande"], + "total_ht": resultat["total_ht"], + "total_ttc": resultat["total_ttc"], + "nb_lignes": resultat["nb_lignes"], + "reference": commande.reference + } + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur création commande: {e}") + raise HTTPException(500, str(e)) + + +@app.put("/commandes/{id}", tags=["US-A2"]) +async def modifier_commande( + id: str, + commande_update: CommandeUpdateRequest, + session: AsyncSession = Depends(get_session) +): + """ + ✏️ Modification d'une commande existante + + **Champs modifiables:** + - `date_commande`: Nouvelle date + - `lignes`: Nouvelles lignes (remplace toutes les lignes existantes) + - `statut`: Nouveau statut + - `reference`: Référence externe + + **Restrictions:** + - Une commande transformée (statut=5) ne peut plus être modifiée + - Une commande annulée (statut=6) ne peut plus être modifiée + + **Note importante:** + Si `lignes` est fourni, TOUTES les lignes existantes seront remplacées + + Args: + id: Numéro de la commande à modifier + commande_update: Champs à mettre à jour + + Returns: + Commande modifiée avec ses nouvelles valeurs + """ + try: + # Vérifier que la commande existe + commande_existante = sage_client.lire_document( + id, + settings.SAGE_TYPE_BON_COMMANDE + ) + + if not commande_existante: + raise HTTPException(404, f"Commande {id} introuvable") + + # Vérifier le statut + statut_actuel = commande_existante.get("statut", 0) + + if statut_actuel == 5: + raise HTTPException( + 400, + f"La commande {id} a déjà été transformée et ne peut plus être modifiée" + ) + + if statut_actuel == 6: + raise HTTPException( + 400, + f"La commande {id} est annulée et ne peut plus être modifiée" + ) + + # Construire les données de mise à jour + update_data = {} + + if commande_update.date_commande: + update_data["date_commande"] = commande_update.date_commande.isoformat() + + if commande_update.lignes is not None: + update_data["lignes"] = [ + { + "article_code": l.article_code, + "quantite": l.quantite, + "prix_unitaire_ht": l.prix_unitaire_ht, + "remise_pourcentage": l.remise_pourcentage, + } + for l in commande_update.lignes + ] + + if commande_update.statut is not None: + update_data["statut"] = commande_update.statut + + if commande_update.reference is not None: + update_data["reference"] = commande_update.reference + + # Appel à la gateway Windows + resultat = sage_client.modifier_commande(id, update_data) + + logger.info(f"✅ Commande {id} modifiée avec succès") + + return { + "success": True, + "message": f"Commande {id} modifiée avec succès", + "commande": resultat + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur modification commande {id}: {e}") + raise HTTPException(500, str(e)) + + @app.get("/devis", tags=["US-A1"]) async def lister_devis( limit: int = Query(100, le=1000), diff --git a/sage_client.py b/sage_client.py index 4d5a0c5..ed51bf0 100644 --- a/sage_client.py +++ b/sage_client.py @@ -382,6 +382,68 @@ class SageGatewayClient: "code": code, "client_data": client_data }).get("data", {}) + + + def modifier_devis(self, numero: str, devis_data: Dict) -> Dict: + """ + ✏️ Modification d'un devis existant + + Args: + numero: Numéro du devis à modifier + devis_data: Dictionnaire contenant les champs à modifier: + - date_devis (str, optional): Nouvelle date + - lignes (List, optional): Nouvelles lignes + - statut (int, optional): Nouveau statut + + Returns: + Devis modifié avec totaux recalculés + """ + return self._post("/sage/devis/update", { + "numero": numero, + "devis_data": devis_data + }).get("data", {}) + + def creer_commande(self, commande_data: Dict) -> Dict: + """ + ➕ Création d'une nouvelle commande (Bon de commande) + + Args: + commande_data: Dictionnaire contenant: + - client_id (str): Code du client + - date_commande (str, optional): Date au format ISO + - reference (str, optional): Référence externe + - lignes (List[Dict]): Liste des lignes avec: + - article_code (str) + - quantite (float) + - prix_unitaire_ht (float, optional) + - remise_pourcentage (float, optional) + + Returns: + Commande créée avec son numéro et ses totaux + """ + return self._post("/sage/commandes/create", commande_data).get("data", {}) + + + def modifier_commande(self, numero: str, commande_data: Dict) -> Dict: + """ + ✏️ Modification d'une commande existante + + Args: + numero: Numéro de la commande à modifier + commande_data: Dictionnaire contenant les champs à modifier: + - date_commande (str, optional): Nouvelle date + - lignes (List, optional): Nouvelles lignes + - statut (int, optional): Nouveau statut + - reference (str, optional): Nouvelle référence + + Returns: + Commande modifiée avec totaux recalculés + """ + return self._post("/sage/commandes/update", { + "numero": numero, + "commande_data": commande_data + }).get("data", {}) + # Instance globale