fix: Introduce Sage document type constants and update document transformation and listing endpoints to use correct Sage types.

This commit is contained in:
Fanilo-Nantenaina 2025-11-28 08:28:51 +03:00
parent 9d0c26b5d8
commit 9b17149b07
3 changed files with 87 additions and 145 deletions

View file

@ -1,14 +1,20 @@
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Optional, List from typing import Optional, List
class Settings(BaseSettings): class Settings(BaseSettings):
model_config = SettingsConfigDict( model_config = SettingsConfigDict(
env_file=".env", env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore"
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore"
) )
SAGE_TYPE_DEVIS: int = 0
SAGE_TYPE_BON_COMMANDE: int = 10
SAGE_TYPE_PREPARATION: int = 20
SAGE_TYPE_BON_LIVRAISON: int = 30
SAGE_TYPE_BON_RETOUR: int = 40
SAGE_TYPE_BON_AVOIR: int = 50
SAGE_TYPE_FACTURE: int = 60
# === SAGE 100c (Windows uniquement) === # === SAGE 100c (Windows uniquement) ===
chemin_base: str chemin_base: str
utilisateur: str = "Administrateur" utilisateur: str = "Administrateur"
@ -31,8 +37,10 @@ class Settings(BaseSettings):
# === CORS === # === CORS ===
cors_origins: List[str] = ["*"] cors_origins: List[str] = ["*"]
settings = Settings() settings = Settings()
def validate_settings(): def validate_settings():
"""Validation au démarrage""" """Validation au démarrage"""
if not settings.chemin_base or not settings.mot_de_passe: if not settings.chemin_base or not settings.mot_de_passe:

113
main.py
View file

