diff --git a/sage_connector.py b/sage_connector.py index 5ab53c3..c432c07 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -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,152 +1077,316 @@ 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 deja realise (statut={statut_actuel})") + + # 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() + doc_source.Read() + + # ======================================== + # É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( - f"Document {numero_source} deja realise (statut={statut_actuel}). " - f"Ce document a deja ete transforme partiellement ou totalement." + "Impossible d'extraire les lignes du document source" ) - # 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: - doc_source.DO_Statut = 2 - doc_source.Write() - logger.info(f"[TRANSFORM] Statut change: 0 -> 2") + nb_lignes = len(lignes_source) + logger.info(f"[TRANSFORM] {nb_lignes} lignes extraites") - 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}") + if nb_lignes == 0: + raise ValueError("Document source vide (aucune ligne)") - # ===== TRANSACTION ===== + # ======================================== + # É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}...") + + process = self.cial.CreateProcess_Document(type_cible) + if not process: + raise RuntimeError( + f"CreateProcess_Document({type_cible}) a retourne None" + ) + + doc_cible = process.Document + try: + doc_cible = win32com.client.CastTo( + doc_cible, "IBODocumentVente3" + ) + except: + pass + + 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: - doc_cible = doc_source.TransformInto(type_cible) + factory_lignes_cible = doc_cible.FactoryDocumentLigne + except: + factory_lignes_cible = doc_cible.FactoryDocumentVenteLigne - if doc_cible is None: - raise RuntimeError( - "TransformInto() a retourne None. " - "Verifiez la configuration Sage et les autorisations." + 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 - logger.info("[TRANSFORM] TransformInto() execute avec succes") + 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: - doc_cible = win32com.client.CastTo( - doc_cible, "IBODocumentVente3" + ligne_obj = win32com.client.CastTo( + ligne_persist, "IBODocumentLigne3" ) except: - pass + ligne_obj = win32com.client.CastTo( + ligne_persist, "IBODocumentVenteLigne3" + ) - doc_cible.Read() + # 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" - ) + if not numero_cible: + 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):