fix: Introduce Sage document type constants and update document transformation and listing endpoints to use correct Sage types.
This commit is contained in:
parent
9d0c26b5d8
commit
9b17149b07
3 changed files with 87 additions and 145 deletions
16
config.py
16
config.py
|
|
@ -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
111
main.py
|
|
@ -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
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue