1741 lines
58 KiB
Python
1741 lines
58 KiB
Python
from fastapi import FastAPI, HTTPException, Header, Depends, Query
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from pydantic import BaseModel, Field, validator
|
||
from typing import Optional, List, Dict
|
||
from datetime import datetime, date
|
||
from enum import Enum
|
||
import uvicorn
|
||
import logging
|
||
import win32com.client
|
||
import time
|
||
from config import settings, validate_settings
|
||
from sage_connector import SageConnector
|
||
import pyodbc
|
||
import os
|
||
|
||
# =====================================================
|
||
# LOGGING
|
||
# =====================================================
|
||
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__)
|
||
|
||
|
||
# =====================================================
|
||
# ENUMS
|
||
# =====================================================
|
||
class TypeDocument(int, Enum):
|
||
DEVIS = 0
|
||
BON_LIVRAISON = 1
|
||
BON_RETOUR = 2
|
||
COMMANDE = 3
|
||
PREPARATION = 4
|
||
FACTURE = 5
|
||
|
||
|
||
# =====================================================
|
||
# MODÈLES
|
||
# =====================================================
|
||
|
||
|
||
class DocumentGetRequest(BaseModel):
|
||
numero: str
|
||
type_doc: int
|
||
|
||
|
||
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
|
||
date_livraison: Optional[date] = None
|
||
reference: Optional[str] = None
|
||
lignes: List[Dict]
|
||
|
||
|
||
class TransformationRequest(BaseModel):
|
||
numero_source: str
|
||
type_source: int
|
||
type_cible: int
|
||
|
||
|
||
class StatutRequest(BaseModel):
|
||
nouveau_statut: int
|
||
|
||
|
||
class ClientCreateRequest(BaseModel):
|
||
intitule: str = Field(..., description="Nom du client (CT_Intitule)")
|
||
compte_collectif: str = Field("411000", description="Compte général rattaché")
|
||
num: Optional[str] = Field(None, description="Laisser vide pour numérotation auto")
|
||
adresse: Optional[str] = None
|
||
code_postal: Optional[str] = None
|
||
ville: Optional[str] = None
|
||
pays: Optional[str] = None
|
||
email: Optional[str] = None
|
||
telephone: Optional[str] = None
|
||
siret: Optional[str] = None
|
||
tva_intra: Optional[str] = None
|
||
|
||
|
||
class ClientUpdateGatewayRequest(BaseModel):
|
||
"""Modèle pour modification client côté gateway"""
|
||
|
||
code: str
|
||
client_data: Dict
|
||
|
||
|
||
class FournisseurCreateRequest(BaseModel):
|
||
intitule: str = Field(..., description="Raison sociale du fournisseur")
|
||
compte_collectif: str = Field("401000", description="Compte général rattaché")
|
||
num: Optional[str] = Field(None, description="Code fournisseur (auto si vide)")
|
||
adresse: Optional[str] = None
|
||
code_postal: Optional[str] = None
|
||
ville: Optional[str] = None
|
||
pays: Optional[str] = None
|
||
email: Optional[str] = None
|
||
telephone: Optional[str] = None
|
||
siret: Optional[str] = None
|
||
tva_intra: Optional[str] = None
|
||
|
||
|
||
class FournisseurCreateRequest(BaseModel):
|
||
intitule: str = Field(..., description="Raison sociale du fournisseur")
|
||
compte_collectif: str = Field("401000", description="Compte général rattaché")
|
||
num: Optional[str] = Field(None, description="Code fournisseur (auto si vide)")
|
||
adresse: Optional[str] = None
|
||
code_postal: Optional[str] = None
|
||
ville: Optional[str] = None
|
||
pays: Optional[str] = None
|
||
email: Optional[str] = None
|
||
telephone: Optional[str] = None
|
||
siret: Optional[str] = None
|
||
tva_intra: Optional[str] = None
|
||
|
||
|
||
class FournisseurUpdateGatewayRequest(BaseModel):
|
||
"""Modèle pour modification fournisseur côté gateway"""
|
||
|
||
code: str
|
||
fournisseur_data: Dict
|
||
|
||
|
||
class DevisUpdateGatewayRequest(BaseModel):
|
||
"""Modèle pour modification devis côté gateway"""
|
||
|
||
numero: str
|
||
devis_data: Dict
|
||
|
||
|
||
class CommandeCreateRequest(BaseModel):
|
||
"""Création d'une commande"""
|
||
|
||
client_id: str
|
||
date_commande: Optional[date] = None
|
||
date_livraison: Optional[date] = None
|
||
reference: Optional[str] = None
|
||
lignes: List[Dict]
|
||
|
||
|
||
class CommandeUpdateGatewayRequest(BaseModel):
|
||
"""Modèle pour modification commande côté gateway"""
|
||
|
||
numero: str
|
||
commande_data: Dict
|
||
|
||
|
||
class LivraisonCreateGatewayRequest(BaseModel):
|
||
"""Création d'une livraison côté gateway"""
|
||
|
||
client_id: str
|
||
date_livraison: Optional[date] = None
|
||
date_livraison_prevue: Optional[date] = None
|
||
lignes: List[Dict]
|
||
reference: Optional[str] = None
|
||
|
||
|
||
class LivraisonUpdateGatewayRequest(BaseModel):
|
||
"""Modèle pour modification livraison côté gateway"""
|
||
|
||
numero: str
|
||
livraison_data: Dict
|
||
|
||
|
||
class AvoirCreateGatewayRequest(BaseModel):
|
||
"""Création d'un avoir côté gateway"""
|
||
|
||
client_id: str
|
||
date_avoir: Optional[date] = None
|
||
date_livraison: Optional[date] = None
|
||
lignes: List[Dict]
|
||
reference: Optional[str] = None
|
||
|
||
|
||
class AvoirUpdateGatewayRequest(BaseModel):
|
||
"""Modèle pour modification avoir côté gateway"""
|
||
|
||
numero: str
|
||
avoir_data: Dict
|
||
|
||
|
||
class FactureCreateGatewayRequest(BaseModel):
|
||
"""Création d'une facture côté gateway"""
|
||
|
||
client_id: str
|
||
date_facture: Optional[date] = None
|
||
date_livraison: Optional[date] = None
|
||
lignes: List[Dict]
|
||
reference: Optional[str] = None
|
||
|
||
|
||
class FactureUpdateGatewayRequest(BaseModel):
|
||
"""Modèle pour modification facture côté gateway"""
|
||
|
||
numero: str
|
||
facture_data: Dict
|
||
|
||
|
||
class PDFGenerationRequest(BaseModel):
|
||
"""Modèle pour génération PDF"""
|
||
|
||
doc_id: str = Field(..., description="Numéro du document")
|
||
type_doc: int = Field(..., ge=0, le=60, description="Type de document Sage")
|
||
|
||
|
||
class ArticleCreateRequest(BaseModel):
|
||
reference: str = Field(..., description="Référence article (max 18 car)")
|
||
designation: str = Field(..., description="Désignation (max 69 car)")
|
||
famille: Optional[str] = Field(None, description="Code famille")
|
||
prix_vente: Optional[float] = Field(None, ge=0, description="Prix vente HT")
|
||
prix_achat: Optional[float] = Field(None, ge=0, description="Prix achat HT")
|
||
stock_reel: Optional[float] = Field(None, ge=0, description="Stock initial")
|
||
stock_mini: Optional[float] = Field(None, ge=0, description="Stock minimum")
|
||
code_ean: Optional[str] = Field(None, description="Code-barres EAN")
|
||
unite_vente: Optional[str] = Field("UN", description="Unité de vente")
|
||
tva_code: Optional[str] = Field(None, description="Code TVA")
|
||
description: Optional[str] = Field(None, description="Description/Commentaire")
|
||
|
||
|
||
class ArticleUpdateGatewayRequest(BaseModel):
|
||
"""Modèle pour modification article côté gateway"""
|
||
|
||
reference: str
|
||
article_data: Dict
|
||
|
||
|
||
class MouvementStockLigneRequest(BaseModel):
|
||
article_ref: str = Field(..., description="Référence de l'article")
|
||
quantite: float = Field(..., gt=0, description="Quantité (>0)")
|
||
depot_code: Optional[str] = Field(None, description="Code du dépôt (ex: '01')")
|
||
prix_unitaire: Optional[float] = Field(
|
||
None, ge=0, description="Prix unitaire (optionnel)"
|
||
)
|
||
commentaire: Optional[str] = Field(None, description="Commentaire ligne")
|
||
numero_lot: Optional[str] = Field(
|
||
None, description="Numéro de lot (pour FIFO/LIFO)"
|
||
)
|
||
stock_mini: Optional[float] = Field(
|
||
None,
|
||
ge=0,
|
||
description="""Stock minimum à définir pour cet article.
|
||
Si fourni, met à jour AS_QteMini dans F_ARTSTOCK.
|
||
Laisser None pour ne pas modifier.""",
|
||
)
|
||
stock_maxi: Optional[float] = Field(
|
||
None,
|
||
ge=0,
|
||
description="""Stock maximum à définir pour cet article.
|
||
Doit être > stock_mini si les deux sont fournis.""",
|
||
)
|
||
|
||
class Config:
|
||
schema_extra = {
|
||
"example": {
|
||
"article_ref": "ARTS-001",
|
||
"quantite": 50.0,
|
||
"depot_code": "01",
|
||
"prix_unitaire": 100.0,
|
||
"commentaire": "Réapprovisionnement",
|
||
"numero_lot": "LOT20241217",
|
||
"stock_mini": 10.0,
|
||
"stock_maxi": 200.0,
|
||
}
|
||
}
|
||
|
||
@validator("stock_maxi")
|
||
def validate_stock_maxi(cls, v, values):
|
||
"""Valide que stock_maxi > stock_mini si les deux sont fournis"""
|
||
if (
|
||
v is not None
|
||
and "stock_mini" in values
|
||
and values["stock_mini"] is not None
|
||
):
|
||
if v <= values["stock_mini"]:
|
||
raise ValueError(
|
||
"stock_maxi doit être strictement supérieur à stock_mini"
|
||
)
|
||
return v
|
||
|
||
|
||
class EntreeStockRequest(BaseModel):
|
||
"""Création d'un bon d'entrée en stock"""
|
||
|
||
date_entree: Optional[date] = Field(
|
||
None, description="Date du mouvement (aujourd'hui par défaut)"
|
||
)
|
||
reference: Optional[str] = Field(None, description="Référence externe")
|
||
depot_code: Optional[str] = Field(
|
||
None, description="Dépôt principal (si applicable)"
|
||
)
|
||
lignes: List[MouvementStockLigneRequest] = Field(
|
||
..., min_items=1, description="Lignes du mouvement"
|
||
)
|
||
commentaire: Optional[str] = Field(None, description="Commentaire général")
|
||
|
||
|
||
class SortieStockRequest(BaseModel):
|
||
"""Création d'un bon de sortie de stock"""
|
||
|
||
date_sortie: Optional[date] = Field(
|
||
None, description="Date du mouvement (aujourd'hui par défaut)"
|
||
)
|
||
reference: Optional[str] = Field(None, description="Référence externe")
|
||
depot_code: Optional[str] = Field(
|
||
None, description="Dépôt principal (si applicable)"
|
||
)
|
||
lignes: List[MouvementStockLigneRequest] = Field(
|
||
..., min_items=1, description="Lignes du mouvement"
|
||
)
|
||
commentaire: Optional[str] = Field(None, description="Commentaire général")
|
||
|
||
|
||
class FamilleCreate(BaseModel):
|
||
"""Modèle pour créer une famille d'articles"""
|
||
|
||
code: str = Field(..., description="Code famille (max 18 car)", max_length=18)
|
||
intitule: str = Field(..., description="Intitulé (max 69 car)", max_length=69)
|
||
type: int = Field(0, description="0=Détail, 1=Total")
|
||
compte_achat: Optional[str] = Field(
|
||
None, description="Compte général d'achat (ex: 607000)"
|
||
)
|
||
compte_vente: Optional[str] = Field(
|
||
None, description="Compte général de vente (ex: 707000)"
|
||
)
|
||
|
||
|
||
# =====================================================
|
||
# SÉCURITÉ
|
||
# =====================================================
|
||
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
|
||
|
||
|
||
# =====================================================
|
||
# APPLICATION
|
||
# =====================================================
|
||
app = FastAPI(
|
||
title="Sage Gateway - Windows Server",
|
||
version="1.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
|
||
|
||
|
||
# =====================================================
|
||
# LIFECYCLE
|
||
# =====================================================
|
||
@app.on_event("startup")
|
||
def startup():
|
||
global sage
|
||
|
||
logger.info("🚀 Démarrage Sage Gateway Windows...")
|
||
|
||
# Validation config
|
||
try:
|
||
validate_settings()
|
||
logger.info(" Configuration validée")
|
||
except ValueError as e:
|
||
logger.error(f" Configuration invalide: {e}")
|
||
raise
|
||
|
||
# Connexion Sage
|
||
sage = SageConnector(
|
||
settings.chemin_base, 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é")
|
||
|
||
|
||
# =====================================================
|
||
# ENDPOINTS - SYSTÈME
|
||
# =====================================================
|
||
@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(),
|
||
}
|
||
|
||
|
||
# =====================================================
|
||
# ENDPOINTS - CLIENTS
|
||
# =====================================================
|
||
@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: ClientUpdateGatewayRequest):
|
||
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))
|
||
|
||
|
||
# DANS main.py
|
||
@app.post("/sage/clients/create", dependencies=[Depends(verify_token)])
|
||
def create_client_endpoint(req: ClientCreateRequest):
|
||
"""Création d'un client dans Sage"""
|
||
try:
|
||
# L'appel au connecteur est fait ici
|
||
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}")
|
||
# Erreur métier (ex: doublon) -> 400 Bad Request
|
||
raise HTTPException(400, str(e))
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique création client: {e}")
|
||
# Erreur technique (ex: COM) -> 500 Internal Server Error
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
# =====================================================
|
||
# ENDPOINTS - ARTICLES
|
||
# =====================================================
|
||
@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))
|
||
|
||
|
||
# =====================================================
|
||
# ENDPOINTS - DEVIS
|
||
# =====================================================
|
||
@app.post("/sage/devis/create", dependencies=[Depends(verify_token)])
|
||
def creer_devis(req: DevisRequest):
|
||
"""Création d'un devis"""
|
||
try:
|
||
# Transformer en format attendu par sage_connector
|
||
devis_data = {
|
||
"client": {"code": req.client_id, "intitule": ""},
|
||
"date_devis": req.date_devis or date.today(),
|
||
"date_livraison": req.date_livraison or date.today(),
|
||
"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:
|
||
# Lecture complète depuis Sage (avec lignes)
|
||
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:
|
||
# Récupération depuis le cache (instantané)
|
||
devis_list = sage.lister_tous_devis_cache(filtre)
|
||
|
||
# Filtrer par statut si demandé
|
||
if statut is not None:
|
||
devis_list = [d for d in devis_list if d.get("statut") == statut]
|
||
|
||
# Limiter le nombre de résultats
|
||
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/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
|
||
# =====================================================
|
||
@app.post("/sage/documents/get", dependencies=[Depends(verify_token)])
|
||
def lire_document(req: DocumentGetRequest):
|
||
"""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}"
|
||
)
|
||
|
||
# Matrice des transformations valides pour VOTRE Sage
|
||
transformations_valides = {
|
||
(0, 10), # Devis → Commande
|
||
(10, 30), # Commande → Bon de livraison
|
||
(10, 60), # Commande → Facture
|
||
(30, 60), # Bon de livraison → Facture
|
||
(0, 60), # Devis → Facture (si autorisé)
|
||
}
|
||
|
||
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}",
|
||
)
|
||
|
||
# Appel au connecteur Sage
|
||
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: ChampLibreRequest):
|
||
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))
|
||
|
||
|
||
# =====================================================
|
||
# ENDPOINTS - CONTACTS
|
||
# =====================================================
|
||
@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 # 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
|
||
# =====================================================
|
||
@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))
|
||
|
||
|
||
# =====================================================
|
||
# ENDPOINTS - PROSPECTS
|
||
# =====================================================
|
||
@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))
|
||
|
||
|
||
# =====================================================
|
||
# ENDPOINTS - FOURNISSEURS
|
||
# =====================================================
|
||
@app.post("/sage/fournisseurs/list", dependencies=[Depends(verify_token)])
|
||
def fournisseurs_list(req: FiltreRequest):
|
||
try:
|
||
# Utiliser le cache au lieu de la lecture directe
|
||
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: FournisseurCreateRequest):
|
||
try:
|
||
# Appel au connecteur Sage
|
||
resultat = sage.creer_fournisseur(req.dict())
|
||
|
||
logger.info(f" Fournisseur créé: {resultat.get('numero')}")
|
||
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
# Erreur métier (ex: doublon)
|
||
logger.warning(f"⚠️ Erreur métier création fournisseur: {e}")
|
||
raise HTTPException(400, str(e))
|
||
|
||
except Exception as e:
|
||
# Erreur technique (ex: COM)
|
||
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: FournisseurUpdateGatewayRequest):
|
||
|
||
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))
|
||
|
||
|
||
# =====================================================
|
||
# ENDPOINTS - AVOIRS
|
||
# =====================================================
|
||
@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:
|
||
# Récupération depuis le cache (instantané)
|
||
avoirs = sage.lister_tous_avoirs_cache(filtre)
|
||
|
||
# Filtrer par statut si demandé
|
||
if statut is not None:
|
||
avoirs = [a for a in avoirs if a.get("statut") == statut]
|
||
|
||
# Limiter le nombre de résultats
|
||
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:
|
||
# Essayer le cache d'abord
|
||
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"}
|
||
|
||
# Pas dans le cache → Lecture directe depuis Sage
|
||
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))
|
||
|
||
|
||
# =====================================================
|
||
# ENDPOINTS - LIVRAISONS
|
||
# =====================================================
|
||
@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:
|
||
# Récupération depuis le cache (instantané)
|
||
livraisons = sage.lister_toutes_livraisons_cache(filtre)
|
||
|
||
# Filtrer par statut si demandé
|
||
if statut is not None:
|
||
livraisons = [l for l in livraisons if l.get("statut") == statut]
|
||
|
||
# Limiter le nombre de résultats
|
||
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:
|
||
# Essayer le cache d'abord
|
||
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"}
|
||
|
||
# Pas dans le cache → Lecture directe depuis Sage
|
||
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: DevisUpdateGatewayRequest):
|
||
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))
|
||
|
||
|
||
# =====================================================
|
||
# ENDPOINTS - CRÉATION ET MODIFICATION COMMANDES
|
||
# =====================================================
|
||
|
||
|
||
@app.post("/sage/commandes/create", dependencies=[Depends(verify_token)])
|
||
def creer_commande_endpoint(req: CommandeCreateRequest):
|
||
try:
|
||
# Transformer en format attendu par sage_connector
|
||
commande_data = {
|
||
"client": {"code": req.client_id, "intitule": ""},
|
||
"date_commande": req.date_commande or date.today(),
|
||
"date_livraison": req.date_livraison or date.today(),
|
||
"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: CommandeUpdateGatewayRequest):
|
||
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: LivraisonCreateGatewayRequest):
|
||
try:
|
||
# Vérifier que le client existe
|
||
client = sage.lire_client(req.client_id)
|
||
if not client:
|
||
raise HTTPException(404, f"Client {req.client_id} introuvable")
|
||
|
||
# Préparer les données pour le connecteur
|
||
livraison_data = {
|
||
"client": {"code": req.client_id, "intitule": ""},
|
||
"date_livraison": req.date_livraison or date.today(),
|
||
"date_livraison_prevue": req.date_livraison or date.today(),
|
||
"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: LivraisonUpdateGatewayRequest):
|
||
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: AvoirCreateGatewayRequest):
|
||
try:
|
||
# Vérifier que le client existe
|
||
client = sage.lire_client(req.client_id)
|
||
if not client:
|
||
raise HTTPException(404, f"Client {req.client_id} introuvable")
|
||
|
||
# Préparer les données pour le connecteur
|
||
avoir_data = {
|
||
"client": {"code": req.client_id, "intitule": ""},
|
||
"date_avoir": req.date_avoir or date.today(),
|
||
"date_livraison": req.date_livraison or date.today(),
|
||
"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: AvoirUpdateGatewayRequest):
|
||
"""
|
||
✏️ 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: FactureCreateGatewayRequest):
|
||
try:
|
||
# Vérifier que le client existe
|
||
client = sage.lire_client(req.client_id)
|
||
if not client:
|
||
raise HTTPException(404, f"Client {req.client_id} introuvable")
|
||
|
||
# Préparer les données pour le connecteur
|
||
facture_data = {
|
||
"client": {"code": req.client_id, "intitule": ""},
|
||
"date_facture": req.date_facture or date.today(),
|
||
"date_livraison": req.date_livraison or date.today(),
|
||
"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: FactureUpdateGatewayRequest):
|
||
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: ArticleCreateRequest):
|
||
try:
|
||
resultat = sage.creer_article(req.dict())
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier création article: {e}")
|
||
raise HTTPException(400, str(e))
|
||
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique création article: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.post("/sage/articles/update", dependencies=[Depends(verify_token)])
|
||
def modifier_article_endpoint(req: ArticleUpdateGatewayRequest):
|
||
try:
|
||
resultat = sage.modifier_article(req.reference, req.article_data)
|
||
return {"success": True, "data": resultat}
|
||
|
||
except ValueError as e:
|
||
logger.warning(f"Erreur métier modification article: {e}")
|
||
raise HTTPException(404, str(e))
|
||
|
||
except Exception as e:
|
||
logger.error(f"Erreur technique modification article: {e}")
|
||
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)}")
|
||
|
||
|
||
# ========================================
|
||
# ROUTE GET : Lister toutes les familles
|
||
# ========================================
|
||
|
||
|
||
@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)}")
|
||
|
||
|
||
# ========================================
|
||
# ROUTE GET : Lire UNE famille par son code
|
||
# ========================================
|
||
|
||
|
||
@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)}")
|
||
|
||
|
||
# ========================================
|
||
# ROUTE GET : Statistiques sur les familles
|
||
# ========================================
|
||
|
||
|
||
@app.get("/sage/familles/stats", response_model=dict)
|
||
async def stats_familles():
|
||
try:
|
||
familles = sage.lister_toutes_familles()
|
||
|
||
# Calculer les stats
|
||
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 10 familles par intitulé (alphabétique)
|
||
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: PDFGenerationRequest):
|
||
try:
|
||
logger.info(f" Génération PDF: {req.doc_id} (type={req.type_doc})")
|
||
|
||
# Appel au connecteur Sage
|
||
pdf_bytes = sage.generer_pdf_document(req.doc_id, req.type_doc)
|
||
|
||
if not pdf_bytes:
|
||
raise HTTPException(500, "PDF vide généré")
|
||
|
||
# Encoder en base64 pour le transport JSON
|
||
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: # Max 100 dépôts
|
||
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()
|
||
|
||
# Lire les attributs identifiés
|
||
code = ""
|
||
numero = 0
|
||
intitule = ""
|
||
contact = ""
|
||
exclu = False
|
||
|
||
try:
|
||
code = getattr(depot, "DE_Code", "").strip()
|
||
except:
|
||
pass
|
||
|
||
try:
|
||
numero = int(getattr(depot, "Compteur", 0))
|
||
except:
|
||
# Fallback : convertir DE_Code en int
|
||
try:
|
||
numero = int(code)
|
||
except:
|
||
numero = 0
|
||
|
||
try:
|
||
intitule = getattr(depot, "DE_Intitule", "")
|
||
except:
|
||
pass
|
||
|
||
try:
|
||
contact = getattr(depot, "DE_Contact", "")
|
||
except:
|
||
pass
|
||
|
||
try:
|
||
exclu = getattr(depot, "DE_Exclure", False)
|
||
except:
|
||
pass
|
||
|
||
# Validation : un dépôt doit avoir au moins un code
|
||
if not code:
|
||
logger.warning(f" ⚠️ Dépôt à l'index {index} sans code")
|
||
index += 1
|
||
continue
|
||
|
||
# Récupérer adresse (objet COM complexe)
|
||
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:
|
||
pass
|
||
except:
|
||
pass
|
||
|
||
# Déterminer si principal (premier non exclu = principal)
|
||
principal = False
|
||
if not exclu and len(depots) == 0:
|
||
principal = True
|
||
|
||
depot_info = {
|
||
"code": code, # ⭐ "01", "02"
|
||
"numero": numero, # ⭐ 1, 2 (depuis Compteur)
|
||
"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:
|
||
# ⚠️ CORRECTION : "Accès refusé" = fin de liste dans cette version Sage
|
||
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: EntreeStockRequest):
|
||
try:
|
||
logger.info(
|
||
f"📦 [ENTREE STOCK] Création bon d'entrée : {len(req.lignes)} ligne(s)"
|
||
)
|
||
|
||
# Préparer les données pour le connecteur
|
||
entree_data = {
|
||
"date_mouvement": req.date_entree or date.today(),
|
||
"reference": req.reference,
|
||
"depot_code": req.depot_code,
|
||
"lignes": [ligne.dict() for ligne in req.lignes],
|
||
"commentaire": req.commentaire,
|
||
}
|
||
|
||
# Appel au connecteur
|
||
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: SortieStockRequest):
|
||
try:
|
||
logger.info(
|
||
f"📤 [SORTIE STOCK] Création bon de sortie : {len(req.lignes)} ligne(s)"
|
||
)
|
||
|
||
# Préparer les données pour le connecteur
|
||
sortie_data = {
|
||
"date_mouvement": req.date_sortie or date.today(),
|
||
"reference": req.reference,
|
||
"depot_code": req.depot_code,
|
||
"lignes": [ligne.dict() for ligne in req.lignes],
|
||
"commentaire": req.commentaire,
|
||
}
|
||
|
||
# Appel au connecteur
|
||
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.get("/sage/modeles/list")
|
||
def lister_modeles_disponibles():
|
||
"""Liste tous les modèles .bgc disponibles pour chaque type de document"""
|
||
try:
|
||
modeles = sage.lister_modeles_crystal()
|
||
|
||
return {"success": True, "data": modeles}
|
||
except Exception as e:
|
||
logger.error(f" Erreur listage modèles: {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/documents/{numero}/pdf", dependencies=[Depends(verify_token)])
|
||
def generer_pdf_document(
|
||
numero: str,
|
||
type_doc: int = Query(..., description="Type document (0=devis, 60=facture, etc.)"),
|
||
modele: str = Query(None, description="Nom du modèle .bgc (optionnel)"),
|
||
base64_encode: bool = Query(True, description="Retourner en base64"),
|
||
):
|
||
"""
|
||
Génère un PDF d'un document Sage avec le modèle spécifié
|
||
"""
|
||
try:
|
||
# LOG pour debug
|
||
logger.info(
|
||
f" PDF Request: numero={numero}, type={type_doc}, modele={modele}, base64={base64_encode}"
|
||
)
|
||
|
||
# Générer le PDF
|
||
pdf_bytes = sage.generer_pdf_document(
|
||
numero=numero, type_doc=type_doc, modele=modele
|
||
)
|
||
|
||
if not pdf_bytes:
|
||
raise HTTPException(404, f"Impossible de générer le PDF pour {numero}")
|
||
|
||
# LOG taille PDF
|
||
logger.info(f" PDF généré: {len(pdf_bytes)} octets")
|
||
|
||
if base64_encode:
|
||
# Retour en JSON avec base64
|
||
import base64
|
||
|
||
pdf_base64 = base64.b64encode(pdf_bytes).decode("utf-8")
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"numero": numero,
|
||
"type": type_doc,
|
||
"modele": modele or "défaut",
|
||
"pdf_base64": pdf_base64,
|
||
"size_bytes": len(pdf_bytes),
|
||
"size_readable": f"{len(pdf_bytes) / 1024:.1f} KB",
|
||
},
|
||
}
|
||
else:
|
||
# Retour direct du fichier PDF
|
||
from fastapi.responses import Response
|
||
|
||
return Response(
|
||
content=pdf_bytes,
|
||
media_type="application/pdf",
|
||
headers={
|
||
"Content-Disposition": f'inline; filename="{numero}.pdf"',
|
||
"Content-Length": str(len(pdf_bytes)), # Taille explicite
|
||
},
|
||
)
|
||
|
||
except HTTPException:
|
||
raise
|
||
except ValueError as e:
|
||
logger.error(f" Erreur métier: {e}")
|
||
raise HTTPException(400, str(e))
|
||
except Exception as e:
|
||
logger.error(f" Erreur technique: {e}", exc_info=True)
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
@app.get("/sage/object-exploration")
|
||
async def explorer_objets_impression_sage(modele:str="Devis client avec détail projet.bgc"):
|
||
try:
|
||
dossier = r"C:\Users\Public\Documents\Sage\Entreprise 100c\fr-FR\Documents standards\Gestion commerciale\Ventes"
|
||
chemin = os.path.join(dossier, modele)
|
||
|
||
if not os.path.exists(chemin):
|
||
return {"error": f"Fichier non trouve: {modele}"}
|
||
expliration = sage.analyser_bgc_complet(chemin)
|
||
|
||
if not expliration:
|
||
raise HTTPException(404, f"ERROR")
|
||
|
||
return {"success": True, "data": expliration}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f" Erreur exploration : {e}")
|
||
raise HTTPException(500, str(e))
|
||
|
||
|
||
# =====================================================
|
||
# LANCEMENT
|
||
# =====================================================
|
||
if __name__ == "__main__":
|
||
uvicorn.run(
|
||
"main:app",
|
||
host=settings.api_host,
|
||
port=settings.api_port,
|
||
reload=False, # Pas de reload en production
|
||
log_level="info",
|
||
)
|