feat: Add /devis endpoint for listing devis and apply minor formatting adjustments.

This commit is contained in:
Fanilo-Nantenaina 2025-11-27 12:40:23 +03:00
parent 307105b8ad
commit 643250850b

447
api.py
View file

@ -17,11 +17,8 @@ from sqlalchemy import select
# Configuration logging # Configuration logging
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[ handlers=[logging.FileHandler("sage_api.log"), logging.StreamHandler()],
logging.FileHandler("sage_api.log"),
logging.StreamHandler()
]
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -35,11 +32,12 @@ from database import (
StatutEmail as StatutEmailEnum, StatutEmail as StatutEmailEnum,
WorkflowLog, WorkflowLog,
SignatureLog, SignatureLog,
StatutSignature as StatutSignatureEnum StatutSignature as StatutSignatureEnum,
) )
from email_queue import email_queue from email_queue import email_queue
from sage_client import sage_client from sage_client import sage_client
# ===================================================== # =====================================================
# ENUMS # ENUMS
# ===================================================== # =====================================================
@ -51,6 +49,7 @@ class TypeDocument(int, Enum):
PREPARATION = 4 PREPARATION = 4
FACTURE = 5 FACTURE = 5
class StatutSignature(str, Enum): class StatutSignature(str, Enum):
EN_ATTENTE = "EN_ATTENTE" EN_ATTENTE = "EN_ATTENTE"
ENVOYE = "ENVOYE" ENVOYE = "ENVOYE"
@ -58,6 +57,7 @@ class StatutSignature(str, Enum):
REFUSE = "REFUSE" REFUSE = "REFUSE"
EXPIRE = "EXPIRE" EXPIRE = "EXPIRE"
class StatutEmail(str, Enum): class StatutEmail(str, Enum):
EN_ATTENTE = "EN_ATTENTE" EN_ATTENTE = "EN_ATTENTE"
EN_COURS = "EN_COURS" EN_COURS = "EN_COURS"
@ -66,6 +66,7 @@ class StatutEmail(str, Enum):
ERREUR = "ERREUR" ERREUR = "ERREUR"
BOUNCE = "BOUNCE" BOUNCE = "BOUNCE"
# ===================================================== # =====================================================
# MODÈLES PYDANTIC # MODÈLES PYDANTIC
# ===================================================== # =====================================================
@ -78,23 +79,27 @@ class ClientResponse(BaseModel):
email: Optional[str] = None email: Optional[str] = None
telephone: Optional[str] = None telephone: Optional[str] = None
class ArticleResponse(BaseModel): class ArticleResponse(BaseModel):
reference: str reference: str
designation: str designation: str
prix_vente: float prix_vente: float
stock_reel: float stock_reel: float
class LigneDevis(BaseModel): class LigneDevis(BaseModel):
article_code: str article_code: str
quantite: float quantite: float
prix_unitaire_ht: Optional[float] = None prix_unitaire_ht: Optional[float] = None
remise_pourcentage: Optional[float] = 0.0 remise_pourcentage: Optional[float] = 0.0
class DevisRequest(BaseModel): class DevisRequest(BaseModel):
client_id: str client_id: str
date_devis: Optional[date] = None date_devis: Optional[date] = None
lignes: List[LigneDevis] lignes: List[LigneDevis]
class DevisResponse(BaseModel): class DevisResponse(BaseModel):
id: str id: str
client_id: str client_id: str
@ -103,12 +108,14 @@ class DevisResponse(BaseModel):
montant_total_ttc: float montant_total_ttc: float
nb_lignes: int nb_lignes: int
class SignatureRequest(BaseModel): class SignatureRequest(BaseModel):
doc_id: str doc_id: str
type_doc: TypeDocument type_doc: TypeDocument
email_signataire: EmailStr email_signataire: EmailStr
nom_signataire: str nom_signataire: str
class EmailEnvoiRequest(BaseModel): class EmailEnvoiRequest(BaseModel):
destinataire: EmailStr destinataire: EmailStr
cc: Optional[List[EmailStr]] = [] cc: Optional[List[EmailStr]] = []
@ -118,101 +125,108 @@ class EmailEnvoiRequest(BaseModel):
document_ids: Optional[List[str]] = None document_ids: Optional[List[str]] = None
type_document: Optional[TypeDocument] = None type_document: Optional[TypeDocument] = None
class RelanceDevisRequest(BaseModel): class RelanceDevisRequest(BaseModel):
doc_id: str doc_id: str
message_personnalise: Optional[str] = None message_personnalise: Optional[str] = None
# ===================================================== # =====================================================
# SERVICES EXTERNES (Universign) # SERVICES EXTERNES (Universign)
# ===================================================== # =====================================================
async def universign_envoyer(doc_id: str, pdf_bytes: bytes, email: str, nom: str) -> Dict: async def universign_envoyer(
doc_id: str, pdf_bytes: bytes, email: str, nom: str
) -> Dict:
"""Envoi signature via API Universign""" """Envoi signature via API Universign"""
import requests import requests
try: try:
api_key = settings.universign_api_key api_key = settings.universign_api_key
api_url = settings.universign_api_url api_url = settings.universign_api_url
auth = (api_key, "") auth = (api_key, "")
# Étape 1: Créer transaction # Étape 1: Créer transaction
response = requests.post( response = requests.post(
f"{api_url}/transactions", f"{api_url}/transactions",
auth=auth, auth=auth,
json={"name": f"Devis {doc_id}", "language": "fr"}, json={"name": f"Devis {doc_id}", "language": "fr"},
timeout=30 timeout=30,
) )
response.raise_for_status() response.raise_for_status()
transaction_id = response.json().get("id") transaction_id = response.json().get("id")
# Étape 2: Upload PDF # Étape 2: Upload PDF
files = {'file': (f'Devis_{doc_id}.pdf', pdf_bytes, 'application/pdf')} files = {"file": (f"Devis_{doc_id}.pdf", pdf_bytes, "application/pdf")}
response = requests.post(f"{api_url}/files", auth=auth, files=files, timeout=30) response = requests.post(f"{api_url}/files", auth=auth, files=files, timeout=30)
response.raise_for_status() response.raise_for_status()
file_id = response.json().get("id") file_id = response.json().get("id")
# Étape 3: Ajouter document # Étape 3: Ajouter document
response = requests.post( response = requests.post(
f"{api_url}/transactions/{transaction_id}/documents", f"{api_url}/transactions/{transaction_id}/documents",
auth=auth, auth=auth,
data={"document": file_id}, data={"document": file_id},
timeout=30 timeout=30,
) )
response.raise_for_status() response.raise_for_status()
document_id = response.json().get("id") document_id = response.json().get("id")
# Étape 4: Créer champ signature # Étape 4: Créer champ signature
response = requests.post( response = requests.post(
f"{api_url}/transactions/{transaction_id}/documents/{document_id}/fields", f"{api_url}/transactions/{transaction_id}/documents/{document_id}/fields",
auth=auth, auth=auth,
data={"type": "signature"}, data={"type": "signature"},
timeout=30 timeout=30,
) )
response.raise_for_status() response.raise_for_status()
field_id = response.json().get("id") field_id = response.json().get("id")
# Étape 5: Assigner signataire # Étape 5: Assigner signataire
response = requests.post( response = requests.post(
f"{api_url}/transactions/{transaction_id}/signatures", f"{api_url}/transactions/{transaction_id}/signatures",
auth=auth, auth=auth,
data={"signer": email, "field": field_id}, data={"signer": email, "field": field_id},
timeout=30 timeout=30,
) )
response.raise_for_status() response.raise_for_status()
# Étape 6: Démarrer transaction # Étape 6: Démarrer transaction
response = requests.post( response = requests.post(
f"{api_url}/transactions/{transaction_id}/start", f"{api_url}/transactions/{transaction_id}/start", auth=auth, timeout=30
auth=auth,
timeout=30
) )
response.raise_for_status() response.raise_for_status()
final_data = response.json() final_data = response.json()
signer_url = final_data.get("actions", [{}])[0].get("url", "") if final_data.get("actions") else "" signer_url = (
final_data.get("actions", [{}])[0].get("url", "")
if final_data.get("actions")
else ""
)
logger.info(f"✅ Signature Universign envoyée: {transaction_id}") logger.info(f"✅ Signature Universign envoyée: {transaction_id}")
return { return {
"transaction_id": transaction_id, "transaction_id": transaction_id,
"signer_url": signer_url, "signer_url": signer_url,
"statut": "ENVOYE" "statut": "ENVOYE",
} }
except Exception as e: except Exception as e:
logger.error(f"❌ Erreur Universign: {e}") logger.error(f"❌ Erreur Universign: {e}")
return {"error": str(e), "statut": "ERREUR"} return {"error": str(e), "statut": "ERREUR"}
async def universign_statut(transaction_id: str) -> Dict: async def universign_statut(transaction_id: str) -> Dict:
"""Récupération statut signature""" """Récupération statut signature"""
import requests import requests
try: try:
response = requests.get( response = requests.get(
f"{settings.universign_api_url}/transactions/{transaction_id}", f"{settings.universign_api_url}/transactions/{transaction_id}",
auth=(settings.universign_api_key, ""), auth=(settings.universign_api_key, ""),
timeout=10 timeout=10,
) )
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
statut_map = { statut_map = {
@ -221,19 +235,20 @@ async def universign_statut(transaction_id: str) -> Dict:
"completed": "SIGNE", "completed": "SIGNE",
"refused": "REFUSE", "refused": "REFUSE",
"expired": "EXPIRE", "expired": "EXPIRE",
"canceled": "REFUSE" "canceled": "REFUSE",
} }
return { return {
"statut": statut_map.get(data.get("state"), "EN_ATTENTE"), "statut": statut_map.get(data.get("state"), "EN_ATTENTE"),
"date_signature": data.get("completed_at") "date_signature": data.get("completed_at"),
} }
else: else:
return {"statut": "ERREUR"} return {"statut": "ERREUR"}
except Exception as e: except Exception as e:
logger.error(f"Erreur statut Universign: {e}") logger.error(f"Erreur statut Universign: {e}")
return {"statut": "ERREUR", "error": str(e)} return {"statut": "ERREUR", "error": str(e)}
# ===================================================== # =====================================================
# CYCLE DE VIE # CYCLE DE VIE
# ===================================================== # =====================================================
@ -242,23 +257,24 @@ async def lifespan(app: FastAPI):
# Init base de données # Init base de données
await init_db() await init_db()
logger.info("✅ Base de données initialisée") logger.info("✅ Base de données initialisée")
# Injecter session_factory dans email_queue # Injecter session_factory dans email_queue
email_queue.session_factory = async_session_factory email_queue.session_factory = async_session_factory
# ⚠️ PAS de sage_connector ici (c'est sur Windows !) # ⚠️ PAS de sage_connector ici (c'est sur Windows !)
# email_queue utilisera sage_client pour générer les PDFs via HTTP # email_queue utilisera sage_client pour générer les PDFs via HTTP
# Démarrer queue # Démarrer queue
email_queue.start(num_workers=settings.max_email_workers) email_queue.start(num_workers=settings.max_email_workers)
logger.info(f"✅ Email queue démarrée") logger.info(f"✅ Email queue démarrée")
yield yield
# Cleanup # Cleanup
email_queue.stop() email_queue.stop()
logger.info("👋 Services arrêtés") logger.info("👋 Services arrêtés")
# ===================================================== # =====================================================
# APPLICATION # APPLICATION
# ===================================================== # =====================================================
@ -266,7 +282,7 @@ app = FastAPI(
title="API Sage 100c Dataven", title="API Sage 100c Dataven",
version="2.0.0", version="2.0.0",
description="API de gestion commerciale - VPS Linux", description="API de gestion commerciale - VPS Linux",
lifespan=lifespan lifespan=lifespan,
) )
app.add_middleware( app.add_middleware(
@ -274,9 +290,10 @@ app.add_middleware(
allow_origins=settings.cors_origins, allow_origins=settings.cors_origins,
allow_methods=["GET", "POST", "PUT", "DELETE"], allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"], allow_headers=["*"],
allow_credentials=True allow_credentials=True,
) )
# ===================================================== # =====================================================
# ENDPOINTS - US-A1 (CRÉATION RAPIDE DEVIS) # ENDPOINTS - US-A1 (CRÉATION RAPIDE DEVIS)
# ===================================================== # =====================================================
@ -290,6 +307,7 @@ async def rechercher_clients(query: Optional[str] = Query(None)):
logger.error(f"Erreur recherche clients: {e}") logger.error(f"Erreur recherche clients: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/articles", response_model=List[ArticleResponse], tags=["US-A1"]) @app.get("/articles", response_model=List[ArticleResponse], tags=["US-A1"])
async def rechercher_articles(query: Optional[str] = Query(None)): async def rechercher_articles(query: Optional[str] = Query(None)):
"""🔍 Recherche articles via gateway Windows""" """🔍 Recherche articles via gateway Windows"""
@ -300,6 +318,7 @@ async def rechercher_articles(query: Optional[str] = Query(None)):
logger.error(f"Erreur recherche articles: {e}") logger.error(f"Erreur recherche articles: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/devis", response_model=DevisResponse, status_code=201, tags=["US-A1"]) @app.post("/devis", response_model=DevisResponse, status_code=201, tags=["US-A1"])
async def creer_devis(devis: DevisRequest): async def creer_devis(devis: DevisRequest):
"""📝 Création de devis via gateway Windows""" """📝 Création de devis via gateway Windows"""
@ -313,30 +332,48 @@ async def creer_devis(devis: DevisRequest):
"article_code": l.article_code, "article_code": l.article_code,
"quantite": l.quantite, "quantite": l.quantite,
"prix_unitaire_ht": l.prix_unitaire_ht, "prix_unitaire_ht": l.prix_unitaire_ht,
"remise_pourcentage": l.remise_pourcentage "remise_pourcentage": l.remise_pourcentage,
} }
for l in devis.lignes for l in devis.lignes
] ],
} }
# Appel HTTP vers Windows # Appel HTTP vers Windows
resultat = sage_client.creer_devis(devis_data) resultat = sage_client.creer_devis(devis_data)
logger.info(f"✅ Devis créé: {resultat.get('numero_devis')}") logger.info(f"✅ Devis créé: {resultat.get('numero_devis')}")
return DevisResponse( return DevisResponse(
id=resultat["numero_devis"], id=resultat["numero_devis"],
client_id=devis.client_id, client_id=devis.client_id,
date_devis=resultat["date_devis"], date_devis=resultat["date_devis"],
montant_total_ht=resultat["total_ht"], montant_total_ht=resultat["total_ht"],
montant_total_ttc=resultat["total_ttc"], montant_total_ttc=resultat["total_ttc"],
nb_lignes=resultat["nb_lignes"] nb_lignes=resultat["nb_lignes"],
) )
except Exception as e: except Exception as e:
logger.error(f"Erreur création devis: {e}") logger.error(f"Erreur création devis: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/devis", tags=["US-A1"])
async def lister_devis(
limit: int = Query(100, le=1000), statut: Optional[int] = Query(None)
):
"""
📋 Liste tous les devis via gateway Windows
"""
try:
# ✅ APPEL VIA SAGE_CLIENT (HTTP vers Windows)
devis_list = sage_client.lister_devis(limit=limit, statut=statut)
return devis_list
except Exception as e:
logger.error(f"Erreur liste devis: {e}")
raise HTTPException(500, str(e))
@app.get("/devis/{id}", tags=["US-A1"]) @app.get("/devis/{id}", tags=["US-A1"])
async def lire_devis(id: str): async def lire_devis(id: str):
"""📄 Lecture d'un devis via gateway Windows""" """📄 Lecture d'un devis via gateway Windows"""
@ -351,6 +388,7 @@ async def lire_devis(id: str):
logger.error(f"Erreur lecture devis: {e}") logger.error(f"Erreur lecture devis: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/devis/{id}/pdf", tags=["US-A1"]) @app.get("/devis/{id}/pdf", tags=["US-A1"])
async def telecharger_devis_pdf(id: str): async def telecharger_devis_pdf(id: str):
"""📄 Téléchargement PDF (généré via email_queue)""" """📄 Téléchargement PDF (généré via email_queue)"""
@ -358,21 +396,20 @@ async def telecharger_devis_pdf(id: str):
# Générer PDF en appelant la méthode de email_queue # Générer PDF en appelant la méthode de email_queue
# qui elle-même appellera sage_client pour récupérer les données # qui elle-même appellera sage_client pour récupérer les données
pdf_bytes = email_queue._generate_pdf(id, TypeDocument.DEVIS) pdf_bytes = email_queue._generate_pdf(id, TypeDocument.DEVIS)
return StreamingResponse( return StreamingResponse(
iter([pdf_bytes]), iter([pdf_bytes]),
media_type="application/pdf", media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename=Devis_{id}.pdf"} headers={"Content-Disposition": f"attachment; filename=Devis_{id}.pdf"},
) )
except Exception as e: except Exception as e:
logger.error(f"Erreur génération PDF: {e}") logger.error(f"Erreur génération PDF: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/devis/{id}/envoyer", tags=["US-A1"]) @app.post("/devis/{id}/envoyer", tags=["US-A1"])
async def envoyer_devis_email( async def envoyer_devis_email(
id: str, id: str, request: EmailEnvoiRequest, session: AsyncSession = Depends(get_session)
request: EmailEnvoiRequest,
session: AsyncSession = Depends(get_session)
): ):
"""📧 Envoi devis par email""" """📧 Envoi devis par email"""
try: try:
@ -380,11 +417,11 @@ async def envoyer_devis_email(
devis = sage_client.lire_devis(id) devis = sage_client.lire_devis(id)
if not devis: if not devis:
raise HTTPException(404, f"Devis {id} introuvable") raise HTTPException(404, f"Devis {id} introuvable")
# Créer logs email pour chaque destinataire # Créer logs email pour chaque destinataire
tous_destinataires = [request.destinataire] + request.cc + request.cci tous_destinataires = [request.destinataire] + request.cc + request.cci
email_logs = [] email_logs = []
for dest in tous_destinataires: for dest in tous_destinataires:
email_log = EmailLog( email_log = EmailLog(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
@ -395,49 +432,92 @@ async def envoyer_devis_email(
type_document=TypeDocument.DEVIS, type_document=TypeDocument.DEVIS,
statut=StatutEmailEnum.EN_ATTENTE, statut=StatutEmailEnum.EN_ATTENTE,
date_creation=datetime.now(), date_creation=datetime.now(),
nb_tentatives=0 nb_tentatives=0,
) )
session.add(email_log) session.add(email_log)
await session.flush() await session.flush()
email_queue.enqueue(email_log.id) email_queue.enqueue(email_log.id)
email_logs.append(email_log.id) email_logs.append(email_log.id)
await session.commit() await session.commit()
logger.info(f"✅ Email devis {id} mis en file: {len(tous_destinataires)} destinataire(s)") logger.info(
f"✅ Email devis {id} mis en file: {len(tous_destinataires)} destinataire(s)"
)
return { return {
"success": True, "success": True,
"email_log_ids": email_logs, "email_log_ids": email_logs,
"devis_id": id, "devis_id": id,
"message": f"{len(tous_destinataires)} email(s) en file d'attente" "message": f"{len(tous_destinataires)} email(s) en file d'attente",
} }
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Erreur envoi email: {e}") logger.error(f"Erreur envoi email: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.put("/devis/{id}/statut", tags=["US-A1"])
async def changer_statut_devis(id: str, nouveau_statut: int = Query(..., ge=0, le=5)):
"""
📄 Changement de statut d'un devis via gateway Windows
"""
try:
# ✅ APPEL VIA SAGE_CLIENT (HTTP vers Windows)
resultat = sage_client.changer_statut_devis(id, nouveau_statut)
logger.info(f"✅ Statut devis {id} changé: {nouveau_statut}")
return {
"success": True,
"devis_id": id,
"statut_ancien": resultat.get("statut_ancien"),
"statut_nouveau": resultat.get("statut_nouveau"),
"message": "Statut mis à jour avec succès",
}
except Exception as e:
logger.error(f"Erreur changement statut: {e}")
raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# ENDPOINTS - US-A2 (WORKFLOW SANS RESSAISIE) # ENDPOINTS - US-A2 (WORKFLOW SANS RESSAISIE)
# ===================================================== # =====================================================
@app.post("/workflow/devis/{id}/to-commande", tags=["US-A2"])
async def devis_vers_commande(
id: str, @app.get("/commandes", tags=["US-A2"])
session: AsyncSession = Depends(get_session) async def lister_commandes(
limit: int = Query(100, le=1000), statut: Optional[int] = Query(None)
): ):
"""
📋 Liste toutes les commandes via gateway Windows
"""
try:
# ✅ APPEL VIA SAGE_CLIENT (HTTP vers Windows)
commandes = sage_client.lister_commandes(limit=limit, statut=statut)
return commandes
except Exception as e:
logger.error(f"Erreur liste commandes: {e}")
raise HTTPException(500, str(e))
@app.post("/workflow/devis/{id}/to-commande", tags=["US-A2"])
async def devis_vers_commande(id: str, session: AsyncSession = Depends(get_session)):
"""🔧 Transformation Devis → Commande via gateway Windows""" """🔧 Transformation Devis → Commande via gateway Windows"""
try: try:
# Appel HTTP vers Windows # Appel HTTP vers Windows
resultat = sage_client.transformer_document( resultat = sage_client.transformer_document(
numero_source=id, numero_source=id,
type_source=TypeDocument.DEVIS, type_source=TypeDocument.DEVIS,
type_cible=TypeDocument.COMMANDE type_cible=TypeDocument.COMMANDE,
) )
# Logger en DB # Logger en DB
workflow_log = WorkflowLog( workflow_log = WorkflowLog(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
@ -447,38 +527,38 @@ async def devis_vers_commande(
type_cible=TypeDocument.COMMANDE, type_cible=TypeDocument.COMMANDE,
nb_lignes=resultat.get("nb_lignes", 0), nb_lignes=resultat.get("nb_lignes", 0),
date_transformation=datetime.now(), date_transformation=datetime.now(),
succes=True succes=True,
) )
session.add(workflow_log) session.add(workflow_log)
await session.commit() await session.commit()
logger.info(f"✅ Transformation: Devis {id} → Commande {resultat['document_cible']}") logger.info(
f"✅ Transformation: Devis {id} → Commande {resultat['document_cible']}"
)
return { return {
"success": True, "success": True,
"document_source": id, "document_source": id,
"document_cible": resultat["document_cible"], "document_cible": resultat["document_cible"],
"nb_lignes": resultat["nb_lignes"] "nb_lignes": resultat["nb_lignes"],
} }
except Exception as e: except Exception as e:
logger.error(f"Erreur transformation: {e}") logger.error(f"Erreur transformation: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/workflow/commande/{id}/to-facture", tags=["US-A2"]) @app.post("/workflow/commande/{id}/to-facture", tags=["US-A2"])
async def commande_vers_facture( async def commande_vers_facture(id: str, session: AsyncSession = Depends(get_session)):
id: str,
session: AsyncSession = Depends(get_session)
):
"""🔧 Transformation Commande → Facture via gateway Windows""" """🔧 Transformation Commande → Facture via gateway Windows"""
try: try:
resultat = sage_client.transformer_document( resultat = sage_client.transformer_document(
numero_source=id, numero_source=id,
type_source=TypeDocument.COMMANDE, type_source=TypeDocument.COMMANDE,
type_cible=TypeDocument.FACTURE type_cible=TypeDocument.FACTURE,
) )
workflow_log = WorkflowLog( workflow_log = WorkflowLog(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
document_source=id, document_source=id,
@ -487,42 +567,39 @@ async def commande_vers_facture(
type_cible=TypeDocument.FACTURE, type_cible=TypeDocument.FACTURE,
nb_lignes=resultat.get("nb_lignes", 0), nb_lignes=resultat.get("nb_lignes", 0),
date_transformation=datetime.now(), date_transformation=datetime.now(),
succes=True succes=True,
) )
session.add(workflow_log) session.add(workflow_log)
await session.commit() await session.commit()
return resultat return resultat
except Exception as e: except Exception as e:
logger.error(f"Erreur transformation: {e}") logger.error(f"Erreur transformation: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# ENDPOINTS - US-A3 (SIGNATURE ÉLECTRONIQUE) # ENDPOINTS - US-A3 (SIGNATURE ÉLECTRONIQUE)
# ===================================================== # =====================================================
@app.post("/signature/universign/send", tags=["US-A3"]) @app.post("/signature/universign/send", tags=["US-A3"])
async def envoyer_signature( async def envoyer_signature(
demande: SignatureRequest, demande: SignatureRequest, session: AsyncSession = Depends(get_session)
session: AsyncSession = Depends(get_session)
): ):
"""✍️ Envoi document pour signature Universign""" """✍️ Envoi document pour signature Universign"""
try: try:
# Générer PDF # Générer PDF
pdf_bytes = email_queue._generate_pdf(demande.doc_id, demande.type_doc) pdf_bytes = email_queue._generate_pdf(demande.doc_id, demande.type_doc)
# Envoi Universign # Envoi Universign
resultat = await universign_envoyer( resultat = await universign_envoyer(
demande.doc_id, demande.doc_id, pdf_bytes, demande.email_signataire, demande.nom_signataire
pdf_bytes,
demande.email_signataire,
demande.nom_signataire
) )
if "error" in resultat: if "error" in resultat:
raise HTTPException(500, resultat["error"]) raise HTTPException(500, resultat["error"])
# Logger en DB # Logger en DB
signature_log = SignatureLog( signature_log = SignatureLog(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
@ -533,42 +610,78 @@ async def envoyer_signature(
email_signataire=demande.email_signataire, email_signataire=demande.email_signataire,
nom_signataire=demande.nom_signataire, nom_signataire=demande.nom_signataire,
statut=StatutSignatureEnum.ENVOYE, statut=StatutSignatureEnum.ENVOYE,
date_envoi=datetime.now() date_envoi=datetime.now(),
) )
session.add(signature_log) session.add(signature_log)
await session.commit() await session.commit()
# MAJ champ libre Sage via gateway Windows # MAJ champ libre Sage via gateway Windows
sage_client.mettre_a_jour_champ_libre( sage_client.mettre_a_jour_champ_libre(
demande.doc_id, demande.doc_id, demande.type_doc, "UniversignID", resultat["transaction_id"]
demande.type_doc,
"UniversignID",
resultat["transaction_id"]
) )
logger.info(f"✅ Signature envoyée: {demande.doc_id}") logger.info(f"✅ Signature envoyée: {demande.doc_id}")
return { return {
"success": True, "success": True,
"transaction_id": resultat["transaction_id"], "transaction_id": resultat["transaction_id"],
"signer_url": resultat["signer_url"] "signer_url": resultat["signer_url"],
} }
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Erreur signature: {e}") logger.error(f"Erreur signature: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
# =====================================================
# ENDPOINTS - US-A5
# =====================================================
@app.post("/devis/valider-remise", response_model=BaremeRemiseResponse, tags=["US-A5"])
async def valider_remise(
client_id: str = Query(..., min_length=1),
remise_pourcentage: float = Query(0.0, ge=0, le=100),
):
"""
💰 US-A5: Validation remise via barème client Sage
"""
try:
# ✅ APPEL VIA SAGE_CLIENT (HTTP vers Windows)
remise_max = sage_client.lire_remise_max_client(client_id)
autorisee = remise_pourcentage <= remise_max
if not autorisee:
message = f"⚠️ Remise trop élevée (max autorisé: {remise_max}%)"
logger.warning(
f"Remise refusée pour {client_id}: {remise_pourcentage}% > {remise_max}%"
)
else:
message = "✅ Remise autorisée"
return BaremeRemiseResponse(
client_id=client_id,
remise_max_autorisee=remise_max,
remise_demandee=remise_pourcentage,
autorisee=autorisee,
message=message,
)
except Exception as e:
logger.error(f"Erreur validation remise: {e}")
raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# ENDPOINTS - US-A6 (RELANCE DEVIS) # ENDPOINTS - US-A6 (RELANCE DEVIS)
# ===================================================== # =====================================================
@app.post("/devis/{id}/relancer-signature", tags=["US-A6"]) @app.post("/devis/{id}/relancer-signature", tags=["US-A6"])
async def relancer_devis_signature( async def relancer_devis_signature(
id: str, id: str, relance: RelanceDevisRequest, session: AsyncSession = Depends(get_session)
relance: RelanceDevisRequest,
session: AsyncSession = Depends(get_session)
): ):
"""📧 Relance devis via Universign""" """📧 Relance devis via Universign"""
try: try:
@ -576,26 +689,26 @@ async def relancer_devis_signature(
devis = sage_client.lire_devis(id) devis = sage_client.lire_devis(id)
if not devis: if not devis:
raise HTTPException(404, f"Devis {id} introuvable") raise HTTPException(404, f"Devis {id} introuvable")
# Récupérer contact via gateway # Récupérer contact via gateway
contact = sage_client.lire_contact_client(devis["client_code"]) contact = sage_client.lire_contact_client(devis["client_code"])
if not contact or not contact.get("email"): if not contact or not contact.get("email"):
raise HTTPException(400, "Aucun email trouvé pour ce client") raise HTTPException(400, "Aucun email trouvé pour ce client")
# Générer PDF # Générer PDF
pdf_bytes = email_queue._generate_pdf(id, TypeDocument.DEVIS) pdf_bytes = email_queue._generate_pdf(id, TypeDocument.DEVIS)
# Envoi Universign # Envoi Universign
resultat = await universign_envoyer( resultat = await universign_envoyer(
id, id,
pdf_bytes, pdf_bytes,
contact["email"], contact["email"],
contact["nom"] or contact["client_intitule"] contact["nom"] or contact["client_intitule"],
) )
if "error" in resultat: if "error" in resultat:
raise HTTPException(500, resultat["error"]) raise HTTPException(500, resultat["error"])
# Logger en DB # Logger en DB
signature_log = SignatureLog( signature_log = SignatureLog(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
@ -608,25 +721,48 @@ async def relancer_devis_signature(
statut=StatutSignatureEnum.ENVOYE, statut=StatutSignatureEnum.ENVOYE,
date_envoi=datetime.now(), date_envoi=datetime.now(),
est_relance=True, est_relance=True,
nb_relances=1 nb_relances=1,
) )
session.add(signature_log) session.add(signature_log)
await session.commit() await session.commit()
return { return {
"success": True, "success": True,
"devis_id": id, "devis_id": id,
"transaction_id": resultat["transaction_id"], "transaction_id": resultat["transaction_id"],
"message": "Relance signature envoyée" "message": "Relance signature envoyée",
} }
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
logger.error(f"Erreur relance: {e}") logger.error(f"Erreur relance: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
# =====================================================
# ENDPOINTS - US-A7
# =====================================================
@app.get("/factures", tags=["US-A7"])
async def lister_factures(
limit: int = Query(100, le=1000), statut: Optional[int] = Query(None)
):
"""
📋 Liste toutes les factures via gateway Windows
"""
try:
# ✅ APPEL VIA SAGE_CLIENT (HTTP vers Windows)
factures = sage_client.lister_factures(limit=limit, statut=statut)
return factures
except Exception as e:
logger.error(f"Erreur liste factures: {e}")
raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# ENDPOINTS - HEALTH # ENDPOINTS - HEALTH
# ===================================================== # =====================================================
@ -634,18 +770,19 @@ async def relancer_devis_signature(
async def health_check(): async def health_check():
"""🏥 Health check""" """🏥 Health check"""
gateway_health = sage_client.health() gateway_health = sage_client.health()
return { return {
"status": "healthy", "status": "healthy",
"sage_gateway": gateway_health, "sage_gateway": gateway_health,
"email_queue": { "email_queue": {
"running": email_queue.running, "running": email_queue.running,
"workers": len(email_queue.workers), "workers": len(email_queue.workers),
"queue_size": email_queue.queue.qsize() "queue_size": email_queue.queue.qsize(),
}, },
"timestamp": datetime.now().isoformat() "timestamp": datetime.now().isoformat(),
} }
@app.get("/", tags=["System"]) @app.get("/", tags=["System"])
async def root(): async def root():
"""🏠 Page d'accueil""" """🏠 Page d'accueil"""
@ -653,9 +790,65 @@ async def root():
"api": "Sage 100c Dataven - VPS Linux", "api": "Sage 100c Dataven - VPS Linux",
"version": "2.0.0", "version": "2.0.0",
"documentation": "/docs", "documentation": "/docs",
"health": "/health" "health": "/health",
} }
# =====================================================
# ENDPOINTS - ADMIN
# =====================================================
@app.get("/admin/cache/info", tags=["Admin"])
async def info_cache():
"""
📊 Informations sur l'état du cache Windows
"""
try:
# ✅ APPEL VIA SAGE_CLIENT (HTTP vers Windows)
cache_info = sage_client.get_cache_info()
return cache_info
except Exception as e:
logger.error(f"Erreur info cache: {e}")
raise HTTPException(500, str(e))
# 🔧 CORRECTION ADMIN: Cache refresh (doit appeler Windows)
@app.post("/admin/cache/refresh", tags=["Admin"])
async def forcer_actualisation():
"""
🔄 Force l'actualisation du cache Windows
"""
try:
# ✅ APPEL VIA SAGE_CLIENT (HTTP vers Windows)
resultat = sage_client.refresh_cache()
cache_info = sage_client.get_cache_info()
return {
"success": True,
"message": "Cache actualisé sur Windows Server",
"info": cache_info,
}
except Exception as e:
logger.error(f"Erreur refresh cache: {e}")
raise HTTPException(500, str(e))
# ✅ RESTE INCHANGÉ: admin/queue/status (local au VPS)
@app.get("/admin/queue/status", tags=["Admin"])
async def statut_queue():
"""
📊 Statut de la queue d'emails (local VPS)
"""
return {
"queue_size": email_queue.queue.qsize(),
"workers": len(email_queue.workers),
"running": email_queue.running,
}
# ===================================================== # =====================================================
# LANCEMENT # LANCEMENT
# ===================================================== # =====================================================
@ -664,5 +857,5 @@ if __name__ == "__main__":
"api:app", "api:app",
host=settings.api_host, host=settings.api_host,
port=settings.api_port, port=settings.api_port,
reload=settings.api_reload reload=settings.api_reload,
) )