@ -490,33 +490,28 @@ def lire_document(numero: str, type_doc: int):
@app.post("/sage/documents/transform", dependencies=[Depends(verify_token)]) @app.post("/sage/documents/transform", dependencies=[Depends(verify_token)])
def transformer_document( def transformer_document(
numero_source: str = Query(..., description="Numéro du document source"), numero_source: str = Query(..., description="Numéro du document source"),
type_source: int = Query( type_source: int = Query(..., description="Type document source"),
..., type_cible: int = Query(..., description="Type document cible"),
ge=0,
le=5,
description="Type document source (0=Devis, 3=Commande, 5=Facture)",
),
type_cible: int = Query(..., ge=0, le=5, description="Type document cible"),
): ):
""" """
🔧 Transformation de document (devis commande facture) 🔧 Transformation de document
CORRECTION FINALE: Query params au lieu de body JSON CORRECTION : Utilise les VRAIS types Sage Dataven
Types de documents: Types valides :
- 0: Devis - 0: Devis
- 1: Bon de livraison - 10: Bon de commande
- 2: Bon de retour - 20: Préparation
- 3: Commande - 30: Bon de livraison
- 4: Préparation - 40: Bon de retour
- 5: Facture - 50: Bon d'avoir
- 60: Facture
Transformations valides: Transformations autorisées :
- Devis (0) Commande (3) - Devis (0) Commande (10)
- Devis (0) Facture (5) - Commande (10) Bon livraison (30)
- Commande (3) Bon livraison (1) - Commande (10) Facture (60)
- Commande (3) Facture (5) - Bon livraison (30) Facture (60)
- Bon livraison (1) Facture (5)
""" """
try: try:
logger.info( logger.info(
@ -524,13 +519,13 @@ def transformer_document(
f"(type {type_source}) → type {type_cible}" f"(type {type_source}) → type {type_cible}"
) )
# Validation des transformations autorisées # ✅ Matrice des transformations valides pour VOTRE Sage
transformations_valides = { transformations_valides = {
(0, 3), # Devis → Commande (0, 10), # Devis → Commande
(0, 5), # Devis → Facture (10, 30), # Commande → Bon de livraison
(3, 1), # Commande → Bon de livraison (10, 60), # Commande → Facture
(3, 5), # Commande → Facture (30, 60), # Bon de livraison → Facture
(1, 5), # Bon de livraison → Facture (0, 60), # Devis → Facture (si autorisé)
} }
if (type_source, type_cible) not in transformations_valides: if (type_source, type_cible) not in transformations_valides:
@ -557,11 +552,9 @@ def transformer_document(
except HTTPException: except HTTPException:
raise raise
except ValueError as e: except ValueError as e:
# Erreurs métier (document introuvable, déjà transformé, etc.)
logger.error(f"❌ Erreur métier transformation: {e}") logger.error(f"❌ Erreur métier transformation: {e}")
raise HTTPException(400, str(e)) raise HTTPException(400, str(e))
except Exception as e: except Exception as e:
# Erreurs techniques (COM, Sage, etc.)
logger.error(f"❌ Erreur technique transformation: {e}", exc_info=True) logger.error(f"❌ Erreur technique transformation: {e}", exc_info=True)
raise HTTPException(500, f"Erreur transformation: {str(e)}") raise HTTPException(500, f"Erreur transformation: {str(e)}")
@ -612,7 +605,7 @@ def contact_read(req: CodeRequest):
def commandes_list(limit: int = 100, statut: Optional[int] = None): def commandes_list(limit: int = 100, statut: Optional[int] = None):
""" """
📋 Liste toutes les commandes 📋 Liste toutes les commandes
CORRECTIONS: Gestion robuste des erreurs + logging détaillé CORRECTION: Filtre sur type 10 (BON_COMMANDE)
""" """
try: try:
if not sage or not sage.cial: if not sage or not sage.cial:
@ -622,7 +615,7 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None):
factory = sage.cial.FactoryDocumentVente factory = sage.cial.FactoryDocumentVente
commandes = [] commandes = []
index = 1 index = 1
max_iterations = limit * 10 # Plus de marge max_iterations = limit * 10
erreurs_consecutives = 0 erreurs_consecutives = 0
max_erreurs = 100 max_erreurs = 100
@ -636,33 +629,26 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None):
try: try:
persist = factory.List(index) persist = factory.List(index)
if persist is None: if persist is None:
logger.debug(f"Fin de liste à l'index {index}")
break break
doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read() doc.Read()
# ✅ CORRECTION 1: Vérifier type de document
doc_type = getattr(doc, "DO_Type", -1) doc_type = getattr(doc, "DO_Type", -1)
# ⚠️ CRITIQUE: Vérifier que c'est bien une commande (type 3) # ✅ CRITIQUE : Filtrer sur type 10 (BON_COMMANDE)
if doc_type != 3: if doc_type != settings.SAGE_TYPE_BON_COMMANDE:
index += 1 index += 1
continue continue
doc_statut = getattr(doc, "DO_Statut", 0) doc_statut = getattr(doc, "DO_Statut", 0)
logger.debug(
f"Index {index}: Type={doc_type}, Statut={doc_statut}, "
f"Numéro={getattr(doc, 'DO_Piece', '?')}"
)
# Filtre statut # Filtre statut
if statut is not None and doc_statut != statut: if statut is not None and doc_statut != statut:
index += 1 index += 1
continue continue
# ✅ CORRECTION 2: Charger client via .Client # Charger client
client_code = "" client_code = ""
client_intitule = "" client_intitule = ""
@ -674,20 +660,8 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None):
client_intitule = getattr( client_intitule = getattr(
client_obj, "CT_Intitule", "" client_obj, "CT_Intitule", ""
).strip() ).strip()
logger.debug(f" Client: {client_code} - {client_intitule}")
except Exception as e: except Exception as e:
logger.debug(f" Erreur chargement client: {e}") logger.debug(f"Erreur chargement client: {e}")
# Fallback sur cache si code disponible
if not client_code:
try:
client_code = getattr(doc, "CT_Num", "").strip()
except:
pass
if client_code and not client_intitule:
client_cache = sage.lire_client(client_code)
if client_cache:
client_intitule = client_cache.get("intitule", "")
commande = { commande = {
"numero": getattr(doc, "DO_Piece", ""), "numero": getattr(doc, "DO_Piece", ""),
@ -700,27 +674,17 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None):
} }
commandes.append(commande) commandes.append(commande)
logger.debug(f" ✅ Commande ajoutée: {commande['numero']}")
erreurs_consecutives = 0 erreurs_consecutives = 0
index += 1 index += 1
except Exception as e: except Exception as e:
erreurs_consecutives += 1 erreurs_consecutives += 1
logger.debug(f"⚠️ Erreur index {index}: {e}")
index += 1 index += 1
if erreurs_consecutives >= max_erreurs: if erreurs_consecutives >= max_erreurs:
logger.warning(
f"⚠️ Arrêt après {max_erreurs} erreurs consécutives"
)
break break
nb_avec_client = sum(1 for c in commandes if c["client_intitule"]) logger.info(f"{len(commandes)} commandes retournées")
logger.info(
f"{len(commandes)} commandes retournées "
f"({nb_avec_client} avec client)"
)
return {"success": True, "data": commandes} return {"success": True, "data": commandes}
@ -733,7 +697,10 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None):
@app.post("/sage/factures/list", dependencies=[Depends(verify_token)]) @app.post("/sage/factures/list", dependencies=[Depends(verify_token)])
def factures_list(limit: int = 100, statut: Optional[int] = None): def factures_list(limit: int = 100, statut: Optional[int] = None):
"""Liste toutes les factures""" """
📋 Liste toutes les factures
CORRECTION: Filtre sur type 60 (FACTURE)
"""
try: try:
with sage._com_context(), sage._lock_com: with sage._com_context(), sage._lock_com:
factory = sage.cial.FactoryDocumentVente factory = sage.cial.FactoryDocumentVente
@ -750,8 +717,8 @@ def factures_list(limit: int = 100, statut: Optional[int] = None):
doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read() doc.Read()
# Filtrer factures (type 5) # ✅ CRITIQUE: Filtrer factures (type 60)
if getattr(doc, "DO_Type", -1) != 5: if getattr(doc, "DO_Type", -1) != settings.SAGE_TYPE_FACTURE:
index += 1 index += 1
continue continue
@ -773,13 +740,6 @@ def factures_list(limit: int = 100, statut: Optional[int] = None):
except: except:
pass pass
# Champ libre dernière relance
derniere_relance = None
try:
derniere_relance = getattr(doc, "DO_DerniereRelance", None)
except:
pass
factures.append( factures.append(
{ {
"numero": getattr(doc, "DO_Piece", ""), "numero": getattr(doc, "DO_Piece", ""),
@ -789,9 +749,6 @@ def factures_list(limit: int = 100, statut: Optional[int] = None):
"total_ht": float(getattr(doc, "DO_TotalHT", 0.0)), "total_ht": float(getattr(doc, "DO_TotalHT", 0.0)),
"total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)), "total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)),
"statut": doc_statut, "statut": doc_statut,
"derniere_relance": (
str(derniere_relance) if derniere_relance else None
),
} }
) )

