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.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
@ -71,6 +71,7 @@ from schemas import (
ContactCreate,
ContactUpdate,
)
from schemas.documents.reglements import ReglementFactureCreate, ReglementMultipleCreate
from schemas.tiers.commercial import (
CollaborateurCreate,
CollaborateurDetails,
@ -2874,6 +2875,371 @@ 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 "<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"])
async def health_check(
sage: SageGatewayClient = Depends(get_sage_client_for_user),

View file

@ -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)

View file

@ -431,6 +431,159 @@ 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:
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:
return self._post("/sage/cache/refresh")

View file

@ -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

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):
# 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