Merge branch 'develop'
This commit is contained in:
commit
fdf359738b
6 changed files with 642 additions and 41 deletions
368
api.py
368
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
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
153
sage_client.py
153
sage_client.py
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
116
schemas/documents/reglements.py
Normal file
116
schemas/documents/reglements.py
Normal 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",
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue