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 typing import Optional, List
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore"
env_file=".env", 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) ===
chemin_base: str
utilisateur: str = "Administrateur"
@ -31,8 +37,10 @@ class Settings(BaseSettings):
# === CORS ===
cors_origins: List[str] = ["*"]
settings = Settings()
def validate_settings():
"""Validation au démarrage"""
if not settings.chemin_base or not settings.mot_de_passe:

111
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)])
def transformer_document(
numero_source: str = Query(..., description="Numéro du document source"),
type_source: int = Query(
...,
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"),
type_source: int = Query(..., description="Type document source"),
type_cible: int = Query(..., 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
- 1: Bon de livraison
- 2: Bon de retour
- 3: Commande
- 4: Préparation
- 5: Facture
- 10: Bon de commande
- 20: Préparation
- 30: Bon de livraison
- 40: Bon de retour
- 50: Bon d'avoir
- 60: Facture
Transformations valides:
- Devis (0) Commande (3)
- Devis (0) Facture (5)
- Commande (3) Bon livraison (1)
- Commande (3) Facture (5)
- Bon livraison (1) Facture (5)
Transformations autorisées :
- Devis (0) Commande (10)
- Commande (10) Bon livraison (30)
- Commande (10) Facture (60)
- Bon livraison (30) Facture (60)
"""
try:
logger.info(
@ -524,13 +519,13 @@ def transformer_document(
f"(type {type_source}) → type {type_cible}"
)
# Validation des transformations autorisées
# ✅ Matrice des transformations valides pour VOTRE Sage
transformations_valides = {
(0, 3), # Devis → Commande
(0, 5), # Devis → Facture
(3, 1), # Commande → Bon de livraison
(3, 5), # Commande → Facture
(1, 5), # Bon de livraison → Facture
(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:
@ -557,11 +552,9 @@ def transformer_document(
except HTTPException:
raise
except ValueError as e:
# Erreurs métier (document introuvable, déjà transformé, etc.)
logger.error(f"❌ Erreur métier transformation: {e}")
raise HTTPException(400, str(e))
except Exception as e:
# Erreurs techniques (COM, Sage, etc.)
logger.error(f"❌ Erreur technique transformation: {e}", exc_info=True)
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):
"""
📋 Liste toutes les commandes
CORRECTIONS: Gestion robuste des erreurs + logging détaillé
CORRECTION: Filtre sur type 10 (BON_COMMANDE)
"""
try:
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
commandes = []
index = 1
max_iterations = limit * 10 # Plus de marge
max_iterations = limit * 10
erreurs_consecutives = 0
max_erreurs = 100
@ -636,33 +629,26 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None):
try:
persist = factory.List(index)
if persist is None:
logger.debug(f"Fin de liste à l'index {index}")
break
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
# ✅ CORRECTION 1: Vérifier type de document
doc_type = getattr(doc, "DO_Type", -1)
# ⚠️ CRITIQUE: Vérifier que c'est bien une commande (type 3)
if doc_type != 3:
# ✅ CRITIQUE : Filtrer sur type 10 (BON_COMMANDE)
if doc_type != settings.SAGE_TYPE_BON_COMMANDE:
index += 1
continue
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
if statut is not None and doc_statut != statut:
index += 1
continue
# ✅ CORRECTION 2: Charger client via .Client
# Charger client
client_code = ""
client_intitule = ""
@ -674,20 +660,8 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None):
client_intitule = getattr(
client_obj, "CT_Intitule", ""
).strip()
logger.debug(f" Client: {client_code} - {client_intitule}")
except Exception as 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 = {
"numero": getattr(doc, "DO_Piece", ""),
@ -700,27 +674,17 @@ def commandes_list(limit: int = 100, statut: Optional[int] = None):
}
commandes.append(commande)
logger.debug(f" ✅ Commande ajoutée: {commande['numero']}")
erreurs_consecutives = 0
index += 1
except Exception as e:
erreurs_consecutives += 1
logger.debug(f"⚠️ Erreur index {index}: {e}")
index += 1
if erreurs_consecutives >= max_erreurs:
logger.warning(
f"⚠️ Arrêt après {max_erreurs} erreurs consécutives"
)
break
nb_avec_client = sum(1 for c in commandes if c["client_intitule"])
logger.info(
f"{len(commandes)} commandes retournées "
f"({nb_avec_client} avec client)"
)
logger.info(f"{len(commandes)} commandes retournées")
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)])
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:
with sage._com_context(), sage._lock_com:
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.Read()
# Filtrer factures (type 5)
if getattr(doc, "DO_Type", -1) != 5:
# ✅ CRITIQUE: Filtrer factures (type 60)
if getattr(doc, "DO_Type", -1) != settings.SAGE_TYPE_FACTURE:
index += 1
continue
@ -773,13 +740,6 @@ def factures_list(limit: int = 100, statut: Optional[int] = None):
except:
pass
# Champ libre dernière relance
derniere_relance = None
try:
derniere_relance = getattr(doc, "DO_DerniereRelance", None)
except:
pass
factures.append(
{
"numero": getattr(doc, "DO_Piece", ""),
@ -789,9 +749,6 @@ def factures_list(limit: int = 100, statut: Optional[int] = None):
"total_ht": float(getattr(doc, "DO_TotalHT", 0.0)),
"total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)),
"statut": doc_statut,
"derniere_relance": (
str(derniere_relance) if derniere_relance else None
),
}
)

View file

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