diff --git a/.gitignore b/.gitignore index 711a83c..5f33a57 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ htmlcov/ dist/ -cleaner.py \ No newline at end of file +*clean*.py \ No newline at end of file diff --git a/main.py b/main.py index b6fc307..8a07ea4 100644 --- a/main.py +++ b/main.py @@ -1680,7 +1680,6 @@ def get_tous_journaux(): try: journaux = sage.lire_tous_journaux() - # Grouper par type par_type = {} for j in journaux: t = j["type_libelle"] @@ -1745,7 +1744,6 @@ def introspection_com(): } with sage._com_context(), sage._lock_com: - # Attributs de cial try: for attr in dir(sage.cial): if not attr.startswith("_"): @@ -1753,7 +1751,6 @@ def introspection_com(): except Exception as e: resultats["cial_error"] = str(e) - # Attributs de BaseCpta try: base_cpta = sage.cial.BaseCpta for attr in dir(base_cpta): @@ -1762,14 +1759,12 @@ def introspection_com(): except Exception as e: resultats["base_cpta_error"] = str(e) - # Attributs de ParametreDossier try: param = sage.cial.BaseCpta.ParametreDossier for attr in dir(param): if not attr.startswith("_"): resultats["param_dossier_attributes"].append(attr) - # Tester spécifiquement les attributs logo possibles resultats["logo_tests"] = {} for logo_attr in [ "Logo", @@ -1842,7 +1837,6 @@ def get_tous_reglements( raise HTTPException(500, str(e)) -# Route: Détail d'un règlement @app.get("/sage/reglements/facture/{facture_no}", dependencies=[Depends(verify_token)]) def get_reglement_facture_detail(facture_no): try: diff --git a/sage_connector.py b/sage_connector.py index a7ea568..9533c84 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -1003,12 +1003,10 @@ class SageConnector: with self._get_sql_connection() as conn: cursor = conn.cursor() - # === DÉTECTION DES COLONNES === logger.info(f"[SQL] Lecture article {reference}...") cursor.execute("SELECT TOP 1 * FROM F_ARTICLE") colonnes_disponibles = [column[0] for column in cursor.description] - # Configuration du mapping complet colonnes_config = { "AR_Ref": "reference", "AR_Design": "designation", @@ -1090,7 +1088,6 @@ class SageConnector: "AR_Exclure": "exclure", } - # Sélection des colonnes disponibles colonnes_a_lire = [ col_sql for col_sql in colonnes_config.keys() @@ -1101,7 +1098,6 @@ class SageConnector: logger.error("[SQL] Aucune colonne mappée trouvée !") return None - # Construction de la requête SQL avec échappement des noms de colonnes colonnes_sql = [] for col in colonnes_a_lire: if " " in col or "/" in col or "è" in col: @@ -1120,7 +1116,6 @@ class SageConnector: logger.info(f"[SQL] Article {reference} non trouvé") return None - # Construction du dictionnaire row_data row_data = {} for idx, col_sql in enumerate(colonnes_a_lire): valeur = row[idx] @@ -1128,10 +1123,8 @@ class SageConnector: valeur = valeur.strip() row_data[col_sql] = valeur - # Mapping de l'article article = _mapper_article_depuis_row(row_data, colonnes_config) - # Enrichissements articles = [ article ] # Liste d'un seul article pour les fonctions d'enrichissement @@ -1876,7 +1869,6 @@ class SageConnector: persist_tiers = None type_tiers = None - # Tentative 1 : Client try: logger.info(" Recherche dans Clients...") persist_tiers = factory_client.ReadNumero(numero_client) @@ -1886,7 +1878,6 @@ class SageConnector: except Exception as e: logger.debug(f" Pas trouvé comme Client: {e}") - # Tentative 2 : Fournisseur (si pas trouvé comme client) if not persist_tiers: try: logger.info(" Recherche dans Fournisseurs...") @@ -1897,7 +1888,6 @@ class SageConnector: except Exception as e: logger.debug(f" Pas trouvé comme Fournisseur: {e}") - # Vérification finale if not persist_tiers: raise ValueError( f"Le tiers '{numero_client}' est introuvable dans Sage 100c. " @@ -1905,7 +1895,6 @@ class SageConnector: f"(Client ou Fournisseur)." ) - # Cast et lecture try: client_obj = win32com.client.CastTo(persist_tiers, "IBOClient3") client_obj.Read() @@ -2348,7 +2337,6 @@ class SageConnector: persist_tiers = None type_tiers = None - # Tentative 1 : Client try: logger.info(" Recherche dans Clients...") persist_tiers = factory_client.ReadNumero(numero) @@ -2358,7 +2346,6 @@ class SageConnector: except Exception as e: logger.debug(f" Pas trouvé comme Client: {e}") - # Tentative 2 : Fournisseur (si pas trouvé comme client) if not persist_tiers: try: logger.info(" Recherche dans Fournisseurs...") @@ -2369,7 +2356,6 @@ class SageConnector: except Exception as e: logger.debug(f" Pas trouvé comme Fournisseur: {e}") - # Vérification finale if not persist_tiers: raise ValueError( f"Le tiers '{numero}' est introuvable dans Sage 100c. " @@ -2377,7 +2363,6 @@ class SageConnector: f"(Client ou Fournisseur)." ) - # Cast et lecture try: client_obj = win32com.client.CastTo(persist_tiers, "IBOClient3") client_obj.Read() @@ -3719,7 +3704,6 @@ class SageConnector: f" [DEBUG] Méthodes disponibles sur client: {methodes_client}" ) - # Chercher spécifiquement les méthodes de verrouillage lock_methods = [ m for m in methodes_client @@ -3736,7 +3720,6 @@ class SageConnector: for attempt in range(max_retries): try: - # Approche 1: ReadLock (méthode préférée) if hasattr(client, "ReadLock"): client.ReadLock() locked = True @@ -3744,7 +3727,6 @@ class SageConnector: logger.info(" Verrouillage via ReadLock() [OK]") break - # Approche 2: Lock elif hasattr(client, "Lock"): client.Lock() locked = True @@ -3752,7 +3734,6 @@ class SageConnector: logger.info(" Verrouillage via Lock() [OK]") break - # Approche 3: LockRecord elif hasattr(client, "LockRecord"): client.LockRecord() locked = True @@ -3760,7 +3741,6 @@ class SageConnector: logger.info(" Verrouillage via LockRecord() [OK]") break - # Approche 4: Read avec paramètre mode écriture else: try: client.Read(1) # 1 = mode écriture @@ -3796,7 +3776,6 @@ class SageConnector: "Vérifiez qu'il n'est pas ouvert dans Sage ou par un autre processus." ) else: - # Autre erreur, propager raise logger.info( @@ -4366,7 +4345,6 @@ class SageConnector: if not champs_modifies: logger.warning("Aucun champ à modifier") - # Déverrouiller si nécessaire if locked: try: if hasattr(client, "ReadUnlock"): @@ -4399,7 +4377,6 @@ class SageConnector: logger.error(f"[ERREUR] {error_detail}") raise RuntimeError(f"Echec Write(): {error_detail}") finally: - # Toujours déverrouiller après Write (succès ou échec) if locked: try: if hasattr(client, "ReadUnlock"): @@ -4414,7 +4391,6 @@ class SageConnector: except Exception as unlock_err: logger.warning(f" Déverrouillage ignoré: {unlock_err}") - # Relire après Write pour retourner les données à jour client.Read() logger.info("=" * 80) @@ -4478,7 +4454,6 @@ class SageConnector: try: logger.info("[ARTICLE] === CREATION ARTICLE ===") - # === Validation données === valide, erreur = valider_donnees_creation(article_data) if not valide: raise ValueError(erreur) @@ -4492,7 +4467,6 @@ class SageConnector: logger.debug(f"BeginTrans non disponible : {e}") try: - # === Découverte dépôts === depots_disponibles = [] depot_a_utiliser = None depot_code_demande = article_data.get("depot_code") @@ -4556,7 +4530,6 @@ class SageConnector: f"[DEPOT] Utilisation : '{depot_a_utiliser['code']}' ({depot_a_utiliser['intitule']})" ) - # === Extraction et validation des données === reference = article_data.get("reference", "").upper().strip() designation = article_data.get("designation", "").strip() if len(designation) > 69: @@ -4569,7 +4542,6 @@ class SageConnector: logger.info(f"[ARTICLE] Référence : {reference}") logger.info(f"[ARTICLE] Désignation : {designation}") - # === Vérifier si article existe === factory = self.cial.FactoryArticle try: article_existant = factory.ReadReference(reference) @@ -4583,7 +4555,6 @@ class SageConnector: ): raise - # === Créer l'article === persist = factory.Create() article = win32com.client.CastTo(persist, "IBOArticle3") article.SetDefault() @@ -4591,7 +4562,6 @@ class SageConnector: article.AR_Ref = reference article.AR_Design = designation - # === Recherche article modèle === logger.info("[MODELE] Recherche article modèle...") article_modele_ref = None article_modele = None @@ -4634,7 +4604,6 @@ class SageConnector: "Aucun article modèle trouvé. Créez au moins un article dans Sage." ) - # === Copie Unite depuis modèle === logger.info("[UNITE] Copie Unite depuis modèle...") unite_trouvee = False try: @@ -4651,7 +4620,6 @@ class SageConnector: "Impossible de copier l'unité depuis le modèle" ) - # === Gestion famille === famille_trouvee = False famille_code_personnalise = article_data.get("famille") @@ -4736,7 +4704,6 @@ class SageConnector: except Exception as e: logger.debug(f" Famille non copiable : {e}") - # === Champs obligatoires depuis modèle === logger.info("[CHAMPS] Copie champs obligatoires...") article.AR_Type = int(getattr(article_modele, "AR_Type", 0)) article.AR_Nature = int(getattr(article_modele, "AR_Nature", 0)) @@ -4744,12 +4711,10 @@ class SageConnector: article.AR_SuiviStock = 2 logger.info(" [OK] Champs de base copiés (AR_SuiviStock=2)") - # === Application des champs fournis === logger.info("[CHAMPS] Application champs fournis...") champs_appliques = [] champs_echoues = [] - # Prix de vente if "prix_vente" in article_data: try: article.AR_PrixVen = float(article_data["prix_vente"]) @@ -4760,7 +4725,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"prix_vente: {e}") - # Prix d'achat if "prix_achat" in article_data: try: article.AR_PrixAchat = float(article_data["prix_achat"]) @@ -4771,7 +4735,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"prix_achat: {e}") - # Coefficient if "coef" in article_data: try: article.AR_Coef = float(article_data["coef"]) @@ -4780,7 +4743,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"coef: {e}") - # Code EAN if "code_ean" in article_data: try: article.AR_CodeBarre = str(article_data["code_ean"]) @@ -4789,7 +4751,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"code_ean: {e}") - # Description -> AR_Langue1 if "description" in article_data: try: article.AR_Langue1 = str(article_data["description"])[:255] @@ -4798,7 +4759,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"description: {e}") - # Pays if "pays" in article_data: try: article.AR_Pays = str(article_data["pays"])[:3].upper() @@ -4807,7 +4767,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"pays: {e}") - # Garantie if "garantie" in article_data: try: article.AR_Garantie = int(article_data["garantie"]) @@ -4816,7 +4775,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"garantie: {e}") - # Délai if "delai" in article_data: try: article.AR_Delai = int(article_data["delai"]) @@ -4825,7 +4783,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"delai: {e}") - # Poids net if "poids_net" in article_data: try: article.AR_PoidsNet = float(article_data["poids_net"]) @@ -4834,7 +4791,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"poids_net: {e}") - # Poids brut if "poids_brut" in article_data: try: article.AR_PoidsBrut = float(article_data["poids_brut"]) @@ -4845,7 +4801,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"poids_brut: {e}") - # Code fiscal if "code_fiscal" in article_data: try: article.AR_CodeFiscal = str(article_data["code_fiscal"])[ @@ -4858,7 +4813,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"code_fiscal: {e}") - # Soumis escompte if "soumis_escompte" in article_data: try: article.AR_Escompte = ( @@ -4871,7 +4825,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"soumis_escompte: {e}") - # Publié if "publie" in article_data: try: article.AR_Publie = 1 if article_data["publie"] else 0 @@ -4880,7 +4833,6 @@ class SageConnector: except Exception as e: champs_echoues.append(f"publie: {e}") - # En sommeil if "en_sommeil" in article_data: try: article.AR_Sommeil = 1 if article_data["en_sommeil"] else 0 @@ -4900,7 +4852,6 @@ class SageConnector: f"[CHAMPS] Appliqués : {len(champs_appliques)} / Échoués : {len(champs_echoues)}" ) - # === Écriture dans Sage === logger.info("[ARTICLE] Écriture dans Sage...") try: article.Write() @@ -4916,7 +4867,6 @@ class SageConnector: logger.error(f" [ERREUR] Write() : {error_detail}") raise RuntimeError(f"Échec création : {error_detail}") - # === Statistiques (AR_Stat après Write) === stats_a_definir = [] for i in range(1, 6): stat_key = f"stat_0{i}" @@ -4938,7 +4888,6 @@ class SageConnector: except Exception as e: logger.warning(f" ⚠ Statistiques : {e}") - # === Commit transaction === if transaction_active: try: self.cial.CptaApplication.CommitTrans() @@ -4946,7 +4895,6 @@ class SageConnector: except Exception as e: logger.warning(f"[COMMIT] Erreur : {e}") - # === Gestion stocks === stock_defini = False has_stock_values = stock_reel or stock_mini or stock_maxi @@ -4955,7 +4903,6 @@ class SageConnector: f"[STOCK] Définition stock (dépôt '{depot_a_utiliser['code']}')..." ) - # Méthode 1 : Créer via COM if stock_reel: try: depot_obj = depot_a_utiliser["objet"] @@ -5004,7 +4951,6 @@ class SageConnector: except Exception as e: logger.warning(f" [WARN] Stock COM : {e}") - # Méthode 2 : Mise à jour SQL si COM échoue ou pour mini/maxi seulement if (stock_mini or stock_maxi) and not stock_defini: try: with self._get_sql_connection() as conn: @@ -5079,13 +5025,11 @@ class SageConnector: except Exception as e: logger.error(f"[STOCK] Erreur SQL : {e}") - # === Construction réponse depuis SQL === logger.info("[RESPONSE] Construction réponse depuis SQL...") try: with self._get_sql_connection() as conn: cursor = conn.cursor() - # Lecture complète article cursor.execute( """ SELECT @@ -5161,7 +5105,6 @@ class SageConnector: else None, } - # Lecture stocks cursor.execute( """ SELECT s.AS_QteSto, s.AS_QteMini, s.AS_QteMaxi, s.AS_QteRes, s.AS_QteCom @@ -5212,7 +5155,6 @@ class SageConnector: except Exception as e: logger.warning(f"[RESPONSE] Erreur lecture SQL : {e}") - # Fallback sur extraction COM si SQL échoue logger.info("[FALLBACK] Extraction COM...") article_cree_persist = factory.ReadReference(reference) if not article_cree_persist: @@ -5227,7 +5169,6 @@ class SageConnector: if not resultat: resultat = {"reference": reference, "designation": designation} - # Forcer les valeurs connues for key in [ "prix_vente", "prix_achat", @@ -5296,7 +5237,6 @@ class SageConnector: champs_modifies = [] champs_echoues = [] - # === Gestion famille === if "famille" in article_data and article_data["famille"]: famille_code_demande = article_data["famille"].upper().strip() logger.info(f"[FAMILLE] Changement : {famille_code_demande}") @@ -5369,7 +5309,6 @@ class SageConnector: logger.error(f" [ERREUR] Famille : {e}") champs_echoues.append(f"famille: {e}") - # === Traitement explicite des champs === if "designation" in article_data: try: designation = str(article_data["designation"])[:69].strip() @@ -5511,7 +5450,6 @@ class SageConnector: logger.info(f"[ARTICLE] Champs modifiés : {', '.join(champs_modifies)}") logger.info("[ARTICLE] Écriture...") - # === Écriture COM === try: article.Write() logger.info("[ARTICLE] Write() réussi") @@ -5528,7 +5466,6 @@ class SageConnector: logger.error(f"[ARTICLE] Erreur Write() : {error_detail}") raise RuntimeError(f"Échec modification : {error_detail}") - # === Statistiques (AR_Stat après Write) === stats_a_modifier = [] for i in range(1, 6): stat_key = f"stat_0{i}" @@ -5555,7 +5492,6 @@ class SageConnector: except Exception as e: logger.warning(f" ⚠ Statistiques : {e}") - # === Gestion stocks mini/maxi via SQL === if "stock_mini" in article_data or "stock_maxi" in article_data: try: with self._get_sql_connection() as conn: @@ -5595,13 +5531,11 @@ class SageConnector: logger.error(f"[STOCK] Erreur SQL : {e}") champs_echoues.append(f"stocks: {e}") - # === Construction réponse depuis SQL === logger.info("[RESPONSE] Construction réponse depuis SQL...") try: with self._get_sql_connection() as conn: cursor = conn.cursor() - # Lecture complète article cursor.execute( """ SELECT @@ -5667,7 +5601,6 @@ class SageConnector: "stat_05": _safe_strip(row[24]) if row[24] else None, } - # Lecture stocks cursor.execute( """ SELECT s.AS_QteSto, s.AS_QteMini, s.AS_QteMaxi, s.AS_QteRes, s.AS_QteCom @@ -5718,7 +5651,6 @@ class SageConnector: except Exception as e: logger.warning(f"[RESPONSE] Erreur lecture SQL : {e}") - # Fallback sur extraction COM article.Read() logger.info( f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)" @@ -7495,7 +7427,6 @@ class SageConnector: cursor.execute(query, params) rows = cursor.fetchall() - # ⚠️⚠️⚠️ VÉRIFIE CETTE LIGNE ⚠️⚠️⚠️ collaborateurs = [collaborators_to_dict(row) for row in rows] logger.info(f"✓ SQL: {len(collaborateurs)} collaborateurs") @@ -7530,7 +7461,6 @@ class SageConnector: if not row: return None - # ⚠️ UTILISER LA FONCTION DE CLASSE EXISTANTE collaborateur = collaborators_to_dict(row) logger.info( @@ -7547,7 +7477,6 @@ class SageConnector: if not self.cial: raise RuntimeError("Connexion Sage non établie") - # Validation préalable if not data.get("nom"): raise ValueError("Le champ 'nom' est obligatoire") @@ -7560,7 +7489,6 @@ class SageConnector: try: with self._com_context(), self._lock_com: - # ===== VÉRIFICATION DOUBLON VIA SQL ===== logger.info("🔍 Vérification doublon...") with self._get_sql_connection() as conn: cursor = conn.cursor() @@ -7575,7 +7503,6 @@ class SageConnector: ) logger.info("✓ Pas de doublon") - # ===== FACTORY + CREATE ===== try: factory = self.cial.FactoryCollaborateur except AttributeError: @@ -7583,7 +7510,6 @@ class SageConnector: persist = factory.Create() - # Cast vers interface collab = None for iface in [ "IBOCollaborateur3", @@ -7599,14 +7525,12 @@ class SageConnector: if not collab: collab = persist - # ===== SETDEFAULT ===== try: collab.SetDefault() logger.info("✓ SetDefault()") except Exception as e: logger.warning(f"SetDefault() ignoré: {e}") - # ===== HELPER ===== def safe_set(obj, attr, value, max_len=None): """Affecte une valeur de manière sécurisée""" if value is None or value == "": @@ -7622,13 +7546,10 @@ class SageConnector: logger.warning(f" ✗ {attr}: {e}") return False - # ===== CHAMPS DIRECTS SUR COLLABORATEUR ===== logger.info("📝 Champs directs...") - # Obligatoire safe_set(collab, "Nom", nom_upper, 35) - # Optionnels safe_set(collab, "Prenom", prenom, 35) safe_set(collab, "Fonction", data.get("fonction"), 35) safe_set(collab, "Service", data.get("service"), 35) @@ -7637,7 +7558,6 @@ class SageConnector: safe_set(collab, "LinkedIn", data.get("linkedin"), 35) safe_set(collab, "Skype", data.get("skype"), 35) - # ===== SOUS-OBJET ADRESSE ===== logger.info("📍 Adresse...") try: adresse_obj = collab.Adresse @@ -7650,7 +7570,6 @@ class SageConnector: except Exception as e: logger.warning(f"⚠️ Erreur Adresse: {e}") - # ===== SOUS-OBJET TELECOM ===== logger.info("📞 Telecom...") try: telecom_obj = collab.Telecom @@ -7661,7 +7580,6 @@ class SageConnector: except Exception as e: logger.warning(f"⚠️ Erreur Telecom: {e}") - # ===== CHAMPS BOOLÉENS (seulement si True) ===== logger.info("🔘 Booléens...") if data.get("vendeur") is True: try: @@ -7690,7 +7608,6 @@ class SageConnector: except Exception: pass - # ===== WRITE ===== logger.info("💾 Write()...") try: collab.Write() @@ -7699,10 +7616,8 @@ class SageConnector: logger.error(f" Write() échoué: {e}") raise RuntimeError(f"Échec Write(): {e}") - # ===== RÉCUPÉRATION DU NUMÉRO ===== numero_cree = None - # Via Read() try: collab.Read() for attr in ["No", "CO_No", "Numero"]: @@ -7716,7 +7631,6 @@ class SageConnector: except Exception: pass - # Via SQL si pas trouvé if not numero_cree: try: with self._get_sql_connection() as conn: @@ -7737,7 +7651,6 @@ class SageConnector: ) logger.info(f"{'=' * 70}") - # Retourner le collaborateur if numero_cree: return self.lire_collaborateur(numero_cree) else: @@ -7761,13 +7674,11 @@ class SageConnector: try: with self._com_context(), self._lock_com: - # ===== LECTURE DU COLLABORATEUR EXISTANT ===== try: factory = self.cial.FactoryCollaborateur except AttributeError: factory = self.cial.CptaApplication.FactoryCollaborateur - # Lire par numéro try: persist = factory.ReadNumero(numero) except Exception as e: @@ -7776,7 +7687,6 @@ class SageConnector: if not persist: raise ValueError(f"Collaborateur {numero} introuvable") - # Cast vers interface collab = None for iface in [ "IBOCollaborateur3", @@ -7792,14 +7702,12 @@ class SageConnector: if not collab: collab = persist - # Charger les données actuelles try: collab.Read() logger.info(f"✓ Collaborateur {numero} chargé") except Exception as e: logger.warning(f"Read() ignoré: {e}") - # ===== HELPER ===== def safe_set(obj, attr, value, max_len=None): """Affecte une valeur de manière sécurisée""" if value is None: @@ -7817,7 +7725,6 @@ class SageConnector: champs_modifies = [] - # ===== CHAMPS DIRECTS SUR COLLABORATEUR ===== logger.info("📝 Champs directs...") champs_directs = { @@ -7834,13 +7741,11 @@ class SageConnector: for py_field, (sage_attr, max_len) in champs_directs.items(): if py_field in data: val = data[py_field] - # Cas spécial: nom en majuscules if py_field == "nom" and val: val = str(val).upper().strip() if safe_set(collab, sage_attr, val, max_len): champs_modifies.append(sage_attr) - # ===== SOUS-OBJET ADRESSE ===== logger.info("📍 Adresse...") try: adresse_obj = collab.Adresse @@ -7864,7 +7769,6 @@ class SageConnector: except Exception as e: logger.warning(f"⚠️ Erreur accès Adresse: {e}") - # ===== SOUS-OBJET TELECOM ===== logger.info("📞 Telecom...") try: telecom_obj = collab.Telecom @@ -7886,7 +7790,6 @@ class SageConnector: except Exception as e: logger.warning(f"⚠️ Erreur accès Telecom: {e}") - # ===== CHAMPS BOOLÉENS ===== logger.info("🔘 Booléens...") champs_bool = { @@ -7907,7 +7810,6 @@ class SageConnector: except Exception as e: logger.warning(f" ✗ {sage_attr}: {e}") - # ===== VÉRIFICATION ===== if not champs_modifies: logger.info("ℹ️ Aucun champ à modifier") return self.lire_collaborateur(numero) @@ -7916,7 +7818,6 @@ class SageConnector: f"📋 {len(champs_modifies)} champ(s) à modifier: {champs_modifies}" ) - # ===== WRITE ===== logger.info("💾 Write()...") try: collab.Write() @@ -7925,7 +7826,6 @@ class SageConnector: logger.error(f" Write() échoué: {e}") raise RuntimeError(f"Échec Write(): {e}") - # ===== RETOUR ===== logger.info(f"\n{'=' * 70}") logger.info(f" COLLABORATEUR MODIFIÉ: N°{numero}") logger.info(f"{'=' * 70}") @@ -7952,7 +7852,6 @@ class SageConnector: societe = society_to_dict(row) societe["exercices"] = build_exercices(row) - # Stocker le numéro de dossier pour la recherche du logo self._numero_dossier = societe.get("numero_dossier") add_logo(societe) diff --git a/utils/articles/articles_data_sql.py b/utils/articles/articles_data_sql.py index fbaa083..f59cb77 100644 --- a/utils/articles/articles_data_sql.py +++ b/utils/articles/articles_data_sql.py @@ -948,7 +948,6 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: return val - # === CHAMPS DE BASE === article["reference"] = get_val("AR_Ref", convert_type=str) article["designation"] = get_val("AR_Design", convert_type=str) article["code_ean"] = get_val("AR_CodeBarre", convert_type=str) @@ -956,7 +955,6 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["edi_code"] = get_val("AR_EdiCode", convert_type=str) article["raccourci"] = get_val("AR_Raccourci", convert_type=str) - # === PRIX === article["prix_vente"] = get_val("AR_PrixVen", 0.0, float) article["prix_achat"] = get_val("AR_PrixAch", 0.0, float) article["coef"] = get_val("AR_Coef", 0.0, float) @@ -970,44 +968,35 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["cout_standard"] = get_val("AR_CoutStd", 0.0, float) - # === UNITÉS ET POIDS (avec normalisation string) === article["unite_vente"] = normalize_string_field(get_val("AR_UniteVen")) article["unite_poids"] = normalize_string_field(get_val("AR_UnitePoids")) article["poids_net"] = get_val("AR_PoidsNet", 0.0, float) article["poids_brut"] = get_val("AR_PoidsBrut", 0.0, float) - # === GAMMES (avec normalisation string) === article["gamme_1"] = normalize_string_field(get_val("AR_Gamme1")) article["gamme_2"] = normalize_string_field(get_val("AR_Gamme2")) - # === TYPE ARTICLE (avec libellé) === type_val = get_val("AR_Type", 0, int) article["type_article"] = type_val article["type_article_libelle"] = TypeArticle.get_label(type_val) - # === FAMILLE === article["famille_code"] = get_val("FA_CodeFamille", convert_type=str) - # === NATURE ET GARANTIE === article["nature"] = get_val("AR_Nature", 0, int) article["garantie"] = get_val("AR_Garantie", 0, int) article["code_fiscal"] = normalize_string_field(get_val("AR_CodeFiscal")) article["pays"] = normalize_string_field(get_val("AR_Pays")) - # === FOURNISSEUR === article["fournisseur_principal"] = get_val("CO_No", 0, int) - # === CONDITIONNEMENT (avec normalisation string) === article["conditionnement"] = normalize_string_field(get_val("AR_Condition")) article["nb_colis"] = get_val("AR_NbColis", 0, int) article["prevision"] = get_val("AR_Prevision", False, bool) - # === SUIVI STOCK (avec libellé) === suivi_stock_val = normalize_enum_to_int(get_val("AR_SuiviStock")) article["suivi_stock"] = suivi_stock_val article["suivi_stock_libelle"] = SuiviStockType.get_label(suivi_stock_val) - # === NOMENCLATURE (avec libellé) === nomenclature_val = normalize_enum_to_int(get_val("AR_Nomencl")) article["nomenclature"] = nomenclature_val article["nomenclature_libelle"] = NomenclatureType.get_label(nomenclature_val) @@ -1015,7 +1004,6 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["qte_composant"] = get_val("AR_QteComp", 0.0, float) article["qte_operatoire"] = get_val("AR_QteOperatoire", 0.0, float) - # === STATUT ARTICLE === sommeil = get_val("AR_Sommeil", 0, int) article["est_actif"] = sommeil == 0 article["en_sommeil"] = sommeil == 1 @@ -1023,7 +1011,6 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["soumis_escompte"] = get_val("AR_Escompte", False, bool) article["delai"] = get_val("AR_Delai", 0, int) - # === STATISTIQUES === article["stat_01"] = get_val("AR_Stat01", convert_type=str) article["stat_02"] = get_val("AR_Stat02", convert_type=str) article["stat_03"] = get_val("AR_Stat03", convert_type=str) @@ -1031,17 +1018,14 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["stat_05"] = get_val("AR_Stat05", convert_type=str) article["hors_statistique"] = get_val("AR_HorsStat", False, bool) - # === CATÉGORIES COMPTABLES === article["categorie_1"] = get_val("CL_No1", 0, int) article["categorie_2"] = get_val("CL_No2", 0, int) article["categorie_3"] = get_val("CL_No3", 0, int) article["categorie_4"] = get_val("CL_No4", 0, int) - # === DATE MODIFICATION === date_modif = get_val("AR_DateModif") article["date_modification"] = str(date_modif) if date_modif else None - # === PARAMÈTRES DE VENTE === article["vente_debit"] = get_val("AR_VteDebit", False, bool) article["non_imprimable"] = get_val("AR_NotImp", False, bool) article["transfere"] = get_val("AR_Transfere", False, bool) @@ -1054,7 +1038,6 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["sous_traitance"] = get_val("AR_SousTraitance", False, bool) article["criticite"] = get_val("AR_Criticite", 0, int) - # === PARAMÈTRES DE PRODUCTION === article["reprise_code_defaut"] = normalize_string_field(get_val("RP_CodeDefaut")) article["delai_fabrication"] = get_val("AR_DelaiFabrication", 0, int) article["delai_peremption"] = get_val("AR_DelaiPeremption", 0, int) @@ -1062,12 +1045,10 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["type_lancement"] = get_val("AR_TypeLancement", 0, int) article["cycle"] = get_val("AR_Cycle", 1, int) - # === MÉDIA ET LANGUES === article["photo"] = get_val("AR_Photo", convert_type=str) article["langue_1"] = get_val("AR_Langue1", convert_type=str) article["langue_2"] = get_val("AR_Langue2", convert_type=str) - # === FRAIS === article["frais_01_denomination"] = get_val( "AR_Frais01FR_Denomination", convert_type=str ) @@ -1078,7 +1059,6 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: "AR_Frais03FR_Denomination", convert_type=str ) - # === CHAMPS PERSONNALISÉS === article["marque_commerciale"] = get_val("Marque commerciale", convert_type=str) objectif_val = get_val("Objectif / Qtés vendues") @@ -1103,7 +1083,6 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["interdire_commande"] = get_val("AR_InterdireCommande", False, bool) article["exclure"] = get_val("AR_Exclure", False, bool) - # === INITIALISATION DES CHAMPS DE STOCK (remplis par enrichissement) === article["stock_reel"] = 0.0 article["stock_mini"] = 0.0 article["stock_maxi"] = 0.0 @@ -1111,7 +1090,6 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["stock_commande"] = 0.0 article["stock_disponible"] = 0.0 - # === INITIALISATION DES CHAMPS DE FAMILLE (remplis par enrichissement) === article["famille_libelle"] = None article["famille_type"] = None article["famille_unite_vente"] = None @@ -1128,7 +1106,6 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["famille_hors_stat"] = None article["famille_pays"] = None - # === INITIALISATION DES CHAMPS FOURNISSEUR/TVA (remplis par enrichissement) === article["fournisseur_nom"] = None article["tva_code"] = None article["tva_taux"] = None @@ -1257,7 +1234,6 @@ def enrichir_fournisseurs_articles(articles: List[Dict], cursor) -> List[Dict]: nb_enrichis = 0 for article in articles: num_fourn = article.get("fournisseur_principal") - # Convertir en string pour correspondre au fournisseur_map num_fourn_str = ( str(num_fourn).strip() if num_fourn not in (None, "", " ") else None ) diff --git a/utils/documents/settle.py b/utils/documents/settle.py index becfe8d..7e01e00 100644 --- a/utils/documents/settle.py +++ b/utils/documents/settle.py @@ -13,7 +13,6 @@ def _get_journal_auto(self, mode_reglement: int) -> str: with self._get_sql_connection() as conn: cursor = conn.cursor() - # Mode Espèces = 2 (selon l'image Sage fournie) if mode_reglement == 2: # Espèces cursor.execute(""" SELECT TOP 1 JO_Num @@ -25,7 +24,6 @@ def _get_journal_auto(self, mode_reglement: int) -> str: ORDER BY JO_Num """) else: - # Autres modes → Banque cursor.execute(""" SELECT TOP 1 JO_Num FROM F_JOURNAUX @@ -40,7 +38,6 @@ def _get_journal_auto(self, mode_reglement: int) -> str: if row: return row[0].strip() - # Fallback: premier journal de trésorerie disponible cursor.execute(""" SELECT TOP 1 JO_Num FROM F_JOURNAUX @@ -74,14 +71,12 @@ def _valider_coherence_journal_mode(self, code_journal: str, mode_reglement: int compte_general = (row[0] or "").strip() - # Mode Espèces (2) doit utiliser un journal caisse (53x) if mode_reglement == 2: if not compte_general.startswith("53"): logger.warning( f"Mode Espèces avec journal non-caisse ({code_journal}, compte {compte_general})" ) else: - # Autres modes doivent utiliser un journal banque (51x) if compte_general.startswith("53"): logger.warning( f"Mode non-espèces avec journal caisse ({code_journal}, compte {compte_general})" @@ -105,7 +100,6 @@ def _get_mode_reglement_libelle(self, mode_reglement: int) -> str: if row: return (row[0] or "").strip() - # Fallback sur les libellés standards libelles = { 0: "Chèque", 1: "Virement", @@ -319,7 +313,6 @@ def lire_tous_reglements( facture = _format_facture(row, echeances) - # Filtrer par statut si demandé if statut_reglement: reste = facture["montants"]["reste_a_regler"] montant_regle = facture["montants"]["montant_regle"] @@ -509,8 +502,6 @@ def lire_facture_reglement_detail(self, do_piece: str) -> Dict: cursor, do_domaine, do_type, do_piece, montant_ttc ) - # Utiliser la même structure que lire_tous_reglements - # mais avec les infos complètes du client types_doc = {6: "Facture", 7: "Avoir"} montant_regle = sum(e["montant_regle"] for e in echeances) reste_a_regler = montant_ttc - montant_regle @@ -607,7 +598,6 @@ def lire_reglement_detail(self, rg_no: int) -> Dict: if not row: raise ValueError(f"Règlement {rg_no} introuvable") - # Récupérer les imputations cursor.execute( """ SELECT @@ -766,11 +756,9 @@ def regler_facture( date_reglement = date_reglement or datetime.now() - # Déduction automatique du journal si non fourni if not code_journal: code_journal = _get_journal_auto(self, mode_reglement) else: - # Valider la cohérence journal/mode _valider_coherence_journal_mode(self, code_journal, mode_reglement) logger.info( @@ -801,7 +789,6 @@ def regler_facture( f"Montant ({montant}€) supérieur au solde ({solde_actuel:.2f}€)" ) - # Récupérer le client client_code = "" try: client_obj = getattr(doc, "Client", None) @@ -811,12 +798,10 @@ def regler_facture( except Exception: pass - # Récupérer l'échéance echeance = _get_premiere_echeance(doc) if not echeance: raise ValueError(f"Facture {numero_facture} sans échéance") - # Exécuter le règlement numero_reglement = _executer_reglement_com( self, doc=doc, @@ -847,7 +832,6 @@ def regler_facture( nouveau_solde = total_ttc - nouveau_montant_regle logger.info(f"Règlement effectué - Solde restant: {nouveau_solde:.2f}€") - # Récupérer le libellé du mode règlement mode_libelle = _get_mode_reglement_libelle(self, mode_reglement) return { @@ -916,17 +900,14 @@ def _executer_reglement_com( ): erreurs = [] - # APPROCHE PRINCIPALE: Créer règlement complet, l'écrire, puis l'assigner au process try: logger.info("Création du règlement via FactoryDocumentReglement...") - # 1. Créer le règlement factory_reg = self.cial.FactoryDocumentReglement reg = factory_reg.Create() reg = win32com.client.CastTo(reg, "IBODocumentReglement") logger.info(" Règlement créé et casté vers IBODocumentReglement") - # 2. Configurer le Journal (objet) try: journal_factory = self.cial.CptaApplication.FactoryJournal journal_persist = journal_factory.ReadNumero(code_journal) @@ -936,7 +917,6 @@ def _executer_reglement_com( except Exception as e: logger.warning(f" Journal: {e}") - # 3. Configurer le TiersPayeur (objet client) try: factory_client = self.cial.CptaApplication.FactoryClient if client_code: @@ -947,7 +927,6 @@ def _executer_reglement_com( except Exception as e: logger.warning(f" TiersPayeur: {e}") - # 4. Configurer les champs simples try: reg.RG_Date = pywintypes.Time(date_reglement) logger.info(f" RG_Date: {date_reglement}") @@ -960,7 +939,6 @@ def _executer_reglement_com( except Exception as e: logger.warning(f" RG_Montant: {e}") - # 5. Mode de règlement via l'objet Reglement try: mode_factory = getattr( self.cial.CptaApplication, "FactoryModeReglement", None @@ -973,7 +951,6 @@ def _executer_reglement_com( except Exception as e: logger.debug(f" Mode règlement via factory: {e}") - # 6. Devise if devise_code != 0: try: reg.RG_Devise = devise_code @@ -987,7 +964,6 @@ def _executer_reglement_com( except Exception as e: logger.debug(f" RG_Cours: {e}") - # Montant en devise try: montant_devise = montant * cours_devise reg.RG_MontantDev = montant_devise @@ -995,7 +971,6 @@ def _executer_reglement_com( except Exception as e: logger.debug(f" RG_MontantDev: {e}") - # 7. TVA sur encaissement if tva_encaissement: try: reg.RG_Encaissement = 1 @@ -1003,7 +978,6 @@ def _executer_reglement_com( except Exception as e: logger.debug(f" RG_Encaissement: {e}") - # 8. Compte général spécifique if compte_general: try: cg_factory = self.cial.CptaApplication.FactoryCompteG @@ -1014,7 +988,6 @@ def _executer_reglement_com( except Exception as e: logger.debug(f" CompteG: {e}") - # 9. Référence et libellé if reference: try: reg.RG_Reference = reference @@ -1039,12 +1012,10 @@ def _executer_reglement_com( except Exception: pass - # 10. ÉCRIRE le règlement reg.Write() numero = getattr(reg, "RG_Piece", None) logger.info(f" Règlement écrit avec numéro: {numero}") - # 11. Créer le lien règlement-échéance via la factory DU RÈGLEMENT try: logger.info(" Création du lien règlement-échéance...") factory_reg_ech = getattr(reg, "FactoryDocumentReglementEcheance", None) @@ -1052,7 +1023,6 @@ def _executer_reglement_com( if factory_reg_ech: reg_ech = factory_reg_ech.Create() - # Cast vers IBODocumentReglementEcheance for iface in [ "IBODocumentReglementEcheance3", "IBODocumentReglementEcheance", @@ -1064,21 +1034,18 @@ def _executer_reglement_com( except Exception: continue - # Définir l'échéance try: reg_ech.Echeance = echeance logger.info(" Echeance définie") except Exception as e: logger.warning(f" Echeance: {e}") - # Définir le montant try: reg_ech.RC_Montant = montant logger.info(f" RC_Montant: {montant}") except Exception as e: logger.warning(f" RC_Montant: {e}") - # Écrire le lien try: reg_ech.SetDefault() except Exception: @@ -1093,16 +1060,13 @@ def _executer_reglement_com( erreurs.append(f"Lien échéance: {e}") logger.warning(f" Erreur création lien: {e}") - # Si le lien a échoué, essayer via le process logger.info(" Tentative via CreateProcess_ReglerEcheances...") try: process = self.cial.CreateProcess_ReglerEcheances() - # Assigner le règlement déjà écrit process.Reglement = reg logger.info(" Règlement assigné au process") - # Ajouter l'échéance try: process.AddDocumentEcheanceMontant(echeance, montant) logger.info(" Échéance ajoutée avec montant") @@ -1134,7 +1098,6 @@ def _executer_reglement_com( erreurs.append(f"FactoryDocumentReglement: {e}") logger.error(f"FactoryDocumentReglement échoué: {e}") - # APPROCHE ALTERNATIVE: Via le mode règlement de l'échéance try: logger.info("Tentative via modification directe de l'échéance...") @@ -1165,7 +1128,6 @@ def _executer_reglement_com( new_reg = factory_reg.Create() new_reg = win32com.client.CastTo(new_reg, "IBODocumentReglement") - # Configurer journal_factory = self.cial.CptaApplication.FactoryJournal journal_persist = journal_factory.ReadNumero(code_journal) if journal_persist: @@ -1181,7 +1143,6 @@ def _executer_reglement_com( new_reg.RG_Montant = montant new_reg.RG_Impute = 1 - # Devise si non EUR if devise_code != 0: try: new_reg.RG_Devise = devise_code @@ -1190,14 +1151,12 @@ def _executer_reglement_com( except Exception: pass - # TVA encaissement if tva_encaissement: try: new_reg.RG_Encaissement = 1 except Exception: pass - # Compte général if compte_general: try: cg_factory = self.cial.CptaApplication.FactoryCompteG @@ -1239,7 +1198,6 @@ def introspecter_reglement(self): result = {} try: with self._com_context(), self._lock_com: - # IBODocumentReglement et sa factory de liens try: factory = self.cial.FactoryDocumentReglement reg = factory.Create() @@ -1248,7 +1206,6 @@ def introspecter_reglement(self): a for a in dir(reg) if not a.startswith("_") ] - # FactoryDocumentReglementEcheance depuis le règlement factory_lien = getattr(reg, "FactoryDocumentReglementEcheance", None) if factory_lien: lien = factory_lien.Create() @@ -1271,14 +1228,12 @@ def introspecter_reglement(self): except Exception as e: result["error_reglement"] = str(e) - # Process try: process = self.cial.CreateProcess_ReglerEcheances() result["Process"] = [a for a in dir(process) if not a.startswith("_")] except Exception as e: result["error_process"] = str(e) - # Échéance et ses attributs try: factory_doc = self.cial.FactoryDocumentVente doc_list = factory_doc.List @@ -1306,7 +1261,6 @@ def introspecter_reglement(self): if not a.startswith("_") ] - # FactoryDocumentReglementEcheance depuis l'échéance factory_lien_ech = getattr( ech, "FactoryDocumentReglementEcheance", @@ -1342,7 +1296,6 @@ def introspecter_reglement(self): except Exception: pass - # Reglement de l'échéance (mode) mode = getattr(ech, "Reglement", None) if mode: result["Echeance_Reglement_mode"] = [ @@ -1382,7 +1335,6 @@ def regler_factures_client( date_reglement = date_reglement or datetime.now() - # Déduction automatique du journal si non fourni if not code_journal: code_journal = _get_journal_auto(self, mode_reglement) @@ -1610,7 +1562,6 @@ def lire_devises(self) -> List[Dict]: with self._get_sql_connection() as conn: cursor = conn.cursor() - # Vérifier d'abord si F_DEVISE existe cursor.execute(""" SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'F_DEVISE' @@ -1640,17 +1591,14 @@ def lire_devises(self) -> List[Dict]: if devises: return devises - # Fallback: Lire depuis P_DOSSIER cursor.execute(""" SELECT N_DeviseCompte, N_DeviseEquival FROM P_DOSSIER """) row = cursor.fetchone() - # Devise par défaut basée sur la config dossier devise_principale = row[0] if row else 0 - # Retourner les devises standards devises_standards = [ { "code": 0, @@ -1709,7 +1657,6 @@ def lire_journaux_tresorerie(self) -> List[Dict]: journaux = [] for row in cursor.fetchall(): compte_general = (row[2] or "").strip() - # Déterminer le type basé sur le compte général if compte_general.startswith("53"): type_journal = "caisse" elif compte_general.startswith("51"): @@ -1735,7 +1682,6 @@ def lire_comptes_generaux( if not self.cial: raise RuntimeError("Connexion Sage non établie") - # Mapping type -> préfixes de comptes prefixes_map = { "client": ["411"], "fournisseur": ["401"], @@ -1763,7 +1709,6 @@ def lire_comptes_generaux( """ params = [] - # Appliquer les filtres if type_compte and type_compte in prefixes_map: prefixes = prefixes_map[type_compte] conditions = " OR ".join(["CG_Num LIKE ?" for _ in prefixes]) diff --git a/utils/documents/validations.py b/utils/documents/validations.py index 5588dd4..6264a2a 100644 --- a/utils/documents/validations.py +++ b/utils/documents/validations.py @@ -104,7 +104,6 @@ def introspecter_document_complet(connector, numero_facture: str) -> Dict: persist = factory.ReadPiece(60, numero_facture) - # 1. Attributs du persist brut persist_attrs = [a for a in dir(persist) if not a.startswith("_")] result["persist"]["all_attrs"] = persist_attrs result["persist"]["methods"] = [] @@ -127,7 +126,6 @@ def introspecter_document_complet(connector, numero_facture: str) -> Dict: {"name": attr, "error": str(e)[:50]} ) - # Chercher spécifiquement les attributs liés à validation/valide result["persist"]["validation_related"] = [ a for a in persist_attrs @@ -137,7 +135,6 @@ def introspecter_document_complet(connector, numero_facture: str) -> Dict: ) ] - # 2. IBODocumentVente3 try: doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() @@ -147,7 +144,6 @@ def introspecter_document_complet(connector, numero_facture: str) -> Dict: result["IBODocumentVente3"]["methods"] = [] result["IBODocumentVente3"]["properties_with_values"] = [] - # Lister les méthodes for attr in doc_attrs: try: val = getattr(doc, attr, None) @@ -156,7 +152,6 @@ def introspecter_document_complet(connector, numero_facture: str) -> Dict: except Exception: pass - # Chercher DO_* properties result["IBODocumentVente3"]["DO_properties"] = [] for attr in doc_attrs: if attr.startswith("DO_"): @@ -170,7 +165,6 @@ def introspecter_document_complet(connector, numero_facture: str) -> Dict: {"name": attr, "error": str(e)[:50]} ) - # Chercher les attributs liés à validation result["IBODocumentVente3"]["validation_related"] = [ a for a in doc_attrs @@ -183,7 +177,6 @@ def introspecter_document_complet(connector, numero_facture: str) -> Dict: except Exception as e: result["IBODocumentVente3"]["error"] = str(e) - # 3. IBODocument3 try: doc3 = win32com.client.CastTo(persist, "IBODocument3") doc3.Read() @@ -202,7 +195,6 @@ def introspecter_document_complet(connector, numero_facture: str) -> Dict: except Exception as e: result["IBODocument3"]["error"] = str(e) - # 4. IPMDocument try: pmdoc = win32com.client.CastTo(persist, "IPMDocument") @@ -215,7 +207,6 @@ def introspecter_document_complet(connector, numero_facture: str) -> Dict: except Exception as e: result["IPMDocument"]["error"] = str(e) - # 5. Chercher FactoryDocument* sur le document result["factories_on_doc"] = [] for attr in persist_attrs: if "Factory" in attr: @@ -235,13 +226,11 @@ def introspecter_validation(connector, numero_facture: str = None) -> Dict: try: with connector._com_context(), connector._lock_com: - # Tous les CreateProcess cial_attrs = [a for a in dir(connector.cial) if not a.startswith("_")] result["all_createprocess"] = [ a for a in cial_attrs if "CreateProcess" in a ] - # Explorer chaque process for process_name in result["all_createprocess"]: try: process = getattr(connector.cial, process_name)() @@ -255,7 +244,6 @@ def introspecter_validation(connector, numero_facture: str = None) -> Dict: except Exception as e: result[process_name] = {"error": str(e)} - # Introspection document si fourni if numero_facture: result["document"] = introspecter_document_complet( connector, numero_facture @@ -270,11 +258,9 @@ def introspecter_validation(connector, numero_facture: str = None) -> Dict: def valider_facture(connector, numero_facture: str) -> Dict: logger.info(f" Validation facture {numero_facture} (SQL direct)") - # Vérifications préalables with connector._get_sql_connection() as conn: cursor = conn.cursor() - # Vérifier que la facture existe et peut être validée cursor.execute( """ SELECT DO_Valide, DO_Statut, DO_TotalTTC, DO_MontantRegle @@ -296,7 +282,6 @@ def valider_facture(connector, numero_facture: str) -> Dict: if statut == 6: # Annulé raise ValueError("Facture annulée, validation impossible") - # Valider via SQL cursor.execute( """ UPDATE F_DOCENTETE @@ -308,7 +293,6 @@ def valider_facture(connector, numero_facture: str) -> Dict: conn.commit() - # Vérifier cursor.execute( """ SELECT DO_Valide, DO_Imprim @@ -400,7 +384,6 @@ def _valider_document_com(connector, numero_facture: str, valider: bool = True) if not persist: raise ValueError(f"Impossible de lire la facture {numero_facture}") - # APPROCHE 1: Accès direct à DO_Valide sur IBODocumentVente3 try: logger.info( " APPROCHE 1: Modification directe DO_Valide sur IBODocumentVente3..." @@ -408,15 +391,12 @@ def _valider_document_com(connector, numero_facture: str, valider: bool = True) doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() - # Vérifier la valeur actuelle valeur_avant = getattr(doc, "DO_Valide", None) logger.info(f" DO_Valide avant: {valeur_avant}") - # Tenter la modification doc.DO_Valide = valeur_cible doc.Write() - # Relire pour vérifier doc.Read() valeur_apres = getattr(doc, "DO_Valide", None) logger.info(f" DO_Valide après: {valeur_apres}") @@ -433,7 +413,6 @@ def _valider_document_com(connector, numero_facture: str, valider: bool = True) erreurs.append(f"IBODocumentVente3.DO_Valide: {e}") logger.warning(f" Erreur: {e}") - # APPROCHE 2: Via IBODocument3 (interface parent) try: logger.info(" APPROCHE 2: Via IBODocument3...") doc3 = win32com.client.CastTo(persist, "IBODocument3") @@ -450,7 +429,6 @@ def _valider_document_com(connector, numero_facture: str, valider: bool = True) except Exception as e: erreurs.append(f"IBODocument3: {e}") - # APPROCHE 3: Chercher un CreateProcess de validation try: logger.info(" APPROCHE 3: Recherche CreateProcess de validation...") cial_attrs = [a for a in dir(connector.cial) if "CreateProcess" in a] @@ -465,7 +443,6 @@ def _valider_document_com(connector, numero_facture: str, valider: bool = True) for proc_name in validation_processes: try: process = getattr(connector.cial, proc_name)() - # Lister les attributs du process proc_attrs = [a for a in dir(process) if not a.startswith("_")] logger.info(f" {proc_name} attrs: {proc_attrs}") @@ -483,7 +460,6 @@ def _valider_document_com(connector, numero_facture: str, valider: bool = True) except Exception as e: erreurs.append(f"CreateProcess: {e}") - # APPROCHE 4: WriteDefault avec paramètres try: logger.info(" APPROCHE 4: WriteDefault...") doc = win32com.client.CastTo(persist, "IBODocumentVente3") @@ -510,7 +486,6 @@ def explorer_toutes_interfaces_validation(connector, numero_facture: str) -> Dic factory = connector.cial.FactoryDocumentVente persist = factory.ReadPiece(60, numero_facture) - # Liste des interfaces à tester interfaces = [ "IBODocumentVente3", "IBODocument3", @@ -544,7 +519,6 @@ def explorer_toutes_interfaces_validation(connector, numero_facture: str) -> Dic props[names[0]] = { "memid": func_desc.memid, "invkind": func_desc.invkind, - # invkind: 1=METHOD, 2=GET, 4=PUT, 8=PUTREF "has_setter": (func_desc.invkind & 4) == 4, } @@ -556,7 +530,6 @@ def explorer_toutes_interfaces_validation(connector, numero_facture: str) -> Dic except Exception as e: result["interfaces"][iface_name] = {"error": str(e)[:100]} - # Explorer aussi FactoryDocumentVente pour des méthodes de validation try: factory_attrs = [a for a in dir(factory) if not a.startswith("_")] result["factory_methods"] = [ diff --git a/utils/functions/data/create_doc.py b/utils/functions/data/create_doc.py index 2cc2275..c64643e 100644 --- a/utils/functions/data/create_doc.py +++ b/utils/functions/data/create_doc.py @@ -27,7 +27,6 @@ def creer_document_vente( transaction_active = False try: - # Démarrage transaction try: self.cial.CptaApplication.BeginTrans() transaction_active = True @@ -35,7 +34,6 @@ def creer_document_vente( 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 @@ -46,13 +44,11 @@ def creer_document_vente( logger.info(f"✓ Document {config.nom_document} créé") - # ===== DATES ===== date_principale = normaliser_date( doc_data.get(config.champ_date_principale) ) doc.DO_Date = pywintypes.Time(date_principale) - # Heure - même datetime, Sage extrait la composante horaire try: doc.DO_Heure = pywintypes.Time(date_principale) logger.debug( @@ -61,7 +57,6 @@ def creer_document_vente( except Exception as e: logger.debug(f"DO_Heure non défini: {e}") - # Date secondaire (livraison, etc.) if config.champ_date_secondaire and doc_data.get( config.champ_date_secondaire ): @@ -72,7 +67,6 @@ def creer_document_vente( 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"]) @@ -87,7 +81,6 @@ def creer_document_vente( 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"] @@ -95,11 +88,9 @@ def creer_document_vente( 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: @@ -109,7 +100,6 @@ def creer_document_vente( 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, @@ -120,10 +110,8 @@ def creer_document_vente( 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) @@ -136,7 +124,6 @@ def creer_document_vente( doc.Write() - # Process() sauf pour devis en brouillon if type_document != TypeDocumentVente.DEVIS: process.Process() logger.info("✓ Process() appelé") @@ -147,7 +134,6 @@ def creer_document_vente( except Exception: logger.debug(" ↳ Process() ignoré pour devis brouillon") - # Commit transaction if transaction_active: try: self.cial.CptaApplication.CommitTrans() @@ -157,7 +143,6 @@ def creer_document_vente( time.sleep(2) - # ===== RÉCUPÉRATION DU NUMÉRO ===== numero_document = _recuperer_numero_document(process, doc) if not numero_document: @@ -167,7 +152,6 @@ def creer_document_vente( logger.info(f"📄 Numéro: {numero_document}") - # ===== RELECTURE POUR TOTAUX ===== doc_final_data = _relire_document_final( self, config=config, @@ -205,21 +189,17 @@ def _appliquer_remise_ligne(ligne_obj, remise_pourcent: float) -> bool: 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 Exception: pass - # 4. Write la ligne ligne_obj.Write() logger.info(f" Remise {remise_pourcent}% appliquée") @@ -236,7 +216,6 @@ def _ajouter_ligne_document( """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") @@ -255,7 +234,6 @@ def _ajouter_ligne_document( quantite = float(ligne_data["quantite"]) - # ===== ASSOCIATION ARTICLE ===== try: ligne_obj.SetDefaultArticleReference(ligne_data["article_code"], quantite) except Exception: @@ -265,7 +243,6 @@ def _ajouter_ligne_document( 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") @@ -277,10 +254,8 @@ def _ajouter_ligne_document( 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}%...") @@ -364,7 +339,6 @@ def _relire_document_final( total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0)) reference_finale = getattr(doc_final, "DO_Ref", "") - # Récupérer le client depuis le document Sage try: client_obj = getattr(doc_final, "Client", None) if client_obj: @@ -373,7 +347,6 @@ def _relire_document_final( except Exception: pass - # Date secondaire if config.champ_date_secondaire: try: date_livr = getattr(doc_final, "DO_DateLivr", None) @@ -382,17 +355,14 @@ def _relire_document_final( 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) - # Fallback pour le code client (priorité: Sage > fallback > doc_data) if not client_code: client_code = client_code_fallback or doc_data.get("client", {}).get("code", "") - # Construction du résultat resultat = { config.champ_numero: numero_document, "total_ht": total_ht, @@ -407,7 +377,6 @@ def _relire_document_final( "reference": reference_finale, } - # Ajout date secondaire si applicable if config.champ_date_secondaire: resultat[config.champ_date_secondaire] = date_secondaire_value @@ -447,7 +416,6 @@ def modifier_document_vente( doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() - # Vérifications statut_actuel = getattr(doc, "DO_Statut", 0) if statut_actuel == 5: @@ -506,7 +474,6 @@ def modifier_document_vente( logger.info(f" Référence: {modif_ref}") logger.info(f" Lignes: {modif_lignes}") - # Reporter référence et statut après les lignes doc_data_temp = doc_data.copy() reference_a_modifier = None statut_a_modifier = None @@ -567,7 +534,6 @@ def modifier_document_vente( elif modif_lignes: logger.info("🔄 REMPLACEMENT COMPLET DES LIGNES...") - # Dates if modif_date: doc.DO_Date = pywintypes.Time( normaliser_date(doc_data_temp.get(config.champ_date_principale)) @@ -580,7 +546,6 @@ def modifier_document_vente( ) champs_modifies.append(config.champ_date_secondaire) - # 🔥 CONFIGURATION SPÉCIFIQUE FACTURE (avant lignes) if type_document == TypeDocumentVente.FACTURE: _configurer_facture(self, doc) @@ -596,7 +561,6 @@ def modifier_document_vente( factory_article = self.cial.FactoryArticle - # Suppression lignes existantes if nb_lignes_initial > 0: logger.info(f" 🗑️ Suppression {nb_lignes_initial} lignes...") for idx in range(nb_lignes_initial, 0, -1): @@ -612,10 +576,8 @@ def modifier_document_vente( logger.warning(f" Ligne {idx}: {e}") logger.info(" ✓ Lignes supprimées") - # Ajout nouvelles lignes avec REMISES logger.info(f" ➕ Ajout {nb_nouvelles} lignes...") for idx, ligne_data in enumerate(nouvelles_lignes, 1): - # 🔥 UTILISE _ajouter_ligne_document qui applique les remises _ajouter_ligne_document( cial=self.cial, factory_lignes=factory_lignes, diff --git a/utils/functions/functions.py b/utils/functions/functions.py index 1aecf5b..d8886d0 100644 --- a/utils/functions/functions.py +++ b/utils/functions/functions.py @@ -167,10 +167,8 @@ def _parser_heure_sage(do_heure) -> str: return "00:00:00" try: - # Convertir en entier pour éliminer les zéros de padding SQL heure_int = int(str(do_heure).strip()) - # Formatter en string 6 caractères (HHMMSS) heure_str = str(heure_int).zfill(6) hh = int(heure_str[0:2]) diff --git a/utils/functions/items_to_dict.py b/utils/functions/items_to_dict.py index 0ff6ddf..6fa9f99 100644 --- a/utils/functions/items_to_dict.py +++ b/utils/functions/items_to_dict.py @@ -80,7 +80,6 @@ def contact_to_dict(row) -> Dict: def _collaborators_to_dict(row) -> Optional[dict]: """Convertit une ligne SQL en dictionnaire collaborateur""" - # Vérifier si le collaborateur existe if not hasattr(row, "Collab_CO_No") or row.Collab_CO_No is None: return None @@ -157,7 +156,6 @@ def collaborators_to_dict(row): def tiers_to_dict(row) -> dict: """Convertit une ligne SQL en dictionnaire tiers""" tiers = { - # IDENTIFICATION "numero": _safe_strip(row.CT_Num), "intitule": _safe_strip(row.CT_Intitule), "type_tiers": row.CT_Type, @@ -167,7 +165,6 @@ def tiers_to_dict(row) -> dict: "siret": _safe_strip(row.CT_Siret), "tva_intra": _safe_strip(row.CT_Identifiant), "code_naf": _safe_strip(row.CT_Ape), - # ADRESSE "contact": _safe_strip(row.CT_Contact), "adresse": _safe_strip(row.CT_Adresse), "complement": _safe_strip(row.CT_Complement), @@ -175,19 +172,16 @@ def tiers_to_dict(row) -> dict: "ville": _safe_strip(row.CT_Ville), "region": _safe_strip(row.CT_CodeRegion), "pays": _safe_strip(row.CT_Pays), - # TELECOM "telephone": _safe_strip(row.CT_Telephone), "telecopie": _safe_strip(row.CT_Telecopie), "email": _safe_strip(row.CT_EMail), "site_web": _safe_strip(row.CT_Site), "facebook": _safe_strip(row.CT_Facebook), "linkedin": _safe_strip(row.CT_LinkedIn), - # TAUX "taux01": row.CT_Taux01, "taux02": row.CT_Taux02, "taux03": row.CT_Taux03, "taux04": row.CT_Taux04, - # STATISTIQUES "statistique01": _safe_strip(row.CT_Statistique01), "statistique02": _safe_strip(row.CT_Statistique02), "statistique03": _safe_strip(row.CT_Statistique03), @@ -198,13 +192,11 @@ def tiers_to_dict(row) -> dict: "statistique08": _safe_strip(row.CT_Statistique08), "statistique09": _safe_strip(row.CT_Statistique09), "statistique10": _safe_strip(row.CT_Statistique10), - # COMMERCIAL "encours_autorise": row.CT_Encours, "assurance_credit": row.CT_Assurance, "langue": row.CT_Langue, "commercial_code": row.CO_No, "commercial": _collaborators_to_dict(row), - # FACTURATION "lettrage_auto": (row.CT_Lettrage == 1), "est_actif": (row.CT_Sommeil == 0), "type_facture": row.CT_Facture, @@ -216,16 +208,12 @@ def tiers_to_dict(row) -> dict: "exclure_relance": (row.CT_NotRappel == 1), "exclure_penalites": (row.CT_NotPenal == 1), "bon_a_payer": row.CT_BonAPayer, - # LOGISTIQUE "priorite_livraison": row.CT_PrioriteLivr, "livraison_partielle": row.CT_LivrPartielle, "delai_transport": row.CT_DelaiTransport, "delai_appro": row.CT_DelaiAppro, - # COMMENTAIRE "commentaire": _safe_strip(row.CT_Commentaire), - # ANALYTIQUE "section_analytique": _safe_strip(row.CA_Num), - # ORGANISATION / SURVEILLANCE "mode_reglement_code": row.MR_No, "surveillance_active": (row.CT_Surveillance == 1), "coface": _safe_strip(row.CT_Coface), @@ -236,7 +224,6 @@ def tiers_to_dict(row) -> dict: "sv_objet_maj": _safe_strip(row.CT_SvObjetMaj), "sv_chiffre_affaires": row.CT_SvCA, "sv_resultat": row.CT_SvResultat, - # COMPTE GENERAL ET CATEGORIES "compte_general": _safe_strip(row.CG_NumPrinc), "categorie_tarif": row.N_CatTarif, "categorie_compta": row.N_CatCompta,