diff --git a/sage_connector.py b/sage_connector.py index 8b34a3a..1030eb3 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -93,6 +93,9 @@ from utils import ( obtenir_champs_assignables, ) +from schemas.documents.doc_config import TypeDocumentVente +from utils.functions.data.create_doc import _creer_document_vente_unifie + logger = logging.getLogger(__name__) @@ -1214,226 +1217,9 @@ class SageConnector: cursor = conn.cursor() return _lire_document_sql(cursor, numero, type_doc=5) - def creer_devis_enrichi(self, devis_data: dict): - if not self.cial: - raise RuntimeError("Connexion Sage non établie") - - logger.info( - f" Début création devis pour client {devis_data['client']['code']} " - ) - - try: - with self._com_context(), self._lock_com: - transaction_active = False - try: - self.cial.CptaApplication.BeginTrans() - transaction_active = True - logger.debug(" Transaction Sage démarrée") - except Exception as e: - logger.warning(f"BeginTrans échoué: {e}") - - try: - process = self.cial.CreateProcess_Document(0) - doc = process.Document - - try: - doc = win32com.client.CastTo(doc, "IBODocumentVente3") - except Exception: - pass - - logger.info(" Document devis créé") - - doc.DO_Date = pywintypes.Time( - normaliser_date(devis_data.get("date_devis")) - ) - - if "date_livraison" in devis_data and devis_data["date_livraison"]: - doc.DO_DateLivr = pywintypes.Time( - normaliser_date(devis_data["date_livraison"]) - ) - - factory_client = self.cial.CptaApplication.FactoryClient - persist_client = factory_client.ReadNumero( - devis_data["client"]["code"] - ) - - if not persist_client: - raise ValueError( - f" Client {devis_data['client']['code']} introuvable" - ) - - client_obj = _cast_client(persist_client) - if not client_obj: - raise ValueError( - f" Impossible de charger le client {devis_data['client']['code']}" - ) - - doc.SetDefaultClient(client_obj) - logger.info(f" Client {devis_data['client']['code']} associé") - doc.DO_Statut = 0 - - doc.Write() - - try: - factory_lignes = doc.FactoryDocumentLigne - except Exception: - factory_lignes = doc.FactoryDocumentVenteLigne - - factory_article = self.cial.FactoryArticle - - logger.info(f" Ajout de {len(devis_data['lignes'])} lignes...") - - for idx, ligne_data in enumerate(devis_data["lignes"], 1): - logger.debug( - f"--- Ligne {idx}: {ligne_data['article_code']} ---" - ) - - persist_article = factory_article.ReadReference( - ligne_data["article_code"] - ) - - if not persist_article: - raise ValueError( - f" Article {ligne_data['article_code']} introuvable" - ) - - article_obj = win32com.client.CastTo( - persist_article, "IBOArticle3" - ) - article_obj.Read() - - prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) - designation_sage = getattr(article_obj, "AR_Design", "") - - if prix_sage == 0: - logger.warning( - f"Article {ligne_data['article_code']} a un prix = 0€" - ) - - ligne_persist = factory_lignes.Create() - - try: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentLigne3" - ) - except Exception: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentVenteLigne3" - ) - - quantite = float(ligne_data["quantite"]) - - try: - ligne_obj.SetDefaultArticleReference( - ligne_data["article_code"], quantite - ) - except Exception: - try: - ligne_obj.SetDefaultArticle(article_obj, quantite) - except Exception: - ligne_obj.DL_Design = ( - designation_sage - or ligne_data.get("designation", "") - ) - ligne_obj.DL_Qte = quantite - - prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - prix_a_utiliser = ligne_data.get("prix_unitaire_ht") - - if prix_a_utiliser is not None and prix_a_utiliser > 0: - ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) - elif prix_auto == 0: - if prix_sage == 0: - raise ValueError( - f"Prix nul pour article {ligne_data['article_code']}" - ) - ligne_obj.DL_PrixUnitaire = float(prix_sage) - - remise = ligne_data.get("remise_pourcentage", 0) - if remise > 0: - try: - ligne_obj.DL_Remise01REM_Valeur = float(remise) - ligne_obj.DL_Remise01REM_Type = 0 - except Exception as e: - logger.warning(f"Remise non appliquée: {e}") - - ligne_obj.Write() - - logger.info(f" {len(devis_data['lignes'])} lignes écrites") - - doc.Write() - - try: - process.Process() - logger.info(" Process() appelé (brouillon)") - except Exception: - logger.debug("Process() ignoré pour brouillon") - - numero_devis = _recuperer_numero_devis(process, doc) - - if not numero_devis: - raise RuntimeError(" Numéro devis vide après création") - - logger.info(f" Numéro: {numero_devis}") - - if transaction_active: - try: - self.cial.CptaApplication.CommitTrans() - logger.info(" Transaction committée") - except Exception: - pass - - import time - - time.sleep(0.5) - - if "reference" in devis_data and devis_data["reference"]: - try: - logger.info( - f" Application de la référence: {devis_data['reference']}" - ) - - doc_reload = self._charger_devis(numero_devis) - - nouvelle_reference = devis_data["reference"] - doc_reload.DO_Ref = ( - str(nouvelle_reference) if nouvelle_reference else "" - ) - doc_reload.Write() - - time.sleep(0.5) - - doc_reload.Read() - - logger.info(f" Référence définie: {nouvelle_reference}") - except Exception as e: - logger.warning( - f"Impossible de définir la référence: {e}", - exc_info=True, - ) - - time.sleep(0.5) - - doc_final_data = self._relire_devis(numero_devis, devis_data) - - logger.info( - f" DEVIS CRÉÉ: {numero_devis} - {doc_final_data['total_ttc']}€ TTC " - ) - - return doc_final_data - - except Exception: - if transaction_active: - try: - self.cial.CptaApplication.RollbackTrans() - logger.error(" Transaction annulée (rollback)") - except Exception: - pass - raise - - except Exception as e: - logger.error(f" ERREUR CRÉATION DEVIS: {e}", exc_info=True) - raise RuntimeError(f"Échec création devis: {str(e)}") + def creer_devis_enrichi(self, devis_data: dict) -> Dict: + """Crée un devis""" + return _creer_document_vente_unifie(self, devis_data, TypeDocumentVente.DEVIS) def _relire_devis(self, numero_devis, devis_data): """Relit le devis créé et extrait les informations finales.""" @@ -1908,7 +1694,7 @@ class SageConnector: ligne_obj.DL_Remise01REM_Valeur = float( ligne_data["remise_pourcentage"] ) - ligne_obj.DL_Remise01REM_Type = 0 + # ligne_obj.DL_Remise01REM_Type = 0 logger.info(" Remise définie") except Exception: logger.debug(" Remise non supportée") @@ -2643,7 +2429,7 @@ class SageConnector: type_tiers = "Fournisseur" logger.info(" ✓ Trouvé comme Fournisseur") except Exception as e: - logger.debug(" Pas trouvé comme Fournisseur: {e}") + logger.debug(f" Pas trouvé comme Fournisseur: {e}") # Vérification finale if not persist_tiers: @@ -5070,280 +4856,8 @@ class SageConnector: raise RuntimeError(f"Erreur technique: {e}") def creer_commande_enrichi(self, commande_data: dict) -> Dict: - if not self.cial: - raise RuntimeError("Connexion Sage non établie") - - logger.info( - f" Début création commande pour client {commande_data['client']['code']}" - ) - - try: - with self._com_context(), self._lock_com: - transaction_active = False - try: - self.cial.CptaApplication.BeginTrans() - transaction_active = True - logger.debug(" Transaction Sage démarrée") - except Exception: - pass - - try: - process = self.cial.CreateProcess_Document( - settings.SAGE_TYPE_BON_COMMANDE - ) - doc = process.Document - - try: - doc = win32com.client.CastTo(doc, "IBODocumentVente3") - except Exception: - pass - - logger.info(" Document commande créé") - - doc.DO_Date = pywintypes.Time( - normaliser_date(commande_data.get("date_commande")) - ) - - if ( - "date_livraison" in commande_data - and commande_data["date_livraison"] - ): - doc.DO_DateLivr = pywintypes.Time( - normaliser_date(commande_data["date_livraison"]) - ) - logger.info( - f" Date livraison: {commande_data['date_livraison']}" - ) - - factory_client = self.cial.CptaApplication.FactoryClient - persist_client = factory_client.ReadNumero( - commande_data["client"]["code"] - ) - - if not persist_client: - raise ValueError( - f"Client {commande_data['client']['code']} introuvable" - ) - - client_obj = _cast_client(persist_client) - if not client_obj: - raise ValueError("Impossible de charger le client") - - doc.SetDefaultClient(client_obj) - doc.Write() - logger.info(f" Client {commande_data['client']['code']} associé") - - if commande_data.get("reference"): - try: - doc.DO_Ref = commande_data["reference"] - logger.info(f" Référence: {commande_data['reference']}") - except Exception: - pass - - try: - factory_lignes = doc.FactoryDocumentLigne - except Exception: - factory_lignes = doc.FactoryDocumentVenteLigne - - factory_article = self.cial.FactoryArticle - - logger.info(f" Ajout de {len(commande_data['lignes'])} lignes...") - - for idx, ligne_data in enumerate(commande_data["lignes"], 1): - logger.info( - f"--- Ligne {idx}: {ligne_data['article_code']} ---" - ) - - persist_article = factory_article.ReadReference( - ligne_data["article_code"] - ) - - if not persist_article: - raise ValueError( - f" Article {ligne_data['article_code']} introuvable dans Sage" - ) - - article_obj = win32com.client.CastTo( - persist_article, "IBOArticle3" - ) - article_obj.Read() - - prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) - designation_sage = getattr(article_obj, "AR_Design", "") - logger.info(f" Prix Sage: {prix_sage}€") - - if prix_sage == 0: - logger.warning( - f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" - ) - - ligne_persist = factory_lignes.Create() - - try: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentLigne3" - ) - except Exception: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentVenteLigne3" - ) - - quantite = float(ligne_data["quantite"]) - - try: - ligne_obj.SetDefaultArticleReference( - ligne_data["article_code"], quantite - ) - logger.info( - " Article associé via SetDefaultArticleReference" - ) - except Exception as e: - logger.warning( - f"SetDefaultArticleReference échoué: {e}, tentative avec objet" - ) - try: - ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(" Article associé via SetDefaultArticle") - except Exception: - logger.error(" Toutes les méthodes ont échoué") - ligne_obj.DL_Design = ( - designation_sage - or ligne_data.get("designation", "") - ) - ligne_obj.DL_Qte = quantite - logger.warning("Configuration manuelle appliquée") - - prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - logger.info(f" Prix auto chargé: {prix_auto}€") - - prix_a_utiliser = ligne_data.get("prix_unitaire_ht") - - if prix_a_utiliser is not None and prix_a_utiliser > 0: - ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) - logger.info(f" Prix personnalisé: {prix_a_utiliser}€") - elif prix_auto == 0 and prix_sage > 0: - ligne_obj.DL_PrixUnitaire = float(prix_sage) - logger.info(f" Prix Sage forcé: {prix_sage}€") - elif prix_auto > 0: - logger.info(f" Prix auto conservé: {prix_auto}€") - - prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - montant_ligne = quantite * prix_final - logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") - - remise = ligne_data.get("remise_pourcentage", 0) - if remise > 0: - try: - ligne_obj.DL_Remise01REM_Valeur = float(remise) - ligne_obj.DL_Remise01REM_Type = 0 - montant_apres_remise = montant_ligne * ( - 1 - remise / 100 - ) - logger.info( - f" Remise {remise}% → {montant_apres_remise}€" - ) - except Exception as e: - logger.warning(f"Remise non appliquée: {e}") - - ligne_obj.Write() - logger.info(f" Ligne {idx} écrite") - - try: - ligne_obj.Read() - prix_enregistre = float( - getattr(ligne_obj, "DL_PrixUnitaire", 0.0) - ) - montant_enregistre = float( - getattr(ligne_obj, "DL_MontantHT", 0.0) - ) - logger.info( - f" Vérif: Prix={prix_enregistre}€, Montant HT={montant_enregistre}€" - ) - except Exception as e: - logger.warning(f"Impossible de vérifier: {e}") - - doc.Write() - process.Process() - - if transaction_active: - self.cial.CptaApplication.CommitTrans() - - time.sleep(2) - - numero_commande = None - try: - doc_result = process.DocumentResult - if doc_result: - doc_result = win32com.client.CastTo( - doc_result, "IBODocumentVente3" - ) - doc_result.Read() - numero_commande = getattr(doc_result, "DO_Piece", "") - except Exception: - pass - - if not numero_commande: - numero_commande = getattr(doc, "DO_Piece", "") - - factory_doc = self.cial.FactoryDocumentVente - persist_reread = factory_doc.ReadPiece( - settings.SAGE_TYPE_BON_COMMANDE, numero_commande - ) - - if persist_reread: - doc_final = win32com.client.CastTo( - persist_reread, "IBODocumentVente3" - ) - doc_final.Read() - - total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) - total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) - reference_finale = getattr(doc_final, "DO_Ref", "") - - date_livraison_final = None - - try: - date_livr = getattr(doc_final, "DO_DateLivr", None) - if date_livr: - date_livraison_final = date_livr.strftime("%Y-%m-%d") - except Exception: - pass - else: - total_ht = 0.0 - total_ttc = 0.0 - reference_finale = commande_data.get("reference", "") - date_livraison_final = commande_data.get("date_livraison") - - logger.info( - f" COMMANDE CRÉÉE: {numero_commande} - {total_ttc}€ TTC " - ) - if date_livraison_final: - logger.info(f" Date livraison: {date_livraison_final}") - - return { - "numero_commande": numero_commande, - "total_ht": total_ht, - "total_ttc": total_ttc, - "nb_lignes": len(commande_data["lignes"]), - "client_code": commande_data["client"]["code"], - "date_commande": str( - normaliser_date(commande_data.get("date_commande")) - ), - "date_livraison": date_livraison_final, - "reference": reference_finale, - } - - except Exception: - if transaction_active: - try: - self.cial.CptaApplication.RollbackTrans() - except Exception: - pass - raise - - except Exception as e: - logger.error(f" Erreur création commande: {e}", exc_info=True) - raise RuntimeError(f"Échec création commande: {str(e)}") + """Crée une commande""" + return _creer_document_vente_unifie(commande_data, TypeDocumentVente.COMMANDE) def modifier_commande(self, numero: str, commande_data: Dict) -> Dict: if not self.cial: @@ -5653,7 +5167,7 @@ class SageConnector: ligne_obj.DL_Remise01REM_Valeur = float( ligne_data["remise_pourcentage"] ) - ligne_obj.DL_Remise01REM_Type = 0 + # ligne_obj.DL_Remise01REM_Type = 0 except Exception: pass @@ -5795,272 +5309,8 @@ class SageConnector: raise RuntimeError(f"Erreur Sage: {error_message}") def creer_livraison_enrichi(self, livraison_data: dict) -> Dict: - if not self.cial: - raise RuntimeError("Connexion Sage non établie") - - logger.info( - f" Début création livraison pour client {livraison_data['client']['code']}" - ) - - try: - with self._com_context(), self._lock_com: - transaction_active = False - try: - self.cial.CptaApplication.BeginTrans() - transaction_active = True - logger.debug(" Transaction Sage démarrée") - except Exception: - pass - - try: - process = self.cial.CreateProcess_Document( - settings.SAGE_TYPE_BON_LIVRAISON - ) - doc = process.Document - - try: - doc = win32com.client.CastTo(doc, "IBODocumentVente3") - except Exception: - pass - - logger.info(" Document livraison créé") - - doc.DO_Date = pywintypes.Time( - normaliser_date(livraison_data.get("date_livraison")) - ) - - if ( - "date_livraison_prevue" in livraison_data - and livraison_data["date_livraison_prevue"] - ): - doc.DO_DateLivr = pywintypes.Time( - normaliser_date(livraison_data["date_livraison_prevue"]) - ) - logger.info( - f" Date livraison prévue: {livraison_data['date_livraison_prevue']}" - ) - - factory_client = self.cial.CptaApplication.FactoryClient - persist_client = factory_client.ReadNumero( - livraison_data["client"]["code"] - ) - - if not persist_client: - raise ValueError( - f"Client {livraison_data['client']['code']} introuvable" - ) - - client_obj = _cast_client(persist_client) - if not client_obj: - raise ValueError("Impossible de charger le client") - - doc.SetDefaultClient(client_obj) - doc.Write() - logger.info(f" Client {livraison_data['client']['code']} associé") - - if livraison_data.get("reference"): - try: - doc.DO_Ref = livraison_data["reference"] - logger.info(f" Référence: {livraison_data['reference']}") - except Exception: - pass - - try: - factory_lignes = doc.FactoryDocumentLigne - except Exception: - factory_lignes = doc.FactoryDocumentVenteLigne - - factory_article = self.cial.FactoryArticle - - logger.info(f" Ajout de {len(livraison_data['lignes'])} lignes...") - - for idx, ligne_data in enumerate(livraison_data["lignes"], 1): - logger.info( - f"--- Ligne {idx}: {ligne_data['article_code']} ---" - ) - - persist_article = factory_article.ReadReference( - ligne_data["article_code"] - ) - - if not persist_article: - raise ValueError( - f" Article {ligne_data['article_code']} introuvable dans Sage" - ) - - article_obj = win32com.client.CastTo( - persist_article, "IBOArticle3" - ) - article_obj.Read() - - prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) - designation_sage = getattr(article_obj, "AR_Design", "") - logger.info(f" Prix Sage: {prix_sage}€") - - if prix_sage == 0: - logger.warning( - f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" - ) - - ligne_persist = factory_lignes.Create() - - try: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentLigne3" - ) - except Exception: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentVenteLigne3" - ) - - quantite = float(ligne_data["quantite"]) - - try: - ligne_obj.SetDefaultArticleReference( - ligne_data["article_code"], quantite - ) - logger.info( - " Article associé via SetDefaultArticleReference" - ) - except Exception as e: - logger.warning( - f"SetDefaultArticleReference échoué: {e}, tentative avec objet" - ) - try: - ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(" Article associé via SetDefaultArticle") - except Exception: - logger.error(" Toutes les méthodes ont échoué") - ligne_obj.DL_Design = ( - designation_sage - or ligne_data.get("designation", "") - ) - ligne_obj.DL_Qte = quantite - logger.warning("Configuration manuelle appliquée") - - prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - logger.info(f" Prix auto chargé: {prix_auto}€") - - prix_a_utiliser = ligne_data.get("prix_unitaire_ht") - - if prix_a_utiliser is not None and prix_a_utiliser > 0: - ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) - logger.info(f" Prix personnalisé: {prix_a_utiliser}€") - elif prix_auto == 0 and prix_sage > 0: - ligne_obj.DL_PrixUnitaire = float(prix_sage) - logger.info(f" Prix Sage forcé: {prix_sage}€") - elif prix_auto > 0: - logger.info(f" Prix auto conservé: {prix_auto}€") - - prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - montant_ligne = quantite * prix_final - logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") - - remise = ligne_data.get("remise_pourcentage", 0) - if remise > 0: - try: - ligne_obj.DL_Remise01REM_Valeur = float(remise) - ligne_obj.DL_Remise01REM_Type = 0 - montant_apres_remise = montant_ligne * ( - 1 - remise / 100 - ) - logger.info( - f" Remise {remise}% → {montant_apres_remise}€" - ) - except Exception as e: - logger.warning(f"Remise non appliquée: {e}") - - ligne_obj.Write() - logger.info(f" Ligne {idx} écrite") - - doc.Write() - process.Process() - - if transaction_active: - self.cial.CptaApplication.CommitTrans() - - time.sleep(2) - - numero_livraison = None - try: - doc_result = process.DocumentResult - if doc_result: - doc_result = win32com.client.CastTo( - doc_result, "IBODocumentVente3" - ) - doc_result.Read() - numero_livraison = getattr(doc_result, "DO_Piece", "") - except Exception: - pass - - if not numero_livraison: - numero_livraison = getattr(doc, "DO_Piece", "") - - factory_doc = self.cial.FactoryDocumentVente - persist_reread = factory_doc.ReadPiece( - settings.SAGE_TYPE_BON_LIVRAISON, numero_livraison - ) - - if persist_reread: - doc_final = win32com.client.CastTo( - persist_reread, "IBODocumentVente3" - ) - doc_final.Read() - - total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) - total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) - reference_finale = getattr(doc_final, "DO_Ref", "") - - date_livraison_prevue_final = None - - try: - date_livr = getattr(doc_final, "DO_DateLivr", None) - if date_livr: - date_livraison_prevue_final = date_livr.strftime( - "%Y-%m-%d" - ) - except Exception: - pass - else: - total_ht = 0.0 - total_ttc = 0.0 - reference_finale = livraison_data.get("reference", "") - date_livraison_prevue_final = livraison_data.get( - "date_livraison_prevue" - ) - - logger.info( - f" LIVRAISON CRÉÉE: {numero_livraison} - {total_ttc}€ TTC " - ) - if date_livraison_prevue_final: - logger.info( - f" Date livraison prévue: {date_livraison_prevue_final}" - ) - - return { - "numero_livraison": numero_livraison, - "total_ht": total_ht, - "total_ttc": total_ttc, - "nb_lignes": len(livraison_data["lignes"]), - "client_code": livraison_data["client"]["code"], - "date_livraison": str( - normaliser_date(livraison_data.get("date_livraison")) - ), - "date_livraison_prevue": date_livraison_prevue_final, - "reference": reference_finale, - } - - except Exception: - if transaction_active: - try: - self.cial.CptaApplication.RollbackTrans() - except Exception: - pass - raise - - except Exception as e: - logger.error(f" Erreur création livraison: {e}", exc_info=True) - raise RuntimeError(f"Échec création livraison: {str(e)}") + """Crée un bon de livraison""" + return _creer_document_vente_unifie(livraison_data, TypeDocumentVente.LIVRAISON) def modifier_livraison(self, numero: str, livraison_data: Dict) -> Dict: if not self.cial: @@ -6311,7 +5561,7 @@ class SageConnector: ligne_obj.DL_Remise01REM_Valeur = float( ligne_data["remise_pourcentage"] ) - ligne_obj.DL_Remise01REM_Type = 0 + # ligne_obj.DL_Remise01REM_Type = 0 except Exception: pass @@ -6445,258 +5695,8 @@ class SageConnector: raise RuntimeError(f"Erreur Sage: {error_message}") def creer_avoir_enrichi(self, avoir_data: dict) -> Dict: - if not self.cial: - raise RuntimeError("Connexion Sage non établie") - - logger.info(f" Début création avoir pour client {avoir_data['client']['code']}") - - try: - with self._com_context(), self._lock_com: - transaction_active = False - try: - self.cial.CptaApplication.BeginTrans() - transaction_active = True - logger.debug(" Transaction Sage démarrée") - except Exception: - pass - - try: - process = self.cial.CreateProcess_Document( - settings.SAGE_TYPE_BON_AVOIR - ) - doc = process.Document - - try: - doc = win32com.client.CastTo(doc, "IBODocumentVente3") - except Exception: - pass - - logger.info(" Document avoir créé") - - doc.DO_Date = pywintypes.Time( - normaliser_date(avoir_data.get("date_avoir")) - ) - - if "date_livraison" in avoir_data and avoir_data["date_livraison"]: - doc.DO_DateLivr = pywintypes.Time( - normaliser_date(avoir_data["date_livraison"]) - ) - logger.info(f" Date livraison: {avoir_data['date_livraison']}") - - factory_client = self.cial.CptaApplication.FactoryClient - persist_client = factory_client.ReadNumero( - avoir_data["client"]["code"] - ) - - if not persist_client: - raise ValueError( - f"Client {avoir_data['client']['code']} introuvable" - ) - - client_obj = _cast_client(persist_client) - if not client_obj: - raise ValueError("Impossible de charger le client") - - doc.SetDefaultClient(client_obj) - doc.Write() - logger.info(f" Client {avoir_data['client']['code']} associé") - - if avoir_data.get("reference"): - try: - doc.DO_Ref = avoir_data["reference"] - logger.info(f" Référence: {avoir_data['reference']}") - except Exception: - pass - - try: - factory_lignes = doc.FactoryDocumentLigne - except Exception: - factory_lignes = doc.FactoryDocumentVenteLigne - - factory_article = self.cial.FactoryArticle - - logger.info(f" Ajout de {len(avoir_data['lignes'])} lignes...") - - for idx, ligne_data in enumerate(avoir_data["lignes"], 1): - logger.info( - f"--- Ligne {idx}: {ligne_data['article_code']} ---" - ) - - persist_article = factory_article.ReadReference( - ligne_data["article_code"] - ) - - if not persist_article: - raise ValueError( - f" Article {ligne_data['article_code']} introuvable dans Sage" - ) - - article_obj = win32com.client.CastTo( - persist_article, "IBOArticle3" - ) - article_obj.Read() - - prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) - designation_sage = getattr(article_obj, "AR_Design", "") - logger.info(f" Prix Sage: {prix_sage}€") - - if prix_sage == 0: - logger.warning( - f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" - ) - - ligne_persist = factory_lignes.Create() - - try: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentLigne3" - ) - except Exception: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentVenteLigne3" - ) - - quantite = float(ligne_data["quantite"]) - - try: - ligne_obj.SetDefaultArticleReference( - ligne_data["article_code"], quantite - ) - logger.info( - " Article associé via SetDefaultArticleReference" - ) - except Exception as e: - logger.warning( - f"SetDefaultArticleReference échoué: {e}, tentative avec objet" - ) - try: - ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(" Article associé via SetDefaultArticle") - except Exception: - logger.error(" Toutes les méthodes ont échoué") - ligne_obj.DL_Design = ( - designation_sage - or ligne_data.get("designation", "") - ) - ligne_obj.DL_Qte = quantite - logger.warning("Configuration manuelle appliquée") - - prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - logger.info(f" Prix auto chargé: {prix_auto}€") - - prix_a_utiliser = ligne_data.get("prix_unitaire_ht") - - if prix_a_utiliser is not None and prix_a_utiliser > 0: - ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) - logger.info(f" Prix personnalisé: {prix_a_utiliser}€") - elif prix_auto == 0 and prix_sage > 0: - ligne_obj.DL_PrixUnitaire = float(prix_sage) - logger.info(f" Prix Sage forcé: {prix_sage}€") - elif prix_auto > 0: - logger.info(f" Prix auto conservé: {prix_auto}€") - - prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - montant_ligne = quantite * prix_final - logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") - - remise = ligne_data.get("remise_pourcentage", 0) - if remise > 0: - try: - ligne_obj.DL_Remise01REM_Valeur = float(remise) - ligne_obj.DL_Remise01REM_Type = 0 - montant_apres_remise = montant_ligne * ( - 1 - remise / 100 - ) - logger.info( - f" Remise {remise}% → {montant_apres_remise}€" - ) - except Exception as e: - logger.warning(f"Remise non appliquée: {e}") - - ligne_obj.Write() - logger.info(f" Ligne {idx} écrite") - - doc.Write() - process.Process() - - if transaction_active: - self.cial.CptaApplication.CommitTrans() - - time.sleep(2) - - numero_avoir = None - try: - doc_result = process.DocumentResult - if doc_result: - doc_result = win32com.client.CastTo( - doc_result, "IBODocumentVente3" - ) - doc_result.Read() - numero_avoir = getattr(doc_result, "DO_Piece", "") - except Exception: - pass - - if not numero_avoir: - numero_avoir = getattr(doc, "DO_Piece", "") - - factory_doc = self.cial.FactoryDocumentVente - persist_reread = factory_doc.ReadPiece( - settings.SAGE_TYPE_BON_AVOIR, numero_avoir - ) - - if persist_reread: - doc_final = win32com.client.CastTo( - persist_reread, "IBODocumentVente3" - ) - doc_final.Read() - - total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) - total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) - reference_finale = getattr(doc_final, "DO_Ref", "") - - date_livraison_final = None - - try: - date_livr = getattr(doc_final, "DO_DateLivr", None) - if date_livr: - date_livraison_final = date_livr.strftime("%Y-%m-%d") - except Exception: - pass - else: - total_ht = 0.0 - total_ttc = 0.0 - reference_finale = avoir_data.get("reference", "") - date_livraison_final = avoir_data.get("date_livraison") - - logger.info(f" AVOIR CRÉÉ: {numero_avoir} - {total_ttc}€ TTC ") - - if date_livraison_final: - logger.info(f" Date livraison: {date_livraison_final}") - - return { - "numero_avoir": numero_avoir, - "total_ht": total_ht, - "total_ttc": total_ttc, - "nb_lignes": len(avoir_data["lignes"]), - "client_code": avoir_data["client"]["code"], - "date_avoir": str( - normaliser_date(avoir_data.get("date_avoir")) - ), - "date_livraison": date_livraison_final, - "reference": reference_finale, - } - - except Exception: - if transaction_active: - try: - self.cial.CptaApplication.RollbackTrans() - except Exception: - pass - raise - - except Exception as e: - logger.error(f" Erreur création avoir: {e}", exc_info=True) - raise RuntimeError(f"Échec création avoir: {str(e)}") + """Crée un avoir""" + return _creer_document_vente_unifie(avoir_data, TypeDocumentVente.AVOIR) def modifier_avoir(self, numero: str, avoir_data: Dict) -> Dict: if not self.cial: @@ -7012,7 +6012,7 @@ class SageConnector: ligne_obj.DL_Remise01REM_Valeur = float( ligne_data["remise_pourcentage"] ) - ligne_obj.DL_Remise01REM_Type = 0 + # ligne_obj.DL_Remise01REM_Type = 0 except Exception: pass @@ -7161,318 +6161,8 @@ class SageConnector: raise RuntimeError(f"Erreur Sage: {error_message}") def creer_facture_enrichi(self, facture_data: dict) -> Dict: - if not self.cial: - raise RuntimeError("Connexion Sage non établie") - - logger.info( - f" Début création facture pour client {facture_data['client']['code']}" - ) - - try: - with self._com_context(), self._lock_com: - transaction_active = False - try: - self.cial.CptaApplication.BeginTrans() - transaction_active = True - logger.debug(" Transaction Sage démarrée") - except Exception: - pass - - try: - process = self.cial.CreateProcess_Document( - settings.SAGE_TYPE_FACTURE - ) - doc = process.Document - - try: - doc = win32com.client.CastTo(doc, "IBODocumentVente3") - except Exception: - pass - - logger.info(" Document facture créé") - - doc.DO_Date = pywintypes.Time( - normaliser_date(facture_data.get("date_facture")) - ) - - if ( - "date_livraison" in facture_data - and facture_data["date_livraison"] - ): - doc.DO_DateLivr = pywintypes.Time( - normaliser_date(facture_data["date_livraison"]) - ) - logger.info( - f" Date livraison: {facture_data['date_livraison']}" - ) - - factory_client = self.cial.CptaApplication.FactoryClient - persist_client = factory_client.ReadNumero( - facture_data["client"]["code"] - ) - - if not persist_client: - raise ValueError( - f"Client {facture_data['client']['code']} introuvable" - ) - - client_obj = _cast_client(persist_client) - if not client_obj: - raise ValueError("Impossible de charger le client") - - doc.SetDefaultClient(client_obj) - doc.Write() - logger.info(f" Client {facture_data['client']['code']} associé") - - if facture_data.get("reference"): - try: - doc.DO_Ref = facture_data["reference"] - logger.info(f" Référence: {facture_data['reference']}") - except Exception: - pass - - logger.info(" Configuration champs spécifiques factures...") - - try: - if hasattr(doc, "DO_CodeJournal"): - try: - param_societe = ( - self.cial.CptaApplication.ParametreSociete - ) - journal_defaut = getattr( - param_societe, "P_CodeJournalVte", "VTE" - ) - doc.DO_CodeJournal = journal_defaut - logger.info(f" Code journal: {journal_defaut}") - except Exception: - doc.DO_CodeJournal = "VTE" - logger.info(" Code journal: VTE (défaut)") - except Exception as e: - logger.debug(f" Code journal: {e}") - - try: - if hasattr(doc, "DO_Souche"): - doc.DO_Souche = 0 - logger.debug(" Souche: 0 (défaut)") - except Exception: - pass - - try: - if hasattr(doc, "DO_Regime"): - doc.DO_Regime = 0 - logger.debug(" Régime: 0 (défaut)") - except Exception: - pass - - try: - factory_lignes = doc.FactoryDocumentLigne - except Exception: - factory_lignes = doc.FactoryDocumentVenteLigne - - factory_article = self.cial.FactoryArticle - - logger.info(f" Ajout de {len(facture_data['lignes'])} lignes...") - - for idx, ligne_data in enumerate(facture_data["lignes"], 1): - logger.info( - f"--- Ligne {idx}: {ligne_data['article_code']} ---" - ) - - persist_article = factory_article.ReadReference( - ligne_data["article_code"] - ) - - if not persist_article: - raise ValueError( - f" Article {ligne_data['article_code']} introuvable dans Sage" - ) - - article_obj = win32com.client.CastTo( - persist_article, "IBOArticle3" - ) - article_obj.Read() - - prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) - designation_sage = getattr(article_obj, "AR_Design", "") - logger.info(f" Prix Sage: {prix_sage}€") - - if prix_sage == 0: - logger.warning( - f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)" - ) - - ligne_persist = factory_lignes.Create() - - try: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentLigne3" - ) - except Exception: - ligne_obj = win32com.client.CastTo( - ligne_persist, "IBODocumentVenteLigne3" - ) - - quantite = float(ligne_data["quantite"]) - - try: - ligne_obj.SetDefaultArticleReference( - ligne_data["article_code"], quantite - ) - logger.info( - " Article associé via SetDefaultArticleReference" - ) - except Exception as e: - logger.warning( - f"SetDefaultArticleReference échoué: {e}, tentative avec objet" - ) - try: - ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(" Article associé via SetDefaultArticle") - except Exception: - logger.error(" Toutes les méthodes ont échoué") - ligne_obj.DL_Design = ( - designation_sage - or ligne_data.get("designation", "") - ) - ligne_obj.DL_Qte = quantite - logger.warning("Configuration manuelle appliquée") - - prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - logger.info(f" Prix auto chargé: {prix_auto}€") - - prix_a_utiliser = ligne_data.get("prix_unitaire_ht") - - if prix_a_utiliser is not None and prix_a_utiliser > 0: - ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser) - logger.info(f" Prix personnalisé: {prix_a_utiliser}€") - elif prix_auto == 0 and prix_sage > 0: - ligne_obj.DL_PrixUnitaire = float(prix_sage) - logger.info(f" Prix Sage forcé: {prix_sage}€") - elif prix_auto > 0: - logger.info(f" Prix auto conservé: {prix_auto}€") - - prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) - montant_ligne = quantite * prix_final - logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€") - - remise = ligne_data.get("remise_pourcentage", 0) - if remise > 0: - try: - ligne_obj.DL_Remise01REM_Valeur = float(remise) - ligne_obj.DL_Remise01REM_Type = 0 - montant_apres_remise = montant_ligne * ( - 1 - remise / 100 - ) - logger.info( - f" Remise {remise}% → {montant_apres_remise}€" - ) - except Exception as e: - logger.warning(f"Remise non appliquée: {e}") - - ligne_obj.Write() - logger.info(f" Ligne {idx} écrite") - - logger.info(" Validation facture...") - - try: - doc.SetClient(client_obj) - logger.debug(" Client réassocié avant validation") - except Exception: - try: - doc.SetDefaultClient(client_obj) - except Exception: - pass - - doc.Write() - - logger.info(" Process()...") - process.Process() - - if transaction_active: - self.cial.CptaApplication.CommitTrans() - logger.info(" Transaction committée") - - time.sleep(2) - - numero_facture = None - try: - doc_result = process.DocumentResult - if doc_result: - doc_result = win32com.client.CastTo( - doc_result, "IBODocumentVente3" - ) - doc_result.Read() - numero_facture = getattr(doc_result, "DO_Piece", "") - except Exception: - pass - - if not numero_facture: - numero_facture = getattr(doc, "DO_Piece", "") - - if not numero_facture: - raise RuntimeError("Numéro facture vide après création") - - logger.info(f" Numéro facture: {numero_facture}") - - factory_doc = self.cial.FactoryDocumentVente - persist_reread = factory_doc.ReadPiece( - settings.SAGE_TYPE_FACTURE, numero_facture - ) - - if persist_reread: - doc_final = win32com.client.CastTo( - persist_reread, "IBODocumentVente3" - ) - doc_final.Read() - - total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) - total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) - reference_finale = getattr(doc_final, "DO_Ref", "") - - date_livraison_final = None - - try: - date_livr = getattr(doc_final, "DO_DateLivr", None) - if date_livr: - date_livraison_final = date_livr.strftime("%Y-%m-%d") - except Exception: - pass - else: - total_ht = 0.0 - total_ttc = 0.0 - reference_finale = facture_data.get("reference", "") - date_livraison_final = facture_data.get("date_livraison") - - logger.info(f" FACTURE CRÉÉE: {numero_facture} - {total_ttc}€ TTC ") - - if date_livraison_final: - logger.info(f" Date livraison: {date_livraison_final}") - - return { - "numero_facture": numero_facture, - "total_ht": total_ht, - "total_ttc": total_ttc, - "nb_lignes": len(facture_data["lignes"]), - "client_code": facture_data["client"]["code"], - "date_facture": str( - normaliser_date(facture_data.get("date_facture")) - ), - "date_livraison": date_livraison_final, - "reference": reference_finale, - } - - except Exception: - if transaction_active: - try: - self.cial.CptaApplication.RollbackTrans() - logger.error(" Transaction annulée (rollback)") - except Exception: - pass - raise - - except Exception as e: - logger.error(f" Erreur création facture: {e}", exc_info=True) - raise RuntimeError(f"Échec création facture: {str(e)}") + """Crée une facture""" + return _creer_document_vente_unifie(facture_data, TypeDocumentVente.FACTURE) def modifier_facture(self, numero: str, facture_data: Dict) -> Dict: if not self.cial: @@ -7788,7 +6478,7 @@ class SageConnector: ligne_obj.DL_Remise01REM_Valeur = float( ligne_data["remise_pourcentage"] ) - ligne_obj.DL_Remise01REM_Type = 0 + # ligne_obj.DL_Remise01REM_Type = 0 except Exception: pass @@ -8516,7 +7206,7 @@ class SageConnector: ) conn.commit() logger.info( - f" [SQL] Stocks mini/maxi mis à jour" + " [SQL] Stocks mini/maxi mis à jour" ) else: cursor.execute( @@ -8539,7 +7229,7 @@ class SageConnector: ), ) conn.commit() - logger.info(f" [SQL] Ligne stock créée") + logger.info(" [SQL] Ligne stock créée") except Exception as e: logger.error(f"[STOCK] Erreur SQL : {e}") diff --git a/schemas/documents/doc_config.py b/schemas/documents/doc_config.py new file mode 100644 index 0000000..3f8c30b --- /dev/null +++ b/schemas/documents/doc_config.py @@ -0,0 +1,79 @@ +from typing import Optional +from enum import Enum +from config import settings + + +class TypeDocumentVente(Enum): + """Types de documents de vente supportés""" + + DEVIS = 0 + COMMANDE = 1 + LIVRAISON = 3 + FACTURE = 6 + AVOIR = 5 + + +class ConfigDocument: + """Configuration spécifique pour chaque type de document""" + + def __init__(self, type_doc: TypeDocumentVente): + self.type_doc = type_doc + self.type_sage = self._get_type_sage() + self.champ_date_principale = self._get_champ_date() + self.champ_numero = self._get_champ_numero() + self.nom_document = self._get_nom_document() + self.champ_date_secondaire = self._get_champ_date_secondaire() + + def _get_type_sage(self) -> int: + mapping = { + TypeDocumentVente.DEVIS: settings.SAGE_TYPE_DEVIS, + TypeDocumentVente.COMMANDE: settings.SAGE_TYPE_BON_COMMANDE, + TypeDocumentVente.LIVRAISON: settings.SAGE_TYPE_BON_LIVRAISON, + TypeDocumentVente.FACTURE: settings.SAGE_TYPE_FACTURE, + TypeDocumentVente.AVOIR: settings.SAGE_TYPE_BON_AVOIR, + } + return mapping[self.type_doc] + + def _get_champ_date(self) -> str: + """Retourne le nom du champ principal dans les données""" + mapping = { + TypeDocumentVente.DEVIS: "date_devis", + TypeDocumentVente.COMMANDE: "date_commande", + TypeDocumentVente.LIVRAISON: "date_livraison", + TypeDocumentVente.FACTURE: "date_facture", + TypeDocumentVente.AVOIR: "date_avoir", + } + return mapping[self.type_doc] + + def _get_champ_date_secondaire(self) -> Optional[str]: + """Retourne le nom du champ secondaire (date de livraison, etc.)""" + mapping = { + TypeDocumentVente.DEVIS: "date_livraison", + TypeDocumentVente.COMMANDE: "date_livraison", + TypeDocumentVente.LIVRAISON: "date_livraison_prevue", + TypeDocumentVente.FACTURE: "date_livraison", + TypeDocumentVente.AVOIR: "date_livraison", + } + return mapping.get(self.type_doc) + + def _get_champ_numero(self) -> str: + """Retourne le nom du champ pour le numéro de document dans le résultat""" + mapping = { + TypeDocumentVente.DEVIS: "numero_devis", + TypeDocumentVente.COMMANDE: "numero_commande", + TypeDocumentVente.LIVRAISON: "numero_livraison", + TypeDocumentVente.FACTURE: "numero_facture", + TypeDocumentVente.AVOIR: "numero_avoir", + } + return mapping[self.type_doc] + + def _get_nom_document(self) -> str: + """Retourne le nom du document pour les logs""" + mapping = { + TypeDocumentVente.DEVIS: "devis", + TypeDocumentVente.COMMANDE: "commande", + TypeDocumentVente.LIVRAISON: "livraison", + TypeDocumentVente.FACTURE: "facture", + TypeDocumentVente.AVOIR: "avoir", + } + return mapping[self.type_doc] diff --git a/utils/functions/data/__init__.py b/utils/functions/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/functions/data/create_doc.py b/utils/functions/data/create_doc.py new file mode 100644 index 0000000..adbba9f --- /dev/null +++ b/utils/functions/data/create_doc.py @@ -0,0 +1,392 @@ +from typing import Dict +import win32com.client +import pywintypes +import time +from schemas.documents.doc_config import TypeDocumentVente, ConfigDocument +import logging +from utils.functions.functions import normaliser_date +from utils.tiers.clients.clients_data import _cast_client + + +logger = logging.getLogger(__name__) + + +def _creer_document_vente_unifie( + self, doc_data: dict, type_document: TypeDocumentVente +) -> Dict: + if not self.cial: + raise RuntimeError("Connexion Sage non établie") + + config = ConfigDocument(type_document) + logger.info( + f"📝 Début création {config.nom_document} pour client {doc_data['client']['code']}" + ) + + try: + with self._com_context(), self._lock_com: + transaction_active = False + + try: + # Démarrage transaction + try: + self.cial.CptaApplication.BeginTrans() + transaction_active = True + logger.debug("✓ Transaction Sage démarrée") + except Exception as e: + logger.warning(f"BeginTrans échoué (non critique): {e}") + + # Création du document + process = self.cial.CreateProcess_Document(config.type_sage) + doc = process.Document + + try: + doc = win32com.client.CastTo(doc, "IBODocumentVente3") + except Exception: + pass + + logger.info(f"✓ Document {config.nom_document} créé") + + # ===== DATES ===== + doc.DO_Date = pywintypes.Time( + normaliser_date(doc_data.get(config.champ_date_principale)) + ) + + # Date secondaire (livraison, etc.) + if config.champ_date_secondaire and doc_data.get( + config.champ_date_secondaire + ): + doc.DO_DateLivr = pywintypes.Time( + normaliser_date(doc_data[config.champ_date_secondaire]) + ) + logger.info( + f"✓ {config.champ_date_secondaire}: {doc_data[config.champ_date_secondaire]}" + ) + + # ===== CLIENT ===== + factory_client = self.cial.CptaApplication.FactoryClient + persist_client = factory_client.ReadNumero(doc_data["client"]["code"]) + + if not persist_client: + raise ValueError(f"Client {doc_data['client']['code']} introuvable") + + client_obj = _cast_client(persist_client) + if not client_obj: + raise ValueError("Impossible de charger le client") + + doc.SetDefaultClient(client_obj) + doc.Write() + logger.info(f"✓ Client {doc_data['client']['code']} associé") + + # ===== RÉFÉRENCE ===== + if doc_data.get("reference"): + try: + doc.DO_Ref = doc_data["reference"] + logger.info(f"✓ Référence: {doc_data['reference']}") + except Exception as e: + logger.warning(f"Référence non définie: {e}") + + # ===== CONFIGURATION SPÉCIFIQUE FACTURE ===== + if type_document == TypeDocumentVente.FACTURE: + _configurer_facture(self, doc) + + # ===== FACTORY LIGNES ===== + try: + factory_lignes = doc.FactoryDocumentLigne + except Exception: + factory_lignes = doc.FactoryDocumentVenteLigne + + factory_article = self.cial.FactoryArticle + + logger.info(f"📦 Ajout de {len(doc_data['lignes'])} lignes...") + + # ===== TRAITEMENT DES LIGNES ===== + for idx, ligne_data in enumerate(doc_data["lignes"], 1): + _ajouter_ligne_document( + cial=self.cial, + factory_lignes=factory_lignes, + factory_article=factory_article, + ligne_data=ligne_data, + idx=idx, + doc=doc, + ) + + # ===== VALIDATION ===== + logger.info("💾 Validation du document...") + + # Pour les factures, réassocier le client avant validation + if type_document == TypeDocumentVente.FACTURE: + try: + doc.SetClient(client_obj) + logger.debug(" ↳ Client réassocié avant validation") + except Exception: + try: + doc.SetDefaultClient(client_obj) + except Exception: + pass + + doc.Write() + + # Process() sauf pour devis en brouillon + if type_document != TypeDocumentVente.DEVIS: + process.Process() + logger.info("✓ Process() appelé") + else: + try: + process.Process() + logger.info("✓ Process() appelé (devis)") + except Exception: + logger.debug(" ↳ Process() ignoré pour devis brouillon") + + # Commit transaction + if transaction_active: + try: + self.cial.CptaApplication.CommitTrans() + logger.info("✓ Transaction committée") + except Exception: + pass + + time.sleep(2) + + # ===== RÉCUPÉRATION DU NUMÉRO ===== + numero_document = _recuperer_numero_document(process, doc) + + if not numero_document: + raise RuntimeError( + f"Numéro {config.nom_document} vide après création" + ) + + logger.info(f"📄 Numéro: {numero_document}") + + # ===== RELECTURE POUR TOTAUX ===== + doc_final_data = _relire_document_final( + self, + config=config, + numero_document=numero_document, + doc_data=doc_data, + ) + + logger.info( + f"✅ {config.nom_document.upper()} CRÉÉ: " + f"{numero_document} - {doc_final_data['total_ttc']}€ TTC" + ) + + return doc_final_data + + except Exception: + if transaction_active: + try: + self.cial.CptaApplication.RollbackTrans() + logger.error(" Transaction annulée (rollback)") + except Exception: + pass + raise + + except Exception as e: + logger.error( + f" ERREUR CRÉATION {config.nom_document.upper()}: {e}", exc_info=True + ) + raise RuntimeError(f"Échec création {config.nom_document}: {str(e)}") + + +def _appliquer_remise_ligne(ligne_obj, remise_pourcent: float) -> bool: + """Applique la remise via FromString - SOLUTION FINALE""" + try: + import pythoncom + + dispatch = ligne_obj._oleobj_ + + # 1. Récupérer l'objet Remise + dispid = dispatch.GetIDsOfNames(0, "Remise") + remise_obj = dispatch.Invoke(dispid, 0, pythoncom.DISPATCH_PROPERTYGET, 1) + remise_wrapper = win32com.client.Dispatch(remise_obj) + + # 2. Définir la remise via FromString + remise_wrapper.FromString(f"{remise_pourcent}%") + + # 3. Calcul (optionnel mais recommandé) + try: + remise_wrapper.Calcul() + except: + pass + + # 4. Write la ligne + ligne_obj.Write() + + logger.info(f" ✅ Remise {remise_pourcent}% appliquée") + return True + + except Exception as e: + logger.error(f" ❌ Erreur remise: {e}") + return False + + +def _ajouter_ligne_document( + cial, factory_lignes, factory_article, ligne_data: dict, idx: int, doc +) -> None: + """VERSION FINALE AVEC REMISES FONCTIONNELLES""" + logger.info(f" ├─ Ligne {idx}: {ligne_data['article_code']}") + + # ===== CRÉATION LIGNE ===== + persist_article = factory_article.ReadReference(ligne_data["article_code"]) + if not persist_article: + raise ValueError(f"Article {ligne_data['article_code']} introuvable") + + article_obj = win32com.client.CastTo(persist_article, "IBOArticle3") + article_obj.Read() + + prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0)) + designation_sage = getattr(article_obj, "AR_Design", "") + + ligne_persist = factory_lignes.Create() + try: + ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3") + except: + ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3") + + quantite = float(ligne_data["quantite"]) + + # ===== ASSOCIATION ARTICLE ===== + try: + ligne_obj.SetDefaultArticleReference(ligne_data["article_code"], quantite) + except: + try: + ligne_obj.SetDefaultArticle(article_obj, quantite) + except: + ligne_obj.DL_Design = designation_sage + ligne_obj.DL_Qte = quantite + + # ===== PRIX ===== + prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0)) + prix_perso = ligne_data.get("prix_unitaire_ht") + + if prix_perso and prix_perso > 0: + ligne_obj.DL_PrixUnitaire = float(prix_perso) + elif prix_auto == 0 and prix_sage > 0: + ligne_obj.DL_PrixUnitaire = float(prix_sage) + + prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0)) + logger.info(f" 💰 Prix: {prix_final}€") + + # ===== WRITE INITIAL ===== + ligne_obj.Write() + + # ===== APPLICATION REMISE (TOUTES LES LIGNES!) ===== + remise = ligne_data.get("remise_pourcentage", 0) + if remise and remise > 0: + logger.info(f" 🎯 Application remise {remise}%...") + _appliquer_remise_ligne(ligne_obj, remise) + + logger.info(f" ✓ Ligne {idx} terminée") + + +def _configurer_facture(self, doc) -> None: + """Configuration spécifique pour les factures""" + logger.info(" 🔧 Configuration spécifique facture...") + + try: + if hasattr(doc, "DO_CodeJournal"): + try: + param_societe = self.cial.CptaApplication.ParametreSociete + journal_defaut = getattr(param_societe, "P_CodeJournalVte", "VTE") + doc.DO_CodeJournal = journal_defaut + logger.debug(f" ✓ Code journal: {journal_defaut}") + except Exception: + doc.DO_CodeJournal = "VTE" + logger.debug(" ✓ Code journal: VTE (défaut)") + except Exception as e: + logger.debug(f" Code journal: {e}") + + try: + if hasattr(doc, "DO_Souche"): + doc.DO_Souche = 0 + logger.debug(" ✓ Souche: 0") + except Exception: + pass + + try: + if hasattr(doc, "DO_Regime"): + doc.DO_Regime = 0 + logger.debug(" ✓ Régime: 0") + except Exception: + pass + + +def _recuperer_numero_document(process, doc) -> str: + """Récupère le numéro du document créé""" + numero = None + + try: + doc_result = process.DocumentResult + if doc_result: + doc_result = win32com.client.CastTo(doc_result, "IBODocumentVente3") + doc_result.Read() + numero = getattr(doc_result, "DO_Piece", "") + except Exception: + pass + + if not numero: + numero = getattr(doc, "DO_Piece", "") + + return numero + + +def _relire_document_final( + self, config: ConfigDocument, numero_document: str, doc_data: dict +) -> Dict: + """ + Relit le document pour obtenir les totaux calculés par Sage + """ + factory_doc = self.cial.FactoryDocumentVente + persist_reread = factory_doc.ReadPiece(config.type_sage, numero_document) + + if persist_reread: + doc_final = win32com.client.CastTo(persist_reread, "IBODocumentVente3") + doc_final.Read() + + total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0)) + total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) + reference_finale = getattr(doc_final, "DO_Ref", "") + + # Date secondaire + date_secondaire_value = None + if config.champ_date_secondaire: + try: + date_livr = getattr(doc_final, "DO_DateLivr", None) + if date_livr: + date_secondaire_value = date_livr.strftime("%Y-%m-%d") + except Exception: + pass + else: + # Valeurs par défaut si relecture échoue + total_ht = 0.0 + total_ttc = 0.0 + reference_finale = doc_data.get("reference", "") + date_secondaire_value = doc_data.get(config.champ_date_secondaire) + + # Construction du résultat + resultat = { + config.champ_numero: numero_document, + "total_ht": total_ht, + "total_ttc": total_ttc, + "nb_lignes": len(doc_data["lignes"]), + "client_code": doc_data["client"]["code"], + config.champ_date_principale: str( + normaliser_date(doc_data.get(config.champ_date_principale)) + ), + "reference": reference_finale, + } + + # Ajout date secondaire si applicable + if config.champ_date_secondaire: + resultat[config.champ_date_secondaire] = date_secondaire_value + + return resultat + + +__all__ = [ + "_creer_document_vente_unifie", + "_ajouter_ligne_document", + "_configurer_facture", + "_recuperer_numero_document", + "_relire_document_final", +] diff --git a/utils/functions/functions.py b/utils/functions/functions.py index 553074c..4cc5e12 100644 --- a/utils/functions/functions.py +++ b/utils/functions/functions.py @@ -98,12 +98,12 @@ def _normaliser_type_document(type_doc: int) -> int: return type_doc mapping_normalisation = { - 1: 10, - 2: 20, - 3: 30, - 4: 40, - 5: 50, - 6: 60, + 1: 10, + 2: 20, + 3: 30, + 4: 40, + 5: 50, + 6: 60, } return mapping_normalisation.get(type_doc, type_doc)