From beabefa3f9cadfad4e89cd84f4be9f5112fb4ca1 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 15 Jan 2026 15:49:15 +0300 Subject: [PATCH] feat(payments): enhance payment processing with new endpoints and schema --- api.py | 104 +++++++++++++++++++++++++------- sage_client.py | 44 ++++++++++++++ schemas/documents/reglements.py | 80 +++++++++++++++++++----- 3 files changed, 192 insertions(+), 36 deletions(-) diff --git a/api.py b/api.py index d364764..03fb2c5 100644 --- a/api.py +++ b/api.py @@ -3037,19 +3037,25 @@ async def regler_facture( try: resultat = sage_client.regler_facture( numero_facture=numero_facture, - montant=reglement.montant, + montant=float(reglement.montant), mode_reglement=reglement.mode_reglement, + code_journal=reglement.code_journal, 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, + devise_code=reglement.devise_code, + cours_devise=float(reglement.cours_devise) + if reglement.cours_devise + else 1.0, + tva_encaissement=reglement.tva_encaissement, + compte_general=reglement.compte_general, ) logger.info( f"Règlement facture {numero_facture}: {reglement.montant}€ - " - f"Solde: {resultat.get('solde_restant', 0)}€" + f"Journal: {reglement.code_journal} - Mode: {reglement.mode_reglement}" ) return { @@ -3150,24 +3156,6 @@ async def get_reglements_client( 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("/journaux/banque", tags=["Règlements"]) async def get_journaux_banque(): try: @@ -3178,6 +3166,80 @@ async def get_journaux_banque(): raise HTTPException(500, str(e)) +@app.get("/reglements/modes", tags=["Référentiels"]) +async def get_modes_reglement(): + """Liste des modes de règlement disponibles dans Sage""" + try: + modes = sage_client.get_modes_reglement() + return {"success": True, "data": {"modes": modes}} + except Exception as e: + logger.error(f"Erreur lecture modes règlement: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/devises", tags=["Référentiels"]) +async def get_devises(): + """Liste des devises disponibles dans Sage""" + try: + devises = sage_client.get_devises() + return {"success": True, "data": {"devises": devises}} + except Exception as e: + logger.error(f"Erreur lecture devises: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/journaux/tresorerie", tags=["Référentiels"]) +async def get_journaux_tresorerie(): + """Liste des journaux de trésorerie (banque + caisse)""" + try: + journaux = sage_client.get_journaux_tresorerie() + return {"success": True, "data": {"journaux": journaux}} + except Exception as e: + logger.error(f"Erreur lecture journaux: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/comptes-generaux", tags=["Référentiels"]) +async def get_comptes_generaux( + prefixe: Optional[str] = Query(None, description="Filtre par préfixe"), + type_compte: Optional[str] = Query( + None, + description="client | fournisseur | banque | caisse | tva | produit | charge", + ), +): + """Liste des comptes généraux""" + try: + comptes = sage_client.get_comptes_generaux( + prefixe=prefixe, type_compte=type_compte + ) + return {"success": True, "data": {"comptes": comptes, "total": len(comptes)}} + except Exception as e: + logger.error(f"Erreur lecture comptes généraux: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/tva/taux", tags=["Référentiels"]) +async def get_tva_taux(): + """Liste des taux de TVA""" + try: + taux = sage_client.get_tva_taux() + return {"success": True, "data": {"taux": taux}} + except Exception as e: + logger.error(f"Erreur lecture taux TVA: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/parametres/encaissement", tags=["Référentiels"]) +async def get_parametres_encaissement(): + """Paramètres TVA sur encaissement""" + try: + params = sage_client.get_parametres_encaissement() + return {"success": True, "data": params} + except Exception as e: + logger.error(f"Erreur lecture paramètres encaissement: {e}") + raise HTTPException(500, str(e)) + + @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 20aeddc..0a37807 100644 --- a/sage_client.py +++ b/sage_client.py @@ -520,6 +520,50 @@ class SageGatewayClient: def get_journaux_banque(self) -> dict: return self._get("/sage/journaux/banque").get("data", {}) + def get_modes_reglement(self) -> List[dict]: + """Récupère les modes de règlement depuis Sage""" + return self._get("/sage/reglements/modes").get("data", {}).get("modes", []) + + def get_devises(self) -> List[dict]: + """Récupère les devises disponibles""" + return self._get("/sage/devises").get("data", {}).get("devises", []) + + def get_journaux_tresorerie(self) -> List[dict]: + """Récupère les journaux de trésorerie (banque + caisse)""" + return ( + self._get("/sage/journaux/tresorerie").get("data", {}).get("journaux", []) + ) + + def get_comptes_generaux( + self, prefixe: str = None, type_compte: str = None + ) -> List[dict]: + """ + Récupère les comptes généraux + + Args: + prefixe: Filtre par préfixe (ex: "41", "51") + type_compte: "client", "fournisseur", "banque", "caisse", "tva" + """ + params = {} + if prefixe: + params["prefixe"] = prefixe + if type_compte: + params["type_compte"] = type_compte + + return ( + self._get("/sage/comptes-generaux", params=params) + .get("data", {}) + .get("comptes", []) + ) + + def get_tva_taux(self) -> List[dict]: + """Récupère les taux de TVA""" + return self._get("/sage/tva/taux").get("data", {}).get("taux", []) + + def get_parametres_encaissement(self) -> dict: + """Récupère les paramètres TVA sur encaissement""" + return self._get("/sage/parametres/encaissement").get("data", {}) + def refresh_cache(self) -> Dict: return self._post("/sage/cache/refresh") diff --git a/schemas/documents/reglements.py b/schemas/documents/reglements.py index ecbb5bf..2a7908c 100644 --- a/schemas/documents/reglements.py +++ b/schemas/documents/reglements.py @@ -1,19 +1,46 @@ from pydantic import BaseModel, Field, field_validator from typing import List, Optional -from datetime import datetime import logging +from decimal import Decimal +from datetime import date 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) + # Montant et devise + montant: Decimal = Field(..., gt=0, description="Montant à régler") + devise_code: Optional[int] = Field(0, description="Code devise (0=EUR par défaut)") + cours_devise: Optional[Decimal] = Field(1.0, description="Cours de la devise") + + # Mode et journal + mode_reglement: int = Field( + ..., ge=0, description="Code mode règlement depuis /reglements/modes" + ) + code_journal: str = Field( + ..., min_length=1, description="Code journal depuis /journaux/tresorerie" + ) + + # Dates + date_reglement: Optional[date] = Field( + None, description="Date du règlement (défaut: aujourd'hui)" + ) + date_echeance: Optional[date] = Field(None, description="Date d'échéance") + + # Références + reference: Optional[str] = Field( + "", max_length=17, description="Référence pièce règlement" + ) + libelle: Optional[str] = Field( + "", max_length=35, description="Libellé du règlement" + ) + + # TVA sur encaissement + tva_encaissement: Optional[bool] = Field( + False, description="Appliquer TVA sur encaissement" + ) @field_validator("montant") def validate_montant(cls, v): @@ -28,6 +55,12 @@ class ReglementFactureCreate(BaseModel): "mode_reglement": 2, "reference": "CHQ-001", "code_journal": "BEU", + "date_reglement": "2024-01-01", + "libelle": "Règlement multiple", + "tva_encaissement": False, + "devise_code": 0, + "cours_devise": 1.0, + "date_echeance": "2024-01-31", } } @@ -35,14 +68,23 @@ class ReglementFactureCreate(BaseModel): 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 + client_id: str = Field(..., description="Code client") + montant_total: Decimal = Field(..., gt=0) + + # Même structure que ReglementFactureCreate + devise_code: Optional[int] = Field(0) + cours_devise: Optional[Decimal] = Field(1.0) + mode_reglement: int = Field(...) + code_journal: str = Field(...) + date_reglement: Optional[date] = None + reference: Optional[str] = Field("") + libelle: Optional[str] = Field("") + tva_encaissement: Optional[bool] = Field(False) + + # Factures spécifiques (optionnel) + numeros_factures: Optional[List[str]] = Field( + None, description="Si vide, règle les plus anciennes en premier" + ) @field_validator("client_id", mode="before") def strip_client_id(cls, v): @@ -61,5 +103,13 @@ class ReglementMultipleCreate(BaseModel): "montant_total": 1000.00, "mode_reglement": 2, "numeros_factures": ["FA00081", "FA00082"], + "reference": "CHQ-001", + "code_journal": "BEU", + "date_reglement": "2024-01-01", + "libelle": "Règlement multiple", + "tva_encaissement": False, + "devise_code": 0, + "cours_devise": 1.0, + "date_echeance": "2024-01-31", } }