From 671d5bac15907f25ee3dd0001736049df3646fda Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Wed, 14 Jan 2026 15:20:16 +0300 Subject: [PATCH 01/13] feat(society): add logo support and preview endpoint --- api.py | 93 +++++++++++++++++++++++++++++++++++++- schemas/society/societe.py | 10 ++-- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/api.py b/api.py index 72e741e..7c3146e 100644 --- a/api.py +++ b/api.py @@ -1,6 +1,6 @@ from fastapi import FastAPI, HTTPException, Path, Query, Depends, status, Body from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import StreamingResponse +from fastapi.responses import StreamingResponse, HTMLResponse, Response from fastapi.encoders import jsonable_encoder from pydantic import BaseModel, Field, EmailStr from typing import List, Optional @@ -2874,6 +2874,97 @@ async def obtenir_informations_societe(): raise HTTPException(500, str(e)) +@app.get("/societe/logo", tags=["Société"]) +async def obtenir_logo_societe(): + """Retourne le logo en tant qu'image directe""" + try: + societe = sage_client.lire_informations_societe() + + if not societe or not societe.get("logo_base64"): + raise HTTPException(404, "Logo introuvable") + + import base64 + + image_data = base64.b64decode(societe["logo_base64"]) + + return Response(content=image_data, media_type=societe["logo_content_type"]) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur récupération logo: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/societe/preview", response_class=HTMLResponse, tags=["Société"]) +async def preview_societe(): + """Page HTML pour visualiser les infos société avec logo""" + try: + societe = sage_client.lire_informations_societe() + + if not societe: + return "

Société introuvable

" + + logo_html = "" + if societe.get("logo_base64"): + logo_html = f'' + else: + logo_html = "

Aucun logo disponible

" + + html = f""" + + + + + Informations Société + + + +
+

Informations Société

