1836 lines
62 KiB
Python
1836 lines
62 KiB
Python
from fastapi import FastAPI, HTTPException, Header, Depends, Query
|
||
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from typing import Optional
|
||
from datetime import datetime
|
||
import uvicorn
|
||
import logging
|
||
import win32com.client
|
||
from config import settings, validate_settings
|
||
from sage_connector import SageConnector
|
||
from schemas import (
|
||
TiersList,
|
||
ContactCreate,
|
||
ContactDelete,
|
||
ContactGet,
|
||
ContactList,
|
||
ContactUpdate,
|
||
ClientCreate,
|
||
ClientUpdate,
|
||
FiltreRequest,
|
||
ChampLibre,
|
||
CodeRequest,
|
||
DevisRequest,
|
||
DocumentGet,
|
||
FournisseurCreate,
|
||
FournisseurUpdate,
|
||
AvoirCreate,
|
||
AvoirUpdate,
|
||
CommandeCreate,
|
||
CommandeUpdate,
|
||
FactureCreate,
|
||
FactureUpdate,
|
||
LivraisonCreate,
|
||
LivraisonUpdate,
|
||
ArticleCreate,
|
||
ArticleUpdate,
|
||
EntreeStock,
|
||
SortieStock,
|
||
FamilleCreate,
|
||
PDFGeneration,
|
||
DevisUpdate,
|
||
CollaborateurCreateRequest,
|
||
CollaborateurListRequest,
|
||
CollaborateurNumeroRequest,
|
||
CollaborateurUpdateRequest,
|
||
)
|
||
from schemas.documents.reglements import (
|
||
ReglementFactureRequest,
|
||
ReglementMultipleRequest,
|
||
)
|
||
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||
handlers=[logging.FileHandler("sage_gateway.log"), logging.StreamHandler()],
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def verify_token(x_sage_token: str = Header(...)):
|
||
"""Vérification du token d'authentification"""
|
||
if x_sage_token != settings.sage_gateway_token:
|
||
logger.warning(f" Token invalide reçu: {x_sage_token[:20]}...")
|
||
raise HTTPException(401, "Token invalide")
|
||
return True
|
||
|
||
|
||
app = FastAPI(
|
||
title="Sage Gateway - Windows Server",
|
||
version="4.0.0",
|
||
description="Passerelle d'accès à Sage 100c pour VPS Linux",
|
||
)
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=settings.cors_origins,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
allow_credentials=True,
|
||
)
|
||
|
||
sage: Optional[SageConnector] = None
|
||
|
||
|
||
@app.on_event("startup")
|
||
def startup():
|
||
global sage
|
||
|
||
logger.info("🚀 Démarrage Sage Gateway Windows...")
|
||
|
||
try:
|
||
validate_settings()
|
||
logger.info(" Configuration validée")
|
||
except ValueError as e:
|
||
logger.error(f" Configuration invalide: {e}")
|
||
raise
|
||
|
||
sage = SageConnector(
|
||
settings.chemin_base,
|
||
settings.sql_server_name,
|
||
settings.sql_server_database,
|
||
settings.utilisateur,
|
||
settings.mot_de_passe,
|
||
)
|
||
|
||
if not sage.connecter():
|
||
raise RuntimeError(" Impossible de se connecter à Sage 100c")
|
||
|
||
logger.info(" Sage Gateway démarré et connecté")
|
||
|
||
|
||
@app.on_event("shutdown")
|
||
def shutdown():
|
||
if sage:
|
||
sage.deconnecter()
|
||
logger.info("👋 Sage Gateway arrêté")
|
||
|
||
|
||
@app.get("/health")
|
||
def health():
|
||
"""Health check"""
|
||
return {
|
||
"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(),
|
||
}
|
||
|
||
|
||
@app.post("/sage/clients/list", dependencies=[Depends(verify_token)])
|
||
def clients_list(req: FiltreRequest):
|
||
"""Liste des clients avec filtre optionnel"""
|
||
try:
|
||
clients = sage.lister_tous_clients(req.filtre)
|
||
return {"success": True, "data": clients}
|
||
except Exception as e:
|
||
logger.error(f"Erreur liste clients: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/clients/update", dependencies=[Depends(verify_token)])
|
||
def modifier_client_endpoint(req: ClientUpdate):
|
||
try:
|
||
resultat = sage.modifier_client(req.code, req.client_data)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier modification client: {e}")
|
||
raise HTTPException(404, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique modification client: {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"""
|
||
try:
|
||
client = sage.lire_client(req.code)
|
||
if not client:
|
||
raise HTTPException(404, f"Client {req.code} non trouvé")
|
||
return {"success": True, "data": client}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture client: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/clients/create", dependencies=[Depends(verify_token)])
|
||
def create_client_endpoint(req: ClientCreate):
|
||
"""Création d'un client dans Sage"""
|
||
try:
|
||
resultat = sage.creer_client(req.dict())
|
||
return {"success": True, "data": resultat}
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier création client: {e}")
|
||
raise HTTPException(400, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique création client: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/articles/list", dependencies=[Depends(verify_token)])
|
||
def articles_list(req: FiltreRequest):
|
||
"""Liste des articles avec filtre optionnel"""
|
||
try:
|
||
articles = sage.lister_tous_articles(req.filtre)
|
||
return {"success": True, "data": articles}
|
||
except Exception as e:
|
||
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"""
|
||
try:
|
||
article = sage.lire_article(req.code)
|
||
if not article:
|
||
raise HTTPException(404, f"Article {req.code} non trouvé")
|
||
return {"success": True, "data": article}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture article: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/devis/create", dependencies=[Depends(verify_token)])
|
||
def creer_devis(req: DevisRequest):
|
||
"""Création d'un devis"""
|
||
try:
|
||
devis_data = {
|
||
"client": {"code": req.client_id, "intitule": ""},
|
||
"date_devis": req.date_devis or datetime.now(),
|
||
"date_livraison": req.date_livraison or datetime.now(),
|
||
"reference": req.reference,
|
||
"lignes": req.lignes,
|
||
}
|
||
|
||
resultat = sage.creer_devis_enrichi(devis_data)
|
||
return {"success": True, "data": resultat}
|
||
except Exception as e:
|
||
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):
|
||
try:
|
||
devis = sage.lire_devis(req.code)
|
||
if not devis:
|
||
raise HTTPException(404, f"Devis {req.code} non trouvé")
|
||
return {"success": True, "data": devis}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture devis: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/devis/list", dependencies=[Depends(verify_token)])
|
||
def devis_list(
|
||
limit: int = Query(1000, description="Nombre max de devis"),
|
||
statut: Optional[int] = Query(None, description="Filtrer par statut"),
|
||
filtre: str = Query("", description="Filtre texte (numero, client)"),
|
||
):
|
||
try:
|
||
devis_list = sage.lister_tous_devis_cache(filtre)
|
||
|
||
if statut is not None:
|
||
devis_list = [d for d in devis_list if d.get("statut") == statut]
|
||
|
||
devis_list = devis_list[:limit]
|
||
|
||
logger.info(f" {len(devis_list)} devis retournés depuis le cache")
|
||
|
||
return {"success": True, "data": devis_list}
|
||
|
||
except Exception as e:
|
||
logger.error(f" Erreur liste devis: {e}", exc_info=True)
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/document/statut", dependencies=[Depends(verify_token)])
|
||
def changer_statut_document(numero: str, type_doc: int, nouveau_statut: int):
|
||
"""Change le statut d'un document"""
|
||
try:
|
||
with sage._com_context(), sage._lock_com:
|
||
factory = sage.cial.FactoryDocumentVente
|
||
persist = factory.ReadPiece(type_doc, numero)
|
||
|
||
if not persist:
|
||
raise HTTPException(404, f"Document {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 document {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))
|
||
|
||
|
||
@app.post("/sage/documents/get", dependencies=[Depends(verify_token)])
|
||
def lire_document(req: DocumentGet):
|
||
"""Lecture d'un document (commande, facture, etc.)"""
|
||
try:
|
||
doc = sage.lire_document(req.numero, req.type_doc)
|
||
if not doc:
|
||
raise HTTPException(404, f"Document {req.numero} non trouvé")
|
||
return {"success": True, "data": doc}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture document: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/documents/transform", dependencies=[Depends(verify_token)])
|
||
def transformer_document(
|
||
numero_source: str = Query(..., description="Numéro du document source"),
|
||
type_source: int = Query(..., description="Type document source"),
|
||
type_cible: int = Query(..., description="Type document cible"),
|
||
):
|
||
try:
|
||
logger.info(
|
||
f"🔄 Transformation demandée: {numero_source} "
|
||
f"(type {type_source}) → type {type_cible}"
|
||
)
|
||
|
||
transformations_valides = {
|
||
(0, 10),
|
||
(10, 30),
|
||
(10, 60),
|
||
(30, 60),
|
||
(0, 60),
|
||
}
|
||
|
||
if (type_source, type_cible) not in transformations_valides:
|
||
logger.error(f" Transformation non autorisée: {type_source} → {type_cible}")
|
||
raise HTTPException(
|
||
400,
|
||
f"Transformation non autorisée: type {type_source} → type {type_cible}. "
|
||
f"Transformations valides: {transformations_valides}",
|
||
)
|
||
|
||
resultat = sage.transformer_document(numero_source, type_source, type_cible)
|
||
|
||
logger.info(
|
||
f" Transformation réussie: {numero_source} → "
|
||
f"{resultat.get('document_cible', '?')} "
|
||
f"({resultat.get('nb_lignes', 0)} lignes)"
|
||
)
|
||
|
||
return {"success": True, "data": resultat}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except ValueError as e:
|
||
logger.error(f" Erreur métier transformation: {e}")
|
||
raise HTTPException(400, str(e))
|
||
except Exception as e:
|
||
logger.error(f" Erreur technique transformation: {e}", exc_info=True)
|
||
raise HTTPException(500, f"Erreur transformation: {str(e)}")
|
||
|
||
|
||
@app.post("/sage/documents/champ-libre", dependencies=[Depends(verify_token)])
|
||
def maj_champ_libre(req: ChampLibre):
|
||
try:
|
||
success = sage.mettre_a_jour_champ_libre(
|
||
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))
|
||
|
||
|
||
@app.post("/sage/documents/derniere-relance", dependencies=[Depends(verify_token)])
|
||
def maj_derniere_relance(doc_id: str, type_doc: int):
|
||
try:
|
||
success = sage.mettre_a_jour_derniere_relance(doc_id, type_doc)
|
||
return {"success": success}
|
||
except Exception as e:
|
||
logger.error(f"Erreur MAJ dernière relance: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/contact/read", dependencies=[Depends(verify_token)])
|
||
def contact_read(req: CodeRequest):
|
||
"""Lecture du contact principal d'un client"""
|
||
try:
|
||
contact = sage.lire_contact_principal_client(req.code)
|
||
if not contact:
|
||
raise HTTPException(404, f"Contact non trouvé pour client {req.code}")
|
||
return {"success": True, "data": contact}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
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 = Query(100, description="Nombre max de commandes"),
|
||
statut: Optional[int] = Query(None, description="Filtrer par statut"),
|
||
filtre: str = Query("", description="Filtre texte"),
|
||
):
|
||
try:
|
||
commandes = sage.lister_toutes_commandes_cache(filtre)
|
||
|
||
return {"success": True, "data": commandes}
|
||
|
||
except Exception as e:
|
||
logger.error(f" Erreur liste commandes: {e}", exc_info=True)
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/factures/list", dependencies=[Depends(verify_token)])
|
||
def factures_list(
|
||
limit: int = Query(100, description="Nombre max de factures"),
|
||
statut: Optional[int] = Query(None, description="Filtrer par statut"),
|
||
filtre: str = Query("", description="Filtre texte"),
|
||
):
|
||
try:
|
||
factures = sage.lister_toutes_factures_cache(filtre)
|
||
|
||
if statut is not None:
|
||
factures = [f for f in factures if f.get("statut") == statut]
|
||
|
||
factures = factures[:limit]
|
||
|
||
logger.info(f" {len(factures)} factures retournées depuis le cache")
|
||
|
||
return {"success": True, "data": factures}
|
||
|
||
except Exception as e:
|
||
logger.error(f" Erreur liste factures: {e}", exc_info=True)
|
||
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
|
||
|
||
try:
|
||
remise_max = float(getattr(client_obj, "CT_RemiseMax", 10.0))
|
||
except Exception:
|
||
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))
|
||
|
||
|
||
@app.post("/sage/cache/refresh", dependencies=[Depends(verify_token)])
|
||
def refresh_cache():
|
||
"""Force le rafraîchissement du cache"""
|
||
try:
|
||
sage.forcer_actualisation_cache()
|
||
return {
|
||
"success": True,
|
||
"message": "Cache actualisé",
|
||
"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_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))
|
||
|
||
|
||
@app.post("/sage/prospects/list", dependencies=[Depends(verify_token)])
|
||
def prospects_list(req: FiltreRequest):
|
||
try:
|
||
prospects = sage.lister_tous_prospects(req.filtre)
|
||
return {"success": True, "data": prospects}
|
||
except Exception as e:
|
||
logger.error(f"Erreur liste prospects: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/prospects/get", dependencies=[Depends(verify_token)])
|
||
def prospect_get(req: CodeRequest):
|
||
try:
|
||
prospect = sage.lire_prospect(req.code)
|
||
if not prospect:
|
||
raise HTTPException(404, f"Prospect {req.code} non trouvé")
|
||
return {"success": True, "data": prospect}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture prospect: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/fournisseurs/list", dependencies=[Depends(verify_token)])
|
||
def fournisseurs_list(req: FiltreRequest):
|
||
try:
|
||
fournisseurs = sage.lister_tous_fournisseurs_cache(req.filtre)
|
||
|
||
logger.info(f" {len(fournisseurs)} fournisseurs retournés depuis le cache")
|
||
|
||
return {"success": True, "data": fournisseurs}
|
||
|
||
except Exception as e:
|
||
logger.error(f" Erreur liste fournisseurs: {e}", exc_info=True)
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/fournisseurs/create", dependencies=[Depends(verify_token)])
|
||
def create_fournisseur_endpoint(req: FournisseurCreate):
|
||
try:
|
||
resultat = sage.creer_fournisseur(req.dict())
|
||
|
||
logger.info(f" Fournisseur créé: {resultat.get('numero')}")
|
||
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f" Erreur métier création fournisseur: {e}")
|
||
raise HTTPException(400, str(e))
|
||
|
||
except Exception as e:
|
||
logger.error(f" Erreur technique création fournisseur: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/fournisseurs/update", dependencies=[Depends(verify_token)])
|
||
def modifier_fournisseur_endpoint(req: FournisseurUpdate):
|
||
try:
|
||
resultat = sage.modifier_fournisseur(req.code, req.fournisseur_data)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier modification fournisseur: {e}")
|
||
raise HTTPException(404, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique modification fournisseur: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/fournisseurs/get", dependencies=[Depends(verify_token)])
|
||
def fournisseur_get(req: CodeRequest):
|
||
"""
|
||
NOUVEAU : Lecture d'un fournisseur par code
|
||
"""
|
||
try:
|
||
fournisseur = sage.lire_fournisseur(req.code)
|
||
if not fournisseur:
|
||
raise HTTPException(404, f"Fournisseur {req.code} non trouvé")
|
||
return {"success": True, "data": fournisseur}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture fournisseur: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/avoirs/list", dependencies=[Depends(verify_token)])
|
||
def avoirs_list(
|
||
limit: int = Query(100, description="Nombre max d'avoirs"),
|
||
statut: Optional[int] = Query(None, description="Filtrer par statut"),
|
||
filtre: str = Query("", description="Filtre texte"),
|
||
):
|
||
try:
|
||
avoirs = sage.lister_tous_avoirs_cache(filtre)
|
||
|
||
if statut is not None:
|
||
avoirs = [a for a in avoirs if a.get("statut") == statut]
|
||
|
||
avoirs = avoirs[:limit]
|
||
|
||
logger.info(f" {len(avoirs)} avoirs retournés depuis le cache")
|
||
|
||
return {"success": True, "data": avoirs}
|
||
|
||
except Exception as e:
|
||
logger.error(f" Erreur liste avoirs: {e}", exc_info=True)
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/avoirs/get", dependencies=[Depends(verify_token)])
|
||
def avoir_get(req: CodeRequest):
|
||
try:
|
||
avoir = sage.lire_avoir_cache(req.code)
|
||
|
||
if avoir:
|
||
logger.info(f" Avoir {req.code} retourné depuis le cache")
|
||
return {"success": True, "data": avoir, "source": "cache"}
|
||
|
||
logger.info(f" Avoir {req.code} absent du cache, lecture depuis Sage...")
|
||
avoir = sage.lire_avoir(req.code)
|
||
|
||
if not avoir:
|
||
raise HTTPException(404, f"Avoir {req.code} non trouvé")
|
||
|
||
return {"success": True, "data": avoir, "source": "sage"}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture avoir: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/livraisons/list", dependencies=[Depends(verify_token)])
|
||
def livraisons_list(
|
||
limit: int = Query(100, description="Nombre max de livraisons"),
|
||
statut: Optional[int] = Query(None, description="Filtrer par statut"),
|
||
filtre: str = Query("", description="Filtre texte"),
|
||
):
|
||
try:
|
||
livraisons = sage.lister_toutes_livraisons_cache(filtre)
|
||
|
||
if statut is not None:
|
||
livraisons = [
|
||
ligne for ligne in livraisons if ligne.get("statut") == statut
|
||
]
|
||
|
||
livraisons = livraisons[:limit]
|
||
|
||
logger.info(f" {len(livraisons)} livraisons retournées depuis le cache")
|
||
|
||
return {"success": True, "data": livraisons}
|
||
|
||
except Exception as e:
|
||
logger.error(f" Erreur liste livraisons: {e}", exc_info=True)
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/livraisons/get", dependencies=[Depends(verify_token)])
|
||
def livraison_get(req: CodeRequest):
|
||
try:
|
||
livraison = sage.lire_livraison_cache(req.code)
|
||
|
||
if livraison:
|
||
logger.info(f" Livraison {req.code} retournée depuis le cache")
|
||
return {"success": True, "data": livraison, "source": "cache"}
|
||
|
||
logger.info(f" Livraison {req.code} absente du cache, lecture depuis Sage...")
|
||
livraison = sage.lire_livraison(req.code)
|
||
|
||
if not livraison:
|
||
raise HTTPException(404, f"Livraison {req.code} non trouvée")
|
||
|
||
return {"success": True, "data": livraison, "source": "sage"}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture livraison: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/devis/update", dependencies=[Depends(verify_token)])
|
||
def modifier_devis_endpoint(req: DevisUpdate):
|
||
try:
|
||
resultat = sage.modifier_devis(req.numero, req.devis_data)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier modification devis: {e}")
|
||
raise HTTPException(404, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique modification devis: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/commandes/create", dependencies=[Depends(verify_token)])
|
||
def creer_commande_endpoint(req: CommandeCreate):
|
||
try:
|
||
commande_data = {
|
||
"client": {"code": req.client_id, "intitule": ""},
|
||
"date_commande": req.date_commande or datetime.now(),
|
||
"date_livraison": req.date_livraison or datetime.now(),
|
||
"reference": req.reference,
|
||
"lignes": req.lignes,
|
||
}
|
||
|
||
resultat = sage.creer_commande_enrichi(commande_data)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier création commande: {e}")
|
||
raise HTTPException(400, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique création commande: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/commandes/update", dependencies=[Depends(verify_token)])
|
||
def modifier_commande_endpoint(req: CommandeUpdate):
|
||
try:
|
||
resultat = sage.modifier_commande(req.numero, req.commande_data)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier modification commande: {e}")
|
||
raise HTTPException(404, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique modification commande: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/livraisons/create", dependencies=[Depends(verify_token)])
|
||
def creer_livraison_endpoint(req: LivraisonCreate):
|
||
try:
|
||
client = sage.lire_client(req.client_id)
|
||
if not client:
|
||
raise HTTPException(404, f"Client {req.client_id} introuvable")
|
||
|
||
livraison_data = {
|
||
"client": {"code": req.client_id, "intitule": ""},
|
||
"date_livraison": req.date_livraison or datetime.now(),
|
||
"date_livraison_prevue": req.date_livraison or datetime.now(),
|
||
"reference": req.reference,
|
||
"lignes": req.lignes,
|
||
}
|
||
|
||
resultat = sage.creer_livraison_enrichi(livraison_data)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier création livraison: {e}")
|
||
raise HTTPException(400, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique création livraison: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/livraisons/update", dependencies=[Depends(verify_token)])
|
||
def modifier_livraison_endpoint(req: LivraisonUpdate):
|
||
try:
|
||
resultat = sage.modifier_livraison(req.numero, req.livraison_data)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier modification livraison: {e}")
|
||
raise HTTPException(404, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique modification livraison: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/avoirs/create", dependencies=[Depends(verify_token)])
|
||
def creer_avoir_endpoint(req: AvoirCreate):
|
||
try:
|
||
client = sage.lire_client(req.client_id)
|
||
if not client:
|
||
raise HTTPException(404, f"Client {req.client_id} introuvable")
|
||
|
||
avoir_data = {
|
||
"client": {"code": req.client_id, "intitule": ""},
|
||
"date_avoir": req.date_avoir or datetime.now(),
|
||
"date_livraison": req.date_livraison or datetime.now(),
|
||
"reference": req.reference,
|
||
"lignes": req.lignes,
|
||
}
|
||
|
||
resultat = sage.creer_avoir_enrichi(avoir_data)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier création avoir: {e}")
|
||
raise HTTPException(400, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique création avoir: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/avoirs/update", dependencies=[Depends(verify_token)])
|
||
def modifier_avoir_endpoint(req: AvoirUpdate):
|
||
"""
|
||
✏️ Modification d'un avoir dans Sage
|
||
"""
|
||
try:
|
||
resultat = sage.modifier_avoir(req.numero, req.avoir_data)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier modification avoir: {e}")
|
||
raise HTTPException(404, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique modification avoir: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/factures/create", dependencies=[Depends(verify_token)])
|
||
def creer_facture_endpoint(req: FactureCreate):
|
||
try:
|
||
client = sage.lire_client(req.client_id)
|
||
if not client:
|
||
raise HTTPException(404, f"Client {req.client_id} introuvable")
|
||
|
||
facture_data = {
|
||
"client": {"code": req.client_id, "intitule": ""},
|
||
"date_facture": req.date_facture or datetime.now(),
|
||
"date_livraison": req.date_livraison or datetime.now(),
|
||
"reference": req.reference,
|
||
"lignes": req.lignes,
|
||
}
|
||
|
||
resultat = sage.creer_facture_enrichi(facture_data)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier création facture: {e}")
|
||
raise HTTPException(400, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique création facture: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/factures/update", dependencies=[Depends(verify_token)])
|
||
def modifier_facture_endpoint(req: FactureUpdate):
|
||
try:
|
||
resultat = sage.modifier_facture(req.numero, req.facture_data)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier modification facture: {e}")
|
||
raise HTTPException(404, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique modification facture: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/articles/create", dependencies=[Depends(verify_token)])
|
||
def create_article_endpoint(req: ArticleCreate):
|
||
try:
|
||
logger.info(f"[ENDPOINT] Création article : {req.reference}")
|
||
logger.debug(f"[ENDPOINT] Données reçues : {req.dict()}")
|
||
|
||
resultat = sage.creer_article(req.dict())
|
||
|
||
logger.info(f"[ENDPOINT] Article créé : {resultat.get('reference')}")
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"[ENDPOINT] Erreur métier création article: {e}")
|
||
raise HTTPException(400, str(e))
|
||
|
||
except Exception as e:
|
||
logger.error(
|
||
f"[ENDPOINT] Erreur technique création article: {e}", exc_info=True
|
||
)
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/articles/update", dependencies=[Depends(verify_token)])
|
||
def modifier_article_endpoint(req: ArticleUpdate):
|
||
try:
|
||
logger.info(f"[ENDPOINT] Modification article : {req.reference}")
|
||
logger.debug(f"[ENDPOINT] Champs à modifier : {list(req.article_data.keys())}")
|
||
|
||
resultat = sage.modifier_article(req.reference, req.article_data)
|
||
|
||
logger.info(f"[ENDPOINT] Article modifié : {req.reference}")
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"[ENDPOINT] Erreur métier modification article: {e}")
|
||
raise HTTPException(404, str(e))
|
||
|
||
except Exception as e:
|
||
logger.error(
|
||
f"[ENDPOINT] Erreur technique modification article: {e}", exc_info=True
|
||
)
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post(
|
||
"/sage/familles/create",
|
||
response_model=dict,
|
||
)
|
||
async def creer_famille(famille: FamilleCreate):
|
||
"""Crée une famille d'articles dans Sage 100c"""
|
||
try:
|
||
resultat = sage.creer_famille(famille.dict())
|
||
return {
|
||
"success": True,
|
||
"message": f"Famille {resultat['code']} créée avec succès",
|
||
"data": resultat,
|
||
}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier création famille : {e}")
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
|
||
except Exception as e:
|
||
logger.error(f"Erreur création famille : {e}", exc_info=True)
|
||
raise HTTPException(status_code=500, detail=f"Erreur serveur : {str(e)}")
|
||
|
||
|
||
@app.get(
|
||
"/sage/familles",
|
||
response_model=dict,
|
||
)
|
||
async def lister_familles(filtre: str = ""):
|
||
try:
|
||
familles = sage.lister_toutes_familles(filtre=filtre)
|
||
|
||
return {
|
||
"success": True,
|
||
"count": len(familles),
|
||
"filtre": filtre if filtre else None,
|
||
"data": familles,
|
||
"meta": {
|
||
"methode": "SQL direct (F_FAMILLE)",
|
||
"temps_reponse": "< 1 seconde",
|
||
},
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Erreur listage familles : {e}", exc_info=True)
|
||
raise HTTPException(status_code=500, detail=f"Erreur serveur : {str(e)}")
|
||
|
||
|
||
@app.get(
|
||
"/sage/familles/{code}",
|
||
response_model=dict,
|
||
)
|
||
async def lire_famille(code: str):
|
||
try:
|
||
familles = sage.lister_toutes_familles()
|
||
|
||
famille = next((f for f in familles if f["code"].upper() == code.upper()), None)
|
||
|
||
if not famille:
|
||
raise HTTPException(status_code=404, detail=f"Famille {code} introuvable")
|
||
|
||
return {"success": True, "data": famille}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture famille {code} : {e}", exc_info=True)
|
||
raise HTTPException(status_code=500, detail=f"Erreur serveur : {str(e)}")
|
||
|
||
|
||
@app.get("/sage/familles/stats", response_model=dict)
|
||
async def stats_familles():
|
||
try:
|
||
familles = sage.lister_toutes_familles()
|
||
|
||
nb_total = len(familles)
|
||
nb_detail = sum(1 for f in familles if f["type"] == 0)
|
||
nb_total_type = sum(1 for f in familles if f["type"] == 1)
|
||
nb_statistiques = sum(1 for f in familles if f["est_statistique"])
|
||
|
||
top_familles = sorted(familles, key=lambda f: f["intitule"])[:10]
|
||
|
||
return {
|
||
"success": True,
|
||
"stats": {
|
||
"total": nb_total,
|
||
"detail": nb_detail,
|
||
"total_type": nb_total_type,
|
||
"statistiques": nb_statistiques,
|
||
"pourcentage_detail": (
|
||
round((nb_detail / nb_total * 100), 2) if nb_total > 0 else 0
|
||
),
|
||
},
|
||
"top_10": [
|
||
{
|
||
"code": f["code"],
|
||
"intitule": f["intitule"],
|
||
"type_libelle": f["type_libelle"],
|
||
}
|
||
for f in top_familles
|
||
],
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Erreur stats familles : {e}", exc_info=True)
|
||
raise HTTPException(status_code=500, detail=f"Erreur serveur : {str(e)}")
|
||
|
||
|
||
@app.post("/sage/documents/generate-pdf", dependencies=[Depends(verify_token)])
|
||
def generer_pdf_document(req: PDFGeneration):
|
||
try:
|
||
logger.info(f" Génération PDF: {req.doc_id} (type={req.type_doc})")
|
||
|
||
pdf_bytes = sage.generer_pdf_document(req.doc_id, req.type_doc)
|
||
|
||
if not pdf_bytes:
|
||
raise HTTPException(500, "PDF vide généré")
|
||
|
||
import base64
|
||
|
||
pdf_base64 = base64.b64encode(pdf_bytes).decode("utf-8")
|
||
|
||
logger.info(f" PDF généré: {len(pdf_bytes)} octets")
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"pdf_base64": pdf_base64,
|
||
"taille_octets": len(pdf_bytes),
|
||
"type_doc": req.type_doc,
|
||
"numero": req.doc_id,
|
||
},
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f" Erreur génération PDF: {e}", exc_info=True)
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/depots/list", dependencies=[Depends(verify_token)])
|
||
def lister_depots():
|
||
try:
|
||
if not sage or not sage.cial:
|
||
raise HTTPException(503, "Service Sage indisponible")
|
||
|
||
with sage._com_context(), sage._lock_com:
|
||
depots = []
|
||
|
||
try:
|
||
factory_depot = sage.cial.FactoryDepot
|
||
|
||
index = 1
|
||
while index <= 100:
|
||
try:
|
||
persist = factory_depot.List(index)
|
||
|
||
if persist is None:
|
||
logger.info(f" ℹ️ Fin de liste à l'index {index} (None)")
|
||
break
|
||
|
||
depot = win32com.client.CastTo(persist, "IBODepot3")
|
||
depot.Read()
|
||
|
||
code = ""
|
||
numero = 0
|
||
intitule = ""
|
||
contact = ""
|
||
exclu = False
|
||
|
||
try:
|
||
code = getattr(depot, "DE_Code", "").strip()
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
numero = int(getattr(depot, "Compteur", 0))
|
||
except Exception:
|
||
try:
|
||
numero = int(code)
|
||
except Exception:
|
||
numero = 0
|
||
|
||
try:
|
||
intitule = getattr(depot, "DE_Intitule", "")
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
contact = getattr(depot, "DE_Contact", "")
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
exclu = getattr(depot, "DE_Exclure", False)
|
||
except Exception:
|
||
pass
|
||
|
||
if not code:
|
||
logger.warning(f" Dépôt à l'index {index} sans code")
|
||
index += 1
|
||
continue
|
||
|
||
adresse_complete = ""
|
||
try:
|
||
adresse_obj = getattr(depot, "Adresse", None)
|
||
if adresse_obj:
|
||
try:
|
||
adresse = getattr(adresse_obj, "Adresse", "")
|
||
cp = getattr(adresse_obj, "CodePostal", "")
|
||
ville = getattr(adresse_obj, "Ville", "")
|
||
adresse_complete = f"{adresse} {cp} {ville}".strip()
|
||
except Exception:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
|
||
principal = False
|
||
if not exclu and len(depots) == 0:
|
||
principal = True
|
||
|
||
depot_info = {
|
||
"code": code,
|
||
"numero": numero,
|
||
"intitule": intitule,
|
||
"adresse": adresse_complete,
|
||
"contact": contact,
|
||
"exclu": exclu,
|
||
"principal": principal,
|
||
"index_sage": index,
|
||
}
|
||
|
||
depots.append(depot_info)
|
||
|
||
logger.info(
|
||
f" Dépôt {index}: code='{code}', compteur={numero}, intitulé='{intitule}'"
|
||
)
|
||
|
||
index += 1
|
||
|
||
except Exception as e:
|
||
error_msg = str(e)
|
||
if "Accès refusé" in error_msg or "-1073741819" in error_msg:
|
||
logger.info(
|
||
f" ℹ️ Fin de liste à l'index {index} (Accès refusé)"
|
||
)
|
||
break
|
||
else:
|
||
logger.error(f" Erreur inattendue index {index}: {e}")
|
||
index += 1
|
||
continue
|
||
|
||
logger.info(f" {len(depots)} dépôt(s) trouvé(s)")
|
||
|
||
if not depots:
|
||
return {
|
||
"success": False,
|
||
"depots": [],
|
||
"message": "Aucun dépôt trouvé dans Sage",
|
||
}
|
||
|
||
return {
|
||
"success": True,
|
||
"depots": depots,
|
||
"nb_depots": len(depots),
|
||
"version_sage": {
|
||
"identifiant_code": "DE_Code (string)",
|
||
"identifiant_numero": "Compteur (int)",
|
||
"fin_liste": "Erreur 'Accès refusé' au lieu de None",
|
||
},
|
||
"conseil": f"Utilisez le 'code' (ex: '{depots[0]['code']}') lors de la création d'articles avec stock",
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f" Erreur lecture dépôts: {e}", exc_info=True)
|
||
raise HTTPException(500, f"Erreur lecture dépôts: {str(e)}")
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f" Erreur: {e}", exc_info=True)
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/stock/entree", dependencies=[Depends(verify_token)])
|
||
def creer_entree_stock(req: EntreeStock):
|
||
try:
|
||
logger.info(
|
||
f"📦 [ENTREE STOCK] Création bon d'entrée : {len(req.lignes)} ligne(s)"
|
||
)
|
||
|
||
entree_data = {
|
||
"date_mouvement": req.date_entree or datetime.now(),
|
||
"reference": req.reference,
|
||
"depot_code": req.depot_code,
|
||
"lignes": [ligne.dict() for ligne in req.lignes],
|
||
"commentaire": req.commentaire,
|
||
}
|
||
|
||
resultat = sage.creer_entree_stock(entree_data)
|
||
|
||
logger.info(f" [ENTREE STOCK] Créé : {resultat.get('numero')}")
|
||
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f" Erreur métier entrée stock : {e}")
|
||
raise HTTPException(400, str(e))
|
||
|
||
except Exception as e:
|
||
logger.error(f" Erreur technique entrée stock : {e}", exc_info=True)
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/stock/sortie", dependencies=[Depends(verify_token)])
|
||
def creer_sortie_stock(req: SortieStock):
|
||
try:
|
||
logger.info(
|
||
f"📤 [SORTIE STOCK] Création bon de sortie : {len(req.lignes)} ligne(s)"
|
||
)
|
||
|
||
sortie_data = {
|
||
"date_mouvement": req.date_sortie or datetime.now(),
|
||
"reference": req.reference,
|
||
"depot_code": req.depot_code,
|
||
"lignes": [ligne.dict() for ligne in req.lignes],
|
||
"commentaire": req.commentaire,
|
||
}
|
||
|
||
resultat = sage.creer_sortie_stock(sortie_data)
|
||
|
||
logger.info(f" [SORTIE STOCK] Créé : {resultat.get('numero')}")
|
||
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f" Erreur métier sortie stock : {e}")
|
||
raise HTTPException(400, str(e))
|
||
|
||
except Exception as e:
|
||
logger.error(f" Erreur technique sortie stock : {e}", exc_info=True)
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/stock/mouvement/{numero}", dependencies=[Depends(verify_token)])
|
||
def lire_mouvement_stock(numero: str):
|
||
try:
|
||
mouvement = sage.lire_mouvement_stock(numero)
|
||
|
||
if not mouvement:
|
||
raise HTTPException(404, f"Mouvement de stock {numero} non trouvé")
|
||
|
||
return {"success": True, "data": mouvement}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f" Erreur lecture mouvement : {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/contacts/create", dependencies=[Depends(verify_token)])
|
||
def contacts_create(req: ContactCreate):
|
||
"""Crée un nouveau contact"""
|
||
try:
|
||
contact = sage.creer_contact(req.dict())
|
||
return {"success": True, "data": contact}
|
||
except ValueError as e:
|
||
logger.error(f"Erreur validation contact: {e}")
|
||
raise HTTPException(400, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur création contact: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/contacts/list", dependencies=[Depends(verify_token)])
|
||
def contacts_list(req: ContactList):
|
||
"""Liste les contacts d'un client"""
|
||
try:
|
||
contacts = sage.lister_contacts(req.numero)
|
||
return {"success": True, "data": contacts}
|
||
except Exception as e:
|
||
logger.error(f"Erreur liste contacts: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/contacts/get", dependencies=[Depends(verify_token)])
|
||
def contacts_get(req: ContactGet):
|
||
"""Récupère un contact spécifique"""
|
||
try:
|
||
contact = sage.obtenir_contact(req.numero, req.contact_numero)
|
||
if not contact:
|
||
raise HTTPException(404, f"Contact {req.contact_numero} non trouvé")
|
||
return {"success": True, "data": contact}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur récupération contact: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/contacts/update", dependencies=[Depends(verify_token)])
|
||
def contacts_update(req: ContactUpdate):
|
||
"""Modifie un contact existant"""
|
||
try:
|
||
contact = sage.modifier_contact(req.numero, req.contact_numero, req.updates)
|
||
return {"success": True, "data": contact}
|
||
except ValueError as e:
|
||
logger.error(f"Erreur validation contact: {e}")
|
||
raise HTTPException(400, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur modification contact: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/contacts/delete", dependencies=[Depends(verify_token)])
|
||
def contacts_delete(req: ContactDelete):
|
||
"""Supprime un contact"""
|
||
try:
|
||
result = sage.supprimer_contact(req.numero, req.contact_numero)
|
||
return {"success": True, "data": result}
|
||
except Exception as e:
|
||
logger.error(f"Erreur suppression contact: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/contacts/set-default", dependencies=[Depends(verify_token)])
|
||
def contacts_set_default(req: ContactGet):
|
||
"""Définit un contact comme contact par défaut"""
|
||
try:
|
||
result = sage.definir_contact_defaut(req.numero, req.contact_numero)
|
||
return {"success": True, "data": result}
|
||
except Exception as e:
|
||
logger.error(f"Erreur définition contact par défaut: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/tiers/list", dependencies=[Depends(verify_token)])
|
||
def tiers_list(req: TiersList):
|
||
"""Liste des tiers avec filtres optionnels"""
|
||
try:
|
||
tiers = sage.lister_tous_tiers(type_tiers=req.type_tiers, filtre=req.filtre)
|
||
return {"success": True, "data": tiers}
|
||
except Exception as e:
|
||
logger.error(f" Erreur liste tiers: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/tiers/get", dependencies=[Depends(verify_token)])
|
||
def tiers_get(req: CodeRequest):
|
||
"""Lecture d'un tiers par code"""
|
||
try:
|
||
tiers = sage.lire_tiers(req.code)
|
||
if not tiers:
|
||
raise HTTPException(404, f"Tiers {req.code} non trouvé")
|
||
return {"success": True, "data": tiers}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f" Erreur lecture tiers: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/collaborateurs/list", dependencies=[Depends(verify_token)])
|
||
def collaborateurs_list(req: CollaborateurListRequest):
|
||
"""Liste tous les collaborateurs"""
|
||
try:
|
||
collaborateurs = sage.lister_tous_collaborateurs(
|
||
req.filtre, req.actifs_seulement
|
||
)
|
||
return {"success": True, "data": collaborateurs}
|
||
except Exception as e:
|
||
logger.error(f"Erreur liste collaborateurs: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/collaborateurs/get", dependencies=[Depends(verify_token)])
|
||
def collaborateur_get(req: CollaborateurNumeroRequest):
|
||
"""Lecture d'un collaborateur par numéro"""
|
||
try:
|
||
collaborateur = sage.lire_collaborateur(req.numero)
|
||
if not collaborateur:
|
||
raise HTTPException(404, f"Collaborateur {req.numero} non trouvé")
|
||
return {"success": True, "data": collaborateur}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture collaborateur: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/collaborateurs/create", dependencies=[Depends(verify_token)])
|
||
def collaborateur_create(req: CollaborateurCreateRequest):
|
||
"""Création d'un collaborateur"""
|
||
try:
|
||
data = req.model_dump(exclude_none=True)
|
||
nouveau = sage.creer_collaborateur(data)
|
||
|
||
if not nouveau:
|
||
raise HTTPException(500, "Échec création collaborateur")
|
||
|
||
return {"success": True, "data": nouveau}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur création collaborateur: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/collaborateurs/update", dependencies=[Depends(verify_token)])
|
||
def collaborateur_update(req: CollaborateurUpdateRequest):
|
||
"""Modification d'un collaborateur"""
|
||
try:
|
||
data = req.model_dump(exclude_unset=True, exclude={"numero"})
|
||
modifie = sage.modifier_collaborateur(req.numero, data)
|
||
|
||
if not modifie:
|
||
raise HTTPException(404, f"Collaborateur {req.numero} non trouvé")
|
||
|
||
return {"success": True, "data": modifie}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur modification collaborateur: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/societe/info", dependencies=[Depends(verify_token)])
|
||
def get_societe_info():
|
||
"""Retourne les informations de la société actuelle depuis P_DOSSIER"""
|
||
try:
|
||
societe_info = sage.lire_informations_societe()
|
||
if not societe_info:
|
||
raise HTTPException(404, "Informations société introuvables dans P_DOSSIER")
|
||
return {"success": True, "data": societe_info}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture info société: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post(
|
||
"/sage/factures/{numero_facture}/regler", dependencies=[Depends(verify_token)]
|
||
)
|
||
def regler_facture_endpoint(
|
||
numero_facture: str,
|
||
req: ReglementFactureRequest,
|
||
):
|
||
try:
|
||
date_reg = (
|
||
datetime.combine(req.date_reglement, datetime.min.time())
|
||
if req.date_reglement
|
||
else None
|
||
)
|
||
|
||
result = sage.regler_facture(
|
||
numero_facture=numero_facture,
|
||
montant=req.montant,
|
||
mode_reglement=req.mode_reglement,
|
||
date_reglement=date_reg,
|
||
reference=req.reference or "",
|
||
libelle=req.libelle or "",
|
||
code_journal=req.code_journal, # None = auto
|
||
devise_code=req.devise_code,
|
||
cours_devise=req.cours_devise,
|
||
tva_encaissement=req.tva_encaissement,
|
||
compte_general=req.compte_general,
|
||
)
|
||
return {
|
||
"success": True,
|
||
"message": "Règlement effectué avec succès",
|
||
"data": result,
|
||
}
|
||
|
||
except ValueError as e:
|
||
raise HTTPException(400, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur règlement {numero_facture}: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/reglements/multiple", dependencies=[Depends(verify_token)])
|
||
def regler_factures_client_endpoint(req: ReglementMultipleRequest):
|
||
try:
|
||
date_reg = (
|
||
datetime.combine(req.date_reglement, datetime.min.time())
|
||
if req.date_reglement
|
||
else None
|
||
)
|
||
resultat = sage.regler_factures_client(
|
||
client_code=req.client_code,
|
||
montant_total=req.montant_total,
|
||
mode_reglement=req.mode_reglement,
|
||
date_reglement=date_reg,
|
||
reference=req.reference or "",
|
||
libelle=req.libelle or "",
|
||
code_journal=req.code_journal, # None = auto
|
||
numeros_factures=req.numeros_factures,
|
||
devise_code=req.devise_code,
|
||
cours_devise=req.cours_devise,
|
||
tva_encaissement=req.tva_encaissement,
|
||
)
|
||
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier règlement multiple {req.client_code}: {e}")
|
||
raise HTTPException(400, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique règlement multiple: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get(
|
||
"/sage/factures/{numero_facture}/reglements", dependencies=[Depends(verify_token)]
|
||
)
|
||
def get_reglements_facture_endpoint(numero_facture: str):
|
||
try:
|
||
resultat = sage.lire_reglements_facture(numero_facture)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Facture introuvable: {numero_facture}")
|
||
raise HTTPException(404, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture règlements {numero_facture}: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/clients/{client_code}/reglements", dependencies=[Depends(verify_token)])
|
||
def get_reglements_client_endpoint(
|
||
client_code: str,
|
||
date_debut: Optional[datetime] = Query(None, description="Date début (filtrage)"),
|
||
date_fin: Optional[datetime] = Query(None, description="Date fin (filtrage)"),
|
||
inclure_soldees: bool = Query(True, description="Inclure les factures soldées"),
|
||
):
|
||
try:
|
||
resultat = sage.lire_reglements_client(
|
||
client_code=client_code,
|
||
date_debut=date_debut,
|
||
date_fin=date_fin,
|
||
inclure_soldees=inclure_soldees,
|
||
)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Client introuvable: {client_code}")
|
||
raise HTTPException(404, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture règlements client {client_code}: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post(
|
||
"/sage/factures/{numero_facture}/valider", dependencies=[Depends(verify_token)]
|
||
)
|
||
def valider_facture_endpoint(numero_facture: str):
|
||
try:
|
||
resultat = sage.valider_facture(numero_facture)
|
||
return {"success": True, "data": resultat}
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier validation {numero_facture}: {e}")
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
except RuntimeError as e:
|
||
logger.error(f"Erreur runtime validation {numero_facture}: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
except Exception as e:
|
||
logger.error(
|
||
f"Erreur technique validation {numero_facture}: {e}", exc_info=True
|
||
)
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@app.post(
|
||
"/sage/factures/{numero_facture}/devalider", dependencies=[Depends(verify_token)]
|
||
)
|
||
def devalider_facture_endpoint(numero_facture: str):
|
||
try:
|
||
resultat = sage.devalider_facture(numero_facture)
|
||
return {"success": True, "data": resultat}
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier dévalidation {numero_facture}: {e}")
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
except RuntimeError as e:
|
||
logger.error(f"Erreur runtime dévalidation {numero_facture}: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
except Exception as e:
|
||
logger.error(
|
||
f"Erreur technique dévalidation {numero_facture}: {e}", exc_info=True
|
||
)
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@app.get(
|
||
"/sage/factures/{numero_facture}/statut-validation",
|
||
dependencies=[Depends(verify_token)],
|
||
)
|
||
def get_statut_validation_endpoint(numero_facture: str):
|
||
try:
|
||
resultat = sage.get_statut_validation(numero_facture)
|
||
return {"success": True, "data": resultat}
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=404, detail=str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture statut {numero_facture}: {e}", exc_info=True)
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@app.get("/sage/reglements/modes", dependencies=[Depends(verify_token)])
|
||
def get_modes_reglement_endpoint():
|
||
"""Récupère les modes de règlement depuis Sage"""
|
||
try:
|
||
modes = sage.lire_modes_reglement()
|
||
return {"success": True, "data": {"modes": modes}}
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture modes règlement: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/devises", dependencies=[Depends(verify_token)])
|
||
def get_devises_endpoint():
|
||
"""Récupère les devises disponibles depuis Sage"""
|
||
try:
|
||
devises = sage.lire_devises()
|
||
return {"success": True, "data": {"devises": devises}}
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture devises: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/journaux/tresorerie", dependencies=[Depends(verify_token)])
|
||
def get_journaux_tresorerie_endpoint():
|
||
"""Récupère les journaux de trésorerie (banque + caisse)"""
|
||
try:
|
||
journaux = sage.lire_journaux_tresorerie()
|
||
return {"success": True, "data": {"journaux": journaux}}
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture journaux trésorerie: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/comptes-generaux", dependencies=[Depends(verify_token)])
|
||
def get_comptes_generaux_endpoint(
|
||
prefixe: Optional[str] = Query(None, description="Filtre par préfixe (ex: 41, 51)"),
|
||
type_compte: Optional[str] = Query(
|
||
None,
|
||
description="Type: client, fournisseur, banque, caisse, tva, produit, charge",
|
||
),
|
||
):
|
||
"""Récupère les comptes généraux"""
|
||
try:
|
||
comptes = sage.lire_comptes_generaux(prefixe=prefixe, type_compte=type_compte)
|
||
return {"success": True, "data": {"comptes": comptes, "total": len(comptes)}}
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture comptes généraux: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/tva/taux", dependencies=[Depends(verify_token)])
|
||
def get_tva_taux_endpoint():
|
||
"""Récupère les taux de TVA"""
|
||
try:
|
||
taux = sage.lire_tva_taux()
|
||
return {"success": True, "data": {"taux": taux}}
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture taux TVA: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/parametres/encaissement", dependencies=[Depends(verify_token)])
|
||
def get_parametres_encaissement_endpoint():
|
||
"""Récupère les paramètres TVA sur encaissement"""
|
||
try:
|
||
params = sage.lire_parametres_encaissement()
|
||
return {"success": True, "data": params}
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture paramètres encaissement: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/journaux")
|
||
def get_tous_journaux():
|
||
try:
|
||
journaux = sage.lire_tous_journaux()
|
||
|
||
# Grouper par type
|
||
par_type = {}
|
||
for j in journaux:
|
||
t = j["type_libelle"]
|
||
if t not in par_type:
|
||
par_type[t] = []
|
||
par_type[t].append(j)
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"journaux": journaux,
|
||
"par_type": par_type,
|
||
"nb_tresorerie": len([j for j in journaux if j["type_code"] == 2]),
|
||
"message": "Pour les règlements, utilisez les journaux de type Trésorerie (type_code=2)",
|
||
},
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture journaux: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/journaux/banque")
|
||
def get_journaux_banque():
|
||
try:
|
||
journaux = sage.lire_journaux_banque()
|
||
|
||
if not journaux:
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"journaux": [],
|
||
"warning": "Aucun journal de trésorerie configuré. "
|
||
"Créez un journal de type 2 (Trésorerie) dans Sage "
|
||
"avant de pouvoir effectuer des règlements.",
|
||
},
|
||
}
|
||
|
||
return {"success": True, "data": {"journaux": journaux}}
|
||
except Exception as e:
|
||
logger.error(f"Erreur lecture journaux: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/reglements/introspection")
|
||
def introspecter_reglements():
|
||
try:
|
||
data = sage.introspecter_reglement()
|
||
return {"success": True, "data": data}
|
||
except Exception as e:
|
||
logger.error(f"Erreur introspection: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/societe/com-introspection")
|
||
def introspection_com():
|
||
"""Liste toutes les propriétés/méthodes COM disponibles"""
|
||
try:
|
||
resultats = {
|
||
"cial_attributes": [],
|
||
"base_cpta_attributes": [],
|
||
"param_dossier_attributes": [],
|
||
}
|
||
|
||
with sage._com_context(), sage._lock_com:
|
||
# Attributs de cial
|
||
try:
|
||
for attr in dir(sage.cial):
|
||
if not attr.startswith("_"):
|
||
resultats["cial_attributes"].append(attr)
|
||
except Exception as e:
|
||
resultats["cial_error"] = str(e)
|
||
|
||
# Attributs de BaseCpta
|
||
try:
|
||
base_cpta = sage.cial.BaseCpta
|
||
for attr in dir(base_cpta):
|
||
if not attr.startswith("_"):
|
||
resultats["base_cpta_attributes"].append(attr)
|
||
except Exception as e:
|
||
resultats["base_cpta_error"] = str(e)
|
||
|
||
# Attributs de ParametreDossier
|
||
try:
|
||
param = sage.cial.BaseCpta.ParametreDossier
|
||
for attr in dir(param):
|
||
if not attr.startswith("_"):
|
||
resultats["param_dossier_attributes"].append(attr)
|
||
|
||
# Tester spécifiquement les attributs logo possibles
|
||
resultats["logo_tests"] = {}
|
||
for logo_attr in [
|
||
"Logo",
|
||
"D_Logo",
|
||
"ImageLogo",
|
||
"LogoImage",
|
||
"GetLogo",
|
||
]:
|
||
try:
|
||
val = getattr(param, logo_attr, None)
|
||
resultats["logo_tests"][logo_attr] = str(type(val))
|
||
except Exception:
|
||
pass
|
||
|
||
except Exception as e:
|
||
resultats["param_dossier_error"] = str(e)
|
||
|
||
return {"success": True, "data": resultats}
|
||
except Exception as e:
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/debug/introspection-validation")
|
||
def introspection_validation(numero_facture: str = None):
|
||
"""
|
||
Introspection des méthodes de validation disponibles dans Sage.
|
||
|
||
Utiliser cette route pour découvrir comment valider un document.
|
||
|
||
Args:
|
||
numero_facture: Optionnel - numéro de facture pour inspecter le document
|
||
"""
|
||
try:
|
||
resultat = sage.introspecter_validation(numero_facture)
|
||
return {"success": True, "data": resultat}
|
||
except Exception as e:
|
||
logger.error(f"Erreur introspection: {e}", exc_info=True)
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@app.get("/sage/debug/introspection-document/{numero_facture}")
|
||
def introspection_document_complet(numero_facture: str):
|
||
"""
|
||
Introspection COMPLÈTE d'un document spécifique.
|
||
|
||
Retourne tous les attributs, méthodes et propriétés disponibles
|
||
sur le document pour découvrir la méthode de validation.
|
||
"""
|
||
try:
|
||
resultat = sage.introspecter_document_complet(numero_facture)
|
||
return {"success": True, "data": resultat}
|
||
except Exception as e:
|
||
logger.error(f"Erreur introspection document: {e}", exc_info=True)
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
uvicorn.run(
|
||
"main:app",
|
||
host=settings.api_host,
|
||
port=settings.api_port,
|
||
reload=False,
|
||
log_level="info",
|
||
)
|