refactor: Replace native Sage TransformInto() with manual document transformation logic, including source data extraction and simplified transformation rules.

This commit is contained in:
Fanilo-Nantenaina 2025-11-28 09:00:59 +03:00
parent 6c1de3583c
commit 6733f506eb

View file

@ -1027,8 +1027,10 @@ class SageConnector:
def transformer_document(self, numero_source, type_source, type_cible):
"""
Transformation de document avec la méthode NATIVE de Sage
CORRECTION : Utilise les VRAIS types Sage Dataven
🔧 Transformation de document - MÉTHODE MANUELLE
TransformInto() n'est pas disponible sur cette installation Sage
On crée manuellement le document cible en copiant les données
"""
if not self.cial:
raise RuntimeError("Connexion Sage non etablie")
@ -1037,58 +1039,36 @@ class SageConnector:
type_cible = int(type_cible)
logger.info(
f"[TRANSFORM] Demande: {numero_source} "
f"[TRANSFORM] Demande MANUELLE: {numero_source} "
f"(type {type_source}) -> type {type_cible}"
)
# ✅ Matrice de transformations pour VOTRE installation Sage
# ✅ Matrice de transformations
transformations_autorisees = {
(
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é)
(0, 10): "Devis -> Commande",
(10, 30): "Commande -> Bon de livraison",
(10, 60): "Commande -> Facture",
(30, 60): "Bon de livraison -> Facture",
(0, 60): "Devis -> Facture",
}
if (type_source, type_cible) not in transformations_autorisees:
raise ValueError(
f"Transformation non autorisee par Sage: "
f"{type_source} -> {type_cible}. "
f"Valides: "
+ ", ".join(
f"{k[0]}->{k[1]}" for k in transformations_autorisees.keys()
)
f"Transformation non autorisee: {type_source} -> {type_cible}"
)
try:
with self._com_context(), self._lock_com:
# ===== LECTURE SOURCE =====
# ========================================
# ÉTAPE 1 : LIRE LE DOCUMENT SOURCE
# ========================================
factory = self.cial.FactoryDocumentVente
persist_source = factory.ReadPiece(type_source, numero_source)
if not persist_source:
logger.warning(
f"[TRANSFORM] ReadPiece failed, searching in List()..."
)
persist_source = self._find_document_in_list(
numero_source, type_source
)
if not persist_source:
raise ValueError(
f"Document {numero_source} (type {type_source}) introuvable"
@ -1097,87 +1077,156 @@ class SageConnector:
doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3")
doc_source.Read()
# VÉRIFICATIONS STATUT
# Vérifications statut
statut_actuel = getattr(doc_source, "DO_Statut", 0)
type_reel = getattr(doc_source, "DO_Type", -1)
logger.info(
f"[TRANSFORM] Document source: type={type_reel}, "
f"statut={statut_actuel}, numero={numero_source}"
f"[TRANSFORM] Source: type={type_reel}, statut={statut_actuel}"
)
# Vérifier cohérence type
if type_reel != type_source:
raise ValueError(
f"Incoherence: document {numero_source} est de type {type_reel}, "
f"pas de type {type_source}"
f"Incoherence: document est de type {type_reel}, pas {type_source}"
)
# RÈGLES DE STATUT
if statut_actuel == 5:
raise ValueError(
f"Document {numero_source} deja transforme (statut=5). "
f"Impossible de le transformer a nouveau."
)
raise ValueError("Document deja transforme (statut=5)")
if statut_actuel == 6:
raise ValueError(
f"Document {numero_source} annule (statut=6). "
f"Impossible de le transformer."
)
raise ValueError("Document annule (statut=6)")
if statut_actuel in [3, 4]:
raise ValueError(
f"Document {numero_source} deja realise (statut={statut_actuel}). "
f"Ce document a deja ete transforme partiellement ou totalement."
)
raise ValueError(f"Document deja realise (statut={statut_actuel})")
# 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)"
)
try:
# Forcer statut "Accepté" si devis brouillon
if type_source == 0 and statut_actuel == 0:
logger.info("[TRANSFORM] Passage devis a statut Accepte (2)")
doc_source.DO_Statut = 2
doc_source.Write()
logger.info(f"[TRANSFORM] Statut change: 0 -> 2")
doc_source.Read()
nouveau_statut = getattr(doc_source, "DO_Statut", 0)
if nouveau_statut != 2:
raise RuntimeError(
f"Echec changement statut: toujours a {nouveau_statut}"
)
except Exception as e:
raise RuntimeError(f"Impossible de changer le statut: {e}")
# ===== TRANSACTION =====
# ========================================
# ÉTAPE 2 : EXTRAIRE LES DONNÉES SOURCE
# ========================================
logger.info("[TRANSFORM] Extraction donnees source...")
# Client
client_code = ""
client_obj = None
try:
client_obj = getattr(doc_source, "Client", None)
if client_obj:
client_obj.Read()
client_code = getattr(client_obj, "CT_Num", "").strip()
except Exception as e:
logger.error(f"Erreur lecture client: {e}")
raise ValueError(f"Impossible de lire le client du document source")
if not client_code:
raise ValueError("Client introuvable dans document source")
logger.info(f"[TRANSFORM] Client: {client_code}")
# Date
date_source = getattr(doc_source, "DO_Date", None)
# Lignes
lignes_source = []
try:
factory_lignes_source = getattr(
doc_source, "FactoryDocumentLigne", None
)
if not factory_lignes_source:
factory_lignes_source = getattr(
doc_source, "FactoryDocumentVenteLigne", None
)
if factory_lignes_source:
index = 1
while index <= 1000:
try:
ligne_p = factory_lignes_source.List(index)
if ligne_p is None:
break
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
ligne.Read()
# Récupérer référence 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
lignes_source.append(
{
"article_ref": article_ref,
"designation": getattr(ligne, "DL_Design", ""),
"quantite": float(
getattr(ligne, "DL_Qte", 0.0)
),
"prix_unitaire": float(
getattr(ligne, "DL_PrixUnitaire", 0.0)
),
"remise": float(
getattr(ligne, "DL_Remise01REM_Valeur", 0.0)
),
"type_remise": int(
getattr(ligne, "DL_Remise01REM_Type", 0)
),
}
)
index += 1
except Exception as e:
logger.debug(f"Erreur ligne {index}: {e}")
break
except Exception as e:
logger.error(f"Erreur extraction lignes: {e}")
raise ValueError(
"Impossible d'extraire les lignes du document source"
)
nb_lignes = len(lignes_source)
logger.info(f"[TRANSFORM] {nb_lignes} lignes extraites")
if nb_lignes == 0:
raise ValueError("Document source vide (aucune ligne)")
# ========================================
# ÉTAPE 3 : TRANSACTION
# ========================================
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug("[TRANSFORM] Transaction demarree")
except:
logger.debug(
"[TRANSFORM] BeginTrans non disponible, continue sans transaction"
)
logger.debug("[TRANSFORM] BeginTrans non disponible")
try:
# ✅✅✅ MÉTHODE NATIVE SAGE: TransformInto() ✅✅✅
logger.info(f"[TRANSFORM] Appel TransformInto({type_cible})...")
# ========================================
# ÉTAPE 4 : CRÉER LE DOCUMENT CIBLE
# ========================================
logger.info(f"[TRANSFORM] Creation document type {type_cible}...")
try:
doc_cible = doc_source.TransformInto(type_cible)
if doc_cible is None:
process = self.cial.CreateProcess_Document(type_cible)
if not process:
raise RuntimeError(
"TransformInto() a retourne None. "
"Verifiez la configuration Sage et les autorisations."
f"CreateProcess_Document({type_cible}) a retourne None"
)
logger.info("[TRANSFORM] TransformInto() execute avec succes")
doc_cible = process.Document
try:
doc_cible = win32com.client.CastTo(
doc_cible, "IBODocumentVente3"
@ -1185,64 +1234,159 @@ class SageConnector:
except:
pass
doc_cible.Read()
logger.info("[TRANSFORM] Document cible cree")
# ========================================
# ÉTAPE 5 : DÉFINIR LA DATE
# ========================================
import pywintypes
if date_source:
try:
doc_cible.DO_Date = date_source
logger.info(f"[TRANSFORM] Date copiee: {date_source}")
except Exception as e:
logger.warning(f"Impossible de copier date: {e}")
doc_cible.DO_Date = pywintypes.Time(datetime.now())
else:
doc_cible.DO_Date = pywintypes.Time(datetime.now())
# ========================================
# ÉTAPE 6 : ASSOCIER LE CLIENT
# ========================================
logger.info(f"[TRANSFORM] Association client {client_code}...")
factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(client_code)
if not persist_client:
raise ValueError(f"Client {client_code} introuvable")
client_obj_cible = self._cast_client(persist_client)
if not client_obj_cible:
raise ValueError(f"Impossible de charger client {client_code}")
doc_cible.SetDefaultClient(client_obj_cible)
doc_cible.Write()
logger.info(f"[TRANSFORM] Client {client_code} associe")
# ========================================
# ÉTAPE 7 : COPIER LES LIGNES
# ========================================
logger.info(f"[TRANSFORM] Copie de {nb_lignes} lignes...")
try:
factory_lignes_cible = doc_cible.FactoryDocumentLigne
except:
factory_lignes_cible = doc_cible.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
for idx, ligne_data in enumerate(lignes_source, 1):
logger.debug(f"[TRANSFORM] Ligne {idx}/{nb_lignes}")
# Charger article
article_ref = ligne_data["article_ref"]
if not article_ref:
logger.warning(
f"Ligne {idx}: pas de reference article, skip"
)
continue
persist_article = factory_article.ReadReference(article_ref)
if not persist_article:
logger.warning(
f"Ligne {idx}: article {article_ref} introuvable, skip"
)
continue
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
# Créer ligne
ligne_persist = factory_lignes_cible.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
# Associer article avec quantité
quantite = ligne_data["quantite"]
try:
ligne_obj.SetDefaultArticleReference(article_ref, quantite)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
# Fallback manuel
ligne_obj.DL_Design = ligne_data["designation"]
ligne_obj.DL_Qte = quantite
# Définir prix
prix = ligne_data["prix_unitaire"]
if prix > 0:
ligne_obj.DL_PrixUnitaire = float(prix)
# Copier remise
remise = ligne_data["remise"]
if remise > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(remise)
ligne_obj.DL_Remise01REM_Type = ligne_data[
"type_remise"
]
except:
pass
# Écrire ligne
ligne_obj.Write()
logger.info(f"[TRANSFORM] {nb_lignes} lignes copiees")
# ========================================
# ÉTAPE 8 : VALIDER LE DOCUMENT
# ========================================
logger.info("[TRANSFORM] Validation document cible...")
doc_cible.Write()
process.Process()
logger.info("[TRANSFORM] Document cible valide")
# ========================================
# ÉTAPE 9 : RÉCUPÉRER LE NUMÉRO
# ========================================
numero_cible = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_cible = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_cible:
numero_cible = getattr(doc_cible, "DO_Piece", "")
if not numero_cible:
raise RuntimeError(
"Numero document cible vide apres transformation"
)
raise RuntimeError("Numero document cible vide")
# Compter les lignes
try:
factory_lignes = doc_cible.FactoryDocumentLigne
except:
factory_lignes = doc_cible.FactoryDocumentVenteLigne
logger.info(f"[TRANSFORM] Document cible cree: {numero_cible}")
nb_lignes = 0
index = 1
while index <= 1000:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
nb_lignes += 1
index += 1
except:
break
logger.info(
f"[TRANSFORM] Document cible cree: {numero_cible} avec {nb_lignes} lignes"
)
except AttributeError as e:
logger.error(f"[TRANSFORM] TransformInto() non disponible: {e}")
raise RuntimeError(
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}")
if "Valeur invalide" in str(e):
raise RuntimeError(
f"Sage refuse la transformation vers le type {type_cible}. "
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}"
)
# Commit transaction
# ========================================
# ÉTAPE 10 : COMMIT & MAJ STATUT SOURCE
# ========================================
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
@ -1250,22 +1394,20 @@ class SageConnector:
except:
pass
# MAJ statut source → Transformé
# Attente indexation
time.sleep(1)
# Marquer source comme "Transformé"
try:
doc_source.Read()
doc_source.DO_Statut = 5
doc_source.Write()
logger.info(
f"[TRANSFORM] Statut source mis a jour: -> 5 (TRANSFORME)"
)
logger.info("[TRANSFORM] Statut source -> 5 (TRANSFORME)")
except Exception as e:
logger.warning(
f"[TRANSFORM] Impossible de MAJ statut source: {e}"
)
logger.warning(f"Impossible MAJ statut source: {e}")
logger.info(
f"[TRANSFORM] SUCCES: "
f"{numero_source} ({type_source}) -> "
f"[TRANSFORM] ✅ SUCCES: {numero_source} ({type_source}) -> "
f"{numero_cible} ({type_cible}) - {nb_lignes} lignes"
)
@ -1280,13 +1422,13 @@ class SageConnector:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.error("[TRANSFORM] Transaction annulee")
logger.error("[TRANSFORM] Transaction annulee (rollback)")
except:
pass
raise
except Exception as e:
logger.error(f"[TRANSFORM] Erreur: {e}", exc_info=True)
logger.error(f"[TRANSFORM] ❌ ERREUR: {e}", exc_info=True)
raise RuntimeError(f"Echec transformation: {str(e)}")
def _find_document_in_list(self, numero, type_doc):