+ + + +
+ Raison sociale: {societe["raison_sociale"]} +
+
+ SIRET: {societe["siret"] or "N/A"} +
+
+ Adresse: {societe["adresse"] or "N/A"} +
+
+ Code postal: {societe["code_postal"] or "N/A"} +
+
+ Ville: {societe["ville"] or "N/A"} +
+
+ Email: {societe["email"] or "N/A"} +
+
+ Téléphone: {societe["telephone"] or "N/A"} +
+
+ + + """ + return html + + except Exception as e: + return f"

Erreur

{str(e)}

" + + @app.get("/health", tags=["System"]) async def health_check( sage: SageGatewayClient = Depends(get_sage_client_for_user), diff --git a/schemas/society/societe.py b/schemas/society/societe.py index 01fa308..08309e5 100644 --- a/schemas/society/societe.py +++ b/schemas/society/societe.py @@ -9,14 +9,12 @@ class ExerciceComptable(BaseModel): class SocieteInfo(BaseModel): - # Identification raison_sociale: str numero_dossier: str siret: Optional[str] = None code_ape: Optional[str] = None numero_tva: Optional[str] = None - # Adresse adresse: Optional[str] = None complement_adresse: Optional[str] = None code_postal: Optional[str] = None @@ -24,27 +22,25 @@ class SocieteInfo(BaseModel): code_region: Optional[str] = None pays: Optional[str] = None - # Contacts telephone: Optional[str] = None telecopie: Optional[str] = None email: Optional[str] = None email_societe: Optional[str] = None site_web: Optional[str] = None - # Informations juridiques capital: float = 0.0 forme_juridique: Optional[str] = None - # Exercices comptables exercices: List[ExerciceComptable] = [] - # Configuration devise_compte: int = 0 devise_equivalent: int = 0 longueur_compte_general: int = 0 longueur_compte_analytique: int = 0 regime_fec: int = 0 - # Autres base_modele: Optional[str] = None marqueur: int = 0 + + logo_base64: Optional[str] = None + logo_content_type: Optional[str] = None From a9df408399e4c0deeceb44d987a4d6c06da92025 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Wed, 14 Jan 2026 18:40:21 +0300 Subject: [PATCH 02/13] 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"], + } + } From 23575fa231bec27f84acb05bd6d62c2df14b69a7 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Wed, 14 Jan 2026 19:58:56 +0300 Subject: [PATCH 03/13] feat(api): add endpoint to fetch bank journals --- api.py | 10 ++++++++++ sage_client.py | 3 +++ 2 files changed, 13 insertions(+) diff --git a/api.py b/api.py index 26c9687..61bfbe2 100644 --- a/api.py +++ b/api.py @@ -3106,6 +3106,16 @@ async def get_modes_reglement(): } +@app.get("/journaux/banque", tags=["Règlements"]) +async def get_journaux_banque(): + try: + resultat = sage_client.get_journaux_banque() + return {"success": True, "data": resultat} + except Exception as e: + logger.error(f"Erreur lecture journaux: {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 a91e66e..1907077 100644 --- a/sage_client.py +++ b/sage_client.py @@ -505,6 +505,9 @@ class SageGatewayClient: "data", {} ) + def get_journaux_banque(self) -> dict: + return self._get("/sage/journaux/banque").get("data", {}) + def refresh_cache(self) -> Dict: return self._post("/sage/cache/refresh") From a824592398c0717d7e8811c8946914c785552d26 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 15 Jan 2026 10:34:40 +0300 Subject: [PATCH 04/13] feat(factures): add invoice validation endpoints and client methods --- api.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++ sage_client.py | 18 +++++++++++ 2 files changed, 99 insertions(+) diff --git a/api.py b/api.py index 7c3146e..73cab27 100644 --- a/api.py +++ b/api.py @@ -2965,6 +2965,87 @@ async def preview_societe(): return f"

Erreur

{str(e)}

" +@app.post("/factures/{numero_facture}/valider", status_code=200, tags=["Factures"]) +async def valider_facture( + numero_facture: str, + session: AsyncSession = Depends(get_session), +): + """ + Valide une facture (pose le cadenas) + """ + try: + resultat = sage_client.valider_facture(numero_facture) + + logger.info( + f"Facture {numero_facture} validée: {resultat.get('action_effectuee')}" + ) + + return { + "success": True, + "message": resultat.get("message", "Facture validée"), + "data": resultat, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur validation facture {numero_facture}: {e}") + raise HTTPException(500, str(e)) + + +@app.post("/factures/{numero_facture}/devalider", status_code=200, tags=["Factures"]) +async def devalider_facture( + numero_facture: str, + session: AsyncSession = Depends(get_session), +): + """ + Dévalide une facture (retire le cadenas) + + Impossible si la facture a des règlements. + """ + try: + resultat = sage_client.devalider_facture(numero_facture) + + logger.info( + f"Facture {numero_facture} dévalidée: {resultat.get('action_effectuee')}" + ) + + return { + "success": True, + "message": resultat.get("message", "Facture dévalidée"), + "data": resultat, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur dévalidation facture {numero_facture}: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/factures/{numero_facture}/statut-validation", tags=["Factures"]) +async def get_statut_validation_facture( + numero_facture: str, + session: AsyncSession = Depends(get_session), +): + """ + Retourne le statut de validation d'une facture + """ + try: + resultat = sage_client.get_statut_validation(numero_facture) + + return { + "success": True, + "data": resultat, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture statut {numero_facture}: {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 f938ab5..7effc22 100644 --- a/sage_client.py +++ b/sage_client.py @@ -431,6 +431,24 @@ class SageGatewayClient: """Lit les informations de la société depuis P_DOSSIER""" return self._get("/sage/societe/info").get("data") + def valider_facture(self, numero_facture: str) -> dict: + """Valide une facture""" + return self._post(f"/sage/factures/{numero_facture}/valider", {}).get( + "data", {} + ) + + def devalider_facture(self, numero_facture: str) -> dict: + """Dévalide une facture""" + return self._post(f"/sage/factures/{numero_facture}/devalider", {}).get( + "data", {} + ) + + def get_statut_validation(self, numero_facture: str) -> dict: + """Récupère le statut de validation""" + return self._get(f"/sage/factures/{numero_facture}/statut-validation").get( + "data", {} + ) + def refresh_cache(self) -> Dict: return self._post("/sage/cache/refresh") From f505dad8a792a5c1000e6e0485740c94e4b601fc Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 15 Jan 2026 11:08:52 +0300 Subject: [PATCH 05/13] refactor: simplify invoice validation methods and error handling --- api.py | 31 ++++++------------------------- sage_client.py | 18 ++++++------------ 2 files changed, 12 insertions(+), 37 deletions(-) diff --git a/api.py b/api.py index 73cab27..fbd8e0b 100644 --- a/api.py +++ b/api.py @@ -2968,82 +2968,63 @@ async def preview_societe(): @app.post("/factures/{numero_facture}/valider", status_code=200, tags=["Factures"]) async def valider_facture( numero_facture: str, - session: AsyncSession = Depends(get_session), + _: AsyncSession = Depends(get_session), ): - """ - Valide une facture (pose le cadenas) - """ try: resultat = sage_client.valider_facture(numero_facture) - logger.info( f"Facture {numero_facture} validée: {resultat.get('action_effectuee')}" ) - return { "success": True, "message": resultat.get("message", "Facture validée"), "data": resultat, } - except HTTPException: raise except Exception as e: logger.error(f"Erreur validation facture {numero_facture}: {e}") - raise HTTPException(500, str(e)) + raise HTTPException(status_code=500, detail=str(e)) @app.post("/factures/{numero_facture}/devalider", status_code=200, tags=["Factures"]) async def devalider_facture( numero_facture: str, - session: AsyncSession = Depends(get_session), + _: AsyncSession = Depends(get_session), ): - """ - Dévalide une facture (retire le cadenas) - - Impossible si la facture a des règlements. - """ try: resultat = sage_client.devalider_facture(numero_facture) - logger.info( f"Facture {numero_facture} dévalidée: {resultat.get('action_effectuee')}" ) - return { "success": True, "message": resultat.get("message", "Facture dévalidée"), "data": resultat, } - except HTTPException: raise except Exception as e: logger.error(f"Erreur dévalidation facture {numero_facture}: {e}") - raise HTTPException(500, str(e)) + raise HTTPException(status_code=500, detail=str(e)) @app.get("/factures/{numero_facture}/statut-validation", tags=["Factures"]) async def get_statut_validation_facture( numero_facture: str, - session: AsyncSession = Depends(get_session), + _: AsyncSession = Depends(get_session), ): - """ - Retourne le statut de validation d'une facture - """ try: resultat = sage_client.get_statut_validation(numero_facture) - return { "success": True, "data": resultat, } - except HTTPException: raise except Exception as e: logger.error(f"Erreur lecture statut {numero_facture}: {e}") - raise HTTPException(500, str(e)) + raise HTTPException(status_code=500, detail=str(e)) @app.get("/health", tags=["System"]) diff --git a/sage_client.py b/sage_client.py index 7effc22..ace8b7c 100644 --- a/sage_client.py +++ b/sage_client.py @@ -432,22 +432,16 @@ class SageGatewayClient: return self._get("/sage/societe/info").get("data") def valider_facture(self, numero_facture: str) -> dict: - """Valide une facture""" - return self._post(f"/sage/factures/{numero_facture}/valider", {}).get( - "data", {} - ) + response = self._post(f"/sage/factures/{numero_facture}/valider", {}) + return response.get("data", {}) def devalider_facture(self, numero_facture: str) -> dict: - """Dévalide une facture""" - return self._post(f"/sage/factures/{numero_facture}/devalider", {}).get( - "data", {} - ) + response = self._post(f"/sage/factures/{numero_facture}/devalider", {}) + return response.get("data", {}) def get_statut_validation(self, numero_facture: str) -> dict: - """Récupère le statut de validation""" - return self._get(f"/sage/factures/{numero_facture}/statut-validation").get( - "data", {} - ) + response = self._get(f"/sage/factures/{numero_facture}/statut-validation") + return response.get("data", {}) def refresh_cache(self) -> Dict: return self._post("/sage/cache/refresh") From eedc628a5fcbf8c38a14f5f99f18aeb235d82b91 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 15 Jan 2026 14:38:26 +0300 Subject: [PATCH 06/13] refactor(models): clean up model comments and whitespace --- database/models/universign.py | 35 ++--------------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/database/models/universign.py b/database/models/universign.py index 8b34ba3..fcb0ffa 100644 --- a/database/models/universign.py +++ b/database/models/universign.py @@ -50,7 +50,7 @@ class LocalDocumentStatus(str, Enum): class SageDocumentType(int, Enum): DEVIS = 0 - BON_COMMANDE = 10 + BON_COMMANDE = 10 PREPARATION = 20 BON_LIVRAISON = 30 BON_RETOUR = 40 @@ -61,8 +61,7 @@ class SageDocumentType(int, Enum): class UniversignTransaction(Base): __tablename__ = "universign_transactions" - # === IDENTIFIANTS === - id = Column(String(36), primary_key=True) # UUID local + id = Column(String(36), primary_key=True) transaction_id = Column( String(255), unique=True, @@ -71,7 +70,6 @@ class UniversignTransaction(Base): comment="ID Universign (ex: tr_abc123)", ) - # === LIEN AVEC LE DOCUMENT SAGE === sage_document_id = Column( String(50), nullable=False, @@ -82,7 +80,6 @@ class UniversignTransaction(Base): SQLEnum(SageDocumentType), nullable=False, comment="Type de document Sage" ) - # === STATUTS UNIVERSIGN (SOURCE DE VÉRITÉ) === universign_status = Column( SQLEnum(UniversignTransactionStatus), nullable=False, @@ -94,7 +91,6 @@ class UniversignTransaction(Base): DateTime, nullable=True, comment="Dernière MAJ du statut Universign" ) - # === STATUT LOCAL (DÉDUIT) === local_status = Column( SQLEnum(LocalDocumentStatus), nullable=False, @@ -103,7 +99,6 @@ class UniversignTransaction(Base): comment="Statut métier simplifié pour l'UI", ) - # === URLS ET MÉTADONNÉES UNIVERSIGN === signer_url = Column(Text, nullable=True, comment="URL de signature") document_url = Column(Text, nullable=True, comment="URL du document signé") @@ -125,17 +120,14 @@ class UniversignTransaction(Base): certificate_url = Column(Text, nullable=True, comment="URL du certificat") - # === SIGNATAIRES === signers_data = Column( Text, nullable=True, comment="JSON des signataires (snapshot)" ) - # === INFORMATIONS MÉTIER === requester_email = Column(String(255), nullable=True) requester_name = Column(String(255), nullable=True) document_name = Column(String(500), nullable=True) - # === DATES CLÉS === created_at = Column( DateTime, default=datetime.now, @@ -150,14 +142,12 @@ class UniversignTransaction(Base): expired_at = Column(DateTime, nullable=True) canceled_at = Column(DateTime, nullable=True) - # === SYNCHRONISATION === last_synced_at = Column( DateTime, nullable=True, comment="Dernière sync réussie avec Universign" ) sync_attempts = Column(Integer, default=0, comment="Nombre de tentatives de sync") sync_error = Column(Text, nullable=True) - # === FLAGS === is_test = Column( Boolean, default=False, comment="Transaction en environnement .alpha" ) @@ -166,7 +156,6 @@ class UniversignTransaction(Base): ) webhook_received = Column(Boolean, default=False, comment="Webhook Universign reçu") - # === RELATION === signers = relationship( "UniversignSigner", back_populates="transaction", cascade="all, delete-orphan" ) @@ -174,7 +163,6 @@ class UniversignTransaction(Base): "UniversignSyncLog", back_populates="transaction", cascade="all, delete-orphan" ) - # === INDEXES COMPOSITES === __table_args__ = ( Index("idx_sage_doc", "sage_document_id", "sage_document_type"), Index("idx_sync_status", "needs_sync", "universign_status"), @@ -190,10 +178,6 @@ class UniversignTransaction(Base): class UniversignSigner(Base): - """ - Détail de chaque signataire d'une transaction - """ - __tablename__ = "universign_signers" id = Column(String(36), primary_key=True) @@ -204,33 +188,27 @@ class UniversignSigner(Base): index=True, ) - # === DONNÉES SIGNATAIRE === email = Column(String(255), nullable=False, index=True) name = Column(String(255), nullable=True) phone = Column(String(50), nullable=True) - # === STATUT === status = Column( SQLEnum(UniversignSignerStatus), default=UniversignSignerStatus.WAITING, nullable=False, ) - # === ACTIONS === viewed_at = Column(DateTime, nullable=True) signed_at = Column(DateTime, nullable=True) refused_at = Column(DateTime, nullable=True) refusal_reason = Column(Text, nullable=True) - # === MÉTADONNÉES === ip_address = Column(String(45), nullable=True) user_agent = Column(Text, nullable=True) signature_method = Column(String(50), nullable=True) - # === ORDRE === order_index = Column(Integer, default=0) - # === RELATION === transaction = relationship("UniversignTransaction", back_populates="signers") def __repr__(self): @@ -238,10 +216,6 @@ class UniversignSigner(Base): class UniversignSyncLog(Base): - """ - Journal de toutes les synchronisations (audit trail) - """ - __tablename__ = "universign_sync_logs" id = Column(Integer, primary_key=True, autoincrement=True) @@ -252,22 +226,18 @@ class UniversignSyncLog(Base): index=True, ) - # === SYNC INFO === sync_type = Column(String(50), nullable=False, comment="webhook, polling, manual") sync_timestamp = Column(DateTime, default=datetime.now, nullable=False, index=True) - # === CHANGEMENTS DÉTECTÉS === previous_status = Column(String(50), nullable=True) new_status = Column(String(50), nullable=True) changes_detected = Column(Text, nullable=True, comment="JSON des changements") - # === RÉSULTAT === success = Column(Boolean, default=True) error_message = Column(Text, nullable=True) http_status_code = Column(Integer, nullable=True) response_time_ms = Column(Integer, nullable=True) - # === RELATION === transaction = relationship("UniversignTransaction", back_populates="sync_logs") def __repr__(self): @@ -287,7 +257,6 @@ class UniversignConfig(Base): api_url = Column(String(500), nullable=False) api_key = Column(String(500), nullable=False, comment="À chiffrer") - # === OPTIONS === webhook_url = Column(String(500), nullable=True) webhook_secret = Column(String(255), nullable=True) From 149d8fb2def3d5a00a4a5e29b40b1d706fb742c3 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 15 Jan 2026 14:38:42 +0300 Subject: [PATCH 07/13] feat(factures): add invoice validation endpoints and client methods --- api.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++ sage_client.py | 18 +++++++++++ 2 files changed, 99 insertions(+) diff --git a/api.py b/api.py index 7c3146e..73cab27 100644 --- a/api.py +++ b/api.py @@ -2965,6 +2965,87 @@ async def preview_societe(): return f"

