feat: Add /devis endpoint for listing devis and apply minor formatting adjustments.
This commit is contained in:
parent
307105b8ad
commit
643250850b
1 changed files with 320 additions and 127 deletions
329
api.py
329
api.py
|
|
@ -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,14 +125,18 @@ 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
|
||||||
|
|
||||||
|
|
@ -139,13 +150,13 @@ async def universign_envoyer(doc_id: str, pdf_bytes: bytes, email: str, nom: str
|
||||||
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")
|
||||||
|
|
@ -155,7 +166,7 @@ async def universign_envoyer(doc_id: str, pdf_bytes: bytes, email: str, nom: str
|
||||||
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")
|
||||||
|
|
@ -165,7 +176,7 @@ async def universign_envoyer(doc_id: str, pdf_bytes: bytes, email: str, nom: str
|
||||||
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")
|
||||||
|
|
@ -175,33 +186,36 @@ async def universign_envoyer(doc_id: str, pdf_bytes: bytes, email: str, nom: str
|
||||||
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
|
||||||
|
|
@ -210,7 +224,7 @@ async def universign_statut(transaction_id: str) -> Dict:
|
||||||
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:
|
||||||
|
|
@ -221,11 +235,11 @@ 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"}
|
||||||
|
|
@ -234,6 +248,7 @@ async def universign_statut(transaction_id: str) -> Dict:
|
||||||
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
|
||||||
# =====================================================
|
# =====================================================
|
||||||
|
|
@ -259,6 +274,7 @@ async def lifespan(app: FastAPI):
|
||||||
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,10 +332,10 @@ 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
|
||||||
|
|
@ -330,13 +349,31 @@ async def creer_devis(devis: DevisRequest):
|
||||||
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)"""
|
||||||
|
|
@ -362,17 +400,16 @@ async def telecharger_devis_pdf(id: str):
|
||||||
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:
|
||||||
|
|
@ -395,7 +432,7 @@ 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)
|
||||||
|
|
@ -406,13 +443,15 @@ async def envoyer_devis_email(
|
||||||
|
|
||||||
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:
|
||||||
|
|
@ -421,21 +460,62 @@ async def envoyer_devis_email(
|
||||||
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
|
||||||
|
|
@ -447,36 +527,36 @@ 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(
|
||||||
|
|
@ -487,7 +567,7 @@ 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)
|
||||||
|
|
@ -499,13 +579,13 @@ async def commande_vers_facture(
|
||||||
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:
|
||||||
|
|
@ -514,10 +594,7 @@ async def envoyer_signature(
|
||||||
|
|
||||||
# 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:
|
||||||
|
|
@ -533,7 +610,7 @@ 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)
|
||||||
|
|
@ -541,10 +618,7 @@ async def envoyer_signature(
|
||||||
|
|
||||||
# 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}")
|
||||||
|
|
@ -552,7 +626,7 @@ async def envoyer_signature(
|
||||||
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:
|
||||||
|
|
@ -561,14 +635,53 @@ async def envoyer_signature(
|
||||||
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:
|
||||||
|
|
@ -590,7 +703,7 @@ async def relancer_devis_signature(
|
||||||
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:
|
||||||
|
|
@ -608,7 +721,7 @@ 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)
|
||||||
|
|
@ -618,7 +731,7 @@ async def relancer_devis_signature(
|
||||||
"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:
|
||||||
|
|
@ -627,6 +740,29 @@ async def relancer_devis_signature(
|
||||||
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
|
||||||
# =====================================================
|
# =====================================================
|
||||||
|
|
@ -641,11 +777,12 @@ async def health_check():
|
||||||
"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,
|
||||||
)
|
)
|
||||||
Loading…
Reference in a new issue