feat: add endpoint to list devis with filters and update devis status endpoint signature and logic.

This commit is contained in:
Fanilo-Nantenaina 2025-11-27 12:44:39 +03:00
parent 8bdfdd0e01
commit 7ee3751ee2

361
main.py
View file

@ -15,14 +15,12 @@ from sage_connector import SageConnector
# ===================================================== # =====================================================
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_gateway.log"), logging.StreamHandler()],
logging.FileHandler("sage_gateway.log"),
logging.StreamHandler()
]
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ===================================================== # =====================================================
# ENUMS # ENUMS
# ===================================================== # =====================================================
@ -34,34 +32,43 @@ class TypeDocument(int, Enum):
PREPARATION = 4 PREPARATION = 4
FACTURE = 5 FACTURE = 5
# ===================================================== # =====================================================
# MODÈLES # MODÈLES
# ===================================================== # =====================================================
class FiltreRequest(BaseModel): class FiltreRequest(BaseModel):
filtre: Optional[str] = "" filtre: Optional[str] = ""
class CodeRequest(BaseModel): class CodeRequest(BaseModel):
code: str code: str
class ChampLibreRequest(BaseModel): class ChampLibreRequest(BaseModel):
doc_id: str doc_id: str
type_doc: int type_doc: int
nom_champ: str nom_champ: str
valeur: str valeur: str
class DevisRequest(BaseModel): class DevisRequest(BaseModel):
client_id: str client_id: str
date_devis: Optional[date] = None date_devis: Optional[date] = None
lignes: List[Dict] # Format: {article_code, quantite, prix_unitaire_ht, remise_pourcentage} lignes: List[
Dict
] # Format: {article_code, quantite, prix_unitaire_ht, remise_pourcentage}
class TransformationRequest(BaseModel): class TransformationRequest(BaseModel):
numero_source: str numero_source: str
type_source: int type_source: int
type_cible: int type_cible: int
class StatutRequest(BaseModel): class StatutRequest(BaseModel):
nouveau_statut: int nouveau_statut: int
# ===================================================== # =====================================================
# SÉCURITÉ # SÉCURITÉ
# ===================================================== # =====================================================
@ -72,13 +79,14 @@ def verify_token(x_sage_token: str = Header(...)):
raise HTTPException(401, "Token invalide") raise HTTPException(401, "Token invalide")
return True return True
# ===================================================== # =====================================================
# APPLICATION # APPLICATION
# ===================================================== # =====================================================
app = FastAPI( app = FastAPI(
title="Sage Gateway - Windows Server", title="Sage Gateway - Windows Server",
version="1.0.0", version="1.0.0",
description="Passerelle d'accès à Sage 100c pour VPS Linux" description="Passerelle d'accès à Sage 100c pour VPS Linux",
) )
app.add_middleware( app.add_middleware(
@ -86,11 +94,12 @@ app.add_middleware(
allow_origins=settings.cors_origins, allow_origins=settings.cors_origins,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
allow_credentials=True allow_credentials=True,
) )
sage: Optional[SageConnector] = None sage: Optional[SageConnector] = None
# ===================================================== # =====================================================
# LIFECYCLE # LIFECYCLE
# ===================================================== # =====================================================
@ -110,9 +119,7 @@ def startup():
# Connexion Sage # Connexion Sage
sage = SageConnector( sage = SageConnector(
settings.chemin_base, settings.chemin_base, settings.utilisateur, settings.mot_de_passe
settings.utilisateur,
settings.mot_de_passe
) )
if not sage.connecter(): if not sage.connecter():
@ -120,12 +127,14 @@ def startup():
logger.info("✅ Sage Gateway démarré et connecté") logger.info("✅ Sage Gateway démarré et connecté")
@app.on_event("shutdown") @app.on_event("shutdown")
def shutdown(): def shutdown():
if sage: if sage:
sage.deconnecter() sage.deconnecter()
logger.info("👋 Sage Gateway arrêté") logger.info("👋 Sage Gateway arrêté")
# ===================================================== # =====================================================
# ENDPOINTS - SYSTÈME # ENDPOINTS - SYSTÈME
# ===================================================== # =====================================================
@ -136,9 +145,10 @@ def health():
"status": "ok", "status": "ok",
"sage_connected": sage is not None and sage.cial is not None, "sage_connected": sage is not None and sage.cial is not None,
"cache_info": sage.get_cache_info() if sage else None, "cache_info": sage.get_cache_info() if sage else None,
"timestamp": datetime.now().isoformat() "timestamp": datetime.now().isoformat(),
} }
# ===================================================== # =====================================================
# ENDPOINTS - CLIENTS # ENDPOINTS - CLIENTS
# ===================================================== # =====================================================
@ -152,6 +162,7 @@ def clients_list(req: FiltreRequest):
logger.error(f"Erreur liste clients: {e}") logger.error(f"Erreur liste clients: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/sage/clients/get", dependencies=[Depends(verify_token)]) @app.post("/sage/clients/get", dependencies=[Depends(verify_token)])
def client_get(req: CodeRequest): def client_get(req: CodeRequest):
"""Lecture d'un client par code""" """Lecture d'un client par code"""
@ -166,6 +177,7 @@ def client_get(req: CodeRequest):
logger.error(f"Erreur lecture client: {e}") logger.error(f"Erreur lecture client: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# ENDPOINTS - ARTICLES # ENDPOINTS - ARTICLES
# ===================================================== # =====================================================
@ -179,6 +191,7 @@ def articles_list(req: FiltreRequest):
logger.error(f"Erreur liste articles: {e}") logger.error(f"Erreur liste articles: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/sage/articles/get", dependencies=[Depends(verify_token)]) @app.post("/sage/articles/get", dependencies=[Depends(verify_token)])
def article_get(req: CodeRequest): def article_get(req: CodeRequest):
"""Lecture d'un article par référence""" """Lecture d'un article par référence"""
@ -193,6 +206,7 @@ def article_get(req: CodeRequest):
logger.error(f"Erreur lecture article: {e}") logger.error(f"Erreur lecture article: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# ENDPOINTS - DEVIS # ENDPOINTS - DEVIS
# ===================================================== # =====================================================
@ -204,7 +218,7 @@ def creer_devis(req: DevisRequest):
devis_data = { devis_data = {
"client": {"code": req.client_id, "intitule": ""}, "client": {"code": req.client_id, "intitule": ""},
"date_devis": req.date_devis or date.today(), "date_devis": req.date_devis or date.today(),
"lignes": req.lignes "lignes": req.lignes,
} }
resultat = sage.creer_devis_enrichi(devis_data) resultat = sage.creer_devis_enrichi(devis_data)
@ -213,6 +227,7 @@ def creer_devis(req: DevisRequest):
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.post("/sage/devis/get", dependencies=[Depends(verify_token)]) @app.post("/sage/devis/get", dependencies=[Depends(verify_token)])
def lire_devis(req: CodeRequest): def lire_devis(req: CodeRequest):
"""Lecture d'un devis""" """Lecture d'un devis"""
@ -227,6 +242,7 @@ def lire_devis(req: CodeRequest):
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.post("/sage/devis/statut", dependencies=[Depends(verify_token)]) @app.post("/sage/devis/statut", dependencies=[Depends(verify_token)])
def changer_statut_devis(doc_id: str, req: StatutRequest): def changer_statut_devis(doc_id: str, req: StatutRequest):
"""Changement de statut d'un devis""" """Changement de statut d'un devis"""
@ -238,6 +254,128 @@ def changer_statut_devis(doc_id: str, req: StatutRequest):
logger.error(f"Erreur MAJ statut: {e}") logger.error(f"Erreur MAJ statut: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/sage/devis/list", dependencies=[Depends(verify_token)])
def devis_list(limit: int = 100, statut: Optional[int] = None):
"""
Liste tous les devis avec filtres optionnels
Utilise le cache pour performances optimales
"""
try:
with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente
devis_list = []
index = 1
max_iterations = limit * 3
erreurs_consecutives = 0
max_erreurs = 50
while (
len(devis_list) < limit
and index < max_iterations
and erreurs_consecutives < max_erreurs
):
try:
persist = factory.List(index)
if persist is None:
break
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
# Filtrer uniquement devis (type 0)
if getattr(doc, "DO_Type", -1) != 0:
index += 1
continue
doc_statut = getattr(doc, "DO_Statut", 0)
# Filtre statut
if statut is not None and doc_statut != statut:
index += 1
continue
# Charger client via .Client
client_code = ""
client_intitule = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code = getattr(client_obj, "CT_Num", "").strip()
client_intitule = getattr(
client_obj, "CT_Intitule", ""
).strip()
except Exception as e:
logger.debug(f"Erreur chargement client: {e}")
devis_list.append(
{
"numero": getattr(doc, "DO_Piece", ""),
"date": str(getattr(doc, "DO_Date", "")),
"client_code": client_code,
"client_intitule": client_intitule,
"total_ht": float(getattr(doc, "DO_TotalHT", 0.0)),
"total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)),
"statut": doc_statut,
}
)
erreurs_consecutives = 0
index += 1
except Exception as e:
erreurs_consecutives += 1
logger.debug(f"Erreur index {index}: {e}")
index += 1
if erreurs_consecutives >= max_erreurs:
break
logger.info(f"{len(devis_list)} devis retournés")
return {"success": True, "data": devis_list}
except Exception as e:
logger.error(f"Erreur liste devis: {e}")
raise HTTPException(500, str(e))
@app.post("/sage/devis/statut", dependencies=[Depends(verify_token)])
def changer_statut_devis_endpoint(numero: str, nouveau_statut: int):
"""Change le statut d'un devis"""
try:
with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente
persist = factory.ReadPiece(0, numero)
if not persist:
raise HTTPException(404, f"Devis {numero} introuvable")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
doc.DO_Statut = nouveau_statut
doc.Write()
logger.info(f"✅ Statut devis {numero}: {statut_actuel}{nouveau_statut}")
return {
"success": True,
"data": {
"numero": numero,
"statut_ancien": statut_actuel,
"statut_nouveau": nouveau_statut,
},
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur changement statut: {e}")
raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# ENDPOINTS - DOCUMENTS # ENDPOINTS - DOCUMENTS
# ===================================================== # =====================================================
@ -255,35 +393,33 @@ def lire_document(numero: str, type_doc: int):
logger.error(f"Erreur lecture document: {e}") logger.error(f"Erreur lecture document: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/sage/documents/transform", dependencies=[Depends(verify_token)]) @app.post("/sage/documents/transform", dependencies=[Depends(verify_token)])
def transformer_document(req: TransformationRequest): def transformer_document(req: TransformationRequest):
"""Transformation de document (devis → commande, etc.)""" """Transformation de document (devis → commande, etc.)"""
try: try:
resultat = sage.transformer_document( resultat = sage.transformer_document(
req.numero_source, req.numero_source, req.type_source, req.type_cible
req.type_source,
req.type_cible
) )
return {"success": True, "data": resultat} return {"success": True, "data": 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))
@app.post("/sage/documents/champ-libre", dependencies=[Depends(verify_token)]) @app.post("/sage/documents/champ-libre", dependencies=[Depends(verify_token)])
def maj_champ_libre(req: ChampLibreRequest): def maj_champ_libre(req: ChampLibreRequest):
"""Mise à jour d'un champ libre""" """Mise à jour d'un champ libre"""
try: try:
success = sage.mettre_a_jour_champ_libre( success = sage.mettre_a_jour_champ_libre(
req.doc_id, req.doc_id, req.type_doc, req.nom_champ, req.valeur
req.type_doc,
req.nom_champ,
req.valeur
) )
return {"success": success} return {"success": success}
except Exception as e: except Exception as e:
logger.error(f"Erreur MAJ champ libre: {e}") logger.error(f"Erreur MAJ champ libre: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# ENDPOINTS - CONTACTS # ENDPOINTS - CONTACTS
# ===================================================== # =====================================================
@ -301,6 +437,181 @@ def contact_read(req: CodeRequest):
logger.error(f"Erreur lecture contact: {e}") logger.error(f"Erreur lecture contact: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.post("/sage/commandes/list", dependencies=[Depends(verify_token)])
def commandes_list(limit: int = 100, statut: Optional[int] = None):
"""Liste toutes les commandes"""
try:
with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente
commandes = []
index = 1
max_iterations = limit * 3
while len(commandes) < limit and index < max_iterations:
try:
persist = factory.List(index)
if persist is None:
break
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
# Filtrer commandes (type 3)
if getattr(doc, "DO_Type", -1) != 3:
index += 1
continue
doc_statut = getattr(doc, "DO_Statut", 0)
if statut is None or doc_statut == statut:
# Charger client
client_code = ""
client_intitule = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code = getattr(client_obj, "CT_Num", "").strip()
client_intitule = getattr(
client_obj, "CT_Intitule", ""
).strip()
except:
pass
commandes.append(
{
"numero": getattr(doc, "DO_Piece", ""),
"date": str(getattr(doc, "DO_Date", "")),
"client_code": client_code,
"client_intitule": client_intitule,
"total_ht": float(getattr(doc, "DO_TotalHT", 0.0)),
"total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)),
"statut": doc_statut,
}
)
index += 1
except:
index += 1
continue
logger.info(f"{len(commandes)} commandes retournées")
return {"success": True, "data": commandes}
except Exception as e:
logger.error(f"Erreur liste commandes: {e}")
raise HTTPException(500, str(e))
@app.post("/sage/factures/list", dependencies=[Depends(verify_token)])
def factures_list(limit: int = 100, statut: Optional[int] = None):
"""Liste toutes les factures"""
try:
with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente
factures = []
index = 1
max_iterations = limit * 3
while len(factures) < limit and index < max_iterations:
try:
persist = factory.List(index)
if persist is None:
break
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
# Filtrer factures (type 5)
if getattr(doc, "DO_Type", -1) != 5:
index += 1
continue
doc_statut = getattr(doc, "DO_Statut", 0)
if statut is None or doc_statut == statut:
# Charger client
client_code = ""
client_intitule = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code = getattr(client_obj, "CT_Num", "").strip()
client_intitule = getattr(
client_obj, "CT_Intitule", ""
).strip()
except:
pass
# Champ libre dernière relance
derniere_relance = None
try:
derniere_relance = getattr(doc, "DO_DerniereRelance", None)
except:
pass
factures.append(
{
"numero": getattr(doc, "DO_Piece", ""),
"date": str(getattr(doc, "DO_Date", "")),
"client_code": client_code,
"client_intitule": client_intitule,
"total_ht": float(getattr(doc, "DO_TotalHT", 0.0)),
"total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)),
"statut": doc_statut,
"derniere_relance": (
str(derniere_relance) if derniere_relance else None
),
}
)
index += 1
except:
index += 1
continue
logger.info(f"{len(factures)} factures retournées")
return {"success": True, "data": factures}
except Exception as e:
logger.error(f"Erreur liste factures: {e}")
raise HTTPException(500, str(e))
@app.post("/sage/client/remise-max", dependencies=[Depends(verify_token)])
def lire_remise_max_client(code: str):
"""Récupère la remise max autorisée pour un client"""
try:
client_obj = sage._lire_client_obj(code)
if not client_obj:
raise HTTPException(404, f"Client {code} introuvable")
remise_max = 10.0 # Défaut
try:
remise_max = float(getattr(client_obj, "CT_RemiseMax", 10.0))
except:
pass
logger.info(f"✅ Remise max client {code}: {remise_max}%")
return {
"success": True,
"data": {"client_code": code, "remise_max": remise_max},
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur lecture remise: {e}")
raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# ENDPOINTS - ADMIN # ENDPOINTS - ADMIN
# ===================================================== # =====================================================
@ -312,21 +623,23 @@ def refresh_cache():
return { return {
"success": True, "success": True,
"message": "Cache actualisé", "message": "Cache actualisé",
"info": sage.get_cache_info() "info": sage.get_cache_info(),
} }
except Exception as e: except Exception as e:
logger.error(f"Erreur refresh cache: {e}") logger.error(f"Erreur refresh cache: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/sage/cache/info", dependencies=[Depends(verify_token)]) @app.get("/sage/cache/info", dependencies=[Depends(verify_token)])
def cache_info(): def cache_info_get():
"""Informations sur le cache""" """Informations sur le cache (endpoint GET)"""
try: try:
return {"success": True, "data": sage.get_cache_info()} return {"success": True, "data": sage.get_cache_info()}
except Exception as e: except Exception as e:
logger.error(f"Erreur info cache: {e}") logger.error(f"Erreur info cache: {e}")
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# LANCEMENT # LANCEMENT
# ===================================================== # =====================================================
@ -336,5 +649,5 @@ if __name__ == "__main__":
host=settings.api_host, host=settings.api_host,
port=settings.api_port, port=settings.api_port,
reload=False, # Pas de reload en production reload=False, # Pas de reload en production
log_level="info" log_level="info",
) )