Erreur

{str(e)}

" +@app.post("/factures/{numero_facture}/valider", status_code=200, tags=["Factures"]) +async def valider_facture( + numero_facture: str, + session: AsyncSession = Depends(get_session), +): + """ + Valide une facture (pose le cadenas) + """ + try: + resultat = sage_client.valider_facture(numero_facture) + + logger.info( + f"Facture {numero_facture} validée: {resultat.get('action_effectuee')}" + ) + + return { + "success": True, + "message": resultat.get("message", "Facture validée"), + "data": resultat, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur validation facture {numero_facture}: {e}") + raise HTTPException(500, str(e)) + + +@app.post("/factures/{numero_facture}/devalider", status_code=200, tags=["Factures"]) +async def devalider_facture( + numero_facture: str, + session: AsyncSession = Depends(get_session), +): + """ + Dévalide une facture (retire le cadenas) + + Impossible si la facture a des règlements. + """ + try: + resultat = sage_client.devalider_facture(numero_facture) + + logger.info( + f"Facture {numero_facture} dévalidée: {resultat.get('action_effectuee')}" + ) + + return { + "success": True, + "message": resultat.get("message", "Facture dévalidée"), + "data": resultat, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur dévalidation facture {numero_facture}: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/factures/{numero_facture}/statut-validation", tags=["Factures"]) +async def get_statut_validation_facture( + numero_facture: str, + session: AsyncSession = Depends(get_session), +): + """ + Retourne le statut de validation d'une facture + """ + try: + resultat = sage_client.get_statut_validation(numero_facture) + + return { + "success": True, + "data": resultat, + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture statut {numero_facture}: {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 f938ab5..7effc22 100644 --- a/sage_client.py +++ b/sage_client.py @@ -431,6 +431,24 @@ class SageGatewayClient: """Lit les informations de la société depuis P_DOSSIER""" return self._get("/sage/societe/info").get("data") + def valider_facture(self, numero_facture: str) -> dict: + """Valide une facture""" + return self._post(f"/sage/factures/{numero_facture}/valider", {}).get( + "data", {} + ) + + def devalider_facture(self, numero_facture: str) -> dict: + """Dévalide une facture""" + return self._post(f"/sage/factures/{numero_facture}/devalider", {}).get( + "data", {} + ) + + def get_statut_validation(self, numero_facture: str) -> dict: + """Récupère le statut de validation""" + return self._get(f"/sage/factures/{numero_facture}/statut-validation").get( + "data", {} + ) + def refresh_cache(self) -> Dict: return self._post("/sage/cache/refresh") From 2f06a083dcb5c44f424dc2e5179ecf903a6d5828 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 15 Jan 2026 14:38:42 +0300 Subject: [PATCH 08/13] refactor: simplify invoice validation methods and error handling --- api.py | 31 ++++++------------------------- sage_client.py | 18 ++++++------------ 2 files changed, 12 insertions(+), 37 deletions(-) diff --git a/api.py b/api.py index 73cab27..fbd8e0b 100644 --- a/api.py +++ b/api.py @@ -2968,82 +2968,63 @@ async def preview_societe(): @app.post("/factures/{numero_facture}/valider", status_code=200, tags=["Factures"]) async def valider_facture( numero_facture: str, - session: AsyncSession = Depends(get_session), + _: AsyncSession = Depends(get_session), ): - """ - Valide une facture (pose le cadenas) - """ try: resultat = sage_client.valider_facture(numero_facture) - logger.info( f"Facture {numero_facture} validée: {resultat.get('action_effectuee')}" ) - return { "success": True, "message": resultat.get("message", "Facture validée"), "data": resultat, } - except HTTPException: raise except Exception as e: logger.error(f"Erreur validation facture {numero_facture}: {e}") - raise HTTPException(500, str(e)) + raise HTTPException(status_code=500, detail=str(e)) @app.post("/factures/{numero_facture}/devalider", status_code=200, tags=["Factures"]) async def devalider_facture( numero_facture: str, - session: AsyncSession = Depends(get_session), + _: AsyncSession = Depends(get_session), ): - """ - Dévalide une facture (retire le cadenas) - - Impossible si la facture a des règlements. - """ try: resultat = sage_client.devalider_facture(numero_facture) - logger.info( f"Facture {numero_facture} dévalidée: {resultat.get('action_effectuee')}" ) - return { "success": True, "message": resultat.get("message", "Facture dévalidée"), "data": resultat, } - except HTTPException: raise except Exception as e: logger.error(f"Erreur dévalidation facture {numero_facture}: {e}") - raise HTTPException(500, str(e)) + raise HTTPException(status_code=500, detail=str(e)) @app.get("/factures/{numero_facture}/statut-validation", tags=["Factures"]) async def get_statut_validation_facture( numero_facture: str, - session: AsyncSession = Depends(get_session), + _: AsyncSession = Depends(get_session), ): - """ - Retourne le statut de validation d'une facture - """ try: resultat = sage_client.get_statut_validation(numero_facture) - return { "success": True, "data": resultat, } - except HTTPException: raise except Exception as e: logger.error(f"Erreur lecture statut {numero_facture}: {e}") - raise HTTPException(500, str(e)) + raise HTTPException(status_code=500, detail=str(e)) @app.get("/health", tags=["System"]) diff --git a/sage_client.py b/sage_client.py index 7effc22..ace8b7c 100644 --- a/sage_client.py +++ b/sage_client.py @@ -432,22 +432,16 @@ class SageGatewayClient: return self._get("/sage/societe/info").get("data") def valider_facture(self, numero_facture: str) -> dict: - """Valide une facture""" - return self._post(f"/sage/factures/{numero_facture}/valider", {}).get( - "data", {} - ) + response = self._post(f"/sage/factures/{numero_facture}/valider", {}) + return response.get("data", {}) def devalider_facture(self, numero_facture: str) -> dict: - """Dévalide une facture""" - return self._post(f"/sage/factures/{numero_facture}/devalider", {}).get( - "data", {} - ) + response = self._post(f"/sage/factures/{numero_facture}/devalider", {}) + return response.get("data", {}) def get_statut_validation(self, numero_facture: str) -> dict: - """Récupère le statut de validation""" - return self._get(f"/sage/factures/{numero_facture}/statut-validation").get( - "data", {} - ) + response = self._get(f"/sage/factures/{numero_facture}/statut-validation") + return response.get("data", {}) def refresh_cache(self) -> Dict: return self._post("/sage/cache/refresh") From 25be0bd5696bcd719733fc667a406fa5aa4fb312 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 15 Jan 2026 14:44:04 +0300 Subject: [PATCH 09/13] 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"], + } + } From cc1609549f4b6f13c7af6e9ecdfff088fc1fece1 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 15 Jan 2026 14:44:04 +0300 Subject: [PATCH 10/13] feat(api): add endpoint to fetch bank journals --- api.py | 10 ++++++++++ sage_client.py | 3 +++ 2 files changed, 13 insertions(+) diff --git a/api.py b/api.py index 26c9687..61bfbe2 100644 --- a/api.py +++ b/api.py @@ -3106,6 +3106,16 @@ async def get_modes_reglement(): } +@app.get("/journaux/banque", tags=["Règlements"]) +async def get_journaux_banque(): + try: + resultat = sage_client.get_journaux_banque() + return {"success": True, "data": resultat} + except Exception as e: + logger.error(f"Erreur lecture journaux: {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 a91e66e..1907077 100644 --- a/sage_client.py +++ b/sage_client.py @@ -505,6 +505,9 @@ class SageGatewayClient: "data", {} ) + def get_journaux_banque(self) -> dict: + return self._get("/sage/journaux/banque").get("data", {}) + def refresh_cache(self) -> Dict: return self._post("/sage/cache/refresh") From beabefa3f9cadfad4e89cd84f4be9f5112fb4ca1 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 15 Jan 2026 15:49:15 +0300 Subject: [PATCH 11/13] 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", } } From 6f2136c3ca17da1c9f0e74de6114f05a3494cd8b Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 15 Jan 2026 16:56:25 +0300 Subject: [PATCH 12/13] feat(sage_client): add new payment parameters and make fields optional --- sage_client.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/sage_client.py b/sage_client.py index 0a37807..6e61085 100644 --- a/sage_client.py +++ b/sage_client.py @@ -447,11 +447,15 @@ class SageGatewayClient: self, numero_facture: str, montant: float, - mode_reglement: int = 2, + mode_reglement: int = 0, date_reglement: str = None, reference: str = "", libelle: str = "", - code_journal: str = "BEU", + code_journal: str = None, + devise_code: int = 0, + cours_devise: float = 1.0, + tva_encaissement: bool = False, + compte_general: str = None, ) -> dict: """Règle une facture""" payload = { @@ -459,10 +463,18 @@ class SageGatewayClient: "mode_reglement": mode_reglement, "reference": reference, "libelle": libelle, - "code_journal": code_journal, + "devise_code": devise_code, + "cours_devise": cours_devise, + "tva_encaissement": tva_encaissement, } + + # Champs optionnels if date_reglement: payload["date_reglement"] = date_reglement + if code_journal: + payload["code_journal"] = code_journal + if compte_general: + payload["compte_general"] = compte_general return self._post(f"/sage/factures/{numero_facture}/regler", payload).get( "data", {} @@ -472,12 +484,15 @@ class SageGatewayClient: self, client_code: str, montant_total: float, - mode_reglement: int = 2, + mode_reglement: int = 0, date_reglement: str = None, reference: str = "", libelle: str = "", - code_journal: str = "BEU", + code_journal: str = None, numeros_factures: list = None, + devise_code: int = 0, + cours_devise: float = 1.0, + tva_encaissement: bool = False, ) -> dict: """Règle plusieurs factures d'un client""" payload = { @@ -486,10 +501,15 @@ class SageGatewayClient: "mode_reglement": mode_reglement, "reference": reference, "libelle": libelle, - "code_journal": code_journal, + "devise_code": devise_code, + "cours_devise": cours_devise, + "tva_encaissement": tva_encaissement, } + if date_reglement: payload["date_reglement"] = date_reglement + if code_journal: + payload["code_journal"] = code_journal if numeros_factures: payload["numeros_factures"] = numeros_factures From b291cbf65f30693a1d893d58a29aae506bef833c Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 15 Jan 2026 17:13:31 +0300 Subject: [PATCH 13/13] feat(reglements): add compte_general field to ReglementFactureCreate --- schemas/documents/reglements.py | 1 + 1 file changed, 1 insertion(+) diff --git a/schemas/documents/reglements.py b/schemas/documents/reglements.py index 2a7908c..3a6248e 100644 --- a/schemas/documents/reglements.py +++ b/schemas/documents/reglements.py @@ -41,6 +41,7 @@ class ReglementFactureCreate(BaseModel): tva_encaissement: Optional[bool] = Field( False, description="Appliquer TVA sur encaissement" ) + compte_general: Optional[str] = Field(None) @field_validator("montant") def validate_montant(cls, v):