View file

@ -1027,19 +1027,11 @@ class SageConnector:
def transformer_document(self, numero_source, type_source, type_cible): def transformer_document(self, numero_source, type_source, type_cible):
""" """
Transformation de document avec la méthode NATIVE de Sage Transformation de document avec la méthode NATIVE de Sage
CORRECTION : Utilise les VRAIS types Sage Dataven
CHANGEMENT MAJEUR:
- Utilise TransformInto() au lieu de CreateProcess_Document()
- Méthode officielle Sage pour les transformations
- Gère automatiquement les numéros, statuts, et lignes
Documentation Sage:
IBODocumentVente3.TransformInto(DO_Type: int) -> IBODocumentVente3
""" """
if not self.cial: if not self.cial:
raise RuntimeError("Connexion Sage non etablie") raise RuntimeError("Connexion Sage non etablie")
# Convertir en int si enum
type_source = int(type_source) type_source = int(type_source)
type_cible = int(type_cible) type_cible = int(type_cible)
@ -1048,24 +1040,28 @@ class SageConnector:
f"(type {type_source}) -> type {type_cible}" f"(type {type_source}) -> type {type_cible}"
) )
# Validation des types # ✅ Matrice de transformations pour VOTRE installation Sage
types_valides = {0, 1, 2, 3, 4, 5}
if type_source not in types_valides or type_cible not in types_valides:
raise ValueError(
f"Types invalides: source={type_source}, cible={type_cible}. "
f"Valeurs valides: {types_valides}"
)
# Matrice de transformations Sage 100c
transformations_autorisees = { transformations_autorisees = {
(0, 3): "Devis -> Commande", (
(0, 1): "Devis -> Bon de livraison", settings.SAGE_TYPE_DEVIS,
(0, 5): "Devis -> Facture", # Peut être supporté selon config settings.SAGE_TYPE_BON_COMMANDE,
(3, 1): "Commande -> Bon de livraison", ): "Devis -> Commande", # 0 → 10
(3, 4): "Commande -> Preparation", (
(3, 5): "Commande -> Facture", # Direct si autorisé settings.SAGE_TYPE_BON_COMMANDE,
(1, 5): "Bon de livraison -> Facture", settings.SAGE_TYPE_BON_LIVRAISON,
(4, 1): "Preparation -> Bon de livraison", ): "Commande -> Bon de livraison", # 10 → 30
(
settings.SAGE_TYPE_BON_COMMANDE,
settings.SAGE_TYPE_FACTURE,
): "Commande -> Facture", # 10 → 60
(
settings.SAGE_TYPE_BON_LIVRAISON,
settings.SAGE_TYPE_FACTURE,
): "Bon de livraison -> Facture", # 30 → 60
(
settings.SAGE_TYPE_DEVIS,
settings.SAGE_TYPE_FACTURE,
): "Devis -> Facture", # 0 → 60 (si autorisé)
} }
if (type_source, type_cible) not in transformations_autorisees: if (type_source, type_cible) not in transformations_autorisees:
@ -1135,8 +1131,8 @@ class SageConnector:
f"Ce document a deja ete transforme partiellement ou totalement." f"Ce document a deja ete transforme partiellement ou totalement."
) )
# Forcer statut "Accepté" si brouillon # Forcer statut "Accepté" si brouillon (uniquement pour devis)
if type_source == 0 and statut_actuel == 0: if type_source == settings.SAGE_TYPE_DEVIS and statut_actuel == 0:
logger.warning( logger.warning(
f"[TRANSFORM] Devis en brouillon (statut=0), " f"[TRANSFORM] Devis en brouillon (statut=0), "
f"passage a 'Accepte' (statut=2)" f"passage a 'Accepte' (statut=2)"
@ -1146,7 +1142,6 @@ class SageConnector:
doc_source.Write() doc_source.Write()
logger.info(f"[TRANSFORM] Statut change: 0 -> 2") logger.info(f"[TRANSFORM] Statut change: 0 -> 2")
# Re-lire
doc_source.Read() doc_source.Read()
nouveau_statut = getattr(doc_source, "DO_Statut", 0) nouveau_statut = getattr(doc_source, "DO_Statut", 0)
if nouveau_statut != 2: if nouveau_statut != 2:
@ -1162,20 +1157,16 @@ class SageConnector:
self.cial.CptaApplication.BeginTrans() self.cial.CptaApplication.BeginTrans()
transaction_active = True transaction_active = True
logger.debug("[TRANSFORM] Transaction demarree") logger.debug("[TRANSFORM] Transaction demarree")
except AttributeError: except:
# BeginTrans n'existe pas sur cette version
logger.debug( logger.debug(
"[TRANSFORM] BeginTrans non disponible, continue sans transaction" "[TRANSFORM] BeginTrans non disponible, continue sans transaction"
) )
except Exception as e:
logger.warning(f"[TRANSFORM] BeginTrans echoue: {e}")
try: try:
# ✅✅✅ MÉTHODE NATIVE SAGE: TransformInto() ✅✅✅ # ✅✅✅ MÉTHODE NATIVE SAGE: TransformInto() ✅✅✅
logger.info(f"[TRANSFORM] Appel TransformInto({type_cible})...") logger.info(f"[TRANSFORM] Appel TransformInto({type_cible})...")
try: try:
# La méthode TransformInto() retourne le nouveau document
doc_cible = doc_source.TransformInto(type_cible) doc_cible = doc_source.TransformInto(type_cible)
if doc_cible is None: if doc_cible is None:
@ -1186,7 +1177,6 @@ class SageConnector:
logger.info("[TRANSFORM] TransformInto() execute avec succes") logger.info("[TRANSFORM] TransformInto() execute avec succes")
# Cast vers le bon type
try: try:
doc_cible = win32com.client.CastTo( doc_cible = win32com.client.CastTo(
doc_cible, "IBODocumentVente3" doc_cible, "IBODocumentVente3"
@ -1194,10 +1184,7 @@ class SageConnector:
except: except:
pass pass
# Lire le document cible
doc_cible.Read() doc_cible.Read()
# Récupérer le numéro
numero_cible = getattr(doc_cible, "DO_Piece", "") numero_cible = getattr(doc_cible, "DO_Piece", "")
if not numero_cible: if not numero_cible:
@ -1228,40 +1215,30 @@ class SageConnector:
) )
except AttributeError as e: except AttributeError as e:
# TransformInto() n'existe pas sur cette version de Sage
logger.error(f"[TRANSFORM] TransformInto() non disponible: {e}") logger.error(f"[TRANSFORM] TransformInto() non disponible: {e}")
raise RuntimeError( raise RuntimeError(
f"La methode TransformInto() n'est pas disponible sur votre version de Sage 100c. " f"La methode TransformInto() n'est pas disponible. "
f"Vous devez soit: " f"Causes possibles:\n"
f"(1) Mettre a jour Sage, ou " f"1. Le module n'est pas active dans votre licence Sage\n"
f"(2) Activer le module de gestion commerciale pour les commandes, ou " f"2. L'utilisateur n'a pas les droits\n"
f"(3) Utiliser l'interface Sage manuellement pour les transformations." f"3. La transformation {type_source}{type_cible} n'est pas supportee"
) )
except Exception as e: except Exception as e:
logger.error(f"[TRANSFORM] TransformInto() echoue: {e}") logger.error(f"[TRANSFORM] TransformInto() echoue: {e}")
# Essayer de déterminer la cause
if "Valeur invalide" in str(e): if "Valeur invalide" in str(e):
raise RuntimeError( raise RuntimeError(
f"Sage refuse la transformation vers le type {type_cible}. " f"Sage refuse la transformation vers le type {type_cible}. "
f"Causes possibles:\n" f"Verifiez:\n"
f"1. Le module 'Commandes' n'est pas active dans votre licence Sage\n" f"1. Que le module est active (Commandes, Factures...)\n"
f"2. L'utilisateur n'a pas les droits sur ce type de document\n" f"2. Les droits utilisateur\n"
f"3. La configuration Sage bloque ce type de transformation\n" f"3. Que le type {type_cible} existe dans votre Sage\n"
f"4. Il manque des parametres obligatoires (depot, tarif, etc.)\n\n" f"4. Les parametres obligatoires (depot, tarif, etc.)"
f"Verifiez dans Sage: Fichier > Autorisations > Gestion Commerciale"
)
elif "Acces refuse" in str(e) or "Access denied" in str(e):
raise RuntimeError(
f"Acces refuse pour creer une commande (type {type_cible}). "
f"Verifiez les droits utilisateur dans Sage: "
f"Fichier > Autorisations > Votre utilisateur"
) )
else: else:
raise RuntimeError( raise RuntimeError(
f"Erreur Sage lors de la transformation: {e}\n" f"Erreur Sage lors de la transformation: {e}"
f"Consultez les logs Sage pour plus de details."
) )
# Commit transaction # Commit transaction
@ -1272,9 +1249,9 @@ class SageConnector:
except: except:
pass pass
# MAJ statut source -> Transformé # MAJ statut source Transformé
try: try:
doc_source.Read() # Re-lire au cas où doc_source.Read()
doc_source.DO_Statut = 5 doc_source.DO_Statut = 5
doc_source.Write() doc_source.Write()
logger.info( logger.info(