feat: add endpoint to list devis with filters and update devis status endpoint signature and logic.
This commit is contained in:
parent
8bdfdd0e01
commit
7ee3751ee2
1 changed files with 344 additions and 31 deletions
361
main.py
361
main.py
|
|
@ -15,14 +15,12 @@ from sage_connector import SageConnector
|
|||
# =====================================================
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler("sage_gateway.log"),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
handlers=[logging.FileHandler("sage_gateway.log"), logging.StreamHandler()],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# =====================================================
|
||||
# ENUMS
|
||||
# =====================================================
|
||||
|
|
@ -34,34 +32,43 @@ class TypeDocument(int, Enum):
|
|||
PREPARATION = 4
|
||||
FACTURE = 5
|
||||
|
||||
|
||||
# =====================================================
|
||||
# MODÈLES
|
||||
# =====================================================
|
||||
class FiltreRequest(BaseModel):
|
||||
filtre: Optional[str] = ""
|
||||
|
||||
|
||||
class CodeRequest(BaseModel):
|
||||
code: str
|
||||
|
||||
|
||||
class ChampLibreRequest(BaseModel):
|
||||
doc_id: str
|
||||
type_doc: int
|
||||
nom_champ: str
|
||||
valeur: str
|
||||
|
||||
|
||||
class DevisRequest(BaseModel):
|
||||
client_id: str
|
||||
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):
|
||||
numero_source: str
|
||||
type_source: int
|
||||
type_cible: int
|
||||
|
||||
|
||||
class StatutRequest(BaseModel):
|
||||
nouveau_statut: int
|
||||
|
||||
|
||||
# =====================================================
|
||||
# SÉCURITÉ
|
||||
# =====================================================
|
||||
|
|
@ -72,13 +79,14 @@ def verify_token(x_sage_token: str = Header(...)):
|
|||
raise HTTPException(401, "Token invalide")
|
||||
return True
|
||||
|
||||
|
||||
# =====================================================
|
||||
# APPLICATION
|
||||
# =====================================================
|
||||
app = FastAPI(
|
||||
title="Sage Gateway - Windows Server",
|
||||
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(
|
||||
|
|
@ -86,11 +94,12 @@ app.add_middleware(
|
|||
allow_origins=settings.cors_origins,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
allow_credentials=True
|
||||
allow_credentials=True,
|
||||
)
|
||||
|
||||
sage: Optional[SageConnector] = None
|
||||
|
||||
|
||||
# =====================================================
|
||||
# LIFECYCLE
|
||||
# =====================================================
|
||||
|
|
@ -110,9 +119,7 @@ def startup():
|
|||
|
||||
# Connexion Sage
|
||||
sage = SageConnector(
|
||||
settings.chemin_base,
|
||||
settings.utilisateur,
|
||||
settings.mot_de_passe
|
||||
settings.chemin_base, settings.utilisateur, settings.mot_de_passe
|
||||
)
|
||||
|
||||
if not sage.connecter():
|
||||
|
|
@ -120,12 +127,14 @@ def startup():
|
|||
|
||||
logger.info("✅ Sage Gateway démarré et connecté")
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
def shutdown():
|
||||
if sage:
|
||||
sage.deconnecter()
|
||||
logger.info("👋 Sage Gateway arrêté")
|
||||
|
||||
|
||||
# =====================================================
|
||||
# ENDPOINTS - SYSTÈME
|
||||
# =====================================================
|
||||
|
|
@ -136,9 +145,10 @@ def health():
|
|||
"status": "ok",
|
||||
"sage_connected": sage is not None and sage.cial is not None,
|
||||
"cache_info": sage.get_cache_info() if sage else None,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# =====================================================
|
||||
# ENDPOINTS - CLIENTS
|
||||
# =====================================================
|
||||
|
|
@ -152,6 +162,7 @@ def clients_list(req: FiltreRequest):
|
|||
logger.error(f"Erreur liste clients: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post("/sage/clients/get", dependencies=[Depends(verify_token)])
|
||||
def client_get(req: CodeRequest):
|
||||
"""Lecture d'un client par code"""
|
||||
|
|
@ -166,6 +177,7 @@ def client_get(req: CodeRequest):
|
|||
logger.error(f"Erreur lecture client: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# =====================================================
|
||||
# ENDPOINTS - ARTICLES
|
||||
# =====================================================
|
||||
|
|
@ -179,6 +191,7 @@ def articles_list(req: FiltreRequest):
|
|||
logger.error(f"Erreur liste articles: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post("/sage/articles/get", dependencies=[Depends(verify_token)])
|
||||
def article_get(req: CodeRequest):
|
||||
"""Lecture d'un article par référence"""
|
||||
|
|
@ -193,6 +206,7 @@ def article_get(req: CodeRequest):
|
|||
logger.error(f"Erreur lecture article: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# =====================================================
|
||||
# ENDPOINTS - DEVIS
|
||||
# =====================================================
|
||||
|
|
@ -204,7 +218,7 @@ def creer_devis(req: DevisRequest):
|
|||
devis_data = {
|
||||
"client": {"code": req.client_id, "intitule": ""},
|
||||
"date_devis": req.date_devis or date.today(),
|
||||
"lignes": req.lignes
|
||||
"lignes": req.lignes,
|
||||
}
|
||||
|
||||
resultat = sage.creer_devis_enrichi(devis_data)
|
||||
|
|
@ -213,6 +227,7 @@ def creer_devis(req: DevisRequest):
|
|||
logger.error(f"Erreur création devis: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post("/sage/devis/get", dependencies=[Depends(verify_token)])
|
||||
def lire_devis(req: CodeRequest):
|
||||
"""Lecture d'un devis"""
|
||||
|
|
@ -227,6 +242,7 @@ def lire_devis(req: CodeRequest):
|
|||
logger.error(f"Erreur lecture devis: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post("/sage/devis/statut", dependencies=[Depends(verify_token)])
|
||||
def changer_statut_devis(doc_id: str, req: StatutRequest):
|
||||
"""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}")
|
||||
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
|
||||
# =====================================================
|
||||
|
|
@ -255,35 +393,33 @@ def lire_document(numero: str, type_doc: int):
|
|||
logger.error(f"Erreur lecture document: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post("/sage/documents/transform", dependencies=[Depends(verify_token)])
|
||||
def transformer_document(req: TransformationRequest):
|
||||
"""Transformation de document (devis → commande, etc.)"""
|
||||
try:
|
||||
resultat = sage.transformer_document(
|
||||
req.numero_source,
|
||||
req.type_source,
|
||||
req.type_cible
|
||||
req.numero_source, req.type_source, req.type_cible
|
||||
)
|
||||
return {"success": True, "data": resultat}
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur transformation: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post("/sage/documents/champ-libre", dependencies=[Depends(verify_token)])
|
||||
def maj_champ_libre(req: ChampLibreRequest):
|
||||
"""Mise à jour d'un champ libre"""
|
||||
try:
|
||||
success = sage.mettre_a_jour_champ_libre(
|
||||
req.doc_id,
|
||||
req.type_doc,
|
||||
req.nom_champ,
|
||||
req.valeur
|
||||
req.doc_id, req.type_doc, req.nom_champ, req.valeur
|
||||
)
|
||||
return {"success": success}
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur MAJ champ libre: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# =====================================================
|
||||
# ENDPOINTS - CONTACTS
|
||||
# =====================================================
|
||||
|
|
@ -301,6 +437,181 @@ def contact_read(req: CodeRequest):
|
|||
logger.error(f"Erreur lecture contact: {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
|
||||
# =====================================================
|
||||
|
|
@ -312,21 +623,23 @@ def refresh_cache():
|
|||
return {
|
||||
"success": True,
|
||||
"message": "Cache actualisé",
|
||||
"info": sage.get_cache_info()
|
||||
"info": sage.get_cache_info(),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur refresh cache: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.get("/sage/cache/info", dependencies=[Depends(verify_token)])
|
||||
def cache_info():
|
||||
"""Informations sur le cache"""
|
||||
def cache_info_get():
|
||||
"""Informations sur le cache (endpoint GET)"""
|
||||
try:
|
||||
return {"success": True, "data": sage.get_cache_info()}
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur info cache: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
# =====================================================
|
||||
# LANCEMENT
|
||||
# =====================================================
|
||||
|
|
@ -336,5 +649,5 @@ if __name__ == "__main__":
|
|||
host=settings.api_host,
|
||||
port=settings.api_port,
|
||||
reload=False, # Pas de reload en production
|
||||
log_level="info"
|
||||
log_level="info",
|
||||
)
|
||||
Loading…
Reference in a new issue