Merge branch 'develop'

This commit is contained in:
Fanilo-Nantenaina 2026-01-16 12:35:17 +03:00
commit fdf359738b
6 changed files with 642 additions and 41 deletions

368
api.py
View file

@ -1,6 +1,6 @@
from fastapi import FastAPI, HTTPException, Path, Query, Depends, status, Body from fastapi import FastAPI, HTTPException, Path, Query, Depends, status, Body
from fastapi.middleware.cors import CORSMiddleware 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 fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Field, EmailStr from pydantic import BaseModel, Field, EmailStr
from typing import List, Optional from typing import List, Optional
@ -71,6 +71,7 @@ from schemas import (
ContactCreate, ContactCreate,
ContactUpdate, ContactUpdate,
) )
from schemas.documents.reglements import ReglementFactureCreate, ReglementMultipleCreate
from schemas.tiers.commercial import ( from schemas.tiers.commercial import (
CollaborateurCreate, CollaborateurCreate,
CollaborateurDetails, CollaborateurDetails,
@ -2874,6 +2875,371 @@ async def obtenir_informations_societe():
raise HTTPException(500, str(e)) 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 "<h1>Société introuvable</h1>"
logo_html = ""
if societe.get("logo_base64"):
logo_html = f'<img src="data:{societe["logo_content_type"]};base64,{societe["logo_base64"]}" style="max-width: 300px; border: 1px solid #ccc; padding: 10px;">'
else:
logo_html = "<p>Aucun logo disponible</p>"
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Informations Société</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 40px; }}
.container {{ max-width: 800px; }}
.logo {{ margin: 20px 0; }}
.info {{ margin: 10px 0; }}
.label {{ font-weight: bold; }}
</style>
</head>
<body>
<div class="container">
<h1>Informations Société</h1>
<div class="logo">
<h2>Logo</h2>
{logo_html}
</div>
<div class="info">
<span class="label">Raison sociale:</span> {societe["raison_sociale"]}
</div>
<div class="info">
<span class="label">SIRET:</span> {societe["siret"] or "N/A"}
</div>
<div class="info">
<span class="label">Adresse:</span> {societe["adresse"] or "N/A"}
</div>
<div class="info">
<span class="label">Code postal:</span> {societe["code_postal"] or "N/A"}
</div>
<div class="info">
<span class="label">Ville:</span> {societe["ville"] or "N/A"}
</div>
<div class="info">
<span class="label">Email:</span> {societe["email"] or "N/A"}
</div>
<div class="info">
<span class="label">Téléphone:</span> {societe["telephone"] or "N/A"}
</div>
</div>
</body>
</html>
"""
return html
except Exception as e:
return f"<h1>Erreur</h1><p>{str(e)}</p>"
@app.post("/factures/{numero_facture}/valider", status_code=200, tags=["Factures"])
async def valider_facture(
numero_facture: str,
_: AsyncSession = Depends(get_session),
):
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(status_code=500, detail=str(e))
@app.post("/factures/{numero_facture}/devalider", status_code=200, tags=["Factures"])
async def devalider_facture(
numero_facture: str,
_: AsyncSession = Depends(get_session),
):
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(status_code=500, detail=str(e))
@app.get("/factures/{numero_facture}/statut-validation", tags=["Factures"])
async def get_statut_validation_facture(
numero_facture: str,
_: AsyncSession = Depends(get_session),
):
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(status_code=500, detail=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=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 "",
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"Journal: {reglement.code_journal} - Mode: {reglement.mode_reglement}"
)
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("/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("/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"]) @app.get("/health", tags=["System"])
async def health_check( async def health_check(
sage: SageGatewayClient = Depends(get_sage_client_for_user), sage: SageGatewayClient = Depends(get_sage_client_for_user),

View file

@ -50,7 +50,7 @@ class LocalDocumentStatus(str, Enum):
class SageDocumentType(int, Enum): class SageDocumentType(int, Enum):
DEVIS = 0 DEVIS = 0
BON_COMMANDE = 10 BON_COMMANDE = 10
PREPARATION = 20 PREPARATION = 20
BON_LIVRAISON = 30 BON_LIVRAISON = 30
BON_RETOUR = 40 BON_RETOUR = 40
@ -61,8 +61,7 @@ class SageDocumentType(int, Enum):
class UniversignTransaction(Base): class UniversignTransaction(Base):
__tablename__ = "universign_transactions" __tablename__ = "universign_transactions"
# === IDENTIFIANTS === id = Column(String(36), primary_key=True)
id = Column(String(36), primary_key=True) # UUID local
transaction_id = Column( transaction_id = Column(
String(255), String(255),
unique=True, unique=True,
@ -71,7 +70,6 @@ class UniversignTransaction(Base):
comment="ID Universign (ex: tr_abc123)", comment="ID Universign (ex: tr_abc123)",
) )
# === LIEN AVEC LE DOCUMENT SAGE ===
sage_document_id = Column( sage_document_id = Column(
String(50), String(50),
nullable=False, nullable=False,
@ -82,7 +80,6 @@ class UniversignTransaction(Base):
SQLEnum(SageDocumentType), nullable=False, comment="Type de document Sage" SQLEnum(SageDocumentType), nullable=False, comment="Type de document Sage"
) )
# === STATUTS UNIVERSIGN (SOURCE DE VÉRITÉ) ===
universign_status = Column( universign_status = Column(
SQLEnum(UniversignTransactionStatus), SQLEnum(UniversignTransactionStatus),
nullable=False, nullable=False,
@ -94,7 +91,6 @@ class UniversignTransaction(Base):
DateTime, nullable=True, comment="Dernière MAJ du statut Universign" DateTime, nullable=True, comment="Dernière MAJ du statut Universign"
) )
# === STATUT LOCAL (DÉDUIT) ===
local_status = Column( local_status = Column(
SQLEnum(LocalDocumentStatus), SQLEnum(LocalDocumentStatus),
nullable=False, nullable=False,
@ -103,7 +99,6 @@ class UniversignTransaction(Base):
comment="Statut métier simplifié pour l'UI", comment="Statut métier simplifié pour l'UI",
) )
# === URLS ET MÉTADONNÉES UNIVERSIGN ===
signer_url = Column(Text, nullable=True, comment="URL de signature") signer_url = Column(Text, nullable=True, comment="URL de signature")
document_url = Column(Text, nullable=True, comment="URL du document signé") 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") certificate_url = Column(Text, nullable=True, comment="URL du certificat")
# === SIGNATAIRES ===
signers_data = Column( signers_data = Column(
Text, nullable=True, comment="JSON des signataires (snapshot)" Text, nullable=True, comment="JSON des signataires (snapshot)"
) )
# === INFORMATIONS MÉTIER ===
requester_email = Column(String(255), nullable=True) requester_email = Column(String(255), nullable=True)
requester_name = Column(String(255), nullable=True) requester_name = Column(String(255), nullable=True)
document_name = Column(String(500), nullable=True) document_name = Column(String(500), nullable=True)
# === DATES CLÉS ===
created_at = Column( created_at = Column(
DateTime, DateTime,
default=datetime.now, default=datetime.now,
@ -150,14 +142,12 @@ class UniversignTransaction(Base):
expired_at = Column(DateTime, nullable=True) expired_at = Column(DateTime, nullable=True)
canceled_at = Column(DateTime, nullable=True) canceled_at = Column(DateTime, nullable=True)
# === SYNCHRONISATION ===
last_synced_at = Column( last_synced_at = Column(
DateTime, nullable=True, comment="Dernière sync réussie avec Universign" DateTime, nullable=True, comment="Dernière sync réussie avec Universign"
) )
sync_attempts = Column(Integer, default=0, comment="Nombre de tentatives de sync") sync_attempts = Column(Integer, default=0, comment="Nombre de tentatives de sync")
sync_error = Column(Text, nullable=True) sync_error = Column(Text, nullable=True)
# === FLAGS ===
is_test = Column( is_test = Column(
Boolean, default=False, comment="Transaction en environnement .alpha" 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") webhook_received = Column(Boolean, default=False, comment="Webhook Universign reçu")
# === RELATION ===
signers = relationship( signers = relationship(
"UniversignSigner", back_populates="transaction", cascade="all, delete-orphan" "UniversignSigner", back_populates="transaction", cascade="all, delete-orphan"
) )
@ -174,7 +163,6 @@ class UniversignTransaction(Base):
"UniversignSyncLog", back_populates="transaction", cascade="all, delete-orphan" "UniversignSyncLog", back_populates="transaction", cascade="all, delete-orphan"
) )
# === INDEXES COMPOSITES ===
__table_args__ = ( __table_args__ = (
Index("idx_sage_doc", "sage_document_id", "sage_document_type"), Index("idx_sage_doc", "sage_document_id", "sage_document_type"),
Index("idx_sync_status", "needs_sync", "universign_status"), Index("idx_sync_status", "needs_sync", "universign_status"),
@ -190,10 +178,6 @@ class UniversignTransaction(Base):
class UniversignSigner(Base): class UniversignSigner(Base):
"""
Détail de chaque signataire d'une transaction
"""
__tablename__ = "universign_signers" __tablename__ = "universign_signers"
id = Column(String(36), primary_key=True) id = Column(String(36), primary_key=True)
@ -204,33 +188,27 @@ class UniversignSigner(Base):
index=True, index=True,
) )
# === DONNÉES SIGNATAIRE ===
email = Column(String(255), nullable=False, index=True) email = Column(String(255), nullable=False, index=True)
name = Column(String(255), nullable=True) name = Column(String(255), nullable=True)
phone = Column(String(50), nullable=True) phone = Column(String(50), nullable=True)
# === STATUT ===
status = Column( status = Column(
SQLEnum(UniversignSignerStatus), SQLEnum(UniversignSignerStatus),
default=UniversignSignerStatus.WAITING, default=UniversignSignerStatus.WAITING,
nullable=False, nullable=False,
) )
# === ACTIONS ===
viewed_at = Column(DateTime, nullable=True) viewed_at = Column(DateTime, nullable=True)
signed_at = Column(DateTime, nullable=True) signed_at = Column(DateTime, nullable=True)
refused_at = Column(DateTime, nullable=True) refused_at = Column(DateTime, nullable=True)
refusal_reason = Column(Text, nullable=True) refusal_reason = Column(Text, nullable=True)
# === MÉTADONNÉES ===
ip_address = Column(String(45), nullable=True) ip_address = Column(String(45), nullable=True)
user_agent = Column(Text, nullable=True) user_agent = Column(Text, nullable=True)
signature_method = Column(String(50), nullable=True) signature_method = Column(String(50), nullable=True)
# === ORDRE ===
order_index = Column(Integer, default=0) order_index = Column(Integer, default=0)
# === RELATION ===
transaction = relationship("UniversignTransaction", back_populates="signers") transaction = relationship("UniversignTransaction", back_populates="signers")
def __repr__(self): def __repr__(self):
@ -238,10 +216,6 @@ class UniversignSigner(Base):
class UniversignSyncLog(Base): class UniversignSyncLog(Base):
"""
Journal de toutes les synchronisations (audit trail)
"""
__tablename__ = "universign_sync_logs" __tablename__ = "universign_sync_logs"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
@ -252,22 +226,18 @@ class UniversignSyncLog(Base):
index=True, index=True,
) )
# === SYNC INFO ===
sync_type = Column(String(50), nullable=False, comment="webhook, polling, manual") sync_type = Column(String(50), nullable=False, comment="webhook, polling, manual")
sync_timestamp = Column(DateTime, default=datetime.now, nullable=False, index=True) sync_timestamp = Column(DateTime, default=datetime.now, nullable=False, index=True)
# === CHANGEMENTS DÉTECTÉS ===
previous_status = Column(String(50), nullable=True) previous_status = Column(String(50), nullable=True)
new_status = Column(String(50), nullable=True) new_status = Column(String(50), nullable=True)
changes_detected = Column(Text, nullable=True, comment="JSON des changements") changes_detected = Column(Text, nullable=True, comment="JSON des changements")
# === RÉSULTAT ===
success = Column(Boolean, default=True) success = Column(Boolean, default=True)
error_message = Column(Text, nullable=True) error_message = Column(Text, nullable=True)
http_status_code = Column(Integer, nullable=True) http_status_code = Column(Integer, nullable=True)
response_time_ms = Column(Integer, nullable=True) response_time_ms = Column(Integer, nullable=True)
# === RELATION ===
transaction = relationship("UniversignTransaction", back_populates="sync_logs") transaction = relationship("UniversignTransaction", back_populates="sync_logs")
def __repr__(self): def __repr__(self):
@ -287,7 +257,6 @@ class UniversignConfig(Base):
api_url = Column(String(500), nullable=False) api_url = Column(String(500), nullable=False)
api_key = Column(String(500), nullable=False, comment="À chiffrer") api_key = Column(String(500), nullable=False, comment="À chiffrer")
# === OPTIONS ===
webhook_url = Column(String(500), nullable=True) webhook_url = Column(String(500), nullable=True)
webhook_secret = Column(String(255), nullable=True) webhook_secret = Column(String(255), nullable=True)

View file

@ -431,6 +431,159 @@ class SageGatewayClient:
"""Lit les informations de la société depuis P_DOSSIER""" """Lit les informations de la société depuis P_DOSSIER"""
return self._get("/sage/societe/info").get("data") return self._get("/sage/societe/info").get("data")
def valider_facture(self, numero_facture: str) -> dict:
response = self._post(f"/sage/factures/{numero_facture}/valider", {})
return response.get("data", {})
def devalider_facture(self, numero_facture: str) -> dict:
response = self._post(f"/sage/factures/{numero_facture}/devalider", {})
return response.get("data", {})
def get_statut_validation(self, numero_facture: str) -> dict:
response = self._get(f"/sage/factures/{numero_facture}/statut-validation")
return response.get("data", {})
def regler_facture(
self,
numero_facture: str,
montant: float,
mode_reglement: int = 0,
date_reglement: str = None,
reference: str = "",
libelle: str = "",
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 = {
"montant": montant,
"mode_reglement": mode_reglement,
"reference": reference,
"libelle": libelle,
"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", {}
)
def regler_factures_client(
self,
client_code: str,
montant_total: float,
mode_reglement: int = 0,
date_reglement: str = None,
reference: str = "",
libelle: str = "",
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 = {
"client_code": client_code,
"montant_total": montant_total,
"mode_reglement": mode_reglement,
"reference": reference,
"libelle": libelle,
"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
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 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: def refresh_cache(self) -> Dict:
return self._post("/sage/cache/refresh") return self._post("/sage/cache/refresh")

View file

@ -4,6 +4,7 @@ from datetime import datetime
from schemas.documents.ligne_document import LigneDocument from schemas.documents.ligne_document import LigneDocument
class FactureCreate(BaseModel): class FactureCreate(BaseModel):
client_id: str client_id: str
date_facture: Optional[datetime] = None date_facture: Optional[datetime] = None

View file

@ -0,0 +1,116 @@
from pydantic import BaseModel, Field, field_validator
from typing import List, Optional
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 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"
)
compte_general: Optional[str] = Field(None)
@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",
"date_reglement": "2024-01-01",
"libelle": "Règlement multiple",
"tva_encaissement": False,
"devise_code": 0,
"cours_devise": 1.0,
"date_echeance": "2024-01-31",
}
}
class ReglementMultipleCreate(BaseModel):
"""Requête de règlement multiple côté VPS"""
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):
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"],
"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",
}
}

View file

@ -9,14 +9,12 @@ class ExerciceComptable(BaseModel):
class SocieteInfo(BaseModel): class SocieteInfo(BaseModel):
# Identification
raison_sociale: str raison_sociale: str
numero_dossier: str numero_dossier: str
siret: Optional[str] = None siret: Optional[str] = None
code_ape: Optional[str] = None code_ape: Optional[str] = None
numero_tva: Optional[str] = None numero_tva: Optional[str] = None
# Adresse
adresse: Optional[str] = None adresse: Optional[str] = None
complement_adresse: Optional[str] = None complement_adresse: Optional[str] = None
code_postal: Optional[str] = None code_postal: Optional[str] = None
@ -24,27 +22,25 @@ class SocieteInfo(BaseModel):
code_region: Optional[str] = None code_region: Optional[str] = None
pays: Optional[str] = None pays: Optional[str] = None
# Contacts
telephone: Optional[str] = None telephone: Optional[str] = None
telecopie: Optional[str] = None telecopie: Optional[str] = None
email: Optional[str] = None email: Optional[str] = None
email_societe: Optional[str] = None email_societe: Optional[str] = None
site_web: Optional[str] = None site_web: Optional[str] = None
# Informations juridiques
capital: float = 0.0 capital: float = 0.0
forme_juridique: Optional[str] = None forme_juridique: Optional[str] = None
# Exercices comptables
exercices: List[ExerciceComptable] = [] exercices: List[ExerciceComptable] = []
# Configuration
devise_compte: int = 0 devise_compte: int = 0
devise_equivalent: int = 0 devise_equivalent: int = 0
longueur_compte_general: int = 0 longueur_compte_general: int = 0
longueur_compte_analytique: int = 0 longueur_compte_analytique: int = 0
regime_fec: int = 0 regime_fec: int = 0
# Autres
base_modele: Optional[str] = None base_modele: Optional[str] = None
marqueur: int = 0 marqueur: int = 0
logo_base64: Optional[str] = None
logo_content_type: Optional[str] = None