From a9df408399e4c0deeceb44d987a4d6c06da92025 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Wed, 14 Jan 2026 18:40:21 +0300 Subject: [PATCH] feat(payments): add payment functionality for invoices --- api.py | 141 ++++++++++++++++++++++++++++++++ sage_client.py | 74 +++++++++++++++++ schemas/documents/factures.py | 1 + schemas/documents/reglements.py | 65 +++++++++++++++ 4 files changed, 281 insertions(+) create mode 100644 schemas/documents/reglements.py diff --git a/api.py b/api.py index 7c3146e..26c9687 100644 --- a/api.py +++ b/api.py @@ -71,6 +71,7 @@ from schemas import ( ContactCreate, ContactUpdate, ) +from schemas.documents.reglements import ReglementFactureCreate, ReglementMultipleCreate from schemas.tiers.commercial import ( CollaborateurCreate, CollaborateurDetails, @@ -2965,6 +2966,146 @@ async def preview_societe(): return f"

Erreur

{str(e)}

" +@app.post("/factures/{numero_facture}/regler", status_code=200, tags=["Règlements"]) +async def regler_facture( + numero_facture: str, + reglement: ReglementFactureCreate, + session: AsyncSession = Depends(get_session), +): + try: + resultat = sage_client.regler_facture( + numero_facture=numero_facture, + montant=reglement.montant, + mode_reglement=reglement.mode_reglement, + date_reglement=reglement.date_reglement.isoformat() + if reglement.date_reglement + else None, + reference=reglement.reference or "", + libelle=reglement.libelle or "", + code_journal=reglement.code_journal, + ) + + logger.info( + f"Règlement facture {numero_facture}: {reglement.montant}€ - " + f"Solde: {resultat.get('solde_restant', 0)}€" + ) + + return { + "success": True, + "message": "Règlement effectué avec succès", + "data": resultat, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur règlement facture {numero_facture}: {e}") + raise HTTPException(500, str(e)) + + +@app.post("/reglements/multiple", status_code=200, tags=["Règlements"]) +async def regler_factures_multiple( + reglement: ReglementMultipleCreate, + session: AsyncSession = Depends(get_session), +): + try: + resultat = sage_client.regler_factures_client( + client_code=reglement.client_id, + montant_total=reglement.montant_total, + mode_reglement=reglement.mode_reglement, + date_reglement=reglement.date_reglement.isoformat() + if reglement.date_reglement + else None, + reference=reglement.reference or "", + libelle=reglement.libelle or "", + code_journal=reglement.code_journal, + numeros_factures=reglement.numeros_factures, + ) + + logger.info( + f"Règlement multiple client {reglement.client_id}: " + f"{resultat.get('montant_effectif', 0)}€ sur {resultat.get('nb_factures_reglees', 0)} facture(s)" + ) + + return { + "success": True, + "message": "Règlements effectués avec succès", + "data": resultat, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur règlement multiple {reglement.client_id}: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/factures/{numero_facture}/reglements", tags=["Règlements"]) +async def get_reglements_facture( + numero_facture: str, + session: AsyncSession = Depends(get_session), +): + try: + resultat = sage_client.get_reglements_facture(numero_facture) + + return { + "success": True, + "data": resultat, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture règlements {numero_facture}: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/clients/{client_id}/reglements", tags=["Règlements"]) +async def get_reglements_client( + client_id: str, + date_debut: Optional[datetime] = Query(None, description="Date début"), + date_fin: Optional[datetime] = Query(None, description="Date fin"), + inclure_soldees: bool = Query(True, description="Inclure les factures soldées"), + session: AsyncSession = Depends(get_session), +): + try: + resultat = sage_client.get_reglements_client( + client_code=client_id, + date_debut=date_debut.isoformat() if date_debut else None, + date_fin=date_fin.isoformat() if date_fin else None, + inclure_soldees=inclure_soldees, + ) + + return { + "success": True, + "data": resultat, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture règlements client {client_id}: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/reglements/modes", tags=["Règlements"]) +async def get_modes_reglement(): + return { + "success": True, + "data": { + "modes": [ + {"code": 1, "libelle": "Virement"}, + {"code": 2, "libelle": "Chèque"}, + {"code": 3, "libelle": "Traite"}, + {"code": 4, "libelle": "Carte bancaire"}, + {"code": 5, "libelle": "LCR"}, + {"code": 6, "libelle": "Prélèvement"}, + {"code": 7, "libelle": "Espèces"}, + ] + }, + } + + @app.get("/health", tags=["System"]) async def health_check( sage: SageGatewayClient = Depends(get_sage_client_for_user), diff --git a/sage_client.py b/sage_client.py index f938ab5..a91e66e 100644 --- a/sage_client.py +++ b/sage_client.py @@ -431,6 +431,80 @@ class SageGatewayClient: """Lit les informations de la société depuis P_DOSSIER""" return self._get("/sage/societe/info").get("data") + def regler_facture( + self, + numero_facture: str, + montant: float, + mode_reglement: int = 2, + date_reglement: str = None, + reference: str = "", + libelle: str = "", + code_journal: str = "BEU", + ) -> dict: + """Règle une facture""" + payload = { + "montant": montant, + "mode_reglement": mode_reglement, + "reference": reference, + "libelle": libelle, + "code_journal": code_journal, + } + if date_reglement: + payload["date_reglement"] = date_reglement + + return self._post(f"/sage/factures/{numero_facture}/regler", payload).get( + "data", {} + ) + + def regler_factures_client( + self, + client_code: str, + montant_total: float, + mode_reglement: int = 2, + date_reglement: str = None, + reference: str = "", + libelle: str = "", + code_journal: str = "BEU", + numeros_factures: list = None, + ) -> dict: + """Règle plusieurs factures d'un client""" + payload = { + "client_code": client_code, + "montant_total": montant_total, + "mode_reglement": mode_reglement, + "reference": reference, + "libelle": libelle, + "code_journal": code_journal, + } + if date_reglement: + payload["date_reglement"] = date_reglement + if numeros_factures: + payload["numeros_factures"] = numeros_factures + + return self._post("/sage/reglements/multiple", payload).get("data", {}) + + def get_reglements_facture(self, numero_facture: str) -> dict: + """Récupère les règlements d'une facture""" + return self._get(f"/sage/factures/{numero_facture}/reglements").get("data", {}) + + def get_reglements_client( + self, + client_code: str, + date_debut: str = None, + date_fin: str = None, + inclure_soldees: bool = True, + ) -> dict: + """Récupère les règlements d'un client""" + params = {"inclure_soldees": inclure_soldees} + if date_debut: + params["date_debut"] = date_debut + if date_fin: + params["date_fin"] = date_fin + + return self._get(f"/sage/clients/{client_code}/reglements", params=params).get( + "data", {} + ) + def refresh_cache(self) -> Dict: return self._post("/sage/cache/refresh") diff --git a/schemas/documents/factures.py b/schemas/documents/factures.py index 0ab6e21..2ab3382 100644 --- a/schemas/documents/factures.py +++ b/schemas/documents/factures.py @@ -4,6 +4,7 @@ from datetime import datetime from schemas.documents.ligne_document import LigneDocument + class FactureCreate(BaseModel): client_id: str date_facture: Optional[datetime] = None diff --git a/schemas/documents/reglements.py b/schemas/documents/reglements.py new file mode 100644 index 0000000..ecbb5bf --- /dev/null +++ b/schemas/documents/reglements.py @@ -0,0 +1,65 @@ +from pydantic import BaseModel, Field, field_validator +from typing import List, Optional +from datetime import datetime +import logging + +logger = logging.getLogger(__name__) + +class ReglementFactureCreate(BaseModel): + """Requête de règlement d'une facture côté VPS""" + + montant: float = Field(..., gt=0) + mode_reglement: int = Field(default=2, ge=1, le=7) + date_reglement: Optional[datetime] = None + reference: Optional[str] = Field(default="", max_length=35) + libelle: Optional[str] = Field(default="", max_length=69) + code_journal: str = Field(default="BEU", max_length=6) + + @field_validator("montant") + def validate_montant(cls, v): + if v <= 0: + raise ValueError("Le montant doit être positif") + return round(v, 2) + + class Config: + json_schema_extra = { + "example": { + "montant": 375.12, + "mode_reglement": 2, + "reference": "CHQ-001", + "code_journal": "BEU", + } + } + + +class ReglementMultipleCreate(BaseModel): + """Requête de règlement multiple côté VPS""" + + client_id: str + montant_total: float = Field(..., gt=0) + mode_reglement: int = Field(default=2, ge=1, le=7) + date_reglement: Optional[datetime] = None + reference: Optional[str] = Field(default="", max_length=35) + libelle: Optional[str] = Field(default="", max_length=69) + code_journal: str = Field(default="BEU", max_length=6) + numeros_factures: Optional[List[str]] = None + + @field_validator("client_id", mode="before") + def strip_client_id(cls, v): + return v.replace("\xa0", "").strip() if v else v + + @field_validator("montant_total") + def validate_montant(cls, v): + if v <= 0: + raise ValueError("Le montant doit être positif") + return round(v, 2) + + class Config: + json_schema_extra = { + "example": { + "client_id": "CLI000001", + "montant_total": 1000.00, + "mode_reglement": 2, + "numeros_factures": ["FA00081", "FA00082"], + } + }