Cleaned projects' files

This commit is contained in:
fanilo 2026-01-18 09:41:22 +01:00
parent 9ffad8287d
commit 31dec46226
9 changed files with 1 additions and 267 deletions

2
.gitignore vendored
View file

@ -34,4 +34,4 @@ htmlcov/
dist/
cleaner.py
*clean*.py

View file

@ -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:

View file

@ -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)

View file

@ -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
)

View file

@ -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])

View file

@ -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"] = [

View file

@ -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,

View file

@ -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])

View file

@ -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,