Sage100-ws/main.py.bak

4514 lines
172 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from fastapi import FastAPI, HTTPException, Header, Depends, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
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
# =====================================================
# 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
lignes: List[
Dict
] # Format: {article_code, quantite, prix_unitaire_ht, remise_pourcentage}
class TransformationRequest(BaseModel):
numero_source: str
type_source: int
type_cible: int
class StatutRequest(BaseModel):
nouveau_statut: int
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
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
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
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
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
# =====================================================
# 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):
"""
✏️ Modification d'un client dans Sage
"""
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(),
"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):
"""
📄 Lecture d'un devis AVEC ses lignes (lecture Sage directe)
Plus lent que /list car charge les lignes depuis Sage
💡 Utiliser /list pour afficher une table rapide
"""
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(100, description="Nombre max de devis"),
statut: Optional[int] = Query(None, description="Filtrer par statut"),
filtre: str = Query("", description="Filtre texte (numero, client)"),
):
"""
📋 Liste rapide des devis depuis le CACHE (sans lignes)
⚡ ULTRA-RAPIDE: Utilise le cache mémoire au lieu de scanner Sage
💡 Pour les détails avec lignes, utiliser GET /sage/devis/get
"""
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"),
):
"""
🔧 Transformation de document
CORRECTION : Utilise les VRAIS types Sage Dataven
Types valides :
- 0: Devis
- 10: Bon de commande
- 20: Préparation
- 30: Bon de livraison
- 40: Bon de retour
- 50: Bon d'avoir
- 60: Facture
Transformations autorisées :
- Devis (0) → Commande (10)
- Commande (10) → Bon livraison (30)
- Commande (10) → Facture (60)
- Bon livraison (30) → Facture (60)
"""
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):
"""Mise à jour d'un champ libre"""
try:
success = sage.mettre_a_jour_champ_libre(
req.doc_id, req.type_doc, req.nom_champ, req.valeur
)
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):
"""📅 Met à jour le champ 'Dernière relance' d'un document"""
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"),
):
"""
📋 Liste rapide des commandes depuis le CACHE (sans lignes)
⚡ ULTRA-RAPIDE: Utilise le cache mémoire
"""
try:
commandes = sage.lister_toutes_commandes_cache(filtre)
if statut is not None:
commandes = [c for c in commandes if c.get("statut") == statut]
commandes = commandes[:limit]
logger.info(f" {len(commandes)} commandes retournées depuis le cache")
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"),
):
"""
📋 Liste rapide des factures depuis le CACHE (sans lignes)
⚡ ULTRA-RAPIDE: Utilise le cache mémoire
💡 Pour les détails avec lignes, utiliser /sage/documents/get
"""
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))
# Script à ajouter temporairement dans main.py pour diagnostiquer
@app.get("/sage/devis/{numero}/diagnostic", dependencies=[Depends(verify_token)])
def diagnostiquer_devis(numero: str):
"""
ENDPOINT DE DIAGNOSTIC: Affiche TOUTES les infos d'un devis
Permet de comprendre pourquoi un devis ne peut pas être transformé
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente
# Essayer ReadPiece
persist = factory.ReadPiece(0, numero)
# Si échec, chercher dans List()
if not persist:
logger.info(f"[DIAG] ReadPiece echoue, recherche dans List()...")
index = 1
while index < 10000:
try:
persist_test = factory.List(index)
if persist_test is None:
break
doc_test = win32com.client.CastTo(
persist_test, "IBODocumentVente3"
)
doc_test.Read()
if (
getattr(doc_test, "DO_Type", -1) == 0
and getattr(doc_test, "DO_Piece", "") == numero
):
persist = persist_test
logger.info(f"[DIAG] Trouve a l'index {index}")
break
index += 1
except:
index += 1
if not persist:
raise HTTPException(404, f"Devis {numero} introuvable")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
# EXTRACTION COMPLÈTE
diagnostic = {
"numero": getattr(doc, "DO_Piece", ""),
"type": getattr(doc, "DO_Type", -1),
"statut": getattr(doc, "DO_Statut", -1),
"statut_libelle": {
0: "Brouillon",
1: "Soumis",
2: "Accepte",
3: "Realise partiellement",
4: "Realise totalement",
5: "Transforme",
6: "Annule",
}.get(getattr(doc, "DO_Statut", -1), "Inconnu"),
"date": str(getattr(doc, "DO_Date", "")),
"total_ht": float(getattr(doc, "DO_TotalHT", 0.0)),
"total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)),
"est_transformable": False,
"raison_blocage": None,
}
# Client
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
diagnostic["client_code"] = getattr(
client_obj, "CT_Num", ""
).strip()
diagnostic["client_intitule"] = getattr(
client_obj, "CT_Intitule", ""
).strip()
except Exception as e:
diagnostic["erreur_client"] = str(e)
# Lignes
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
lignes = []
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3")
ligne.Read()
article_ref = ""
try:
article_ref = getattr(ligne, "AR_Ref", "").strip()
if not article_ref:
article_obj = getattr(ligne, "Article", None)
if article_obj:
article_obj.Read()
article_ref = getattr(article_obj, "AR_Ref", "").strip()
except:
pass
lignes.append(
{
"index": index,
"article": article_ref,
"designation": getattr(ligne, "DL_Design", ""),
"quantite": float(getattr(ligne, "DL_Qte", 0.0)),
"prix_unitaire": float(
getattr(ligne, "DL_PrixUnitaire", 0.0)
),
"montant_ht": float(getattr(ligne, "DL_MontantHT", 0.0)),
}
)
index += 1
except:
break
diagnostic["nb_lignes"] = len(lignes)
diagnostic["lignes"] = lignes
# ANALYSE TRANSFORMABILITÉ
statut = diagnostic["statut"]
if statut == 5:
diagnostic["raison_blocage"] = "Document deja transforme (statut=5)"
elif statut == 6:
diagnostic["raison_blocage"] = "Document annule (statut=6)"
elif statut in [3, 4]:
diagnostic["raison_blocage"] = (
f"Document deja realise partiellement ou totalement (statut={statut}). "
f"Une commande/BL/facture existe probablement deja."
)
diagnostic["suggestion"] = (
"Cherchez les documents lies a ce devis dans Sage. "
"Il a peut-etre deja ete transforme manuellement."
)
elif statut == 0:
diagnostic["est_transformable"] = True
diagnostic["action_requise"] = (
"Statut 'Brouillon'. Le systeme le passera automatiquement a 'Accepte' "
"avant transformation."
)
elif statut == 2:
diagnostic["est_transformable"] = True
diagnostic["action_requise"] = (
"Statut 'Accepte'. Transformation possible."
)
else:
diagnostic["raison_blocage"] = f"Statut inconnu ou non gere: {statut}"
# Champs libres (pour Universign, etc.)
champs_libres = {}
try:
for champ in ["UniversignID", "DerniereRelance", "DO_Ref"]:
try:
valeur = getattr(doc, f"DO_{champ}", None)
if valeur:
champs_libres[champ] = str(valeur)
except:
pass
except:
pass
if champs_libres:
diagnostic["champs_libres"] = champs_libres
logger.info(
f"[DIAG] Devis {numero}: statut={statut}, transformable={diagnostic['est_transformable']}"
)
return {"success": True, "diagnostic": diagnostic}
except HTTPException:
raise
except Exception as e:
logger.error(f"[DIAG] Erreur diagnostic: {e}", exc_info=True)
raise HTTPException(500, str(e))
@app.get("/sage/diagnostic/configuration", dependencies=[Depends(verify_token)])
def diagnostic_configuration():
"""
DIAGNOSTIC COMPLET de la configuration Sage
Teste:
- Quelles méthodes COM sont disponibles
- Quels types de documents sont autorisés
- Quelles permissions l'utilisateur a
- Version de Sage
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
diagnostic = {
"connexion": "OK",
"chemin_base": sage.chemin_base,
"utilisateur": sage.utilisateur,
}
# Version Sage
try:
version = getattr(sage.cial, "Version", "Inconnue")
diagnostic["version_sage"] = str(version)
except:
diagnostic["version_sage"] = "Non disponible"
# Test des méthodes disponibles sur IBSCIALApplication3
methodes_disponibles = []
methodes_a_tester = [
"CreateProcess_Document",
"FactoryDocumentVente",
"FactoryArticle",
"CptaApplication",
"BeginTrans",
"CommitTrans",
"RollbackTrans",
]
for methode in methodes_a_tester:
try:
if hasattr(sage.cial, methode):
methodes_disponibles.append(methode)
except:
pass
diagnostic["methodes_cial_disponibles"] = methodes_disponibles
# Test des types de documents autorisés
types_autorises = []
types_bloques = []
for type_doc in range(6): # 0-5
try:
# Essayer de créer un process (sans le valider)
process = sage.cial.CreateProcess_Document(type_doc)
if process:
types_autorises.append(
{
"type": type_doc,
"libelle": {
0: "Devis",
1: "Bon de livraison",
2: "Bon de retour",
3: "Commande",
4: "Preparation",
5: "Facture",
}[type_doc],
}
)
# Ne pas valider, juste tester
del process
except Exception as e:
types_bloques.append(
{
"type": type_doc,
"libelle": {
0: "Devis",
1: "Bon de livraison",
2: "Bon de retour",
3: "Commande",
4: "Preparation",
5: "Facture",
}[type_doc],
"erreur": str(e)[:200],
}
)
diagnostic["types_documents_autorises"] = types_autorises
diagnostic["types_documents_bloques"] = types_bloques
# Test TransformInto() sur un devis test
try:
factory = sage.cial.FactoryDocumentVente
# Chercher n'importe quel devis
index = 1
devis_test = None
while index < 100:
try:
persist = factory.List(index)
if persist is None:
break
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
if getattr(doc, "DO_Type", -1) == 0: # Devis
devis_test = doc
break
index += 1
except:
index += 1
if devis_test:
# Tester si TransformInto existe
if hasattr(devis_test, "TransformInto"):
diagnostic["transforminto_disponible"] = True
diagnostic["transforminto_test"] = "Methode existe (non testee)"
else:
diagnostic["transforminto_disponible"] = False
diagnostic["transforminto_test"] = (
"Methode TransformInto() inexistante"
)
else:
diagnostic["transforminto_disponible"] = (
"Impossible de tester (aucun devis trouve)"
)
except Exception as e:
diagnostic["transforminto_disponible"] = False
diagnostic["transforminto_erreur"] = str(e)
# Modules Sage actifs
try:
# Tester l'accès aux différentes factories
modules = {}
try:
sage.cial.FactoryDocumentVente
modules["Ventes"] = "OK"
except:
modules["Ventes"] = "INACCESSIBLE"
try:
sage.cial.CptaApplication.FactoryClient
modules["Clients"] = "OK"
except:
modules["Clients"] = "INACCESSIBLE"
try:
sage.cial.FactoryArticle
modules["Articles"] = "OK"
except:
modules["Articles"] = "INACCESSIBLE"
diagnostic["modules_actifs"] = modules
except Exception as e:
diagnostic["modules_actifs_erreur"] = str(e)
# Compter documents existants
try:
counts = {}
factory = sage.cial.FactoryDocumentVente
for type_doc in range(6):
count = 0
index = 1
while index < 1000:
try:
persist = factory.List(index)
if persist is None:
break
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
if getattr(doc, "DO_Type", -1) == type_doc:
count += 1
index += 1
except:
index += 1
break
counts[
{
0: "Devis",
1: "Bons_livraison",
2: "Bons_retour",
3: "Commandes",
4: "Preparations",
5: "Factures",
}[type_doc]
] = count
diagnostic["documents_existants"] = counts
except Exception as e:
diagnostic["documents_existants_erreur"] = str(e)
logger.info("[DIAG] Configuration Sage analysee")
return {"success": True, "diagnostic": diagnostic}
except HTTPException:
raise
except Exception as e:
logger.error(f"[DIAG] Erreur diagnostic config: {e}", exc_info=True)
raise HTTPException(500, str(e))
@app.get("/sage/diagnostic/types-reels", dependencies=[Depends(verify_token)])
def decouvrir_types_reels():
"""
DIAGNOSTIC CRITIQUE: Découvre les VRAIS types de documents Sage
Au lieu de deviner les types (0-5), on va:
1. Créer manuellement un document de chaque type dans Sage
2. Les lister ici pour voir leurs vrais numéros de type
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente
# Parcourir TOUS les documents
documents_par_type = {}
index = 1
max_docs = 500 # Limiter pour ne pas bloquer
logger.info("[DIAG] Scan de tous les documents...")
while index < max_docs:
try:
persist = factory.List(index)
if persist is None:
break
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
# Récupérer le type ET le sous-type
type_doc = getattr(doc, "DO_Type", -1)
piece = getattr(doc, "DO_Piece", "")
statut = getattr(doc, "DO_Statut", -1)
# Essayer de récupérer le domaine (vente/achat)
domaine = "Inconnu"
try:
domaine_val = getattr(doc, "DO_Domaine", -1)
domaine = {0: "Vente", 1: "Achat"}.get(
domaine_val, f"Code {domaine_val}"
)
except:
pass
# Récupérer la catégorie
categorie = "Inconnue"
try:
cat_val = getattr(doc, "DO_Categorie", -1)
categorie = str(cat_val)
except:
pass
# Grouper par type
if type_doc not in documents_par_type:
documents_par_type[type_doc] = {
"count": 0,
"exemples": [],
"domaine": domaine,
"categorie": categorie,
}
documents_par_type[type_doc]["count"] += 1
# Garder quelques exemples
if len(documents_par_type[type_doc]["exemples"]) < 3:
documents_par_type[type_doc]["exemples"].append(
{
"numero": piece,
"statut": statut,
"domaine": domaine,
"categorie": categorie,
}
)
index += 1
except Exception as e:
logger.debug(f"Erreur index {index}: {e}")
index += 1
# Formater le résultat
types_trouves = []
for type_num, infos in sorted(documents_par_type.items()):
types_trouves.append(
{
"type_code": type_num,
"nombre_documents": infos["count"],
"domaine": infos["domaine"],
"categorie": infos["categorie"],
"exemples": infos["exemples"],
"suggestion_libelle": _deviner_libelle_type(
type_num, infos["exemples"]
),
}
)
logger.info(
f"[DIAG] {len(types_trouves)} types de documents distincts trouves"
)
return {
"success": True,
"types_documents_reels": types_trouves,
"instructions": (
"Pour identifier les types corrects:\n"
"1. Creez manuellement dans Sage: 1 Bon de commande, 1 BL, 1 Facture\n"
"2. Appelez de nouveau cet endpoint\n"
"3. Les nouveaux types apparaitront avec leurs numeros corrects"
),
"total_documents_scannes": index - 1,
}
except Exception as e:
logger.error(f"[DIAG] Erreur decouverte types: {e}", exc_info=True)
raise HTTPException(500, str(e))
def _deviner_libelle_type(type_num, exemples):
"""Devine le libellé d'un type basé sur les numéros de pièce"""
if not exemples:
return "Type inconnu"
# Analyser les préfixes des numéros
prefixes = [ex["numero"][:2] for ex in exemples if ex["numero"]]
prefix_commun = max(set(prefixes), key=prefixes.count) if prefixes else ""
# Deviner selon le type_num et les préfixes
suggestions = {
0: "Devis (DE)",
1: "Bon de livraison (BL)",
2: "Bon de retour (BR)",
3: "Bon de commande (BC)",
4: "Preparation de livraison (PL)",
5: "Facture (FA)",
6: "Facture d'avoir (AV)",
7: "Bon d'avoir financier (BA)",
}
libelle_base = suggestions.get(type_num, f"Type {type_num}")
if prefix_commun:
libelle_base += f" - Detecte: prefix '{prefix_commun}'"
return libelle_base
@app.post("/sage/test-creation-par-type", dependencies=[Depends(verify_token)])
def tester_creation_par_type(type_doc: int = Query(..., ge=0, le=20)):
"""
TEST: Essaie de créer un document d'un type spécifique
Permet de tester tous les types possibles (0-20) pour trouver
lesquels fonctionnent sur votre installation
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
logger.info(f"[TEST] Tentative creation type {type_doc}...")
try:
# Essayer de créer un process
process = sage.cial.CreateProcess_Document(type_doc)
if not process:
return {
"success": False,
"type": type_doc,
"resultat": "Process NULL retourne",
}
# Si on arrive ici, le type est valide !
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
# Récupérer les infos du document créé
type_reel = getattr(doc, "DO_Type", -1)
domaine = getattr(doc, "DO_Domaine", -1)
# NE PAS VALIDER le document (pas de Write/Process)
# On veut juste savoir si la création est possible
del process
del doc
return {
"success": True,
"type_demande": type_doc,
"type_reel_doc": type_reel,
"domaine": {0: "Vente", 1: "Achat"}.get(domaine, domaine),
"resultat": "CREATION POSSIBLE",
"note": "Document non valide (test uniquement)",
}
except Exception as e:
return {
"success": False,
"type": type_doc,
"erreur": str(e),
"resultat": "CREATION IMPOSSIBLE",
}
except Exception as e:
logger.error(f"[TEST] Erreur test type {type_doc}: {e}")
raise HTTPException(500, str(e))
@app.get("/sage/diagnostic/facture-requirements", dependencies=[Depends(verify_token)])
def diagnostiquer_exigences_facture():
"""
DIAGNOSTIC: Découvre les champs obligatoires pour créer une facture
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
# Créer un process facture de test
process = sage.cial.CreateProcess_Document(60)
doc_test = process.Document
try:
doc_test = win32com.client.CastTo(doc_test, "IBODocumentVente3")
except:
pass
# Tester tous les champs potentiellement obligatoires
champs_a_tester = [
"DO_ModeRegl",
"DO_CondRegl",
"DO_CodeJournal",
"DO_Souche",
"DO_TypeCalcul",
"DO_CodeTaxe1",
"CT_Num",
"DO_Date",
"DO_Statut",
]
resultats = {}
for champ in champs_a_tester:
try:
valeur = getattr(doc_test, champ, None)
resultats[champ] = {
"valeur_defaut": str(valeur) if valeur is not None else "None",
"accessible": True,
}
except Exception as e:
resultats[champ] = {
"valeur_defaut": "N/A",
"accessible": False,
"erreur": str(e)[:100],
}
# Ne pas valider le document de test
del process
del doc_test
return {
"success": True,
"champs_facture": resultats,
"conseil": "Les champs avec valeur_defaut=None ou 0 sont souvent obligatoires",
}
except Exception as e:
logger.error(f"[DIAG] Erreur: {e}", exc_info=True)
raise HTTPException(500, str(e))
@app.post("/sage/test/creer-facture-vide", dependencies=[Depends(verify_token)])
def tester_creation_facture_vide():
"""
🧪 TEST: Crée une facture vide pour identifier les champs obligatoires
Ce test permet de découvrir EXACTEMENT quels champs Sage exige
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
logger.info("[TEST] Creation facture test...")
# 1. Créer le process
process = sage.cial.CreateProcess_Document(60)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
# 2. Définir UNIQUEMENT les champs absolument critiques
import pywintypes
# Date (obligatoire)
doc.DO_Date = pywintypes.Time(datetime.now())
# Client (obligatoire) - Utiliser le premier client disponible
factory_client = sage.cial.CptaApplication.FactoryClient
persist_client = factory_client.List(1)
if persist_client:
client = sage._cast_client(persist_client)
if client:
doc.SetDefaultClient(client)
client_code = getattr(client, "CT_Num", "?")
logger.info(f"[TEST] Client test: {client_code}")
# 3. Écrire sans Process() pour voir les valeurs par défaut
doc.Write()
doc.Read()
# 4. Analyser tous les champs
champs_analyse = {}
for attr in dir(doc):
if attr.startswith("DO_") or attr.startswith("CT_"):
try:
valeur = getattr(doc, attr, None)
if valeur is not None:
champs_analyse[attr] = {
"valeur": str(valeur),
"type": type(valeur).__name__,
}
except:
pass
logger.info(f"[TEST] {len(champs_analyse)} champs analyses")
# 5. Tester Process() pour voir l'erreur exacte
erreur_process = None
try:
process.Process()
logger.info("[TEST] Process() reussi (inattendu!)")
except Exception as e:
erreur_process = str(e)
logger.info(f"[TEST] Process() echoue comme prevu: {e}")
# Ne pas commit - c'est juste un test
try:
sage.cial.CptaApplication.RollbackTrans()
except:
pass
return {
"success": True,
"champs_definis": champs_analyse,
"erreur_process": erreur_process,
"conseil": "Les champs manquants dans l'erreur sont probablement obligatoires",
}
except Exception as e:
logger.error(f"[TEST] Erreur: {e}", exc_info=True)
raise HTTPException(500, str(e))
@app.get("/sage/config/parametres-facture", dependencies=[Depends(verify_token)])
def verifier_parametres_facture():
"""
🔍 Vérifie les paramètres Sage pour la création de factures
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
parametres = {}
# Paramètres société
try:
param_societe = sage.cial.CptaApplication.ParametreSociete
parametres["societe"] = {
"journal_vente_defaut": getattr(
param_societe, "P_CodeJournalVte", "N/A"
),
"mode_reglement_defaut": getattr(
param_societe, "P_ModeRegl", "N/A"
),
"souche_facture": getattr(param_societe, "P_SoucheFacture", "N/A"),
}
except Exception as e:
parametres["erreur_societe"] = str(e)
# Tester un client existant
try:
factory_client = sage.cial.CptaApplication.FactoryClient
persist = factory_client.List(1)
if persist:
client = sage._cast_client(persist)
if client:
parametres["exemple_client"] = {
"code": getattr(client, "CT_Num", "?"),
"mode_reglement": getattr(client, "CT_ModeRegl", "N/A"),
"conditions_reglement": getattr(
client, "CT_CondRegl", "N/A"
),
}
except Exception as e:
parametres["erreur_client"] = str(e)
# Journaux disponibles
try:
factory_journal = sage.cial.CptaApplication.FactoryJournal
journaux = []
index = 1
while index <= 20: # Max 20 journaux
try:
persist_journal = factory_journal.List(index)
if persist_journal is None:
break
# Cast en journal
journal = win32com.client.CastTo(persist_journal, "IBOJournal3")
journal.Read()
journaux.append(
{
"code": getattr(journal, "JO_Num", "?"),
"intitule": getattr(journal, "JO_Intitule", "?"),
"type": getattr(journal, "JO_Type", "?"),
}
)
index += 1
except:
index += 1
break
parametres["journaux_disponibles"] = journaux
except Exception as e:
parametres["erreur_journaux"] = str(e)
return {
"success": True,
"parametres": parametres,
"conseil": "Utilisez ces valeurs pour remplir les champs obligatoires des factures",
}
except Exception as e:
logger.error(f"Erreur verification config: {e}", exc_info=True)
raise HTTPException(500, str(e))
@app.get("/sage/diagnostic/statuts-globaux", dependencies=[Depends(verify_token)])
def diagnostiquer_statuts_globaux():
"""
📊 MATRICE COMPLÈTE DES STATUTS SAGE
Retourne pour CHAQUE type de document :
- Tous les statuts possibles avec leurs descriptions
- Les statuts requis pour transformation
- Les changements de statuts après transformation
- Les restrictions de changement de statut
Cette route analyse la base Sage pour découvrir les règles réelles
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente
# Définition des types de documents
types_documents = {
0: "Devis",
10: "Bon de commande",
20: "Préparation",
30: "Bon de livraison",
40: "Bon de retour",
50: "Bon d'avoir",
60: "Facture",
}
# Descriptions standard des statuts Sage
descriptions_statuts = {
0: "Brouillon",
1: "Soumis/En attente",
2: "Accepté/Validé",
3: "Réalisé partiellement",
4: "Réalisé totalement",
5: "Transformé",
6: "Annulé",
}
matrice_complete = {}
logger.info(
"[DIAG] 🔍 Analyse des statuts pour tous les types de documents..."
)
# Pour chaque type de document
for type_doc, libelle_type in types_documents.items():
logger.info(f"[DIAG] Analyse type {type_doc} ({libelle_type})...")
analyse_type = {
"type": type_doc,
"libelle": libelle_type,
"statuts_observes": {},
"exemples_par_statut": {},
"nb_documents_total": 0,
}
# Scanner tous les documents de ce type
index = 1
max_scan = 1000
while index < max_scan:
try:
persist = factory.List(index)
if persist is None:
break
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
doc_type = getattr(doc, "DO_Type", -1)
# Filtrer sur le type qu'on analyse
if doc_type != type_doc:
index += 1
continue
analyse_type["nb_documents_total"] += 1
# Récupérer le statut
statut = getattr(doc, "DO_Statut", -1)
# Compter les statuts observés
if statut not in analyse_type["statuts_observes"]:
analyse_type["statuts_observes"][statut] = {
"count": 0,
"description": descriptions_statuts.get(
statut, f"Statut {statut}"
),
"exemples": [],
}
analyse_type["statuts_observes"][statut]["count"] += 1
# Garder quelques exemples
if (
len(analyse_type["statuts_observes"][statut]["exemples"])
< 3
):
numero = getattr(doc, "DO_Piece", "")
date = str(getattr(doc, "DO_Date", ""))
analyse_type["statuts_observes"][statut]["exemples"].append(
{
"numero": numero,
"date": date,
"total_ttc": float(
getattr(doc, "DO_TotalTTC", 0.0)
),
}
)
index += 1
except Exception as e:
index += 1
continue
# Trier les statuts par nombre d'occurrences
analyse_type["statuts_par_frequence"] = sorted(
[
{
"statut": s,
"description": info["description"],
"count": info["count"],
"pourcentage": (
round(
info["count"]
/ analyse_type["nb_documents_total"]
* 100,
1,
)
if analyse_type["nb_documents_total"] > 0
else 0
),
}
for s, info in analyse_type["statuts_observes"].items()
],
key=lambda x: x["count"],
reverse=True,
)
matrice_complete[type_doc] = analyse_type
logger.info(
f"[DIAG] Type {type_doc}: {analyse_type['nb_documents_total']} docs, "
f"{len(analyse_type['statuts_observes'])} statuts différents"
)
# RÈGLES DE TRANSFORMATION
regles_transformation = {
"transformations_valides": [
{
"source_type": 0,
"source_libelle": "Devis",
"cible_type": 10,
"cible_libelle": "Bon de commande",
"statut_source_requis": [2],
"statut_source_requis_description": ["Accepté/Validé"],
"statut_source_apres": 5,
"statut_source_apres_description": "Transformé",
"statut_cible_initial": 2,
"statut_cible_initial_description": "Accepté/Validé",
},
{
"source_type": 10,
"source_libelle": "Bon de commande",
"cible_type": 30,
"cible_libelle": "Bon de livraison",
"statut_source_requis": [2],
"statut_source_requis_description": ["Accepté/Validé"],
"statut_source_apres": 5,
"statut_source_apres_description": "Transformé",
"statut_cible_initial": 2,
"statut_cible_initial_description": "Accepté/Validé",
},
{
"source_type": 10,
"source_libelle": "Bon de commande",
"cible_type": 60,
"cible_libelle": "Facture",
"statut_source_requis": [2],
"statut_source_requis_description": ["Accepté/Validé"],
"statut_source_apres": 5,
"statut_source_apres_description": "Transformé",
"statut_cible_initial": 2,
"statut_cible_initial_description": "Accepté/Validé",
},
{
"source_type": 30,
"source_libelle": "Bon de livraison",
"cible_type": 60,
"cible_libelle": "Facture",
"statut_source_requis": [2],
"statut_source_requis_description": ["Accepté/Validé"],
"statut_source_apres": 5,
"statut_source_apres_description": "Transformé",
"statut_cible_initial": 2,
"statut_cible_initial_description": "Accepté/Validé",
},
{
"source_type": 0,
"source_libelle": "Devis",
"cible_type": 60,
"cible_libelle": "Facture",
"statut_source_requis": [2],
"statut_source_requis_description": ["Accepté/Validé"],
"statut_source_apres": 5,
"statut_source_apres_description": "Transformé",
"statut_cible_initial": 2,
"statut_cible_initial_description": "Accepté/Validé",
},
],
"statuts_bloquants_pour_transformation": [
{
"statut": 5,
"description": "Transformé",
"raison": "Le document a déjà été transformé",
},
{
"statut": 6,
"description": "Annulé",
"raison": "Le document est annulé",
},
{
"statut": 3,
"description": "Réalisé partiellement",
"raison": "Un document cible existe probablement déjà (transformation partielle effectuée)",
},
{
"statut": 4,
"description": "Réalisé totalement",
"raison": "Le document a été entièrement réalisé (transformation déjà effectuée)",
},
],
"changements_statut_autorises": {
"0_Brouillon": {
"vers": [2, 6],
"descriptions": ["Accepté/Validé", "Annulé"],
"note": "Un brouillon peut être accepté ou annulé",
},
"2_Accepte": {
"vers": [5, 6],
"descriptions": ["Transformé", "Annulé"],
"note": "Un document accepté peut être transformé ou annulé",
},
"5_Transforme": {
"vers": [],
"descriptions": [],
"note": "Un document transformé ne peut plus changer de statut",
},
"6_Annule": {
"vers": [],
"descriptions": [],
"note": "Un document annulé ne peut plus changer de statut",
},
},
}
return {
"success": True,
"matrice_statuts_par_type": matrice_complete,
"regles_transformation": regles_transformation,
"legende_statuts": descriptions_statuts,
"types_documents": types_documents,
"date_analyse": datetime.now().isoformat(),
"note": "Cette matrice est construite à partir des documents réels dans votre base Sage",
}
except Exception as e:
logger.error(f"[DIAG] Erreur diagnostic global: {e}", exc_info=True)
raise HTTPException(500, str(e))
@app.get(
"/sage/diagnostic/statuts-permis/{numero}", dependencies=[Depends(verify_token)]
)
def diagnostiquer_statuts_permis(numero: str):
"""
🔍 DIAGNOSTIC CRITIQUE: Découvre TOUS les statuts possibles pour un document
Teste tous les statuts de 0 à 10 pour identifier lesquels sont valides
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente
# Chercher le document (tous types confondus)
persist = None
type_doc_trouve = None
# Essayer ReadPiece pour différents types
for type_test in range(7): # 0-6
try:
persist_test = factory.ReadPiece(type_test, numero)
if persist_test:
persist = persist_test
type_doc_trouve = type_test
logger.info(
f"[DIAG] Document {numero} trouvé avec ReadPiece(type={type_test})"
)
break
except:
continue
# Si pas trouvé, chercher dans List()
if not persist:
index = 1
while index < 10000:
try:
persist_test = factory.List(index)
if persist_test is None:
break
doc_test = win32com.client.CastTo(
persist_test, "IBODocumentVente3"
)
doc_test.Read()
if getattr(doc_test, "DO_Piece", "") == numero:
persist = persist_test
type_doc_trouve = getattr(doc_test, "DO_Type", -1)
logger.info(
f"[DIAG] Document {numero} trouvé dans List() à l'index {index}"
)
break
index += 1
except:
index += 1
if not persist:
raise HTTPException(404, f"Document {numero} introuvable")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
# Infos du document
statut_actuel = getattr(doc, "DO_Statut", -1)
type_actuel = getattr(doc, "DO_Type", -1)
diagnostic = {
"numero": numero,
"type_document": type_actuel,
"type_libelle": {
0: "Devis",
10: "Bon de commande",
20: "Préparation",
30: "Bon de livraison",
40: "Bon de retour",
50: "Bon d'avoir",
60: "Facture",
}.get(type_actuel, f"Type {type_actuel}"),
"statut_actuel": statut_actuel,
"statut_actuel_libelle": {
0: "Brouillon",
1: "Soumis/En attente",
2: "Accepté/Validé",
3: "Réalisé partiellement",
4: "Réalisé totalement",
5: "Transformé",
6: "Annulé",
}.get(statut_actuel, f"Statut {statut_actuel}"),
"tests_statuts": [],
}
# Tester tous les statuts de 0 à 10
logger.info(f"[DIAG] Test des statuts pour {numero}...")
for statut_test in range(11):
resultat_test = {
"statut": statut_test,
"libelle": {
0: "Brouillon",
1: "Soumis/En attente",
2: "Accepté/Validé",
3: "Réalisé partiellement",
4: "Réalisé totalement",
5: "Transformé",
6: "Annulé",
7: "Statut 7",
8: "Statut 8",
9: "Statut 9",
10: "Statut 10",
}.get(statut_test, f"Statut {statut_test}"),
"autorise": False,
"erreur": None,
"est_statut_actuel": (statut_test == statut_actuel),
}
# Si c'est le statut actuel, on sait qu'il est valide
if statut_test == statut_actuel:
resultat_test["autorise"] = True
resultat_test["note"] = "Statut actuel du document"
else:
# Tester le changement de statut
try:
# Relire le document
doc.Read()
# Essayer de changer le statut
doc.DO_Statut = statut_test
# Essayer d'écrire
doc.Write()
# Si on arrive ici, le statut est valide !
resultat_test["autorise"] = True
resultat_test["note"] = "Changement de statut réussi"
logger.info(f"[DIAG] Statut {statut_test} AUTORISÉ")
# Restaurer le statut d'origine immédiatement
doc.Read()
doc.DO_Statut = statut_actuel
doc.Write()
except Exception as e:
erreur_str = str(e)
resultat_test["autorise"] = False
resultat_test["erreur"] = erreur_str
logger.debug(
f"[DIAG] Statut {statut_test} REFUSÉ: {erreur_str[:100]}"
)
# Restaurer en cas d'erreur
try:
doc.Read()
except:
pass
diagnostic["tests_statuts"].append(resultat_test)
# Résumé
statuts_autorises = [
t["statut"] for t in diagnostic["tests_statuts"] if t["autorise"]
]
statuts_refuses = [
t["statut"] for t in diagnostic["tests_statuts"] if not t["autorise"]
]
diagnostic["resume"] = {
"nb_statuts_autorises": len(statuts_autorises),
"statuts_autorises": statuts_autorises,
"statuts_autorises_libelles": [
t["libelle"] for t in diagnostic["tests_statuts"] if t["autorise"]
],
"nb_statuts_refuses": len(statuts_refuses),
"statuts_refuses": statuts_refuses,
}
# Recommandations
recommendations = []
if 2 in statuts_autorises and statut_actuel == 0:
recommendations.append(
" Vous pouvez passer ce document de 'Brouillon' (0) à 'Accepté' (2)"
)
if 5 in statuts_autorises:
recommendations.append(
" Le statut 'Transformé' (5) est disponible - utilisé après transformation"
)
if 6 in statuts_autorises:
recommendations.append(" Vous pouvez annuler ce document (statut 6)")
if not any(s in statuts_autorises for s in [2, 3, 4]):
recommendations.append(
" Aucun statut de validation (2/3/4) n'est disponible - "
"le document a peut-être déjà été traité"
)
diagnostic["recommendations"] = recommendations
logger.info(
f"[DIAG] Statuts autorisés pour {numero}: "
f"{statuts_autorises} / Refusés: {statuts_refuses}"
)
return {"success": True, "diagnostic": diagnostic}
except HTTPException:
raise
except Exception as e:
logger.error(f"[DIAG] Erreur diagnostic statuts: {e}", exc_info=True)
raise HTTPException(500, str(e))
@app.get(
"/sage/diagnostic/erreur-transformation/{numero}",
dependencies=[Depends(verify_token)],
)
def diagnostiquer_erreur_transformation(
numero: str, type_source: int = Query(...), type_cible: int = Query(...)
):
"""
🔍 DIAGNOSTIC AVANCÉ: Analyse pourquoi une transformation échoue
Vérifie:
- Statut du document source
- Statuts autorisés
- Lignes du document
- Client associé
- Champs obligatoires manquants
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente
# Lire le document source
persist = factory.ReadPiece(type_source, numero)
if not persist:
persist = sage._find_document_in_list(numero, type_source)
if not persist:
raise HTTPException(
404, f"Document {numero} (type {type_source}) introuvable"
)
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
diagnostic = {
"numero": numero,
"type_source": type_source,
"type_cible": type_cible,
"problemes_detectes": [],
"avertissements": [],
"suggestions": [],
}
# 1. Vérifier le statut
statut_actuel = getattr(doc, "DO_Statut", -1)
diagnostic["statut_actuel"] = statut_actuel
if statut_actuel == 5:
diagnostic["problemes_detectes"].append(
{
"severite": "BLOQUANT",
"champ": "DO_Statut",
"valeur": 5,
"message": "Document déjà transformé (statut=5)",
}
)
elif statut_actuel == 6:
diagnostic["problemes_detectes"].append(
{
"severite": "BLOQUANT",
"champ": "DO_Statut",
"valeur": 6,
"message": "Document annulé (statut=6)",
}
)
elif statut_actuel in [3, 4]:
diagnostic["avertissements"].append(
{
"severite": "ATTENTION",
"champ": "DO_Statut",
"valeur": statut_actuel,
"message": f"Document déjà réalisé (statut={statut_actuel}). "
f"Un document cible existe peut-être déjà.",
}
)
elif statut_actuel == 0:
diagnostic["suggestions"].append(
"Le document est en 'Brouillon' (statut=0). "
"Le système le passera automatiquement à 'Accepté' (statut=2) avant transformation."
)
# 2. Vérifier le client
client_code = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code = getattr(client_obj, "CT_Num", "").strip()
except:
pass
if not client_code:
diagnostic["problemes_detectes"].append(
{
"severite": "BLOQUANT",
"champ": "CT_Num",
"valeur": None,
"message": "Aucun client associé au document",
}
)
else:
diagnostic["client_code"] = client_code
# 3. Vérifier les lignes
try:
factory_lignes = getattr(doc, "FactoryDocumentLigne", None) or getattr(
doc, "FactoryDocumentVenteLigne", None
)
nb_lignes = 0
lignes_problemes = []
if factory_lignes:
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3")
ligne.Read()
nb_lignes += 1
# Vérifier article
article_ref = ""
try:
article_ref = getattr(ligne, "AR_Ref", "").strip()
if not article_ref:
article_obj = getattr(ligne, "Article", None)
if article_obj:
article_obj.Read()
article_ref = getattr(
article_obj, "AR_Ref", ""
).strip()
except:
pass
if not article_ref:
lignes_problemes.append(
{
"ligne": index,
"probleme": "Aucune référence article",
}
)
# Vérifier prix
prix = float(getattr(ligne, "DL_PrixUnitaire", 0.0))
if prix == 0:
lignes_problemes.append(
{"ligne": index, "probleme": "Prix unitaire = 0"}
)
index += 1
except:
break
diagnostic["nb_lignes"] = nb_lignes
if nb_lignes == 0:
diagnostic["problemes_detectes"].append(
{
"severite": "BLOQUANT",
"champ": "Lignes",
"valeur": 0,
"message": "Document vide (aucune ligne)",
}
)
if lignes_problemes:
diagnostic["avertissements"].append(
{
"severite": "ATTENTION",
"champ": "Lignes",
"message": f"{len(lignes_problemes)} ligne(s) avec des problèmes",
"details": lignes_problemes,
}
)
except Exception as e:
diagnostic["avertissements"].append(
{
"severite": "ERREUR",
"champ": "Lignes",
"message": f"Impossible de lire les lignes: {e}",
}
)
# 4. Vérifier les totaux
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
diagnostic["totaux"] = {"total_ht": total_ht, "total_ttc": total_ttc}
if total_ht == 0 and total_ttc == 0:
diagnostic["avertissements"].append(
{
"severite": "ATTENTION",
"champ": "Totaux",
"message": "Tous les totaux sont à 0",
}
)
# 5. Vérifier si la transformation est autorisée
transformations_valides = {(0, 10), (10, 30), (10, 60), (30, 60), (0, 60)}
if (type_source, type_cible) not in transformations_valides:
diagnostic["problemes_detectes"].append(
{
"severite": "BLOQUANT",
"champ": "Transformation",
"message": f"Transformation {type_source}{type_cible} non autorisée. "
f"Transformations valides: {transformations_valides}",
}
)
# Résumé
nb_bloquants = sum(
1
for p in diagnostic["problemes_detectes"]
if p.get("severite") == "BLOQUANT"
)
nb_avertissements = len(diagnostic["avertissements"])
diagnostic["resume"] = {
"peut_transformer": nb_bloquants == 0,
"nb_problemes_bloquants": nb_bloquants,
"nb_avertissements": nb_avertissements,
}
if nb_bloquants == 0:
diagnostic["suggestions"].append(
" Aucun problème bloquant détecté. La transformation devrait fonctionner."
)
else:
diagnostic["suggestions"].append(
f" {nb_bloquants} problème(s) bloquant(s) doivent être résolus avant la transformation."
)
return {"success": True, "diagnostic": diagnostic}
except HTTPException:
raise
except Exception as e:
logger.error(f"[DIAG] Erreur diagnostic transformation: {e}", exc_info=True)
raise HTTPException(500, str(e))
# =====================================================
# ENDPOINTS - PROSPECTS
# =====================================================
@app.post("/sage/prospects/list", dependencies=[Depends(verify_token)])
def prospects_list(req: FiltreRequest):
"""📋 Liste tous les prospects (CT_Type=0 AND CT_Prospect=1)"""
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):
"""📄 Lecture d'un prospect par code"""
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):
"""
⚡ Liste rapide des fournisseurs depuis le CACHE
Utilise le cache mémoire pour une réponse instantanée
🔄 Cache actualisé automatiquement toutes les 15 minutes
"""
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):
"""
Création d'un fournisseur dans Sage
Utilise FactoryFournisseur.Create() directement
"""
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):
"""
✏️ Modification d'un fournisseur dans Sage
"""
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"),
):
"""
📋 Liste rapide des avoirs depuis le CACHE (avec lignes)
⚡ ULTRA-RAPIDE: Utilise le cache mémoire
LIGNES INCLUSES: Contrairement aux anciennes méthodes
💡 Pour forcer une relecture depuis Sage, utiliser /sage/avoirs/get
"""
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):
"""
📄 Lecture d'un avoir (depuis cache en priorité)
⚡ Essaie d'abord le cache (instantané)
🔄 Si introuvable, force une relecture depuis Sage
"""
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"),
):
"""
📋 Liste rapide des livraisons depuis le CACHE (avec lignes)
⚡ ULTRA-RAPIDE: Utilise le cache mémoire
LIGNES INCLUSES: Contrairement aux anciennes méthodes
💡 Pour forcer une relecture depuis Sage, utiliser /sage/livraisons/get
"""
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):
"""
📄 Lecture d'une livraison (depuis cache en priorité)
⚡ Essaie d'abord le cache (instantané)
🔄 Si introuvable, force une relecture depuis Sage
"""
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):
"""
✏️ Modification d'un devis dans Sage
Permet de modifier:
- La date du devis
- Les lignes (remplace toutes les lignes)
- Le statut
"""
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):
"""
Création d'une commande (Bon de commande) dans Sage
"""
try:
# Transformer en format attendu par sage_connector
commande_data = {
"client": {"code": req.client_id, "intitule": ""},
"date_commande": req.date_commande 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):
"""
✏️ Modification d'une commande dans Sage
Permet de modifier:
- La date de la commande
- Les lignes (remplace toutes les lignes)
- Le statut
- La référence externe
"""
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):
"""
Création d'une livraison (Bon de livraison) dans Sage
"""
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(),
"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):
"""
✏️ Modification d'une livraison dans Sage
"""
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):
"""
Création d'un avoir (Bon d'avoir) dans Sage
"""
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(),
"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):
"""
Création d'une facture dans Sage
NOTE: Les factures peuvent avoir des champs obligatoires supplémentaires
selon la configuration Sage (DO_CodeJournal, DO_Souche, etc.)
"""
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(),
"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):
"""
✏️ Modification d'une facture dans Sage
ATTENTION: Les factures comptabilisées peuvent être verrouillées
"""
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):
"""
Création d'un article dans Sage
**Usage typique**: Créer un article avec stock pour éviter l'erreur 2881
"""
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):
"""
✏️ Modification d'un article dans Sage
**Usage critique**: Augmenter le stock pour résoudre l'erreur 2881
Example:
```json
{
"reference": "ART001",
"article_data": {
"stock_reel": 100.0
}
}
```
"""
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/documents/generate-pdf", dependencies=[Depends(verify_token)])
def generer_pdf_document(req: PDFGenerationRequest):
"""
📄 Génération PDF d'un document (endpoint généralisé)
**Supporte tous les types de documents Sage:**
- Devis (0)
- Bons de commande (10)
- Bons de livraison (30)
- Factures (60)
- Avoirs (50)
**Process:**
1. Charge le document depuis Sage
2. Génère le PDF via l'état Sage correspondant
3. Retourne le PDF en base64
Args:
req: Requête contenant doc_id et type_doc
Returns:
{
"success": true,
"data": {
"pdf_base64": "JVBERi0xLjQK...",
"taille_octets": 12345,
"type_doc": 0,
"numero": "DE00001"
}
}
"""
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.post("/sage/admin/clean-locks")
def nettoyer_verrous_sage():
"""
🧹 Nettoyage des verrous Sage (cbRegFile)
À utiliser uniquement si l'API est bloquée
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
# Forcer la fermeture de toutes les transactions en attente
for _ in range(10):
try:
sage.cial.CptaApplication.RollbackTrans()
logger.info(" Rollback effectué")
except:
break
# Déconnecter/reconnecter
sage.deconnecter()
time.sleep(2)
sage.connecter()
return {
"success": True,
"message": "Verrous nettoyés, connexion Sage réinitialisée"
}
except Exception as e:
logger.error(f"Erreur nettoyage verrous: {e}")
raise HTTPException(500, str(e))
@app.get("/sage/diagnostic/transform-deep/{numero_source}", dependencies=[Depends(verify_token)])
def diagnostic_transformation_approfondi(
numero_source: str,
type_source: int = Query(..., description="Type document source"),
type_cible: int = Query(..., description="Type document cible")
):
"""
🔬 DIAGNOSTIC ULTRA-APPROFONDI : Transformation de document
Cette route va :
1. Créer le document cible comme dans transformer_document()
2. Scanner TOUS les champs DO_* du document
3. Comparer avec une facture manuelle réussie
4. Lister les champs manquants ou invalides
5. NE PAS commit (rollback à la fin)
**Process** :
- Lit le devis source
- Crée une facture cible
- Associe le client
- Copie les lignes
- SCAN complet des champs AVANT Process()
- Tentative Process() et lecture des erreurs
- Rollback (rien n'est sauvegardé)
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
logger.info(f"[DIAG] === DIAGNOSTIC APPROFONDI TRANSFORMATION ===")
logger.info(f"[DIAG] Source: {numero_source} (type {type_source})")
logger.info(f"[DIAG] Cible: type {type_cible}")
diagnostic = {
"numero_source": numero_source,
"type_source": type_source,
"type_cible": type_cible,
"etapes": [],
"champs_document": {},
"champs_manquants": [],
"erreurs_sage": [],
"recommandations": []
}
# ========================================
# ÉTAPE 1 : LIRE LE DOCUMENT SOURCE
# ========================================
diagnostic["etapes"].append("Lecture document source")
factory = sage.cial.FactoryDocumentVente
persist_source = factory.ReadPiece(type_source, numero_source)
if not persist_source:
raise HTTPException(404, f"Document {numero_source} introuvable")
doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3")
doc_source.Read()
# Client
client_code = ""
try:
client_obj = getattr(doc_source, "Client", None)
if client_obj:
client_obj.Read()
client_code = getattr(client_obj, "CT_Num", "").strip()
except:
pass
if not client_code:
raise HTTPException(400, "Client introuvable dans document source")
diagnostic["client_code"] = client_code
# Lignes
lignes_source = []
try:
factory_lignes_source = getattr(doc_source, "FactoryDocumentLigne", None) or \
getattr(doc_source, "FactoryDocumentVenteLigne", None)
if factory_lignes_source:
index = 1
while index <= 100:
try:
ligne_p = factory_lignes_source.List(index)
if ligne_p is None:
break
ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3")
ligne.Read()
article_ref = ""
try:
article_ref = getattr(ligne, "AR_Ref", "").strip()
if not article_ref:
article_obj = getattr(ligne, "Article", None)
if article_obj:
article_obj.Read()
article_ref = getattr(article_obj, "AR_Ref", "").strip()
except:
pass
lignes_source.append({
"article_ref": article_ref,
"quantite": float(getattr(ligne, "DL_Qte", 0.0)),
"prix_unitaire": float(getattr(ligne, "DL_PrixUnitaire", 0.0))
})
index += 1
except:
break
except:
pass
diagnostic["nb_lignes_source"] = len(lignes_source)
diagnostic["etapes"].append(f"Source lu: {len(lignes_source)} lignes")
# ========================================
# ÉTAPE 2 : TRANSACTION DE TEST
# ========================================
try:
sage.cial.CptaApplication.BeginTrans()
diagnostic["etapes"].append("Transaction démarrée")
except:
diagnostic["etapes"].append("Transaction non supportée")
try:
# ========================================
# ÉTAPE 3 : CRÉER DOCUMENT CIBLE
# ========================================
diagnostic["etapes"].append(f"Création document type {type_cible}")
process = sage.cial.CreateProcess_Document(type_cible)
doc_cible = process.Document
try:
doc_cible = win32com.client.CastTo(doc_cible, "IBODocumentVente3")
except:
pass
# Date
import pywintypes
date_source = getattr(doc_source, "DO_Date", None)
if date_source:
doc_cible.DO_Date = date_source
else:
doc_cible.DO_Date = pywintypes.Time(datetime.now())
# ========================================
# ÉTAPE 4 : ASSOCIER CLIENT
# ========================================
diagnostic["etapes"].append(f"Association client {client_code}")
factory_client = sage.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(client_code)
if not persist_client:
raise ValueError(f"Client {client_code} introuvable")
client_obj_cible = sage._cast_client(persist_client)
try:
doc_cible.SetDefaultClient(client_obj_cible)
except:
doc_cible.SetClient(client_obj_cible)
doc_cible.DO_Ref = numero_source
doc_cible.Write()
diagnostic["etapes"].append("Client associé + 1er Write()")
# ========================================
# ÉTAPE 5 : COPIER LIGNES
# ========================================
try:
factory_lignes_cible = doc_cible.FactoryDocumentLigne
except:
factory_lignes_cible = doc_cible.FactoryDocumentVenteLigne
factory_article = sage.cial.FactoryArticle
for idx, ligne_data in enumerate(lignes_source, 1):
article_ref = ligne_data["article_ref"]
if not article_ref:
continue
persist_article = factory_article.ReadReference(article_ref)
if not persist_article:
continue
article_obj = win32com.client.CastTo(persist_article, "IBOArticle3")
article_obj.Read()
ligne_persist = factory_lignes_cible.Create()
try:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3")
except:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3")
quantite = ligne_data["quantite"]
try:
ligne_obj.SetDefaultArticleReference(article_ref, quantite)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Qte = quantite
prix = ligne_data["prix_unitaire"]
if prix > 0:
ligne_obj.DL_PrixUnitaire = float(prix)
ligne_obj.Write()
diagnostic["etapes"].append(f"{len(lignes_source)} lignes copiées")
# ========================================
# ÉTAPE 6 : CHAMPS FACTURES
# ========================================
if type_cible == 60:
doc_cible.DO_CodeJournal = "VTE"
doc_cible.DO_Souche = 0
doc_cible.DO_Regime = 0
diagnostic["etapes"].append("Champs factures définis")
# Write final
doc_cible.Write()
doc_cible.Read()
diagnostic["etapes"].append("Write() final effectué")
# ========================================
# ÉTAPE 7 : SCAN COMPLET DES CHAMPS
# ========================================
diagnostic["etapes"].append("Scan complet des champs DO_*")
champs = {}
champs_vides = []
champs_null = []
# Liste TOUS les attributs du document
for attr in dir(doc_cible):
if attr.startswith("DO_"):
try:
valeur = getattr(doc_cible, attr, None)
# Ignorer les méthodes
if callable(valeur):
continue
# Catégoriser selon la valeur
if valeur is None:
champs_null.append(attr)
champs[attr] = {"valeur": "NULL", "type": "null"}
elif isinstance(valeur, str):
if valeur == "":
champs_vides.append(attr)
champs[attr] = {"valeur": "", "type": "string_vide"}
else:
champs[attr] = {"valeur": valeur, "type": "string", "len": len(valeur)}
elif isinstance(valeur, (int, float)):
if valeur == 0:
champs[attr] = {"valeur": 0, "type": "zero"}
else:
champs[attr] = {"valeur": valeur, "type": type(valeur).__name__}
else:
champs[attr] = {"valeur": str(valeur)[:50], "type": type(valeur).__name__}
except Exception as e:
champs[attr] = {"erreur": str(e)[:100]}
diagnostic["champs_document"] = champs
diagnostic["nb_champs_total"] = len(champs)
diagnostic["nb_champs_null"] = len(champs_null)
diagnostic["nb_champs_vides"] = len(champs_vides)
diagnostic["champs_null"] = champs_null
diagnostic["champs_vides"] = champs_vides
# ========================================
# ÉTAPE 8 : LIRE LES ERREURS DU DOCUMENT
# ========================================
erreurs_doc = []
try:
if hasattr(doc_cible, "Errors"):
nb_erreurs = doc_cible.Errors.Count
if nb_erreurs > 0:
for i in range(nb_erreurs):
try:
err = doc_cible.Errors.Item(i)
erreurs_doc.append({
"description": str(getattr(err, "Description", "?")),
"type": str(getattr(err, "Type", "?")),
"code": str(getattr(err, "Number", "?")),
"field": str(getattr(err, "Field", "?"))
})
except:
pass
except:
pass
diagnostic["erreurs_document"] = erreurs_doc
diagnostic["etapes"].append(f"Erreurs document: {len(erreurs_doc)}")
# ========================================
# ÉTAPE 9 : TENTER PROCESS()
# ========================================
diagnostic["etapes"].append("Tentative Process()...")
try:
process.Process()
diagnostic["process_resultat"] = "SUCCÈS"
diagnostic["etapes"].append("Process() RÉUSSI !")
except Exception as e:
diagnostic["process_resultat"] = "ÉCHEC"
diagnostic["process_erreur"] = str(e)
diagnostic["etapes"].append(f"Process() échoué: {str(e)[:100]}")
# Lire erreurs du process
erreurs_process = []
try:
if hasattr(process, "Errors"):
nb_err_process = process.Errors.Count
if nb_err_process > 0:
for i in range(nb_err_process):
try:
err = process.Errors.Item(i)
erreurs_process.append({
"description": str(getattr(err, "Description", "?")),
"type": str(getattr(err, "Type", "?")),
"field": str(getattr(err, "Field", "?"))
})
except:
pass
except:
pass
diagnostic["erreurs_process"] = erreurs_process
# ========================================
# ROLLBACK (NE PAS SAUVEGARDER)
# ========================================
try:
sage.cial.CptaApplication.RollbackTrans()
diagnostic["etapes"].append("Rollback effectué (rien sauvegardé)")
except:
diagnostic["etapes"].append("Pas de rollback (pas de transaction)")
# ========================================
# RECOMMANDATIONS
# ========================================
if diagnostic["process_resultat"] == "ÉCHEC":
diagnostic["recommandations"].append(
"Process() a échoué. Analysez les champs NULL et vides ci-dessus."
)
# Champs suspects
champs_suspects = []
if type_cible == 60: # Facture
champs_critiques_facture = [
"DO_ModeRegl", "DO_CondRegl", "DO_CodeJournal",
"DO_Souche", "DO_Regime", "DO_TypeCalcul"
]
for champ in champs_critiques_facture:
if champ in champs_vides or champ in champs_null:
champs_suspects.append(champ)
if champs_suspects:
diagnostic["champs_manquants"] = champs_suspects
diagnostic["recommandations"].append(
f"Champs critiques manquants pour facture: {', '.join(champs_suspects)}"
)
# Comparer avec une facture manuelle
diagnostic["recommandations"].append(
"SOLUTION : Créez une facture MANUELLEMENT dans Sage, "
"puis appelez GET /sage/diagnostic/facture-manuelle/{numero} "
"pour voir les différences"
)
else:
diagnostic["recommandations"].append(
"Process() a RÉUSSI ! La transformation devrait fonctionner."
)
return {
"success": True,
"diagnostic": diagnostic
}
except Exception as e:
# Rollback en cas d'erreur
try:
sage.cial.CptaApplication.RollbackTrans()
except:
pass
raise HTTPException(500, f"Erreur diagnostic: {str(e)}")
except HTTPException:
raise
except Exception as e:
logger.error(f"[DIAG] Erreur: {e}", exc_info=True)
raise HTTPException(500, str(e))
@app.get("/sage/diagnostic/facture-manuelle/{numero}", dependencies=[Depends(verify_token)])
def analyser_facture_manuelle(numero: str):
"""
🔍 ANALYSE D'UNE FACTURE CRÉÉE MANUELLEMENT DANS SAGE
Scanne TOUS les champs d'une facture qui a été créée avec succès
pour voir la différence avec ce qu'on crée par API
**Process** :
1. Créer une facture manuellement dans Sage
2. Noter son numéro (ex: FA00001)
3. Appeler cette route : GET /sage/diagnostic/facture-manuelle/FA00001
4. Comparer les champs avec ceux du diagnostic transformation
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente
# Essayer type 60 (facture)
persist = factory.ReadPiece(60, numero)
if not persist:
# Chercher dans List()
index = 1
while index < 1000:
try:
persist_test = factory.List(index)
if persist_test is None:
break
doc_test = win32com.client.CastTo(persist_test, "IBODocumentVente3")
doc_test.Read()
if getattr(doc_test, "DO_Piece", "") == numero:
persist = persist_test
break
index += 1
except:
index += 1
if not persist:
raise HTTPException(404, f"Facture {numero} introuvable")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
# Scanner TOUS les champs DO_*
champs = {}
champs_remplis = []
champs_vides = []
champs_null = []
for attr in dir(doc):
if attr.startswith("DO_"):
try:
valeur = getattr(doc, attr, None)
if callable(valeur):
continue
if valeur is None:
champs_null.append(attr)
champs[attr] = "NULL"
elif isinstance(valeur, str):
if valeur == "":
champs_vides.append(attr)
champs[attr] = ""
else:
champs_remplis.append(attr)
champs[attr] = valeur
elif isinstance(valeur, (int, float)):
if valeur != 0:
champs_remplis.append(attr)
champs[attr] = valeur
else:
champs[attr] = str(valeur)[:50]
except:
pass
return {
"success": True,
"facture_numero": numero,
"type_doc": getattr(doc, "DO_Type", -1),
"statut": getattr(doc, "DO_Statut", -1),
"champs_analyse": {
"total": len(champs),
"remplis": len(champs_remplis),
"vides": len(champs_vides),
"null": len(champs_null)
},
"champs_remplis": champs_remplis,
"champs_vides": champs_vides,
"champs_null": champs_null,
"tous_champs": champs,
"conseil": (
"Comparez ces champs avec ceux du diagnostic transformation. "
"Les champs présents ici mais NULL/vides dans le diagnostic "
"sont probablement les champs manquants."
)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur analyse facture: {e}", exc_info=True)
raise HTTPException(500, str(e))
# À AJOUTER dans main.py
@app.get("/sage/diagnostic/transform-final/{numero_source}", dependencies=[Depends(verify_token)])
def diagnostic_transformation_final(
numero_source: str,
type_source: int = Query(..., description="Type document source"),
type_cible: int = Query(..., description="Type document cible")
):
"""
🔬 DIAGNOSTIC ULTIME : Scanner le document AVANT Process()
Cette route va :
1. Créer le document EXACTEMENT comme transformer_document()
2. Lire doc_cible.Errors APRÈS chaque Write()
3. Scanner TOUS les champs DO_* du document
4. Comparer avec une facture manuelle (FA00001)
5. NE PAS appeler Process() (rollback à la fin)
**Process** :
- Reproduit transformer_document() jusqu'au Process()
- S'arrête juste avant pour lire les erreurs
- Rollback (rien n'est sauvegardé)
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
logger.info(f"[DIAG-FINAL] === DIAGNOSTIC TRANSFORMATION FINALE ===")
logger.info(f"[DIAG-FINAL] Source: {numero_source} (type {type_source})")
logger.info(f"[DIAG-FINAL] Cible: type {type_cible}")
diagnostic = {
"numero_source": numero_source,
"type_source": type_source,
"type_cible": type_cible,
"etapes": [],
"champs_document": {},
"erreurs_sage": [],
"comparaison_facture_manuelle": {}
}
# ========================================
# ÉTAPE 1 : LIRE LE DOCUMENT SOURCE
# ========================================
diagnostic["etapes"].append("Lecture document source")
factory = sage.cial.FactoryDocumentVente
persist_source = factory.ReadPiece(type_source, numero_source)
if not persist_source:
raise HTTPException(404, f"Document {numero_source} introuvable")
doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3")
doc_source.Read()
# Client
client_code = ""
try:
client_obj = getattr(doc_source, "Client", None)
if client_obj:
client_obj.Read()
client_code = getattr(client_obj, "CT_Num", "").strip()
except:
pass
if not client_code:
raise HTTPException(400, "Client introuvable dans document source")
diagnostic["client_code"] = client_code
# Lignes
lignes_source = []
try:
factory_lignes_source = getattr(doc_source, "FactoryDocumentLigne", None) or \
getattr(doc_source, "FactoryDocumentVenteLigne", None)
if factory_lignes_source:
index = 1
while index <= 100:
try:
ligne_p = factory_lignes_source.List(index)
if ligne_p is None:
break
ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3")
ligne.Read()
article_ref = ""
try:
article_ref = getattr(ligne, "AR_Ref", "").strip()
if not article_ref:
article_obj = getattr(ligne, "Article", None)
if article_obj:
article_obj.Read()
article_ref = getattr(article_obj, "AR_Ref", "").strip()
except:
pass
lignes_source.append({
"article_ref": article_ref,
"quantite": float(getattr(ligne, "DL_Qte", 0.0)),
"prix_unitaire": float(getattr(ligne, "DL_PrixUnitaire", 0.0))
})
index += 1
except:
break
except:
pass
diagnostic["nb_lignes_source"] = len(lignes_source)
diagnostic["etapes"].append(f"Source lu: {len(lignes_source)} lignes")
# ========================================
# ÉTAPE 2 : TRANSACTION DE TEST
# ========================================
try:
sage.cial.CptaApplication.BeginTrans()
diagnostic["etapes"].append("Transaction démarrée")
except:
diagnostic["etapes"].append("Transaction non supportée")
try:
# ========================================
# ÉTAPE 3 : CRÉER DOCUMENT CIBLE
# ========================================
diagnostic["etapes"].append(f"Création document type {type_cible}")
process = sage.cial.CreateProcess_Document(type_cible)
doc_cible = process.Document
try:
doc_cible = win32com.client.CastTo(doc_cible, "IBODocumentVente3")
except:
pass
# Date
import pywintypes
date_source = getattr(doc_source, "DO_Date", None)
if date_source:
doc_cible.DO_Date = date_source
else:
doc_cible.DO_Date = pywintypes.Time(datetime.now())
# ========================================
# ÉTAPE 4 : ASSOCIER CLIENT
# ========================================
diagnostic["etapes"].append(f"Association client {client_code}")
factory_client = sage.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(client_code)
if not persist_client:
raise ValueError(f"Client {client_code} introuvable")
client_obj_cible = sage._cast_client(persist_client)
try:
doc_cible.SetDefaultClient(client_obj_cible)
except:
doc_cible.SetClient(client_obj_cible)
doc_cible.DO_Ref = numero_source
# PREMIER WRITE
doc_cible.Write()
diagnostic["etapes"].append("1er Write() effectué")
# LIRE ERREURS APRÈS 1ER WRITE
erreurs_apres_write1 = []
try:
if hasattr(doc_cible, "Errors"):
nb_err = doc_cible.Errors.Count
if nb_err > 0:
for i in range(nb_err):
try:
err = doc_cible.Errors.Item(i)
erreurs_apres_write1.append({
"description": str(getattr(err, "Description", "?")),
"type": str(getattr(err, "Type", "?")),
"field": str(getattr(err, "Field", "?"))
})
except:
pass
except:
pass
diagnostic["erreurs_apres_write1"] = erreurs_apres_write1
# ========================================
# ÉTAPE 5 : COPIER LIGNES
# ========================================
try:
factory_lignes_cible = doc_cible.FactoryDocumentLigne
except:
factory_lignes_cible = doc_cible.FactoryDocumentVenteLigne
factory_article = sage.cial.FactoryArticle
for idx, ligne_data in enumerate(lignes_source, 1):
article_ref = ligne_data["article_ref"]
if not article_ref:
continue
persist_article = factory_article.ReadReference(article_ref)
if not persist_article:
continue
article_obj = win32com.client.CastTo(persist_article, "IBOArticle3")
article_obj.Read()
ligne_persist = factory_lignes_cible.Create()
try:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3")
except:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3")
quantite = ligne_data["quantite"]
try:
ligne_obj.SetDefaultArticleReference(article_ref, quantite)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Qte = quantite
prix = ligne_data["prix_unitaire"]
if prix > 0:
ligne_obj.DL_PrixUnitaire = float(prix)
ligne_obj.Write()
diagnostic["etapes"].append(f"{len(lignes_source)} lignes copiées")
# ========================================
# ÉTAPE 6 : CHAMPS FACTURES
# ========================================
if type_cible == 60:
# DO_Souche
try:
souche = getattr(doc_source, "DO_Souche", 0)
doc_cible.DO_Souche = souche
except:
pass
# DO_Regime
try:
regime = getattr(doc_source, "DO_Regime", None)
if regime is not None:
doc_cible.DO_Regime = regime
except:
pass
# DO_Transaction
try:
doc_cible.DO_Transaction = 11
except:
pass
diagnostic["etapes"].append("Champs factures définis")
# Write final
doc_cible.Write()
doc_cible.Read()
diagnostic["etapes"].append("Write() final effectué")
# ========================================
# ÉTAPE 7 : LIRE ERREURS DOCUMENT
# ========================================
erreurs_document = []
try:
if hasattr(doc_cible, "Errors"):
nb_erreurs = doc_cible.Errors.Count
if nb_erreurs > 0:
for i in range(nb_erreurs):
try:
err = doc_cible.Errors.Item(i)
erreurs_document.append({
"description": str(getattr(err, "Description", "?")),
"type": str(getattr(err, "Type", "?")),
"code": str(getattr(err, "Number", "?")),
"field": str(getattr(err, "Field", "?"))
})
except:
pass
except:
pass
diagnostic["erreurs_document"] = erreurs_document
diagnostic["nb_erreurs_document"] = len(erreurs_document)
# ========================================
# ÉTAPE 8 : SCAN COMPLET DES CHAMPS
# ========================================
champs = {}
champs_vides = []
champs_null = []
champs_zero = []
for attr in dir(doc_cible):
if attr.startswith("DO_"):
try:
valeur = getattr(doc_cible, attr, None)
if callable(valeur):
continue
if valeur is None:
champs_null.append(attr)
champs[attr] = {"valeur": "NULL", "type": "null"}
elif isinstance(valeur, str):
if valeur == "":
champs_vides.append(attr)
champs[attr] = {"valeur": "", "type": "string_vide"}
else:
champs[attr] = {"valeur": valeur, "type": "string", "len": len(valeur)}
elif isinstance(valeur, (int, float)):
if valeur == 0:
champs_zero.append(attr)
champs[attr] = {"valeur": 0, "type": "zero"}
else:
champs[attr] = {"valeur": valeur, "type": type(valeur).__name__}
else:
champs[attr] = {"valeur": str(valeur)[:50], "type": type(valeur).__name__}
except Exception as e:
champs[attr] = {"erreur": str(e)[:100]}
diagnostic["champs_document"] = champs
diagnostic["nb_champs_total"] = len(champs)
diagnostic["nb_champs_null"] = len(champs_null)
diagnostic["nb_champs_vides"] = len(champs_vides)
diagnostic["nb_champs_zero"] = len(champs_zero)
diagnostic["champs_null"] = champs_null
diagnostic["champs_vides"] = champs_vides
diagnostic["champs_zero"] = champs_zero
# ========================================
# ÉTAPE 9 : COMPARER AVEC FACTURE MANUELLE
# ========================================
# Charger FA00001
try:
persist_ref = factory.ReadPiece(60, "FA00001")
if persist_ref:
doc_ref = win32com.client.CastTo(persist_ref, "IBODocumentVente3")
doc_ref.Read()
champs_ref = {}
for attr in dir(doc_ref):
if attr.startswith("DO_"):
try:
valeur = getattr(doc_ref, attr, None)
if not callable(valeur):
if valeur is None:
champs_ref[attr] = "NULL"
elif isinstance(valeur, str):
champs_ref[attr] = valeur if valeur else ""
elif isinstance(valeur, (int, float)):
champs_ref[attr] = valeur
else:
champs_ref[attr] = str(valeur)[:50]
except:
pass
# Comparer
champs_manquants = []
champs_differents = []
for attr, val_ref in champs_ref.items():
if attr in champs:
val_cible = champs[attr].get("valeur")
# Si la référence a une valeur mais pas nous
if val_ref != "" and val_ref != 0 and val_ref != "NULL":
if val_cible == "" or val_cible == 0 or val_cible == "NULL":
champs_manquants.append({
"champ": attr,
"valeur_ref": val_ref,
"valeur_cible": val_cible,
"severite": "CRITIQUE"
})
elif val_ref != val_cible:
champs_differents.append({
"champ": attr,
"valeur_ref": val_ref,
"valeur_cible": val_cible
})
diagnostic["comparaison_facture_manuelle"] = {
"champs_manquants": champs_manquants,
"champs_differents": champs_differents,
"nb_champs_manquants": len(champs_manquants),
"nb_champs_differents": len(champs_differents)
}
except Exception as e:
diagnostic["comparaison_facture_manuelle"] = {"erreur": str(e)}
# ========================================
# ROLLBACK (NE PAS SAUVEGARDER)
# ========================================
try:
sage.cial.CptaApplication.RollbackTrans()
diagnostic["etapes"].append("Rollback effectué (rien sauvegardé)")
except:
diagnostic["etapes"].append("Pas de rollback (pas de transaction)")
# ========================================
# ANALYSE ET RECOMMANDATIONS
# ========================================
recommendations = []
if erreurs_document:
recommendations.append(
f"CRITIQUE: {len(erreurs_document)} erreur(s) détectée(s) dans le document ! "
f"Voir 'erreurs_document' pour les détails."
)
for err in erreurs_document:
if err.get("field"):
recommendations.append(
f"Champ problématique: {err['field']} - {err['description']}"
)
if diagnostic["comparaison_facture_manuelle"].get("champs_manquants"):
recommendations.append(
f"CRITIQUE: {len(diagnostic['comparaison_facture_manuelle']['champs_manquants'])} "
f"champ(s) manquant(s) par rapport à FA00001 ! "
f"Voir 'comparaison_facture_manuelle.champs_manquants'."
)
if not erreurs_document and not diagnostic["comparaison_facture_manuelle"].get("champs_manquants"):
recommendations.append(
"Aucune erreur détectée ! Le document devrait pouvoir être validé."
)
diagnostic["recommendations"] = recommendations
return {
"success": True,
"diagnostic": diagnostic
}
except Exception as e:
# Rollback en cas d'erreur
try:
sage.cial.CptaApplication.RollbackTrans()
except:
pass
raise HTTPException(500, f"Erreur diagnostic: {str(e)}")
except HTTPException:
raise
except Exception as e:
logger.error(f"[DIAG-FINAL] Erreur: {e}", exc_info=True)
raise HTTPException(500, str(e))
# À ajouter dans main.py
@app.get("/sage/diagnostic/article-requirements", dependencies=[Depends(verify_token)])
def diagnostiquer_exigences_article():
"""
🔍 DIAGNOSTIC: Découvre les champs obligatoires pour créer un article
Cette route va :
1. Créer un article de test
2. Scanner TOUS les champs AR_*
3. Identifier les champs avec des valeurs par défaut
4. NE PAS sauvegarder (rollback)
"""
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
with sage._com_context(), sage._lock_com:
# Transaction de test
try:
sage.cial.CptaApplication.BeginTrans()
except:
pass
try:
factory = sage.cial.FactoryArticle
# Créer un article test
persist = factory.Create()
article_test = win32com.client.CastTo(persist, "IBOArticle3")
article_test.SetDefault()
# Scanner TOUS les champs AR_*
champs = {}
champs_null = []
champs_vides = []
champs_remplis = []
for attr in dir(article_test):
if attr.startswith("AR_") or attr.startswith("FA_") or attr.startswith("TA_"):
try:
valeur = getattr(article_test, attr, None)
if callable(valeur):
continue
if valeur is None:
champs_null.append(attr)
champs[attr] = {"valeur": "NULL", "type": "null", "obligatoire_probable": True}
elif isinstance(valeur, str):
if valeur == "":
champs_vides.append(attr)
champs[attr] = {"valeur": "", "type": "string_vide", "obligatoire_probable": True}
else:
champs_remplis.append(attr)
champs[attr] = {"valeur": valeur, "type": "string", "len": len(valeur)}
elif isinstance(valeur, (int, float, bool)):
if valeur == 0 or valeur == False:
champs[attr] = {"valeur": valeur, "type": type(valeur).__name__, "obligatoire_probable": False}
else:
champs_remplis.append(attr)
champs[attr] = {"valeur": valeur, "type": type(valeur).__name__}
else:
champs[attr] = {"valeur": str(valeur)[:50], "type": type(valeur).__name__}
except Exception as e:
champs[attr] = {"erreur": str(e)[:100]}
# Lire un article existant pour comparaison
champs_article_reel = {}
try:
# Charger le premier article de la liste
persist_reel = factory.List(1)
if persist_reel:
article_reel = win32com.client.CastTo(persist_reel, "IBOArticle3")
article_reel.Read()
for attr in champs.keys():
try:
val_reel = getattr(article_reel, attr, None)
if val_reel is not None and val_reel != "" and val_reel != 0:
champs_article_reel[attr] = val_reel
except:
pass
except:
pass
# Rollback
try:
sage.cial.CptaApplication.RollbackTrans()
except:
pass
return {
"success": True,
"champs_analyse": {
"total": len(champs),
"null": len(champs_null),
"vides": len(champs_vides),
"remplis": len(champs_remplis)
},
"champs_probablement_obligatoires": champs_null + champs_vides,
"champs_remplis_par_defaut": champs_remplis,
"tous_champs": champs,
"exemple_article_reel": champs_article_reel,
"conseil": (
"Les champs NULL ou vides sont probablement obligatoires. "
"Les champs remplis par défaut ont des valeurs automatiques."
)
}
except Exception as e:
try:
sage.cial.CptaApplication.RollbackTrans()
except:
pass
raise HTTPException(500, f"Erreur diagnostic: {str(e)}")
except HTTPException:
raise
except Exception as e:
logger.error(f"[DIAG] Erreur: {e}", exc_info=True)
raise HTTPException(500, str(e))
@app.post("/sage/diagnostic/article", dependencies=[Depends(verify_token)])
def diagnostic_article_complet(
mode: str = Query("create", regex="^(inspect|create)$"),
reference: str = Query("TEST_DIAG"),
reference_modele: str = Query(None, description="Article existant à copier/inspecter")
):
"""
🔧 DIAGNOSTIC UNIFIÉ pour articles Sage
Modes:
- inspect: Analyse un article existant pour voir sa structure
- create: Crée un article de test en copiant la structure d'un modèle
Paramètres:
- reference: Référence du nouvel article (mode create)
- reference_modele: Référence d'un article existant à analyser/copier
"""
def est_serializable(val):
"""Vérifie si une valeur est sérialisable en JSON"""
if val is None:
return True
if isinstance(val, (str, int, float, bool)):
return True
if str(type(val)).startswith("<COMObject"):
return False
try:
import json
json.dumps(val)
return True
except:
return False
try:
if not sage or not sage.cial:
raise HTTPException(503, "Service Sage indisponible")
factory = sage.cial.FactoryArticle
# ========================================
# MODE 1: INSPECTION d'un article existant
# ========================================
if mode == "inspect":
if not reference_modele:
raise HTTPException(400, "reference_modele requis en mode inspect")
try:
with sage._com_context(), sage._lock_com:
article_modele = factory.ReadReference(reference_modele)
# Scanner TOUS les attributs
attributs_unite = []
champs_remplis = {}
champs_vides = {}
champs_non_serialisables = []
for attr in dir(article_modele):
if attr.startswith('_') or attr[0].islower():
continue
try:
val = getattr(article_modele, attr, None)
# Ignorer les méthodes
if callable(val):
continue
# Vérifier si sérialisable
if not est_serializable(val):
champs_non_serialisables.append(attr)
continue
# Catégoriser les unités
if 'Unit' in attr or 'Vent' in attr or 'unite' in attr.lower():
if val is not None and str(val) not in ['None', '']:
attributs_unite.append({attr: str(val)})
# Catégoriser remplis/vides
val_str = str(val) if val is not None else 'None'
if val is not None and val_str not in ['', 'None', '0', '0.0', 'False']:
champs_remplis[attr] = val_str[:100]
else:
champs_vides[attr] = val_str
except Exception as e:
logger.debug(f"Erreur lecture {attr}: {e}")
continue
return {
"mode": "inspect",
"reference_inspectee": reference_modele,
"resume": {
"champs_remplis": len(champs_remplis),
"champs_vides": len(champs_vides),
"champs_non_serialisables": len(champs_non_serialisables),
"attributs_unite_detectes": len(attributs_unite)
},
"attributs_unite": attributs_unite,
"champs_critiques_remplis": {
k: v for k, v in champs_remplis.items()
if k in ["AR_Design", "AR_Type", "FA_CodeFamille", "TA_Code",
"AR_PrixVen", "AR_Coef", "AR_PrixAch"]
},
"tous_champs_remplis": champs_remplis,
"champs_vides": champs_vides,
"conseil": (
f" {len(attributs_unite)} attribut(s) d'unité trouvé(s). "
f"Utilisez mode='create' avec ce reference_modele pour tester la création."
)
}
except Exception as e:
raise HTTPException(404, f"Article '{reference_modele}' introuvable: {str(e)}")
# ========================================
# MODE 2: CRÉATION avec modèle (si fourni)
# ========================================
with sage._com_context(), sage._lock_com:
try:
sage.cial.CptaApplication.BeginTrans()
except:
pass
try:
# 📋 Étape 1: Charger le modèle si fourni
champs_modele = {}
logs_copie = []
article_modele_obj = None
# CHAMPS À EXCLURE (doivent être uniques ou auto-générés)
champs_exclus = {
"AR_Ref", # Référence (on met la nôtre)
"AR_Raccourci", # Doit être unique !
"AR_CodeBarre", # Code-barres doit être unique ! CRITIQUE
"AR_Photo", # Chemin photo spécifique
"cbMarq", # ID interne Sage
"cbCreateur", # Créateur
"cbModification",# Date modif
"IsPersistant" # Flag interne
}
if reference_modele:
try:
article_modele_obj = factory.ReadReference(reference_modele)
# Extraire TOUS les champs lisibles (valeurs simples uniquement)
for attr in dir(article_modele_obj):
if attr.startswith('_') or attr[0].islower():
continue
# Exclure les champs problématiques
if attr in champs_exclus:
logs_copie.append(f"⏭️ {attr} EXCLU (doit être unique)")
continue
try:
val = getattr(article_modele_obj, attr, None)
# Ignorer méthodes et objets COM
if callable(val) or not est_serializable(val):
continue
# Garder les valeurs non-None et non-vides
if val is not None and str(val) not in ['None', '']:
champs_modele[attr] = val
logs_copie.append(f" {attr} = {val}")
except:
continue
logger.info(f"📋 Modèle: {len(champs_modele)} champs extraits")
except Exception as e:
logger.warning(f" Modèle '{reference_modele}' non chargé: {e}")
# 🆕 Étape 2: Créer le nouvel article
persist = factory.Create()
article = win32com.client.CastTo(persist, "IBOArticle3")
article.SetDefault()
# 📝 Étape 3: Appliquer le modèle OU défauts
article.AR_Ref = reference.upper()
logs_application = []
if champs_modele:
# Appliquer TOUS les champs du modèle
for champ, valeur in champs_modele.items():
try:
setattr(article, champ, valeur)
logs_application.append(f" {champ} = {valeur}")
except Exception as e:
logs_application.append(f" {champ}: {str(e)[:50]}")
else:
# Défauts minimaux si pas de modèle
article.AR_Design = f"Test {reference}"
article.AR_Type = 0
article.AR_Sommeil = False
# 🔧 Étape 4: Copier les objets COM critiques (OBLIGATOIRES pour Sage)
fallbacks_appliques = []
fallbacks_echecs = []
if article_modele_obj:
logger.info(f"[DIAGNOSTIC] Copie des objets COM depuis {reference_modele}...")
# ===== FAMILLE =====
try:
famille_obj = getattr(article_modele_obj, "Famille", None)
if famille_obj is not None:
article.Famille = famille_obj
fallbacks_appliques.append(f" Famille copiée depuis {reference_modele}")
logger.info("[DIAGNOSTIC] Famille copiée avec succès")
else:
fallbacks_echecs.append(" Famille: objet NULL dans modèle")
logger.warning("[DIAGNOSTIC] Famille NULL dans modèle")
except Exception as e:
fallbacks_echecs.append(f"Famille: {str(e)[:80]}")
logger.error(f"[DIAGNOSTIC] Erreur copie Famille: {e}")
# ===== TAXE =====
try:
taxe_obj = getattr(article_modele_obj, "Taxe1", None)
if taxe_obj is not None:
article.Taxe1 = taxe_obj
fallbacks_appliques.append(f" Taxe1 copiée depuis {reference_modele}")
logger.info("[DIAGNOSTIC] Taxe1 copiée avec succès")
else:
# Taxe NULL - essayer de charger une taxe par défaut
logger.warning("[DIAGNOSTIC] Taxe1 NULL dans modèle, recherche taxe par défaut...")
factory_taxe = sage.cial.FactoryTaxe
taxe_trouvee = False
for code_taxe in ["1", "2", "Normal", "20.00"]:
try:
taxe_defaut = factory_taxe.ReadIntitule(code_taxe)
if taxe_defaut:
article.Taxe1 = taxe_defaut
fallbacks_appliques.append(f" Taxe par défaut '{code_taxe}' chargée")
logger.info(f"[DIAGNOSTIC] Taxe '{code_taxe}' chargée")
taxe_trouvee = True
break
except Exception as e:
logger.debug(f"[DIAGNOSTIC] Taxe '{code_taxe}' échouée: {e}")
continue
if not taxe_trouvee:
fallbacks_echecs.append(" Taxe: aucune taxe par défaut trouvée")
except Exception as e:
fallbacks_echecs.append(f"Taxe: {str(e)[:80]}")
logger.error(f"[DIAGNOSTIC] Erreur copie Taxe: {e}")
# ===== UNITÉ DE VENTE =====
try:
unite_obj = getattr(article_modele_obj, "UniteVente", None)
if unite_obj is not None:
article.UniteVente = unite_obj
fallbacks_appliques.append(f" UniteVente copiée depuis {reference_modele}")
logger.info("[DIAGNOSTIC] UniteVente copiée avec succès")
else:
# Unité NULL - essayer de charger une unité par défaut
logger.warning("[DIAGNOSTIC] UniteVente NULL dans modèle, recherche unité par défaut...")
factory_unite = sage.cial.FactoryUnite
unite_trouvee = False
for code_unite in ["UN", "U", "PCE", "Unité"]:
try:
unite_defaut = factory_unite.ReadIntitule(code_unite)
if unite_defaut:
article.UniteVente = unite_defaut
fallbacks_appliques.append(f" Unité par défaut '{code_unite}' chargée")
logger.info(f"[DIAGNOSTIC] Unité '{code_unite}' chargée")
unite_trouvee = True
break
except Exception as e:
logger.debug(f"[DIAGNOSTIC] Unité '{code_unite}' échouée: {e}")
continue
if not unite_trouvee:
fallbacks_echecs.append(" Unité: aucune unité par défaut trouvée")
except Exception as e:
fallbacks_echecs.append(f"Unité: {str(e)[:80]}")
logger.error(f"[DIAGNOSTIC] Erreur copie Unité: {e}")
else:
fallbacks_echecs.append(" Aucun modèle fourni - objets COM non définis")
# 📊 Étape 5: Scanner l'état final avant Write
etat_final = {}
champs_scan = [
"AR_Ref", "AR_Design", "AR_Type", "AR_Sommeil", "AR_Raccourci", "AR_CodeBarre",
"FA_CodeFamille", "TA_Code", "AR_PrixVen",
"AR_UniteVen", "AR_UniteVente", "AR_Unite"
]
for champ in champs_scan:
try:
val = getattr(article, champ, None)
etat_final[champ] = str(val) if val is not None else "None"
except:
etat_final[champ] = "N/A"
# 💾 Étape 6: Tenter Write()
erreur_write = None
sage_errors = []
write_success = False
try:
article.Write()
write_success = True
erreur_write = " SUCCESS - Article créé !"
logger.info(f"[DIAGNOSTIC] Article {reference} créé avec succès")
except Exception as e:
erreur_write = str(e)
logger.error(f"[DIAGNOSTIC] Échec Write(): {e}")
# Extraire erreurs Sage
try:
sage_error = sage.cial.CptaApplication.LastError
if sage_error:
sage_errors.append({
"source": "CptaApplication",
"description": sage_error.Description,
"number": sage_error.Number
})
except:
pass
try:
if hasattr(article, "Errors") and article.Errors:
for i in range(1, article.Errors.Count + 1):
try:
err = article.Errors.Item(i)
sage_errors.append({
"source": "article.Errors",
"field": getattr(err, "Field", ""),
"description": getattr(err, "Description", ""),
"number": getattr(err, "Number", "")
})
except:
pass
except:
pass
# 🔄 Rollback
try:
sage.cial.CptaApplication.RollbackTrans()
except:
pass
# 📋 Diagnostic final
diagnostic = {
"mode": "create",
"success": write_success,
"reference_test": reference,
"modele_utilise": reference_modele if reference_modele else None,
"champs_copies_du_modele": len(champs_modele) if champs_modele else 0,
"fallbacks_appliques": fallbacks_appliques,
"fallbacks_echecs": fallbacks_echecs if fallbacks_echecs else None,
"etat_final": etat_final,
"resultat_write": erreur_write,
"sage_errors": sage_errors,
}
# Logs détaillés si échec
if not write_success and reference_modele:
diagnostic["logs_copie_modele"] = logs_copie[:30]
diagnostic["logs_application"] = logs_application[:30]
# Conseils
if write_success:
diagnostic["message"] = "🎉 Article créé avec succès (rollback effectué)"
else:
diagnostic["message"] = " Échec de création"
# Analyser quels champs posent problème
champs_none = [k for k, v in etat_final.items() if v in ['None', 'N/A']]
if champs_none:
diagnostic["champs_encore_vides"] = champs_none
diagnostic["conseil"] = (
f"Les champs suivants sont toujours vides: {', '.join(champs_none)}. "
"Ils sont probablement obligatoires. Vérifiez votre paramétrage Sage "
"ou utilisez mode='inspect' pour voir les valeurs dans un article valide."
)
return diagnostic
except Exception as e:
try:
sage.cial.CptaApplication.RollbackTrans()
except:
pass
raise HTTPException(500, f"Erreur création: {str(e)}")
except HTTPException:
raise
except Exception as e:
logger.error(f"[DIAGNOSTIC] Erreur: {e}", exc_info=True)
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",
)