Sage100-ws/sage_connector.py

8014 lines
344 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import win32com.client
import pythoncom
from datetime import datetime, date
from typing import Dict, List, Optional
import threading
import time
import logging
import pyodbc
from contextlib import contextmanager
import pywintypes
from utils.articles.articles_data_sql import (
enrichir_stock_emplacements,
enrichir_gammes_articles,
enrichir_tarifs_clients,
enrichir_nomenclature,
enrichir_compta_articles,
enrichir_fournisseurs_multiples,
enrichir_depots_details,
enrichir_emplacements_details,
enrichir_gammes_enumeres,
enrichir_references_enumerees,
enrichir_medias_articles,
enrichir_prix_gammes,
enrichir_conditionnements,
_mapper_article_depuis_row,
enrichir_stocks_articles,
enrichir_fournisseurs_articles,
enrichir_familles_articles,
enrichir_tva_articles,
)
from utils.tiers.clients.clients_data import (
_extraire_client,
_cast_client,
)
from utils.articles.stock_check import verifier_stock_suffisant
from utils.articles.articles_data_com import _extraire_article
from utils.tiers.tiers_data_sql import _build_tiers_select_query
from utils.functions.functions import (
_safe_strip,
_safe_int,
_clean_str,
_try_set_attribute,
_get_type_libelle,
)
from utils.functions.items_to_dict import (
contacts_to_dict,
contact_to_dict,
tiers_to_dict,
society_to_dict,
)
from utils.functions.sage_utilities import (
peut_etre_transforme,
lire_erreurs_sage,
)
from utils.documents.documents_data_sql import (
_rechercher_devis_par_numero,
_lire_document_sql,
_lister_documents_avec_lignes_sql,
)
from utils.documents.devis.devis_check import _rechercher_devis_dans_liste
from utils.tiers.contacts.contacts import (
_get_contacts,
_chercher_contact_en_base,
_lire_contact_depuis_base,
)
from utils import (
valider_donnees_creation,
valider_donnees_modification,
)
from schemas.documents.doc_config import TypeDocumentVente
from utils.functions.data.create_doc import (
creer_document_vente,
modifier_document_vente,
)
from utils.functions.items_to_dict import collaborators_to_dict
from utils.functions.society.societe_data import (
get_societe_row,
add_logo,
build_exercices,
recuperer_logo_com,
)
from utils.documents.settle import (
lire_modes_reglement,
lire_devises,
lire_journaux_tresorerie,
lire_comptes_generaux,
lire_tva_taux,
lire_parametres_encaissement,
lire_tous_reglements,
lire_facture_reglement_detail,
lire_reglement_detail,
regler_facture as _regler_facture,
regler_factures_client as _regler_factures_client,
lire_reglements_client as _lire_reglements_client,
lire_reglements_facture as _lire_reglements_facture,
lire_journaux_banque as _lire_journaux,
lire_tous_journaux as _lire,
introspecter_reglement as _intro,
)
from utils.documents.validations import (
valider_facture as _valider,
devalider_facture as _devalider,
get_statut_validation as _get_statut,
introspecter_validation as _introspect,
explorer_toutes_interfaces_validation as _introspect_doc,
)
logger = logging.getLogger(__name__)
class SageConnector:
def __init__(
self,
chemin_base,
sql_server_name,
sql_server_database,
utilisateur="<Administrateur>",
mot_de_passe="",
):
self.chemin_base = chemin_base
self.utilisateur = utilisateur
self.mot_de_passe = mot_de_passe
self.cial = None
self.sql_server = sql_server_name
self.sql_database = sql_server_database
self.sql_conn_string = (
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
f"SERVER={self.sql_server};"
f"DATABASE={self.sql_database};"
f"Trusted_Connection=yes;"
f"Encrypt=no;"
)
self._lock_com = threading.RLock()
self._thread_local = threading.local()
@contextmanager
def _com_context(self):
if not hasattr(self._thread_local, "com_initialized"):
try:
pythoncom.CoInitialize()
self._thread_local.com_initialized = True
logger.debug(
f"COM initialisé pour thread {threading.current_thread().name}"
)
except Exception as e:
logger.error(f"Erreur initialisation COM: {e}")
raise
try:
yield
finally:
pass
@contextmanager
def _get_sql_connection(self):
"""Context manager pour connexions SQL"""
conn = None
try:
conn = pyodbc.connect(self.sql_conn_string, timeout=10)
yield conn
except pyodbc.Error as e:
logger.error(f" Erreur SQL: {e}")
raise RuntimeError(f"Erreur SQL: {str(e)}")
finally:
if conn:
conn.close()
def _cleanup_com_thread(self):
"""Nettoie COM pour le thread actuel (à appeler à la fin)"""
if hasattr(self._thread_local, "com_initialized"):
try:
pythoncom.CoUninitialize()
delattr(self._thread_local, "com_initialized")
logger.debug(
f"COM nettoyé pour thread {threading.current_thread().name}"
)
except Exception:
pass
def connecter(self):
"""Connexion initiale à Sage - VERSION HYBRIDE"""
try:
with self._com_context():
self.cial = win32com.client.gencache.EnsureDispatch(
"Objets100c.Cial.Stream"
)
self.cial.Name = self.chemin_base
self.cial.Loggable.UserName = self.utilisateur
self.cial.Loggable.UserPwd = self.mot_de_passe
self.cial.Open()
logger.info(f" Connexion COM Sage réussie: {self.chemin_base}")
try:
with self._get_sql_connection() as conn:
conn.cursor()
except Exception as e:
logger.warning(f"SQL non disponible: {e}")
logger.warning(" Les lectures utiliseront COM (plus lent)")
return True
except Exception as e:
logger.error(f" Erreur connexion Sage: {e}", exc_info=True)
return False
def deconnecter(self):
"""Déconnexion propre"""
if self.cial:
try:
with self._com_context():
self.cial.Close()
logger.info("Connexion Sage fermée")
except Exception:
pass
def lister_tous_fournisseurs(self, filtre=""):
"""Liste tous les fournisseurs avec leur commercial"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = _build_tiers_select_query()
query += """
FROM F_COMPTET t
LEFT JOIN F_COLLABORATEUR c ON t.CO_No = c.CO_No
WHERE t.CT_Type = 1
"""
params = []
if filtre:
query += " AND (t.CT_Num LIKE ? OR t.CT_Intitule LIKE ?)"
params.extend([f"%{filtre}%", f"%{filtre}%"])
query += " ORDER BY t.CT_Intitule"
cursor.execute(query, params)
rows = cursor.fetchall()
fournisseurs = []
for row in rows:
fournisseur = tiers_to_dict(row)
fournisseur["contacts"] = _get_contacts(row.CT_Num, conn)
fournisseurs.append(fournisseur)
logger.info(
f" SQL: {len(fournisseurs)} fournisseurs avec {len(fournisseur)} champs"
)
return fournisseurs
except Exception as e:
logger.error(f" Erreur SQL fournisseurs: {e}")
raise RuntimeError(f"Erreur lecture fournisseurs: {str(e)}")
def lire_fournisseur(self, code_fournisseur):
"""Lit un fournisseur avec son commercial"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = _build_tiers_select_query()
query += """
FROM F_COMPTET t
LEFT JOIN F_COLLABORATEUR c ON t.CO_No = c.CO_No
WHERE t.CT_Num = ? AND t.CT_Type = 1
"""
cursor.execute(query, (code_fournisseur.upper(),))
row = cursor.fetchone()
if not row:
return None
fournisseur = tiers_to_dict(row)
fournisseur["contacts"] = _get_contacts(row.CT_Num, conn)
logger.info(
f" SQL: Fournisseur {code_fournisseur} avec {len(fournisseur)} champs"
)
return fournisseur
except Exception as e:
logger.error(f" Erreur SQL fournisseur {code_fournisseur}: {e}")
return None
def creer_fournisseur(self, fournisseur_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(" === VALIDATION DONNÉES FOURNISSEUR ===")
if not fournisseur_data.get("intitule"):
raise ValueError("Le champ 'intitule' est obligatoire")
intitule = str(fournisseur_data["intitule"])[:69].strip()
num_prop = (
str(fournisseur_data.get("num", "")).upper()[:17].strip()
if fournisseur_data.get("num")
else ""
)
compte = str(fournisseur_data.get("compte_collectif", "4010000"))[
:13
].strip()
adresse = str(fournisseur_data.get("adresse", ""))[:35].strip()
code_postal = str(fournisseur_data.get("code_postal", ""))[:9].strip()
ville = str(fournisseur_data.get("ville", ""))[:35].strip()
pays = str(fournisseur_data.get("pays", ""))[:35].strip()
telephone = str(fournisseur_data.get("telephone", ""))[:21].strip()
email = str(fournisseur_data.get("email", ""))[:69].strip()
siret = str(fournisseur_data.get("siret", ""))[:14].strip()
tva_intra = str(fournisseur_data.get("tva_intra", ""))[:25].strip()
logger.info(f" intitule: '{intitule}' (len={len(intitule)})")
logger.info(f" num: '{num_prop or 'AUTO'}' (len={len(num_prop)})")
logger.info(f" compte: '{compte}' (len={len(compte)})")
factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur
persist = factory_fournisseur.Create()
fournisseur = win32com.client.CastTo(persist, "IBOFournisseur3")
fournisseur.SetDefault()
logger.info(" Objet fournisseur créé et initialisé")
logger.info(" Définition des champs obligatoires...")
fournisseur.CT_Intitule = intitule
logger.debug(f" CT_Intitule: '{intitule}'")
try:
fournisseur.CT_Type = 1
logger.debug(" CT_Type: 1 (Fournisseur)")
except Exception:
logger.debug(" CT_Type non défini (géré par FactoryFournisseur)")
try:
fournisseur.CT_Qualite = "FOU"
logger.debug(" CT_Qualite: 'FOU'")
except Exception:
logger.debug(" CT_Qualite non défini (pas critique)")
try:
factory_compte = self.cial.CptaApplication.FactoryCompteG
persist_compte = factory_compte.ReadNumero(compte)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
fournisseur.CompteGPrinc = compte_obj
logger.debug(f" CompteGPrinc: objet '{compte}' assigné")
else:
logger.warning(
f" Compte {compte} introuvable - utilisation défaut"
)
except Exception as e:
logger.warning(f" Erreur CompteGPrinc: {e}")
if num_prop:
fournisseur.CT_Num = num_prop
logger.debug(f" CT_Num fourni: '{num_prop}'")
else:
try:
if hasattr(fournisseur, "SetDefaultNumPiece"):
fournisseur.SetDefaultNumPiece()
num_genere = getattr(fournisseur, "CT_Num", "")
logger.debug(f" CT_Num auto-généré: '{num_genere}'")
else:
num_genere = factory_fournisseur.GetNextNumero()
if num_genere:
fournisseur.CT_Num = num_genere
logger.debug(
f" CT_Num auto (GetNextNumero): '{num_genere}'"
)
else:
import time
num_genere = f"FOUR{int(time.time()) % 1000000}"
fournisseur.CT_Num = num_genere
logger.warning(f" CT_Num fallback: '{num_genere}'")
except Exception as e:
logger.error(f" Impossible de générer CT_Num: {e}")
raise ValueError(
"Impossible de générer le numéro fournisseur automatiquement"
)
try:
if hasattr(fournisseur, "N_CatTarif"):
fournisseur.N_CatTarif = 1
if hasattr(fournisseur, "N_CatCompta"):
fournisseur.N_CatCompta = 1
if hasattr(fournisseur, "N_Period"):
fournisseur.N_Period = 1
logger.debug(" Catégories (N_*) initialisées")
except Exception as e:
logger.warning(f" Catégories: {e}")
logger.info(" Définition champs optionnels...")
if any([adresse, code_postal, ville, pays]):
try:
adresse_obj = fournisseur.Adresse
if adresse:
adresse_obj.Adresse = adresse
if code_postal:
adresse_obj.CodePostal = code_postal
if ville:
adresse_obj.Ville = ville
if pays:
adresse_obj.Pays = pays
logger.debug(" Adresse définie")
except Exception as e:
logger.warning(f" Adresse: {e}")
if telephone or email:
try:
telecom_obj = fournisseur.Telecom
if telephone:
telecom_obj.Telephone = telephone
if email:
telecom_obj.EMail = email
logger.debug(" Télécom défini")
except Exception as e:
logger.warning(f" Télécom: {e}")
if siret:
try:
fournisseur.CT_Siret = siret
logger.debug(f" SIRET: '{siret}'")
except Exception as e:
logger.warning(f" SIRET: {e}")
if tva_intra:
try:
fournisseur.CT_Identifiant = tva_intra
logger.debug(f" TVA intra: '{tva_intra}'")
except Exception as e:
logger.warning(f" TVA: {e}")
try:
if hasattr(fournisseur, "CT_Lettrage"):
fournisseur.CT_Lettrage = True
if hasattr(fournisseur, "CT_Sommeil"):
fournisseur.CT_Sommeil = False
logger.debug(" Options par défaut définies")
except Exception as e:
logger.debug(f" Options: {e}")
logger.info(" === DIAGNOSTIC PRÉ-WRITE ===")
num_avant_write = getattr(fournisseur, "CT_Num", "")
if not num_avant_write:
logger.error(" CRITIQUE: CT_Num toujours vide !")
raise ValueError("Le numéro fournisseur (CT_Num) est obligatoire")
logger.info(f" CT_Num confirmé: '{num_avant_write}'")
logger.info(" Écriture du fournisseur dans Sage...")
try:
fournisseur.Write()
logger.info(" Write() réussi !")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
logger.error(f" Erreur Sage: {error_detail}")
except Exception:
pass
if (
"doublon" in error_detail.lower()
or "existe" in error_detail.lower()
):
raise ValueError(f"Ce fournisseur existe déjà : {error_detail}")
raise RuntimeError(f"Échec Write(): {error_detail}")
try:
fournisseur.Read()
except Exception as e:
logger.warning(f"Impossible de relire: {e}")
num_final = getattr(fournisseur, "CT_Num", "")
if not num_final:
raise RuntimeError("CT_Num vide après Write()")
logger.info(f" FOURNISSEUR CRÉÉ: {num_final} - {intitule} ")
resultat = {
"numero": num_final,
"intitule": intitule,
"compte_collectif": compte,
"type": 1,
"est_fournisseur": True,
"adresse": adresse or None,
"code_postal": code_postal or None,
"ville": ville or None,
"pays": pays or None,
"email": email or None,
"telephone": telephone or None,
"siret": siret or None,
"tva_intra": tva_intra or None,
}
return resultat
except ValueError as e:
logger.error(f" Erreur métier: {e}")
raise
except Exception as e:
logger.error(f" Erreur création fournisseur: {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = f"Erreur Sage: {err.Description}"
except Exception:
pass
raise RuntimeError(f"Erreur technique Sage: {error_message}")
def modifier_fournisseur(self, code: str, fournisseur_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f" Recherche fournisseur {code}...")
factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur
persist = factory_fournisseur.ReadNumero(code)
if not persist:
raise ValueError(f"Fournisseur {code} introuvable")
fournisseur = _cast_client(persist)
if not fournisseur:
raise ValueError(f"Impossible de charger le fournisseur {code}")
logger.info(
f" Fournisseur {code} trouvé: {getattr(fournisseur, 'CT_Intitule', '')}"
)
logger.info(" Mise à jour des champs...")
champs_modifies = []
if "intitule" in fournisseur_data:
intitule = str(fournisseur_data["intitule"])[:69].strip()
fournisseur.CT_Intitule = intitule
champs_modifies.append(f"intitule='{intitule}'")
if any(
k in fournisseur_data
for k in ["adresse", "code_postal", "ville", "pays"]
):
try:
adresse_obj = fournisseur.Adresse
if "adresse" in fournisseur_data:
adresse = str(fournisseur_data["adresse"])[:35].strip()
adresse_obj.Adresse = adresse
champs_modifies.append("adresse")
if "code_postal" in fournisseur_data:
cp = str(fournisseur_data["code_postal"])[:9].strip()
adresse_obj.CodePostal = cp
champs_modifies.append("code_postal")
if "ville" in fournisseur_data:
ville = str(fournisseur_data["ville"])[:35].strip()
adresse_obj.Ville = ville
champs_modifies.append("ville")
if "pays" in fournisseur_data:
pays = str(fournisseur_data["pays"])[:35].strip()
adresse_obj.Pays = pays
champs_modifies.append("pays")
except Exception as e:
logger.warning(f"Erreur mise à jour adresse: {e}")
if "email" in fournisseur_data or "telephone" in fournisseur_data:
try:
telecom_obj = fournisseur.Telecom
if "email" in fournisseur_data:
email = str(fournisseur_data["email"])[:69].strip()
telecom_obj.EMail = email
champs_modifies.append("email")
if "telephone" in fournisseur_data:
tel = str(fournisseur_data["telephone"])[:21].strip()
telecom_obj.Telephone = tel
champs_modifies.append("telephone")
except Exception as e:
logger.warning(f"Erreur mise à jour télécom: {e}")
if "siret" in fournisseur_data:
try:
siret = str(fournisseur_data["siret"])[:14].strip()
fournisseur.CT_Siret = siret
champs_modifies.append("siret")
except Exception as e:
logger.warning(f"Erreur mise à jour SIRET: {e}")
if "tva_intra" in fournisseur_data:
try:
tva = str(fournisseur_data["tva_intra"])[:25].strip()
fournisseur.CT_Identifiant = tva
champs_modifies.append("tva_intra")
except Exception as e:
logger.warning(f"Erreur mise à jour TVA: {e}")
if not champs_modifies:
logger.warning("Aucun champ à modifier")
return {
"numero": getattr(fournisseur, "CT_Num", "").strip(),
"intitule": getattr(fournisseur, "CT_Intitule", "").strip(),
"type": 1,
"est_fournisseur": True,
}
logger.info(f" Champs à modifier: {', '.join(champs_modifies)}")
logger.info(" Écriture des modifications...")
try:
fournisseur.Write()
logger.info(" Write() réussi !")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f" Erreur Write(): {error_detail}")
raise RuntimeError(f"Échec modification: {error_detail}")
fournisseur.Read()
logger.info(
f" FOURNISSEUR MODIFIÉ: {code} ({len(champs_modifies)} champs) "
)
numero = getattr(fournisseur, "CT_Num", "").strip()
intitule = getattr(fournisseur, "CT_Intitule", "").strip()
data = {
"numero": numero,
"intitule": intitule,
"type": 1,
"est_fournisseur": True,
}
try:
adresse_obj = getattr(fournisseur, "Adresse", None)
if adresse_obj:
data["adresse"] = getattr(adresse_obj, "Adresse", "").strip()
data["code_postal"] = getattr(
adresse_obj, "CodePostal", ""
).strip()
data["ville"] = getattr(adresse_obj, "Ville", "").strip()
except Exception:
data["adresse"] = ""
data["code_postal"] = ""
data["ville"] = ""
try:
telecom_obj = getattr(fournisseur, "Telecom", None)
if telecom_obj:
data["telephone"] = getattr(
telecom_obj, "Telephone", ""
).strip()
data["email"] = getattr(telecom_obj, "EMail", "").strip()
except Exception:
data["telephone"] = ""
data["email"] = ""
return data
except ValueError as e:
logger.error(f" Erreur métier: {e}")
raise
except Exception as e:
logger.error(f" Erreur modification fournisseur: {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = f"Erreur Sage: {err.Description}"
except Exception:
pass
raise RuntimeError(f"Erreur technique Sage: {error_message}")
def lister_tous_clients(self, filtre=""):
"""Liste tous les clients avec leur commercial"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = _build_tiers_select_query()
query += """
FROM F_COMPTET t
LEFT JOIN F_COLLABORATEUR c ON t.CO_No = c.CO_No
WHERE t.CT_Type = 0
"""
params = []
if filtre:
query += " AND (t.CT_Num LIKE ? OR t.CT_Intitule LIKE ?)"
params.extend([f"%{filtre}%", f"%{filtre}%"])
query += " ORDER BY t.CT_Intitule"
cursor.execute(query, params)
rows = cursor.fetchall()
clients = []
for row in rows:
client = tiers_to_dict(row)
client["contacts"] = _get_contacts(row.CT_Num, conn)
clients.append(client)
logger.info(f" SQL: {len(clients)} clients avec {len(client)} champs")
return clients
except Exception as e:
logger.error(f" Erreur SQL clients: {e}")
raise RuntimeError(f"Erreur lecture clients: {str(e)}")
def lire_client(self, code_client):
"""Lit un client avec son commercial"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = _build_tiers_select_query()
query += """
FROM F_COMPTET t
LEFT JOIN F_COLLABORATEUR c ON t.CO_No = c.CO_No
WHERE t.CT_Num = ? AND t.CT_Type = 0
"""
cursor.execute(query, (code_client.upper(),))
row = cursor.fetchone()
if not row:
return None
client = tiers_to_dict(row)
client["contacts"] = _get_contacts(row.CT_Num, conn)
logger.info(f" SQL: Client {code_client} avec {len(client)} champs")
return client
except Exception as e:
logger.error(f" Erreur SQL client {code_client}: {e}")
return None
def lister_tous_articles(self, filtre=""):
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
logger.info("[SQL] Détection des colonnes de F_ARTICLE...")
cursor.execute("SELECT TOP 1 * FROM F_ARTICLE")
colonnes_disponibles = [column[0] for column in cursor.description]
logger.info(
f"[SQL] Colonnes F_ARTICLE trouvées : {len(colonnes_disponibles)}"
)
colonnes_config = {
"AR_Ref": "reference",
"AR_Design": "designation",
"AR_CodeBarre": "code_barre",
"AR_EdiCode": "edi_code",
"AR_Raccourci": "raccourci",
"AR_PrixVen": "prix_vente",
"AR_PrixAch": "prix_achat",
"AR_Coef": "coef",
"AR_PUNet": "prix_net",
"AR_PrixAchNouv": "prix_achat_nouveau",
"AR_CoefNouv": "coef_nouveau",
"AR_PrixVenNouv": "prix_vente_nouveau",
"AR_DateApplication": "date_application_prix",
"AR_CoutStd": "cout_standard",
"AR_UniteVen": "unite_vente",
"AR_UnitePoids": "unite_poids",
"AR_PoidsNet": "poids_net",
"AR_PoidsBrut": "poids_brut",
"AR_Gamme1": "gamme_1",
"AR_Gamme2": "gamme_2",
"FA_CodeFamille": "famille_code",
"AR_Type": "type_article",
"AR_Nature": "nature",
"AR_Garantie": "garantie",
"AR_CodeFiscal": "code_fiscal",
"AR_Pays": "pays",
"CO_No": "fournisseur_principal",
"AR_Condition": "conditionnement",
"AR_NbColis": "nb_colis",
"AR_Prevision": "prevision",
"AR_SuiviStock": "suivi_stock",
"AR_Nomencl": "nomenclature",
"AR_QteComp": "qte_composant",
"AR_QteOperatoire": "qte_operatoire",
"AR_Sommeil": "sommeil",
"AR_Substitut": "article_substitut",
"AR_Escompte": "soumis_escompte",
"AR_Delai": "delai",
"AR_Stat01": "stat_01",
"AR_Stat02": "stat_02",
"AR_Stat03": "stat_03",
"AR_Stat04": "stat_04",
"AR_Stat05": "stat_05",
"AR_HorsStat": "hors_statistique",
"CL_No1": "categorie_1",
"CL_No2": "categorie_2",
"CL_No3": "categorie_3",
"CL_No4": "categorie_4",
"AR_DateModif": "date_modification",
"AR_VteDebit": "vente_debit",
"AR_NotImp": "non_imprimable",
"AR_Transfere": "transfere",
"AR_Publie": "publie",
"AR_Contremarque": "contremarque",
"AR_FactPoids": "fact_poids",
"AR_FactForfait": "fact_forfait",
"AR_SaisieVar": "saisie_variable",
"AR_Fictif": "fictif",
"AR_SousTraitance": "sous_traitance",
"AR_Criticite": "criticite",
"RP_CodeDefaut": "reprise_code_defaut",
"AR_DelaiFabrication": "delai_fabrication",
"AR_DelaiPeremption": "delai_peremption",
"AR_DelaiSecurite": "delai_securite",
"AR_TypeLancement": "type_lancement",
"AR_Cycle": "cycle",
"AR_Photo": "photo",
"AR_Langue1": "langue_1",
"AR_Langue2": "langue_2",
"AR_Frais01FR_Denomination": "frais_01_denomination",
"AR_Frais02FR_Denomination": "frais_02_denomination",
"AR_Frais03FR_Denomination": "frais_03_denomination",
"Marque commerciale": "marque_commerciale",
"Objectif / Qtés vendues": "objectif_qtes_vendues",
"Pourcentage teneur en or": "pourcentage_or",
"1ère commercialisation": "premiere_commercialisation",
"AR_InterdireCommande": "interdire_commande",
"AR_Exclure": "exclure",
}
colonnes_a_lire = [
col_sql
for col_sql in colonnes_config.keys()
if col_sql in colonnes_disponibles
]
if not colonnes_a_lire:
logger.error("[SQL] Aucune colonne mappée trouvée !")
colonnes_a_lire = ["AR_Ref", "AR_Design", "AR_PrixVen"]
logger.info(f"[SQL] Colonnes sélectionnées : {len(colonnes_a_lire)}")
colonnes_sql = []
for col in colonnes_a_lire:
if " " in col or "/" in col or "è" in col:
colonnes_sql.append(f"[{col}]")
else:
colonnes_sql.append(col)
colonnes_str = ", ".join(colonnes_sql)
query = f"SELECT {colonnes_str} FROM F_ARTICLE WHERE 1=1"
params = []
if filtre:
conditions = []
if "AR_Ref" in colonnes_a_lire:
conditions.append("AR_Ref LIKE ?")
params.append(f"%{filtre}%")
if "AR_Design" in colonnes_a_lire:
conditions.append("AR_Design LIKE ?")
params.append(f"%{filtre}%")
if "AR_CodeBarre" in colonnes_a_lire:
conditions.append("AR_CodeBarre LIKE ?")
params.append(f"%{filtre}%")
if conditions:
query += " AND (" + " OR ".join(conditions) + ")"
query += " ORDER BY AR_Ref"
logger.debug(f"[SQL] Requête : {query[:200]}...")
cursor.execute(query, params)
rows = cursor.fetchall()
logger.info(f"[SQL] {len(rows)} lignes récupérées")
articles = []
for row in rows:
row_data = {}
for idx, col_sql in enumerate(colonnes_a_lire):
valeur = row[idx]
if isinstance(valeur, str):
valeur = valeur.strip()
row_data[col_sql] = valeur
if "Marque commerciale" in row_data:
logger.debug(
f"[DEBUG] Marque commerciale trouvée: {row_data['Marque commerciale']}"
)
article_data = _mapper_article_depuis_row(row_data, colonnes_config)
articles.append(article_data)
articles = enrichir_stocks_articles(articles, cursor)
articles = enrichir_familles_articles(articles, cursor)
articles = enrichir_fournisseurs_articles(articles, cursor)
articles = enrichir_tva_articles(articles, cursor)
articles = enrichir_stock_emplacements(articles, cursor)
articles = enrichir_gammes_articles(articles, cursor)
articles = enrichir_tarifs_clients(articles, cursor)
articles = enrichir_nomenclature(articles, cursor)
articles = enrichir_compta_articles(articles, cursor)
articles = enrichir_fournisseurs_multiples(articles, cursor)
articles = enrichir_depots_details(articles, cursor)
articles = enrichir_emplacements_details(articles, cursor)
articles = enrichir_gammes_enumeres(articles, cursor)
articles = enrichir_references_enumerees(articles, cursor)
articles = enrichir_medias_articles(articles, cursor)
articles = enrichir_prix_gammes(articles, cursor)
articles = enrichir_conditionnements(articles, cursor)
return articles
except Exception as e:
logger.error(f" Erreur SQL articles: {e}", exc_info=True)
raise RuntimeError(f"Erreur lecture articles: {str(e)}")
def lire_article(self, reference):
"""
Lit un article complet depuis SQL avec tous les enrichissements
Version fusionnée complète
"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
logger.info(f"[SQL] Lecture article {reference}...")
cursor.execute("SELECT TOP 1 * FROM F_ARTICLE")
colonnes_disponibles = [column[0] for column in cursor.description]
colonnes_config = {
"AR_Ref": "reference",
"AR_Design": "designation",
"AR_CodeBarre": "code_barre",
"AR_EdiCode": "edi_code",
"AR_Raccourci": "raccourci",
"AR_PrixVen": "prix_vente",
"AR_PrixAch": "prix_achat",
"AR_Coef": "coef",
"AR_PUNet": "prix_net",
"AR_PrixAchNouv": "prix_achat_nouveau",
"AR_CoefNouv": "coef_nouveau",
"AR_PrixVenNouv": "prix_vente_nouveau",
"AR_DateApplication": "date_application_prix",
"AR_CoutStd": "cout_standard",
"AR_UniteVen": "unite_vente",
"AR_UnitePoids": "unite_poids",
"AR_PoidsNet": "poids_net",
"AR_PoidsBrut": "poids_brut",
"AR_Gamme1": "gamme_1",
"AR_Gamme2": "gamme_2",
"FA_CodeFamille": "famille_code",
"AR_Type": "type_article",
"AR_Nature": "nature",
"AR_Garantie": "garantie",
"AR_CodeFiscal": "code_fiscal",
"AR_Pays": "pays",
"CO_No": "fournisseur_principal",
"AR_Condition": "conditionnement",
"AR_NbColis": "nb_colis",
"AR_Prevision": "prevision",
"AR_SuiviStock": "suivi_stock",
"AR_Nomencl": "nomenclature",
"AR_QteComp": "qte_composant",
"AR_QteOperatoire": "qte_operatoire",
"AR_Sommeil": "sommeil",
"AR_Substitut": "article_substitut",
"AR_Escompte": "soumis_escompte",
"AR_Delai": "delai",
"AR_Stat01": "stat_01",
"AR_Stat02": "stat_02",
"AR_Stat03": "stat_03",
"AR_Stat04": "stat_04",
"AR_Stat05": "stat_05",
"AR_HorsStat": "hors_statistique",
"CL_No1": "categorie_1",
"CL_No2": "categorie_2",
"CL_No3": "categorie_3",
"CL_No4": "categorie_4",
"AR_DateModif": "date_modification",
"AR_VteDebit": "vente_debit",
"AR_NotImp": "non_imprimable",
"AR_Transfere": "transfere",
"AR_Publie": "publie",
"AR_Contremarque": "contremarque",
"AR_FactPoids": "fact_poids",
"AR_FactForfait": "fact_forfait",
"AR_SaisieVar": "saisie_variable",
"AR_Fictif": "fictif",
"AR_SousTraitance": "sous_traitance",
"AR_Criticite": "criticite",
"RP_CodeDefaut": "reprise_code_defaut",
"AR_DelaiFabrication": "delai_fabrication",
"AR_DelaiPeremption": "delai_peremption",
"AR_DelaiSecurite": "delai_securite",
"AR_TypeLancement": "type_lancement",
"AR_Cycle": "cycle",
"AR_Photo": "photo",
"AR_Langue1": "langue_1",
"AR_Langue2": "langue_2",
"AR_Frais01FR_Denomination": "frais_01_denomination",
"AR_Frais02FR_Denomination": "frais_02_denomination",
"AR_Frais03FR_Denomination": "frais_03_denomination",
"Marque commerciale": "marque_commerciale",
"Objectif / Qtés vendues": "objectif_qtes_vendues",
"Pourcentage teneur en or": "pourcentage_or",
"1ère commercialisation": "premiere_commercialisation",
"AR_InterdireCommande": "interdire_commande",
"AR_Exclure": "exclure",
}
colonnes_a_lire = [
col_sql
for col_sql in colonnes_config.keys()
if col_sql in colonnes_disponibles
]
if not colonnes_a_lire:
logger.error("[SQL] Aucune colonne mappée trouvée !")
return None
colonnes_sql = []
for col in colonnes_a_lire:
if " " in col or "/" in col or "è" in col:
colonnes_sql.append(f"[{col}]")
else:
colonnes_sql.append(col)
colonnes_str = ", ".join(colonnes_sql)
query = f"SELECT {colonnes_str} FROM F_ARTICLE WHERE AR_Ref = ?"
logger.debug(f"[SQL] Requête : {query[:200]}...")
cursor.execute(query, (reference.upper(),))
row = cursor.fetchone()
if not row:
logger.info(f"[SQL] Article {reference} non trouvé")
return None
row_data = {}
for idx, col_sql in enumerate(colonnes_a_lire):
valeur = row[idx]
if isinstance(valeur, str):
valeur = valeur.strip()
row_data[col_sql] = valeur
article = _mapper_article_depuis_row(row_data, colonnes_config)
articles = [
article
] # Liste d'un seul article pour les fonctions d'enrichissement
articles = enrichir_stocks_articles(articles, cursor)
articles = enrichir_familles_articles(articles, cursor)
articles = enrichir_fournisseurs_articles(articles, cursor)
articles = enrichir_tva_articles(articles, cursor)
articles = enrichir_stock_emplacements(articles, cursor)
articles = enrichir_gammes_articles(articles, cursor)
articles = enrichir_tarifs_clients(articles, cursor)
articles = enrichir_nomenclature(articles, cursor)
articles = enrichir_compta_articles(articles, cursor)
articles = enrichir_fournisseurs_multiples(articles, cursor)
articles = enrichir_depots_details(articles, cursor)
articles = enrichir_emplacements_details(articles, cursor)
articles = enrichir_gammes_enumeres(articles, cursor)
articles = enrichir_references_enumerees(articles, cursor)
articles = enrichir_medias_articles(articles, cursor)
articles = enrichir_prix_gammes(articles, cursor)
articles = enrichir_conditionnements(articles, cursor)
logger.info(f" Article {reference} lu avec tous les enrichissements")
return articles[0]
except Exception as e:
logger.error(f" Erreur SQL article {reference}: {e}", exc_info=True)
return None
def obtenir_contact(self, numero: str, contact_numero: int) -> Optional[Dict]:
"""
Récupère un contact spécifique par son CT_No
"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT
CT_Num, CT_No, N_Contact,
CT_Civilite, CT_Nom, CT_Prenom, CT_Fonction,
N_Service,
CT_Telephone, CT_TelPortable, CT_Telecopie, CT_EMail,
CT_Facebook, CT_LinkedIn, CT_Skype
FROM F_CONTACTT
WHERE CT_Num = ? AND CT_No = ?
"""
cursor.execute(query, [numero, contact_numero])
row = cursor.fetchone()
if not row:
return None
return contact_to_dict(row)
except Exception as e:
logger.error(f"Erreur obtention contact: {e}")
raise RuntimeError(f"Erreur lecture contact: {str(e)}")
def lister_tous_devis_cache(self, filtre=""):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lister_documents_avec_lignes_sql(cursor, type_doc=0, filtre=filtre)
def lire_devis_cache(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=0)
def lister_toutes_commandes_cache(self, filtre=""):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lister_documents_avec_lignes_sql(cursor, type_doc=1, filtre=filtre)
def lire_commande_cache(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=1)
def lister_toutes_factures_cache(self, filtre=""):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lister_documents_avec_lignes_sql(cursor, type_doc=6, filtre=filtre)
def lire_facture_cache(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=6)
def lister_tous_fournisseurs_cache(self, filtre=""):
return self.lister_tous_fournisseurs()
def lire_fournisseur_cache(self, code):
return self.lire_fournisseur()
def lister_toutes_livraisons_cache(self, filtre=""):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lister_documents_avec_lignes_sql(cursor, type_doc=3, filtre=filtre)
def lire_livraison_cache(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=3)
def lister_tous_avoirs_cache(self, filtre=""):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lister_documents_avec_lignes_sql(cursor, type_doc=5, filtre=filtre)
def lire_avoir_cache(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=5)
def creer_devis_enrichi(self, devis_data: dict) -> Dict:
"""Crée un devis"""
return creer_document_vente(self, devis_data, TypeDocumentVente.DEVIS)
def _relire_devis(self, numero_devis, devis_data):
"""Relit le devis créé et extrait les informations finales."""
factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(0, numero_devis)
if not persist_reread:
logger.debug("ReadPiece échoué, recherche dans List()...")
persist_reread = _rechercher_devis_dans_liste(numero_devis, factory_doc)
if persist_reread:
doc_final = win32com.client.CastTo(persist_reread, "IBODocumentVente3")
doc_final.Read()
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
statut_final = getattr(doc_final, "DO_Statut", 0)
reference_final = getattr(doc_final, "DO_Ref", "")
date_livraison_final = None
try:
date_livr = getattr(doc_final, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except Exception:
pass
else:
total_calcule = sum(
ligne.get("montant_ligne_ht", 0) for ligne in devis_data["lignes"]
)
total_ht = total_calcule
total_ttc = round(total_calcule * 1.20, 2)
statut_final = 0
reference_final = devis_data.get("reference", "")
date_livraison_final = devis_data.get("date_livraison")
logger.info(f" Total HT: {total_ht}")
logger.info(f" Total TTC: {total_ttc}")
logger.info(f" Statut final: {statut_final}")
if reference_final:
logger.info(f" Référence: {reference_final}")
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
return {
"numero_devis": numero_devis,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(devis_data["lignes"]),
"client_code": devis_data["client"]["code"],
"date_devis": str(devis_data.get("date_devis", "")),
"date_livraison": date_livraison_final,
"reference": reference_final,
"statut": statut_final,
}
def modifier_devis(self, numero: str, devis_data: dict) -> Dict:
"""Modifie un devis"""
return modifier_document_vente(
self, numero, devis_data, TypeDocumentVente.DEVIS
)
def _charger_devis(self, numero: str):
"""Charge un devis depuis Sage."""
logger.info(f" Chargement devis {numero}...")
factory = self.cial.FactoryDocumentVente
logger.info(" Tentative ReadPiece(0, numero)...")
persist = factory.ReadPiece(0, numero)
if not persist:
logger.warning(" ReadPiece a échoué, recherche dans la liste...")
persist = _rechercher_devis_par_numero(numero, factory)
if not persist:
raise ValueError(f" Devis {numero} INTROUVABLE")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
logger.info(f" Devis {numero} chargé")
return doc
def lire_devis(self, numero_devis):
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
devis = _lire_document_sql(cursor, numero_devis, type_doc=0)
if not devis:
return None
return devis
except Exception as e:
logger.error(f" Erreur SQL lecture devis {numero_devis}: {e}")
return None
def lire_document(self, numero, type_doc):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc)
def transformer_document(
self,
numero_source,
type_source,
type_cible,
conserver_document_source=True,
verifier_doublons=True,
):
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
type_source = int(type_source)
type_cible = int(type_cible)
logger.info(
f"[TRANSFORM] Transformation: {numero_source} ({type_source}) → type {type_cible}"
)
transformations_valides = {
(0, 10): ("Vente", "CreateProcess_Commander"),
(0, 60): ("Vente", "CreateProcess_Facturer"),
(10, 30): ("Vente", "CreateProcess_Livrer"),
(10, 60): ("Vente", "CreateProcess_Facturer"),
(30, 60): ("Vente", "CreateProcess_Facturer"),
}
if (type_source, type_cible) not in transformations_valides:
raise ValueError(
f"Transformation non autorisée: "
f"{_get_type_libelle(type_source)}{_get_type_libelle(type_cible)}"
)
module, methode = transformations_valides[(type_source, type_cible)]
logger.info(f"[TRANSFORM] Méthode: Transformation.{module}.{methode}()")
try:
with (
self._com_context(),
self._lock_com,
self._get_sql_connection() as conn,
):
cursor = conn.cursor()
if verifier_doublons:
logger.info("[TRANSFORM] Vérification des doublons...")
verif = peut_etre_transforme(
cursor, numero_source, type_source, type_cible
)
if not verif["possible"]:
docs = [
d["numero"] for d in verif.get("documents_existants", [])
]
raise ValueError(
f"{verif['raison']}. Document(s) existant(s): {', '.join(docs)}"
)
logger.info("[TRANSFORM] Aucun doublon détecté")
factory = self.cial.FactoryDocumentVente
logger.info(f"[TRANSFORM] Lecture de {numero_source}...")
if not factory.ExistPiece(type_source, numero_source):
raise ValueError(f"Document {numero_source} introuvable")
persist_source = factory.ReadPiece(type_source, numero_source)
if not persist_source:
raise ValueError(f"Impossible de lire {numero_source}")
doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3")
doc_source.Read()
statut_source = getattr(doc_source, "DO_Statut", 0)
nb_lignes_source = 0
try:
factory_lignes = getattr(doc_source, "FactoryDocumentLigne", None)
if factory_lignes:
lignes_list = factory_lignes.List
nb_lignes_source = lignes_list.Count if lignes_list else 0
except Exception:
pass
logger.info(
f"[TRANSFORM] Source: statut={statut_source}, {nb_lignes_source} ligne(s)"
)
if nb_lignes_source == 0:
raise ValueError(f"Document {numero_source} vide (0 lignes)")
logger.info("[TRANSFORM] Création du transformer...")
transformation = getattr(self.cial, "Transformation", None)
if not transformation:
raise RuntimeError("API Transformation non disponible")
module_obj = getattr(transformation, module, None)
if not module_obj:
raise RuntimeError(f"Module {module} non disponible")
methode_func = getattr(module_obj, methode, None)
if not methode_func:
raise RuntimeError(f"Méthode {methode} non disponible")
transformer = methode_func()
if not transformer:
raise RuntimeError("Échec création transformer")
logger.info("[TRANSFORM] Transformer créé")
logger.info("[TRANSFORM] Configuration...")
if hasattr(transformer, "ConserveDocuments"):
try:
transformer.ConserveDocuments = conserver_document_source
logger.info(
f"[TRANSFORM] ConserveDocuments = {conserver_document_source}"
)
except Exception as e:
logger.warning(
f"[TRANSFORM] ConserveDocuments non modifiable: {e}"
)
logger.info("[TRANSFORM] Ajout du document...")
try:
transformer.AddDocument(doc_source)
logger.info("[TRANSFORM] Document ajouté")
except Exception as e:
raise RuntimeError(f"Impossible d'ajouter le document: {e}")
try:
can_process = getattr(transformer, "CanProcess", False)
logger.info(f"[TRANSFORM] CanProcess: {can_process}")
except Exception:
can_process = True
if not can_process:
erreurs = lire_erreurs_sage(transformer, "Transformer")
if erreurs:
msgs = [f"{e['field']}: {e['description']}" for e in erreurs]
raise RuntimeError(
f"Transformation impossible: {' | '.join(msgs)}"
)
raise RuntimeError("Transformation impossible (CanProcess=False)")
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug("[TRANSFORM] Transaction démarrée")
except Exception:
pass
try:
logger.info("[TRANSFORM] Process()...")
try:
transformer.Process()
logger.info("[TRANSFORM] Process() réussi")
except Exception as e:
logger.error(f"[TRANSFORM] Erreur Process(): {e}")
erreurs = lire_erreurs_sage(transformer, "Transformer")
if erreurs:
msgs = [
f"{e['field']}: {e['description']}" for e in erreurs
]
raise RuntimeError(f"Échec: {' | '.join(msgs)}")
raise RuntimeError(f"Échec transformation: {e}")
logger.info("[TRANSFORM] Récupération des résultats...")
list_results = getattr(transformer, "ListDocumentsResult", None)
if not list_results:
raise RuntimeError("ListDocumentsResult non disponible")
documents_crees = []
index = 1
while index <= 100:
try:
doc_result = list_results.Item(index)
if doc_result is None:
break
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_cible = getattr(doc_result, "DO_Piece", "").strip()
total_ht = float(getattr(doc_result, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_result, "DO_TotalTTC", 0.0))
nb_lignes = 0
try:
factory_lignes_result = getattr(
doc_result, "FactoryDocumentLigne", None
)
if factory_lignes_result:
lignes_list = factory_lignes_result.List
nb_lignes = lignes_list.Count if lignes_list else 0
except Exception:
pass
documents_crees.append(
{
"numero": numero_cible,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": nb_lignes,
}
)
logger.info(
f"[TRANSFORM] Document créé: {numero_cible} "
f"({nb_lignes} lignes, {total_ht}€ HT)"
)
index += 1
except Exception:
logger.debug(f"Fin de liste à index {index}")
break
if not documents_crees:
raise RuntimeError("Aucun document créé après Process()")
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.debug("[TRANSFORM] Transaction committée")
except Exception:
pass
time.sleep(1.5)
doc_principal = documents_crees[0]
logger.info(
f"[TRANSFORM] SUCCÈS: {numero_source}{doc_principal['numero']}"
)
logger.info(
f"[TRANSFORM] • {doc_principal['nb_lignes']} ligne(s)"
)
logger.info(
f"[TRANSFORM] • {doc_principal['total_ht']}€ HT / "
f"{doc_principal['total_ttc']}€ TTC"
)
return {
"success": True,
"document_source": numero_source,
"document_cible": doc_principal["numero"],
"type_source": type_source,
"type_cible": type_cible,
"nb_documents_crees": len(documents_crees),
"documents": documents_crees,
"nb_lignes": doc_principal["nb_lignes"],
"total_ht": doc_principal["total_ht"],
"total_ttc": doc_principal["total_ttc"],
"methode_transformation": f"{module}.{methode}",
}
except Exception:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.error("[TRANSFORM] Transaction annulée (rollback)")
except Exception:
pass
raise
except ValueError as e:
logger.error(f"[TRANSFORM] Erreur métier: {e}")
raise
except RuntimeError as e:
logger.error(f"[TRANSFORM] Erreur technique: {e}")
raise
except Exception as e:
logger.error(f"[TRANSFORM] Erreur inattendue: {e}", exc_info=True)
raise RuntimeError(f"Échec transformation: {str(e)}")
def mettre_a_jour_champ_libre(self, doc_id, type_doc, nom_champ, valeur):
"""Mise à jour champ libre pour Universign ID"""
try:
with self._com_context(), self._lock_com:
factory = self.cial.FactoryDocumentVente
persist = factory.ReadPiece(type_doc, doc_id)
if persist:
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
try:
setattr(doc, f"DO_{nom_champ}", valeur)
doc.Write()
logger.debug(f"Champ libre {nom_champ} = {valeur} sur {doc_id}")
return True
except Exception as e:
logger.warning(f"Impossible de mettre à jour {nom_champ}: {e}")
except Exception as e:
logger.error(f"Erreur MAJ champ libre: {e}")
return False
def _lire_client_obj(self, code_client):
"""Retourne l'objet client Sage brut (pour remises)"""
if not self.cial:
return None
try:
with self._com_context(), self._lock_com:
factory = self.cial.CptaApplication.FactoryClient
persist = factory.ReadNumero(code_client)
if persist:
return _cast_client(persist)
except Exception:
pass
return None
def lire_contact_principal_client(self, code_client):
if not self.cial:
return None
try:
with self._com_context(), self._lock_com:
factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(code_client)
if not persist_client:
return None
client = _cast_client(persist_client)
if not client:
return None
contact_info = {
"client_code": code_client,
"client_intitule": getattr(client, "CT_Intitule", ""),
"email": None,
"nom": None,
"telephone": None,
}
try:
telecom = getattr(client, "Telecom", None)
if telecom:
contact_info["email"] = getattr(telecom, "EMail", "")
contact_info["telephone"] = getattr(telecom, "Telephone", "")
except Exception:
pass
try:
contact_info["nom"] = (
getattr(client, "CT_Contact", "")
or contact_info["client_intitule"]
)
except Exception:
contact_info["nom"] = contact_info["client_intitule"]
return contact_info
except Exception as e:
logger.error(f"Erreur lecture contact client {code_client}: {e}")
return None
def mettre_a_jour_derniere_relance(self, doc_id, type_doc):
date_relance = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return self.mettre_a_jour_champ_libre(
doc_id, type_doc, "DerniereRelance", date_relance
)
def lister_tous_prospects(self, filtre=""):
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT
CT_Num, CT_Intitule, CT_Adresse, CT_Ville,
CT_CodePostal, CT_Telephone, CT_EMail
FROM F_COMPTET
WHERE CT_Type = 0 AND CT_Prospect = 1
"""
params = []
if filtre:
query += " AND (CT_Num LIKE ? OR CT_Intitule LIKE ?)"
params.extend([f"%{filtre}%", f"%{filtre}%"])
query += " ORDER BY CT_Intitule"
cursor.execute(query, params)
rows = cursor.fetchall()
prospects = []
for row in rows:
prospects.append(
{
"numero": _safe_strip(row.CT_Num),
"intitule": _safe_strip(row.CT_Intitule),
"adresse": _safe_strip(row.CT_Adresse),
"ville": _safe_strip(row.CT_Ville),
"code_postal": _safe_strip(row.CT_CodePostal),
"telephone": _safe_strip(row.CT_Telephone),
"email": _safe_strip(row.CT_EMail),
"type": 0,
"est_prospect": True,
}
)
logger.info(f" SQL: {len(prospects)} prospects")
return prospects
except Exception as e:
logger.error(f" Erreur SQL prospects: {e}")
return []
def lire_prospect(self, code_prospect):
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
CT_Num, CT_Intitule, CT_Type, CT_Qualite,
CT_Adresse, CT_Complement, CT_Ville, CT_CodePostal, CT_Pays,
CT_Telephone, CT_Portable, CT_EMail, CT_Telecopie,
CT_Siret, CT_Identifiant, CT_Sommeil, CT_Prospect,
CT_Contact, CT_FormeJuridique, CT_Secteur
FROM F_COMPTET
WHERE CT_Num = ? AND CT_Type = 0 AND CT_Prospect = 1
""",
(code_prospect.upper(),),
)
row = cursor.fetchone()
if not row:
return None
return {
"numero": _safe_strip(row.CT_Num),
"intitule": _safe_strip(row.CT_Intitule),
"type": 0,
"qualite": _safe_strip(row.CT_Qualite),
"est_prospect": True,
"adresse": _safe_strip(row.CT_Adresse),
"complement": _safe_strip(row.CT_Complement),
"ville": _safe_strip(row.CT_Ville),
"code_postal": _safe_strip(row.CT_CodePostal),
"pays": _safe_strip(row.CT_Pays),
"telephone": _safe_strip(row.CT_Telephone),
"portable": _safe_strip(row.CT_Portable),
"email": _safe_strip(row.CT_EMail),
"telecopie": _safe_strip(row.CT_Telecopie),
"siret": _safe_strip(row.CT_Siret),
"tva_intra": _safe_strip(row.CT_Identifiant),
"est_actif": (row.CT_Sommeil == 0),
"contact": _safe_strip(row.CT_Contact),
"forme_juridique": _safe_strip(row.CT_FormeJuridique),
"secteur": _safe_strip(row.CT_Secteur),
}
except Exception as e:
logger.error(f" Erreur SQL prospect {code_prospect}: {e}")
return None
def lire_avoir(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=50)
def lire_livraison(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=30)
def creer_contact(self, contact_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with (
self._com_context(),
self._lock_com,
self._get_sql_connection() as conn,
):
logger.info("=" * 80)
logger.info("[CREATION CONTACT F_CONTACTT]")
logger.info("=" * 80)
if not contact_data.get("numero"):
raise ValueError("numero (code client) obligatoire")
if not contact_data.get("nom"):
raise ValueError("nom obligatoire")
numero_client = _clean_str(contact_data["numero"], 17).upper()
nom = _clean_str(contact_data["nom"], 35)
prenom = _clean_str(contact_data.get("prenom", ""), 35)
logger.info(f" CLIENT: {numero_client}")
logger.info(f" CONTACT: {prenom} {nom}")
logger.info(f"[1] Chargement du tiers: {numero_client}")
factory_client = self.cial.CptaApplication.FactoryClient
factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur
persist_tiers = None
type_tiers = None
try:
logger.info(" Recherche dans Clients...")
persist_tiers = factory_client.ReadNumero(numero_client)
if persist_tiers:
type_tiers = "Client"
logger.info(" Trouvé comme Client")
except Exception as e:
logger.debug(f" Pas trouvé comme Client: {e}")
if not persist_tiers:
try:
logger.info(" Recherche dans Fournisseurs...")
persist_tiers = factory_fournisseur.ReadNumero(numero_client)
if persist_tiers:
type_tiers = "Fournisseur"
logger.info(" Trouvé comme Fournisseur")
except Exception as e:
logger.debug(f" Pas trouvé comme Fournisseur: {e}")
if not persist_tiers:
raise ValueError(
f"Le tiers '{numero_client}' est introuvable dans Sage 100c. "
f"Vérifiez que le code est exact et que le tiers existe "
f"(Client ou Fournisseur)."
)
try:
client_obj = win32com.client.CastTo(persist_tiers, "IBOClient3")
client_obj.Read()
logger.info(f" OK {type_tiers} chargé")
except Exception as e:
raise ValueError(
f"Erreur lors du chargement du {type_tiers} '{numero_client}': {e}"
)
logger.info("[2] Creation via FactoryTiersContact")
if not hasattr(client_obj, "FactoryTiersContact"):
raise RuntimeError("FactoryTiersContact non trouvee sur le client")
factory_contact = client_obj.FactoryTiersContact
logger.info(
f" OK FactoryTiersContact: {type(factory_contact).__name__}"
)
persist = factory_contact.Create()
logger.info(f" Objet cree: {type(persist).__name__}")
contact = None
interfaces_a_tester = [
"IBOTiersContact3",
"IBOTiersContact",
"IBOContactT3",
"IBOContactT",
]
for interface_name in interfaces_a_tester:
try:
temp = win32com.client.CastTo(persist, interface_name)
if hasattr(temp, "_prop_map_put_"):
props = list(temp._prop_map_put_.keys())
logger.info(f" Test {interface_name}: props={props[:15]}")
if "Nom" in props or "CT_Nom" in props:
contact = temp
logger.info(f" OK Cast reussi vers {interface_name}")
break
except Exception as e:
logger.debug(f" {interface_name}: {str(e)[:50]}")
if not contact:
logger.error(" ERROR Aucun cast ne fonctionne")
raise RuntimeError(
"Impossible de caster vers une interface contact valide"
)
logger.info("[3] Configuration du contact")
if hasattr(contact, "_prop_map_put_"):
props = list(contact._prop_map_put_.keys())
logger.info(f" Proprietes disponibles: {props}")
try:
contact.Nom = nom
logger.info(f" OK Nom = {nom}")
except Exception as e:
logger.error(f" ERROR Impossible de definir Nom: {e}")
raise RuntimeError(f"Echec definition Nom: {e}")
if prenom:
try:
contact.Prenom = prenom
logger.info(f" OK Prenom = {prenom}")
except Exception as e:
logger.warning(f" WARN Prenom: {e}")
if contact_data.get("civilite"):
civilite_map = {"M.": 0, "Mme": 1, "Mlle": 2, "Societe": 3}
civilite_code = civilite_map.get(contact_data["civilite"])
if civilite_code is not None:
try:
contact.Civilite = civilite_code
logger.info(f" OK Civilite = {civilite_code}")
except Exception as e:
logger.warning(f" WARN Civilite: {e}")
if contact_data.get("fonction"):
fonction = _clean_str(contact_data["fonction"], 35)
try:
contact.Fonction = fonction
logger.info(f" OK Fonction = {fonction}")
except Exception as e:
logger.warning(f" WARN Fonction: {e}")
if contact_data.get("service_code") is not None:
try:
service = _safe_int(contact_data["service_code"])
if service is not None and hasattr(contact, "ServiceContact"):
contact.ServiceContact = service
logger.info(f" OK ServiceContact = {service}")
except Exception as e:
logger.warning(f" WARN ServiceContact: {e}")
logger.info("[4] Coordonnees (Telecom)")
if hasattr(contact, "Telecom"):
try:
telecom = contact.Telecom
logger.info(f" Type Telecom: {type(telecom).__name__}")
if contact_data.get("telephone"):
telephone = _clean_str(contact_data["telephone"], 21)
if _try_set_attribute(telecom, "Telephone", telephone):
logger.info(f" Telephone = {telephone}")
if contact_data.get("portable"):
portable = _clean_str(contact_data["portable"], 21)
if _try_set_attribute(telecom, "Portable", portable):
logger.info(f" Portable = {portable}")
if contact_data.get("email"):
email = _clean_str(contact_data["email"], 69)
if _try_set_attribute(telecom, "EMail", email):
logger.info(f" EMail = {email}")
if contact_data.get("telecopie"):
fax = _clean_str(contact_data["telecopie"], 21)
if _try_set_attribute(telecom, "Telecopie", fax):
logger.info(f" Telecopie = {fax}")
except Exception as e:
logger.warning(f" WARN Erreur Telecom: {e}")
logger.info("[5] Reseaux sociaux")
if contact_data.get("facebook"):
facebook = _clean_str(contact_data["facebook"], 69)
try:
contact.Facebook = facebook
logger.info(f" Facebook = {facebook}")
except Exception:
pass
if contact_data.get("linkedin"):
linkedin = _clean_str(contact_data["linkedin"], 69)
try:
contact.LinkedIn = linkedin
logger.info(f" LinkedIn = {linkedin}")
except Exception:
pass
if contact_data.get("skype"):
skype = _clean_str(contact_data["skype"], 69)
try:
contact.Skype = skype
logger.info(f" Skype = {skype}")
except Exception:
pass
try:
contact.SetDefault()
logger.info(" OK SetDefault() applique")
except Exception as e:
logger.warning(f" WARN SetDefault(): {e}")
logger.info("[6] Enregistrement du contact")
contact_cree_malgre_erreur = False
contact_no = None
n_contact = None
erreur_com = None
try:
contact.Write()
logger.info(" Write() reussi")
try:
contact.Read()
logger.info(" Read() reussi")
except Exception as read_err:
logger.warning(f" WARN Read() échoué: {read_err}")
try:
contact_no = getattr(contact, "CT_No", None)
n_contact = getattr(contact, "N_Contact", None)
logger.info(
f" IDs COM: CT_No={contact_no}, N_Contact={n_contact}"
)
except Exception:
pass
if not contact_no:
logger.info(
" CT_No non disponible via COM - Recherche en base..."
)
import time
time.sleep(0.3)
contact_sql = _chercher_contact_en_base(
conn,
numero_client=numero_client,
nom=nom,
prenom=prenom if prenom else None,
)
if contact_sql:
logger.info(
f" Contact trouvé en base: CT_No={contact_sql['contact_numero']}"
)
contact_no = contact_sql["contact_numero"]
n_contact = contact_sql["n_contact"]
else:
logger.warning(" Contact non trouvé en base immédiatement")
except Exception as e:
erreur_com = str(e)
logger.warning(f" Write() a levé une exception: {erreur_com}")
if (
"existe déjà" in erreur_com.lower()
or "already exists" in erreur_com.lower()
):
logger.info(
" Erreur 'existe déjà' détectée - Vérification en base..."
)
import time
time.sleep(0.5)
contact_sql = _chercher_contact_en_base(
conn,
numero_client=numero_client,
nom=nom,
prenom=prenom if prenom else None,
)
if contact_sql:
logger.info(" Contact CRÉÉ malgré l'erreur COM !")
logger.info(
f" CT_No={contact_sql['contact_numero']}, N_Contact={contact_sql['n_contact']}"
)
contact_cree_malgre_erreur = True
contact_no = contact_sql["contact_numero"]
n_contact = contact_sql["n_contact"]
else:
logger.error(" Contact NON trouvé en base - Erreur réelle")
raise RuntimeError(f"Echec enregistrement: {erreur_com}")
else:
logger.error(f" Erreur Write non gérée: {erreur_com}")
raise RuntimeError(f"Echec enregistrement: {erreur_com}")
est_defaut = contact_data.get("est_defaut", False)
if est_defaut and (contact_no or n_contact):
logger.info("[7] Definition comme contact par defaut")
try:
nom_complet = f"{prenom} {nom}".strip() if prenom else nom
persist_client = factory_client.ReadNumero(numero_client)
client_obj = win32com.client.CastTo(
persist_client, "IBOClient3"
)
client_obj.Read()
client_obj.CT_Contact = nom_complet
logger.info(f" CT_Contact = '{nom_complet}'")
if contact_no and hasattr(client_obj, "CT_NoContact"):
try:
client_obj.CT_NoContact = contact_no
logger.info(f" CT_NoContact = {contact_no}")
except Exception:
pass
client_obj.Write()
client_obj.Read()
logger.info(" OK Contact par defaut defini")
except Exception as e:
logger.warning(f" WARN Echec: {e}")
est_defaut = False
logger.info("=" * 80)
if contact_cree_malgre_erreur:
logger.info(
f"[SUCCES] Contact créé MALGRÉ erreur COM: {prenom} {nom}"
)
logger.info(
" (Bug connu de Sage 100c - Le contact est bien en base)"
)
else:
logger.info(f"[SUCCES] Contact créé: {prenom} {nom}")
logger.info(f" Lié au client {numero_client}")
if contact_no:
logger.info(f" CT_No={contact_no}")
logger.info("=" * 80)
logger.info("[7] Construction du retour")
contact_dict = None
if contact_no:
logger.info(f" Stratégie 1: Lecture base (CT_No={contact_no})")
try:
contact_dict = _lire_contact_depuis_base(
conn, numero_client=numero_client, contact_no=contact_no
)
if contact_dict:
logger.info(
f" Lecture base réussie: {len(contact_dict)} champs"
)
logger.info(f" Type: {type(contact_dict)}")
logger.info(f" Keys: {list(contact_dict.keys())}")
logger.info(
f" Sample: numero={contact_dict.get('numero')}, nom={contact_dict.get('nom')}"
)
else:
logger.warning(
" _lire_contact_depuis_base() retourne None"
)
except Exception as e:
logger.error(f" Erreur lecture base: {e}", exc_info=True)
contact_dict = None
if not contact_dict:
logger.info(" Stratégie 2: Lecture objet COM (contacts_to_dict)")
try:
contact_dict = contacts_to_dict(
contact,
numero_client=numero_client,
contact_numero=contact_no,
n_contact=n_contact,
)
if contact_dict:
logger.info(
f" contacts_to_dict réussi: {len(contact_dict)} champs"
)
else:
logger.warning(" contacts_to_dict() retourne None/vide")
except Exception as e:
logger.error(f" Erreur contacts_to_dict: {e}", exc_info=True)
contact_dict = None
if not contact_dict:
logger.info(" Stratégie 3: Construction manuelle (fallback)")
contact_dict = self._construire_contact_minimal(
numero_client=numero_client,
contact_no=contact_no,
n_contact=n_contact,
nom=nom,
prenom=prenom,
contact_data=contact_data,
)
logger.info(
f" Contact minimal construit: {len(contact_dict)} champs"
)
if not contact_dict or not isinstance(contact_dict, dict):
logger.error(
f" ERREUR: contact_dict invalide: type={type(contact_dict)}, value={contact_dict}"
)
raise RuntimeError(
"Impossible de construire le dictionnaire de retour"
)
contact_dict["est_defaut"] = est_defaut
logger.info(" DICT FINAL AVANT RETURN:")
logger.info(f" Type: {type(contact_dict)}")
logger.info(f" Len: {len(contact_dict)}")
logger.info(f" Keys: {list(contact_dict.keys())}")
for key, value in contact_dict.items():
logger.info(f" {key}: {value} (type: {type(value).__name__})")
logger.info(" RETURN contact_dict")
return contact_dict
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def _construire_contact_minimal(
self,
numero_client: str,
contact_no: Optional[int],
n_contact: Optional[int],
nom: str,
prenom: Optional[str],
contact_data: Dict,
) -> Dict:
logger.info(
f" _construire_contact_minimal(client={numero_client}, CT_No={contact_no})"
)
civilite_map_reverse = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"}
civilite_input = contact_data.get("civilite")
civilite_code = None
if civilite_input:
if isinstance(civilite_input, int):
civilite_code = civilite_map_reverse.get(civilite_input)
else:
civilite_code = civilite_input
result = {
"numero": numero_client,
"contact_numero": contact_no,
"n_contact": n_contact,
"civilite": civilite_code,
"nom": nom,
"prenom": prenom,
"fonction": contact_data.get("fonction"),
"service_code": contact_data.get("service_code"),
"telephone": contact_data.get("telephone"),
"portable": contact_data.get("portable"),
"telecopie": contact_data.get("telecopie"),
"email": contact_data.get("email"),
"facebook": contact_data.get("facebook"),
"linkedin": contact_data.get("linkedin"),
"skype": contact_data.get("skype"),
"est_defaut": False,
}
logger.info(
f" Contact minimal: numero={result['numero']}, nom={result['nom']}, email={result['email']}"
)
return result
def modifier_contact(self, numero: str, contact_numero: int, updates: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info(f"[MODIFICATION CONTACT] CT_No={contact_numero}")
logger.info("=" * 80)
logger.info("[1] Chargement du tiers")
factory_client = self.cial.CptaApplication.FactoryClient
factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur
persist_tiers = None
type_tiers = None
try:
logger.info(" Recherche dans Clients...")
persist_tiers = factory_client.ReadNumero(numero)
if persist_tiers:
type_tiers = "Client"
logger.info(" Trouvé comme Client")
except Exception as e:
logger.debug(f" Pas trouvé comme Client: {e}")
if not persist_tiers:
try:
logger.info(" Recherche dans Fournisseurs...")
persist_tiers = factory_fournisseur.ReadNumero(numero)
if persist_tiers:
type_tiers = "Fournisseur"
logger.info(" Trouvé comme Fournisseur")
except Exception as e:
logger.debug(f" Pas trouvé comme Fournisseur: {e}")
if not persist_tiers:
raise ValueError(
f"Le tiers '{numero}' est introuvable dans Sage 100c. "
f"Vérifiez que le code est exact et que le tiers existe "
f"(Client ou Fournisseur)."
)
try:
client_obj = win32com.client.CastTo(persist_tiers, "IBOClient3")
client_obj.Read()
logger.info(f" OK {type_tiers} chargé: {client_obj.CT_Intitule}")
except Exception as e:
raise ValueError(
f"Erreur lors du chargement du {type_tiers} '{numero}': {e}"
)
logger.info("[2] Chargement du contact")
contact = None
nom_recherche = None
prenom_recherche = None
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""SELECT CT_No, CT_Nom, CT_Prenom, cbMarq
FROM F_CONTACTT
WHERE CT_Num = ? AND CT_No = ?""",
[numero, contact_numero],
)
row = cursor.fetchone()
if not row:
raise ValueError(
f"Contact CT_No={contact_numero} non trouve en base"
)
ct_no_sql = row.CT_No
nom_recherche = row.CT_Nom.strip() if row.CT_Nom else ""
prenom_recherche = (
row.CT_Prenom.strip() if row.CT_Prenom else ""
)
cbmarq_sql = row.cbMarq
logger.info(
f" Contact SQL: CT_No={ct_no_sql}, cbMarq={cbmarq_sql}"
)
logger.info(
f" Nom='{nom_recherche}', Prenom='{prenom_recherche}'"
)
except ValueError:
raise
except Exception as e:
raise ValueError(f"Erreur lecture contact en base: {e}")
logger.info(" Strategie 1: Parcours FactoryTiersContact du client...")
try:
factory_contact = client_obj.FactoryTiersContact
if hasattr(factory_contact, "List"):
liste_contacts = factory_contact.List
if liste_contacts and hasattr(liste_contacts, "Count"):
count = liste_contacts.Count
logger.info(f" {count} contact(s) pour ce client")
for i in range(count):
try:
item = liste_contacts.Item(i + 1)
temp_contact = None
for iface in [
"IBOTiersContact3",
"IBOTiersContact",
"IBOContactT3",
]:
try:
temp_contact = win32com.client.CastTo(
item, iface
)
break
except Exception:
continue
if not temp_contact:
continue
temp_contact.Read()
nom_com = getattr(temp_contact, "Nom", "") or ""
prenom_com = (
getattr(temp_contact, "Prenom", "") or ""
)
if (
nom_com.strip().lower() == nom_recherche.lower()
and prenom_com.strip().lower()
== prenom_recherche.lower()
):
contact = temp_contact
logger.info(
f" OK Contact trouve a l'index {i + 1}: '{prenom_com}' '{nom_com}'"
)
break
except Exception as e:
logger.debug(f" Item {i + 1} erreur: {e}")
continue
except Exception as e:
logger.warning(f" Strategie 1 echouee: {e}")
if not contact:
logger.info(" Strategie 2: FactoryDossierContact.ReadNomPrenom...")
try:
factory_dossier = (
self.cial.CptaApplication.FactoryDossierContact
)
persist = factory_dossier.ReadNomPrenom(
nom_recherche, prenom_recherche
)
if persist:
contact = win32com.client.CastTo(
persist, "IBOTiersContact3"
)
contact.Read()
logger.info(" OK Contact charge via ReadNomPrenom")
except Exception as e:
logger.warning(f" Strategie 2 echouee: {e}")
if not contact:
logger.info(" Strategie 3: Variations nom/prenom...")
try:
factory_dossier = (
self.cial.CptaApplication.FactoryDossierContact
)
variations = [
(nom_recherche.upper(), prenom_recherche.upper()),
(nom_recherche.lower(), prenom_recherche.lower()),
(nom_recherche.capitalize(), prenom_recherche.capitalize()),
(nom_recherche, ""),
]
for nom_var, prenom_var in variations:
try:
persist = factory_dossier.ReadNomPrenom(
nom_var, prenom_var
)
if persist:
contact = win32com.client.CastTo(
persist, "IBOTiersContact3"
)
contact.Read()
logger.info(
f" OK Contact trouve avec: '{nom_var}'/'{prenom_var}'"
)
break
except Exception:
continue
except Exception as e:
logger.warning(f" Strategie 3 echouee: {e}")
if not contact:
logger.info(
" Strategie 4: Parcours global FactoryDossierContact..."
)
try:
factory_dossier = (
self.cial.CptaApplication.FactoryDossierContact
)
if hasattr(factory_dossier, "List"):
liste = factory_dossier.List
if liste and hasattr(liste, "Count"):
count = min(liste.Count, 500)
logger.info(f" Parcours de {count} contacts...")
for i in range(count):
try:
item = liste.Item(i + 1)
temp = win32com.client.CastTo(
item, "IBOTiersContact3"
)
temp.Read()
nom = getattr(temp, "Nom", "") or ""
prenom = getattr(temp, "Prenom", "") or ""
if (
nom.strip().lower() == nom_recherche.lower()
and prenom.strip().lower()
== prenom_recherche.lower()
):
contact = temp
logger.info(
f" OK Contact trouve a l'index global {i + 1}"
)
break
except Exception:
continue
except Exception as e:
logger.warning(f" Strategie 4 echouee: {e}")
if not contact:
logger.error(
f" ECHEC: Impossible de charger le contact CT_No={contact_numero}"
)
raise ValueError(
f"Contact CT_No={contact_numero} introuvable via COM. "
f"Nom='{nom_recherche}', Prenom='{prenom_recherche}'"
)
logger.info(f" OK Contact charge: {contact.Nom}")
logger.info("[3] Application des modifications")
modifications_appliquees = []
if "civilite" in updates:
civilite_map = {"M.": 0, "Mme": 1, "Mlle": 2, "Societe": 3}
civilite_code = civilite_map.get(updates["civilite"])
if civilite_code is not None:
try:
contact.Civilite = civilite_code
logger.info(f" Civilite = {civilite_code}")
modifications_appliquees.append("civilite")
except Exception as e:
logger.warning(f" WARN Civilite: {e}")
if "nom" in updates:
nom = _clean_str(updates["nom"], 35)
if nom:
try:
contact.Nom = nom
logger.info(f" Nom = {nom}")
modifications_appliquees.append("nom")
except Exception as e:
logger.warning(f" WARN Nom: {e}")
if "prenom" in updates:
prenom = _clean_str(updates["prenom"], 35)
try:
contact.Prenom = prenom
logger.info(f" Prenom = {prenom}")
modifications_appliquees.append("prenom")
except Exception as e:
logger.warning(f" WARN Prenom: {e}")
if "fonction" in updates:
fonction = _clean_str(updates["fonction"], 35)
try:
contact.Fonction = fonction
logger.info(f" Fonction = {fonction}")
modifications_appliquees.append("fonction")
except Exception as e:
logger.warning(f" WARN Fonction: {e}")
if "service_code" in updates:
service = _safe_int(updates["service_code"])
if service is not None and hasattr(contact, "ServiceContact"):
try:
contact.ServiceContact = service
logger.info(f" ServiceContact = {service}")
modifications_appliquees.append("service_code")
except Exception as e:
logger.warning(f" WARN ServiceContact: {e}")
if hasattr(contact, "Telecom"):
try:
telecom = contact.Telecom
if "telephone" in updates:
telephone = _clean_str(updates["telephone"], 21)
if _try_set_attribute(telecom, "Telephone", telephone):
logger.info(f" Telephone = {telephone}")
modifications_appliquees.append("telephone")
if "portable" in updates:
portable = _clean_str(updates["portable"], 21)
if _try_set_attribute(telecom, "Portable", portable):
logger.info(f" Portable = {portable}")
modifications_appliquees.append("portable")
if "email" in updates:
email = _clean_str(updates["email"], 69)
if _try_set_attribute(telecom, "EMail", email):
logger.info(f" EMail = {email}")
modifications_appliquees.append("email")
if "telecopie" in updates:
fax = _clean_str(updates["telecopie"], 21)
if _try_set_attribute(telecom, "Telecopie", fax):
logger.info(f" Telecopie = {fax}")
modifications_appliquees.append("telecopie")
except Exception as e:
logger.warning(f" WARN Telecom: {e}")
if "facebook" in updates:
facebook = _clean_str(updates["facebook"], 69)
try:
contact.Facebook = facebook
logger.info(f" Facebook = {facebook}")
modifications_appliquees.append("facebook")
except Exception as e:
logger.warning(f" WARN Facebook: {e}")
if "linkedin" in updates:
linkedin = _clean_str(updates["linkedin"], 69)
try:
contact.LinkedIn = linkedin
logger.info(f" LinkedIn = {linkedin}")
modifications_appliquees.append("linkedin")
except Exception as e:
logger.warning(f" WARN LinkedIn: {e}")
if "skype" in updates:
skype = _clean_str(updates["skype"], 69)
try:
contact.Skype = skype
logger.info(f" Skype = {skype}")
modifications_appliquees.append("skype")
except Exception as e:
logger.warning(f" WARN Skype: {e}")
logger.info(
f" Modifications preparees: {', '.join(modifications_appliquees) if modifications_appliquees else 'aucune'}"
)
logger.info("[4] Enregistrement")
try:
contact.Write()
logger.info(" Write() reussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f" ERROR Write: {error_detail}")
raise RuntimeError(f"Echec modification contact: {error_detail}")
try:
contact.Read()
logger.info(" Read() reussi")
except Exception as e:
logger.warning(f" WARN Read() apres Write: {e}")
est_defaut_demande = updates.get("est_defaut")
est_actuellement_defaut = False
if est_defaut_demande:
logger.info("[5] Definition comme contact par defaut")
try:
nom_complet = (
f"{contact.Prenom} {contact.Nom}".strip()
if contact.Prenom
else contact.Nom
)
persist_client = factory_client.ReadNumero(numero)
client_obj = win32com.client.CastTo(
persist_client, "IBOClient3"
)
client_obj.Read()
client_obj.CT_Contact = nom_complet
logger.info(f" CT_Contact = '{nom_complet}'")
if hasattr(client_obj, "CT_NoContact"):
try:
client_obj.CT_NoContact = contact_numero
logger.info(f" CT_NoContact = {contact_numero}")
except Exception as e:
logger.warning(f" WARN CT_NoContact: {e}")
client_obj.Write()
client_obj.Read()
logger.info(" OK Contact par defaut defini")
est_actuellement_defaut = True
except Exception as e:
logger.warning(f" WARN Echec definition contact defaut: {e}")
logger.info("[6] Construction du retour")
contact_dict = None
try:
with self._get_sql_connection() as conn:
contact_dict = _lire_contact_depuis_base(
conn, numero_client=numero, contact_no=contact_numero
)
if contact_dict:
logger.info(" Lecture base reussie")
except Exception as e:
logger.warning(f" Lecture base echouee: {e}")
if not contact_dict:
try:
contact_dict = contacts_to_dict(
contact,
numero_client=numero,
contact_numero=contact_numero,
n_contact=None,
)
if contact_dict:
logger.info(" contacts_to_dict reussi")
except Exception as e:
logger.warning(f" contacts_to_dict echoue: {e}")
if not contact_dict:
logger.info(" Construction manuelle du retour")
contact_dict = {
"numero": numero,
"contact_numero": contact_numero,
"n_contact": None,
"civilite": None,
"nom": getattr(contact, "Nom", nom_recherche),
"prenom": getattr(contact, "Prenom", prenom_recherche),
"fonction": getattr(contact, "Fonction", None),
"service_code": None,
"telephone": None,
"portable": None,
"telecopie": None,
"email": None,
"facebook": None,
"linkedin": None,
"skype": None,
}
if hasattr(contact, "Telecom"):
try:
telecom = contact.Telecom
contact_dict["telephone"] = getattr(
telecom, "Telephone", None
)
contact_dict["portable"] = getattr(
telecom, "Portable", None
)
contact_dict["email"] = getattr(telecom, "EMail", None)
contact_dict["telecopie"] = getattr(
telecom, "Telecopie", None
)
except Exception:
pass
contact_dict["est_defaut"] = est_actuellement_defaut
logger.info("=" * 80)
logger.info(f"[SUCCES] Contact modifie: CT_No={contact_numero}")
logger.info(
f" Modifications: {', '.join(modifications_appliquees) if modifications_appliquees else 'aucune'}"
)
logger.info("=" * 80)
return contact_dict
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def definir_contact_defaut(self, numero: str, contact_numero: int) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info(
f"[DEFINIR CONTACT PAR DEFAUT] Client={numero}, Contact={contact_numero}"
)
logger.info("=" * 80)
logger.info("[1] Recuperation infos contact")
nom_contact = None
prenom_contact = None
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT CT_Nom, CT_Prenom FROM F_CONTACTT WHERE CT_Num = ? AND CT_No = ?",
[numero, contact_numero],
)
row = cursor.fetchone()
if not row:
raise ValueError(
f"Contact CT_No={contact_numero} non trouve"
)
nom_contact = row.CT_Nom.strip() if row.CT_Nom else ""
prenom_contact = row.CT_Prenom.strip() if row.CT_Prenom else ""
nom_complet = (
f"{prenom_contact} {nom_contact}".strip()
if prenom_contact
else nom_contact
)
logger.info(f" OK Contact trouve: {nom_complet}")
except Exception as e:
raise ValueError(f"Contact introuvable: {e}")
logger.info("[2] Chargement du client")
factory_client = self.cial.CptaApplication.FactoryClient
try:
persist_client = factory_client.ReadNumero(numero)
if not persist_client:
raise ValueError(f"Client {numero} non trouve")
client = win32com.client.CastTo(persist_client, "IBOClient3")
client.Read()
logger.info(f" OK Client charge: {client.CT_Intitule}")
except Exception as e:
raise ValueError(f"Client introuvable: {e}")
logger.info("[3] Definition du contact par defaut")
ancien_contact = getattr(client, "CT_Contact", "")
client.CT_Contact = nom_complet
logger.info(f" CT_Contact: '{ancien_contact}' -> '{nom_complet}'")
if hasattr(client, "CT_NoContact"):
try:
client.CT_NoContact = contact_numero
logger.info(f" CT_NoContact = {contact_numero}")
except Exception:
pass
logger.info("[4] Enregistrement")
try:
client.Write()
client.Read()
logger.info(" OK Client mis a jour")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
raise RuntimeError(f"Echec mise a jour: {error_detail}")
logger.info("=" * 80)
logger.info(f"[SUCCES] Contact par defaut: {nom_complet}")
logger.info("=" * 80)
return {
"numero": numero,
"contact_numero": contact_numero,
"contact_nom": nom_complet,
"client_intitule": client.CT_Intitule,
"est_defaut": True,
"date_modification": datetime.now().isoformat(),
}
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def lister_contacts(self, numero: str) -> List[Dict]:
try:
with self._get_sql_connection() as conn:
return _get_contacts(numero, conn)
except Exception as e:
logger.error(f"Erreur liste contacts: {e}")
raise RuntimeError(f"Erreur lecture contacts: {str(e)}")
def supprimer_contact(self, numero: str, contact_numero: int) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info(f"[SUPPRESSION CONTACT] CT_No={contact_numero}")
logger.info("=" * 80)
logger.info("[1] Recuperation infos contact")
nom_contact = None
prenom_contact = None
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT CT_Nom, CT_Prenom FROM F_CONTACTT WHERE CT_Num = ? AND CT_No = ?",
[numero, contact_numero],
)
row = cursor.fetchone()
if not row:
raise ValueError(
f"Contact CT_No={contact_numero} non trouve"
)
nom_contact = row.CT_Nom.strip() if row.CT_Nom else ""
prenom_contact = row.CT_Prenom.strip() if row.CT_Prenom else ""
logger.info(
f" OK Contact trouve: {prenom_contact} {nom_contact}"
)
except Exception as e:
raise ValueError(f"Contact introuvable: {e}")
logger.info("[2] Chargement du contact")
factory_dossier = self.cial.CptaApplication.FactoryDossierContact
try:
persist = factory_dossier.ReadNomPrenom(nom_contact, prenom_contact)
if not persist:
raise ValueError("Contact non trouvable via ReadNomPrenom")
contact = win32com.client.CastTo(persist, "IBOTiersContact3")
contact.Read()
logger.info(f" OK Contact charge: {contact.Nom}")
except Exception as e:
raise ValueError(f"Contact introuvable: {e}")
logger.info("[3] Suppression")
try:
contact.Remove()
logger.info(" OK Remove() reussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f" ERROR Remove: {error_detail}")
raise RuntimeError(f"Echec suppression contact: {error_detail}")
logger.info("=" * 80)
logger.info(
f"[SUCCES] Contact supprime: {prenom_contact} {nom_contact}"
)
logger.info("=" * 80)
return {
"numero": numero,
"contact_numero": contact_numero,
"nom": nom_contact,
"prenom": prenom_contact,
"supprime": True,
"date_suppression": datetime.now().isoformat(),
}
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def creer_client(self, client_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non etablie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info("[CREATION CLIENT SAGE - DIAGNOSTIC COMPLET]")
logger.info("=" * 80)
def clean_str(value, max_len: int) -> str:
if value is None or str(value).lower() in ("none", "null", ""):
return ""
return str(value)[:max_len].strip()
def safe_int(value, default=None):
if value is None:
return default
try:
return int(value)
except (ValueError, TypeError):
return default
def safe_float(value, default=None):
if value is None:
return default
try:
return float(value)
except (ValueError, TypeError):
return default
def try_set_attribute(obj, attr_name, value, variants=None):
"""Essaie de definir un attribut avec plusieurs variantes de noms"""
if variants is None:
variants = [attr_name]
else:
variants = [attr_name] + variants
for variant in variants:
try:
if hasattr(obj, variant):
setattr(obj, variant, value)
logger.debug(f" {variant} = {value} [OK]")
return True
except Exception as e:
logger.debug(f" {variant} echec: {str(e)[:50]}")
return False
if not client_data.get("intitule"):
raise ValueError("intitule obligatoire")
if not client_data.get("numero"):
raise ValueError("numero obligatoire")
intitule = clean_str(client_data["intitule"], 69)
numero = clean_str(client_data["numero"], 17).upper()
type_tiers = safe_int(client_data.get("type_tiers"), 0)
logger.info("[ETAPE 1] CREATION OBJET")
factory_map = {
0: ("FactoryClient", "IBOClient3"),
1: ("FactoryFourniss", "IBOFournisseur3"),
2: ("FactorySalarie", "IBOSalarie3"),
3: ("FactoryAutre", "IBOAutre3"),
}
factory_name, interface_name = factory_map[type_tiers]
factory = getattr(self.cial.CptaApplication, factory_name)
persist = factory.Create()
client = win32com.client.CastTo(persist, interface_name)
logger.info(f" Objet cree: {interface_name}")
logger.info("[ETAPE 2] CONFIGURATION OBLIGATOIRE")
client.CT_Intitule = intitule
client.CT_Num = numero
logger.info(f" CT_Num = {numero}")
logger.info(f" CT_Intitule = {intitule}")
qualite = clean_str(client_data.get("qualite", "CLI"), 17)
if qualite:
client.CT_Qualite = qualite
logger.info(f" CT_Qualite = {qualite}")
client.SetDefault()
logger.info(" SetDefault() applique")
if client_data.get("raccourci"):
raccourci = clean_str(client_data["raccourci"], 7).upper().strip()
try:
factory_client = self.cial.CptaApplication.FactoryClient
exist_client = factory_client.ReadRaccourci(raccourci)
if exist_client:
logger.warning(
f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]"
)
else:
client.CT_Raccourci = raccourci
logger.info(f" CT_Raccourci = {raccourci} [OK]")
except Exception:
try:
client.CT_Raccourci = raccourci
logger.info(f" CT_Raccourci = {raccourci} [OK]")
except Exception as e2:
logger.warning(
f" CT_Raccourci = {raccourci} [ECHEC: {e2}]"
)
try:
if not hasattr(client, "CT_Type") or client.CT_Type is None:
client.CT_Type = type_tiers
logger.info(f" CT_Type force a {type_tiers}")
except Exception:
pass
COMPTES_DEFAUT = {0: "4110000", 1: "4010000", 2: "421", 3: "471"}
compte = clean_str(
client_data.get("compte_general")
or COMPTES_DEFAUT.get(type_tiers, "4110000"),
13,
)
factory_compte = self.cial.CptaApplication.FactoryCompteG
compte_trouve = False
comptes_a_tester = [
compte,
COMPTES_DEFAUT.get(type_tiers, "4110000"),
"4110000",
"411000",
"411",
"4010000",
"401000",
"401",
]
for test_compte in comptes_a_tester:
try:
persist_compte = factory_compte.ReadNumero(test_compte)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
type_compte = getattr(compte_obj, "CG_Type", None)
if type_compte == 0:
client.CompteGPrinc = compte_obj
compte = test_compte
compte_trouve = True
logger.info(
f" CompteGPrinc = {test_compte} (Type: {type_compte}) [OK]"
)
break
else:
logger.debug(
f" Compte {test_compte} - Type {type_compte} incompatible"
)
except Exception as e:
logger.debug(f" Compte {test_compte} - erreur: {e}")
if not compte_trouve:
raise RuntimeError("Aucun compte general valide trouve")
logger.info(" Configuration categories:")
try:
factory_cat_tarif = self.cial.CptaApplication.FactoryCategorieTarif
for cat_id in ["0", "1"]:
try:
persist_cat = factory_cat_tarif.ReadIntitule(cat_id)
if persist_cat:
cat_tarif_obj = win32com.client.CastTo(
persist_cat, "IBOCategorieTarif3"
)
cat_tarif_obj.Read()
client.CatTarif = cat_tarif_obj
logger.info(f" CatTarif = {cat_id} [OK]")
break
except Exception:
continue
except Exception as e:
logger.warning(f" CatTarif erreur: {e}")
try:
factory_cat_compta = (
self.cial.CptaApplication.FactoryCategorieCompta
)
for cat_id in ["0", "1"]:
try:
persist_cat = factory_cat_compta.ReadIntitule(cat_id)
if persist_cat:
cat_compta_obj = win32com.client.CastTo(
persist_cat, "IBOCategorieCompta3"
)
cat_compta_obj.Read()
client.CatCompta = cat_compta_obj
logger.info(f" CatCompta = {cat_id} [OK]")
break
except Exception:
continue
except Exception as e:
logger.warning(f" CatCompta erreur: {e}")
logger.info("[ETAPE 3] IDENTIFICATION")
if client_data.get("classement"):
try_set_attribute(
client,
"CT_Classement",
clean_str(client_data["classement"], 17),
)
if client_data.get("raccourci"):
raccourci = clean_str(client_data["raccourci"], 7).upper()
try_set_attribute(client, "CT_Raccourci", raccourci)
if client_data.get("siret"):
try_set_attribute(
client, "CT_Siret", clean_str(client_data["siret"], 15)
)
if client_data.get("tva_intra"):
try_set_attribute(
client,
"CT_Identifiant",
clean_str(client_data["tva_intra"], 25),
)
if client_data.get("code_naf"):
try_set_attribute(
client, "CT_Ape", clean_str(client_data["code_naf"], 7)
)
logger.info("[ETAPE 4] ADRESSE")
if client_data.get("contact"):
contact_nom = clean_str(client_data["contact"], 35)
try:
client.CT_Contact = contact_nom
logger.info(f" CT_Contact (client) = {contact_nom} [OK]")
except Exception as e:
logger.warning(f" CT_Contact (client) [ECHEC: {e}]")
try:
adresse_obj = client.Adresse
adresse_obj.Contact = contact_nom
logger.info(f" Contact (adresse) = {contact_nom} [OK]")
except Exception as e:
logger.warning(f" Contact (adresse) [ECHEC: {e}]")
try:
adresse_obj = client.Adresse
logger.info(" Objet Adresse OK")
if client_data.get("adresse"):
adresse_obj.Adresse = clean_str(client_data["adresse"], 35)
if client_data.get("complement"):
adresse_obj.Complement = clean_str(
client_data["complement"], 35
)
if client_data.get("code_postal"):
adresse_obj.CodePostal = clean_str(
client_data["code_postal"], 9
)
if client_data.get("ville"):
adresse_obj.Ville = clean_str(client_data["ville"], 35)
if client_data.get("region"):
adresse_obj.CodeRegion = clean_str(client_data["region"], 25)
if client_data.get("pays"):
adresse_obj.Pays = clean_str(client_data["pays"], 35)
except Exception as e:
logger.error(f" Adresse erreur: {e}")
logger.info("[ETAPE 5] TELECOM")
try:
telecom_obj = client.Telecom
logger.info(" Objet Telecom OK")
if client_data.get("telephone"):
telecom_obj.Telephone = clean_str(client_data["telephone"], 21)
if client_data.get("telecopie"):
telecom_obj.Telecopie = clean_str(client_data["telecopie"], 21)
if client_data.get("email"):
telecom_obj.EMail = clean_str(client_data["email"], 69)
if client_data.get("site_web"):
telecom_obj.Site = clean_str(client_data["site_web"], 69)
if client_data.get("portable"):
portable = clean_str(client_data["portable"], 21)
try_set_attribute(telecom_obj, "Portable", portable)
logger.info(f" Portable = {portable}")
if client_data.get("facebook"):
facebook = clean_str(client_data["facebook"], 69)
if not try_set_attribute(telecom_obj, "Facebook", facebook):
try_set_attribute(client, "CT_Facebook", facebook)
logger.info(f" Facebook = {facebook}")
if client_data.get("linkedin"):
linkedin = clean_str(client_data["linkedin"], 69)
if not try_set_attribute(telecom_obj, "LinkedIn", linkedin):
try_set_attribute(client, "CT_LinkedIn", linkedin)
logger.info(f" LinkedIn = {linkedin}")
except Exception as e:
logger.error(f" Telecom erreur: {e}")
logger.info("[ETAPE 6] TAUX")
for i in range(1, 5):
val = client_data.get(f"taux{i:02d}")
if val is not None:
try_set_attribute(client, f"CT_Taux{i:02d}", safe_float(val))
logger.info("[ETAPE 7] STATISTIQUES")
stat01 = client_data.get("statistique01") or client_data.get("secteur")
if stat01:
try_set_attribute(client, "CT_Statistique01", clean_str(stat01, 21))
for i in range(2, 11):
val = client_data.get(f"statistique{i:02d}")
if val:
try_set_attribute(
client, f"CT_Statistique{i:02d}", clean_str(val, 21)
)
logger.info("[ETAPE 8] COMMERCIAL")
if client_data.get("encours_autorise"):
try_set_attribute(
client,
"CT_Encours",
safe_float(client_data["encours_autorise"]),
)
if client_data.get("assurance_credit"):
try_set_attribute(
client,
"CT_Assurance",
safe_float(client_data["assurance_credit"]),
)
if client_data.get("langue") is not None:
try_set_attribute(
client, "CT_Langue", safe_int(client_data["langue"])
)
if client_data.get("commercial_code") is not None:
co_no = safe_int(client_data["commercial_code"])
if not try_set_attribute(client, "CO_No", co_no):
try:
factory_collab = (
self.cial.CptaApplication.FactoryCollaborateur
)
persist_collab = factory_collab.ReadIntitule(str(co_no))
if persist_collab:
collab_obj = win32com.client.CastTo(
persist_collab, "IBOCollaborateur3"
)
collab_obj.Read()
client.Collaborateur = collab_obj
logger.debug(
f" Collaborateur (objet) = {co_no} [OK]"
)
except Exception as e:
logger.debug(f" Collaborateur echec: {e}")
logger.info("[ETAPE 9] FACTURATION")
try_set_attribute(
client,
"CT_Lettrage",
1 if client_data.get("lettrage_auto", True) else 0,
)
try_set_attribute(
client, "CT_Sommeil", 0 if client_data.get("est_actif", True) else 1
)
try_set_attribute(
client, "CT_Facture", safe_int(client_data.get("type_facture", 1))
)
if client_data.get("est_prospect") is not None:
try_set_attribute(
client, "CT_Prospect", 1 if client_data["est_prospect"] else 0
)
factu_map = {
"CT_BLFact": "bl_en_facture",
"CT_Saut": "saut_page",
"CT_ValidEch": "validation_echeance",
"CT_ControlEnc": "controle_encours",
"CT_NotRappel": "exclure_relance",
"CT_NotPenal": "exclure_penalites",
"CT_BonAPayer": "bon_a_payer",
}
for attr, key in factu_map.items():
if client_data.get(key) is not None:
try_set_attribute(client, attr, safe_int(client_data[key]))
logger.info("[ETAPE 10] LOGISTIQUE")
logistique_map = {
"CT_PrioriteLivr": "priorite_livraison",
"CT_LivrPartielle": "livraison_partielle",
"CT_DelaiTransport": "delai_transport",
"CT_DelaiAppro": "delai_appro",
}
for attr, key in logistique_map.items():
if client_data.get(key) is not None:
try_set_attribute(client, attr, safe_int(client_data[key]))
if client_data.get("commentaire"):
try_set_attribute(
client,
"CT_Commentaire",
clean_str(client_data["commentaire"], 35),
)
logger.info("[ETAPE 12] ANALYTIQUE")
if client_data.get("section_analytique"):
try_set_attribute(
client,
"CA_Num",
clean_str(client_data["section_analytique"], 13),
)
logger.info("[ETAPE 13] ORGANISATION")
if client_data.get("mode_reglement_code") is not None:
mr_no = safe_int(client_data["mode_reglement_code"])
if not try_set_attribute(client, "MR_No", mr_no):
try:
factory_mr = self.cial.CptaApplication.FactoryModeRegl
persist_mr = factory_mr.ReadIntitule(str(mr_no))
if persist_mr:
mr_obj = win32com.client.CastTo(
persist_mr, "IBOModeRegl3"
)
mr_obj.Read()
client.ModeRegl = mr_obj
logger.debug(f" ModeRegl (objet) = {mr_no} [OK]")
except Exception as e:
logger.debug(f" ModeRegl echec: {e}")
if client_data.get("surveillance_active") is not None:
surveillance = 1 if client_data["surveillance_active"] else 0
try:
client.CT_Surveillance = surveillance
logger.info(f" CT_Surveillance = {surveillance} [OK]")
except Exception as e:
logger.warning(f" CT_Surveillance [ECHEC: {e}]")
if client_data.get("coface"):
coface = clean_str(client_data["coface"], 25)
try:
client.CT_Coface = coface
logger.info(f" CT_Coface = {coface} [OK]")
except Exception as e:
logger.warning(f" CT_Coface [ECHEC: {e}]")
if client_data.get("forme_juridique"):
try_set_attribute(
client,
"CT_SvFormeJuri",
clean_str(client_data["forme_juridique"], 33),
)
if client_data.get("effectif"):
try_set_attribute(
client, "CT_SvEffectif", clean_str(client_data["effectif"], 11)
)
if client_data.get("sv_regularite"):
try_set_attribute(
client, "CT_SvRegul", clean_str(client_data["sv_regularite"], 3)
)
if client_data.get("sv_cotation"):
try_set_attribute(
client,
"CT_SvCotation",
clean_str(client_data["sv_cotation"], 5),
)
if client_data.get("sv_objet_maj"):
try_set_attribute(
client,
"CT_SvObjetMaj",
clean_str(client_data["sv_objet_maj"], 61),
)
ca = client_data.get("ca_annuel") or client_data.get(
"sv_chiffre_affaires"
)
if ca:
try_set_attribute(client, "CT_SvCA", safe_float(ca))
if client_data.get("sv_resultat"):
try_set_attribute(
client, "CT_SvResultat", safe_float(client_data["sv_resultat"])
)
logger.info("=" * 80)
logger.info("[DIAGNOSTIC PRE-WRITE]")
champs_diagnostic = [
"CT_Num",
"CT_Intitule",
"CT_Type",
"CT_Qualite",
"CT_Facture",
"CT_Lettrage",
"CT_Sommeil",
]
for champ in champs_diagnostic:
try:
valeur = getattr(client, champ, "ATTRIBUT_INEXISTANT")
logger.info(f" {champ}: {valeur}")
except Exception as e:
logger.info(f" {champ}: ERREUR ({str(e)[:50]})")
try:
compte_obj = client.CompteGPrinc
if compte_obj:
logger.info(f" CompteGPrinc.CG_Num: {compte_obj.CG_Num}")
logger.info(f" CompteGPrinc.CG_Type: {compte_obj.CG_Type}")
else:
logger.error(" CompteGPrinc: NULL !!!")
except Exception as e:
logger.error(f" CompteGPrinc: ERREUR - {e}")
logger.info("=" * 80)
logger.info("[WRITE]")
try:
client.Write()
client.Read()
logger.info("[OK] Write reussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f"[ERREUR] {error_detail}")
raise RuntimeError(f"Echec Write(): {error_detail}")
num_final = getattr(client, "CT_Num", numero)
logger.info("=" * 80)
logger.info(f"[SUCCES] CLIENT CREE: {num_final}")
logger.info("=" * 80)
return {
"numero": num_final,
"intitule": intitule,
"type_tiers": type_tiers,
"qualite": qualite,
"compte_general": compte,
"date_creation": datetime.now().isoformat(),
}
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def modifier_client(self, code: str, client_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info(f"[MODIFICATION CLIENT SAGE - {code}]")
logger.info("=" * 80)
def clean_str(value, max_len: int) -> str:
if value is None or str(value).lower() in ("none", "null", ""):
return ""
return str(value)[:max_len].strip()
def safe_int(value, default=None):
if value is None:
return default
try:
return int(value)
except (ValueError, TypeError):
return default
def safe_float(value, default=None):
if value is None:
return default
try:
return float(value)
except (ValueError, TypeError):
return default
def try_set_attribute(obj, attr_name, value, variants=None):
"""Essaie de définir un attribut avec plusieurs variantes de noms"""
if variants is None:
variants = [attr_name]
else:
variants = [attr_name] + variants
for variant in variants:
try:
if hasattr(obj, variant):
setattr(obj, variant, value)
logger.debug(f" {variant} = {value} [OK]")
return True
except Exception as e:
logger.debug(f" {variant} echec: {str(e)[:50]}")
return False
champs_modifies = []
logger.info("[ETAPE 1] CHARGEMENT CLIENT")
factory_client = self.cial.CptaApplication.FactoryClient
persist = factory_client.ReadNumero(code)
if not persist:
raise ValueError(f"Client {code} introuvable")
client = win32com.client.CastTo(persist, "IBOClient3")
methodes_client = [m for m in dir(client) if not m.startswith("_")]
logger.info(
f" [DEBUG] Méthodes disponibles sur client: {methodes_client}"
)
lock_methods = [
m
for m in methodes_client
if "lock" in m.lower()
or "read" in m.lower()
or "write" in m.lower()
]
logger.info(f" [DEBUG] Méthodes lock/read/write: {lock_methods}")
max_retries = 3
retry_delay = 1.0
locked = False
lock_method_used = None
for attempt in range(max_retries):
try:
if hasattr(client, "ReadLock"):
client.ReadLock()
locked = True
lock_method_used = "ReadLock"
logger.info(" Verrouillage via ReadLock() [OK]")
break
elif hasattr(client, "Lock"):
client.Lock()
locked = True
lock_method_used = "Lock"
logger.info(" Verrouillage via Lock() [OK]")
break
elif hasattr(client, "LockRecord"):
client.LockRecord()
locked = True
lock_method_used = "LockRecord"
logger.info(" Verrouillage via LockRecord() [OK]")
break
else:
try:
client.Read(1) # 1 = mode écriture
lock_method_used = "Read(1)"
logger.info(" Verrouillage via Read(1) [OK]")
except TypeError:
client.Read()
lock_method_used = "Read()"
logger.info(
" Read() simple (pas de verrouillage explicite)"
)
break
except Exception as lock_err:
err_str = str(lock_err)
is_lock_error = (
"en cours d'utilisation" in err_str
or "-2512" in err_str
or "locked" in err_str.lower()
)
if is_lock_error:
if attempt < max_retries - 1:
logger.warning(
f" Tentative {attempt + 1}/{max_retries} - "
f"Client verrouillé, attente {retry_delay}s..."
)
time.sleep(retry_delay)
retry_delay *= 2 # Backoff exponentiel
else:
raise RuntimeError(
f"Client {code} verrouillé après {max_retries} tentatives. "
"Vérifiez qu'il n'est pas ouvert dans Sage ou par un autre processus."
)
else:
raise
logger.info(
f" Client chargé: {getattr(client, 'CT_Intitule', '')} (via {lock_method_used})"
)
logger.info("[ETAPE 2] IDENTIFICATION")
if "intitule" in client_data:
intitule = clean_str(client_data["intitule"], 69)
client.CT_Intitule = intitule
champs_modifies.append("intitule")
logger.info(f" CT_Intitule = {intitule}")
if "qualite" in client_data:
qualite = clean_str(client_data["qualite"], 17)
if try_set_attribute(client, "CT_Qualite", qualite):
champs_modifies.append("qualite")
if "classement" in client_data:
if try_set_attribute(
client,
"CT_Classement",
clean_str(client_data["classement"], 17),
):
champs_modifies.append("classement")
if "raccourci" in client_data:
raccourci = clean_str(client_data["raccourci"], 7).upper()
try:
exist_client = factory_client.ReadRaccourci(raccourci)
if exist_client and exist_client.CT_Num != code:
logger.warning(
f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]"
)
else:
if try_set_attribute(client, "CT_Raccourci", raccourci):
champs_modifies.append("raccourci")
except Exception:
if try_set_attribute(client, "CT_Raccourci", raccourci):
champs_modifies.append("raccourci")
if "siret" in client_data:
if try_set_attribute(
client, "CT_Siret", clean_str(client_data["siret"], 15)
):
champs_modifies.append("siret")
if "tva_intra" in client_data:
if try_set_attribute(
client,
"CT_Identifiant",
clean_str(client_data["tva_intra"], 25),
):
champs_modifies.append("tva_intra")
if "code_naf" in client_data:
if try_set_attribute(
client, "CT_Ape", clean_str(client_data["code_naf"], 7)
):
champs_modifies.append("code_naf")
adresse_keys = [
"contact",
"adresse",
"complement",
"code_postal",
"ville",
"region",
"pays",
]
if any(k in client_data for k in adresse_keys):
logger.info("[ETAPE 3] ADRESSE")
try:
if "contact" in client_data:
contact_nom = clean_str(client_data["contact"], 35)
try:
client.CT_Contact = contact_nom
champs_modifies.append("contact (client)")
logger.info(
f" CT_Contact (client) = {contact_nom} [OK]"
)
except Exception as e:
logger.warning(f" CT_Contact (client) [ECHEC: {e}]")
try:
adresse_obj = client.Adresse
adresse_obj.Contact = contact_nom
champs_modifies.append("contact (adresse)")
logger.info(f" Contact (adresse) = {contact_nom} [OK]")
except Exception as e:
logger.warning(f" Contact (adresse) [ECHEC: {e}]")
adresse_obj = client.Adresse
if "adresse" in client_data:
adresse_obj.Adresse = clean_str(client_data["adresse"], 35)
champs_modifies.append("adresse")
if "complement" in client_data:
adresse_obj.Complement = clean_str(
client_data["complement"], 35
)
champs_modifies.append("complement")
if "code_postal" in client_data:
adresse_obj.CodePostal = clean_str(
client_data["code_postal"], 9
)
champs_modifies.append("code_postal")
if "ville" in client_data:
adresse_obj.Ville = clean_str(client_data["ville"], 35)
champs_modifies.append("ville")
if "region" in client_data:
adresse_obj.CodeRegion = clean_str(
client_data["region"], 25
)
champs_modifies.append("region")
if "pays" in client_data:
adresse_obj.Pays = clean_str(client_data["pays"], 35)
champs_modifies.append("pays")
logger.info(
f" Adresse mise à jour ({len([k for k in adresse_keys if k in client_data])} champs)"
)
except Exception as e:
logger.error(f" Adresse erreur: {e}")
telecom_keys = [
"telephone",
"telecopie",
"email",
"site_web",
"portable",
"facebook",
"linkedin",
]
if any(k in client_data for k in telecom_keys):
logger.info("[ETAPE 4] TELECOM")
try:
telecom_obj = client.Telecom
if "telephone" in client_data:
telecom_obj.Telephone = clean_str(
client_data["telephone"], 21
)
champs_modifies.append("telephone")
if "telecopie" in client_data:
telecom_obj.Telecopie = clean_str(
client_data["telecopie"], 21
)
champs_modifies.append("telecopie")
if "email" in client_data:
telecom_obj.EMail = clean_str(client_data["email"], 69)
champs_modifies.append("email")
if "site_web" in client_data:
telecom_obj.Site = clean_str(client_data["site_web"], 69)
champs_modifies.append("site_web")
if "portable" in client_data:
portable = clean_str(client_data["portable"], 21)
if try_set_attribute(telecom_obj, "Portable", portable):
champs_modifies.append("portable")
if "facebook" in client_data:
facebook = clean_str(client_data["facebook"], 69)
if not try_set_attribute(telecom_obj, "Facebook", facebook):
try_set_attribute(client, "CT_Facebook", facebook)
champs_modifies.append("facebook")
if "linkedin" in client_data:
linkedin = clean_str(client_data["linkedin"], 69)
if not try_set_attribute(telecom_obj, "LinkedIn", linkedin):
try_set_attribute(client, "CT_LinkedIn", linkedin)
champs_modifies.append("linkedin")
logger.info(
f" Telecom mis à jour ({len([k for k in telecom_keys if k in client_data])} champs)"
)
except Exception as e:
logger.error(f" Telecom erreur: {e}")
if "compte_general" in client_data:
logger.info("[ETAPE 5] COMPTE GENERAL")
compte = clean_str(client_data["compte_general"], 13)
factory_compte = self.cial.CptaApplication.FactoryCompteG
try:
persist_compte = factory_compte.ReadNumero(compte)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
type_compte = getattr(compte_obj, "CG_Type", None)
if type_compte == 0:
client.CompteGPrinc = compte_obj
champs_modifies.append("compte_general")
logger.info(f" CompteGPrinc = {compte} [OK]")
else:
logger.warning(
f" Compte {compte} - Type {type_compte} incompatible"
)
except Exception as e:
logger.warning(f" CompteGPrinc erreur: {e}")
if (
"categorie_tarifaire" in client_data
or "categorie_comptable" in client_data
):
logger.info("[ETAPE 6] CATEGORIES")
if "categorie_tarifaire" in client_data:
try:
cat_id = str(client_data["categorie_tarifaire"])
factory_cat_tarif = (
self.cial.CptaApplication.FactoryCategorieTarif
)
persist_cat = factory_cat_tarif.ReadIntitule(cat_id)
if persist_cat:
cat_tarif_obj = win32com.client.CastTo(
persist_cat, "IBOCategorieTarif3"
)
cat_tarif_obj.Read()
client.CatTarif = cat_tarif_obj
champs_modifies.append("categorie_tarifaire")
logger.info(f" CatTarif = {cat_id} [OK]")
except Exception as e:
logger.warning(f" CatTarif erreur: {e}")
if "categorie_comptable" in client_data:
try:
cat_id = str(client_data["categorie_comptable"])
factory_cat_compta = (
self.cial.CptaApplication.FactoryCategorieCompta
)
persist_cat = factory_cat_compta.ReadIntitule(cat_id)
if persist_cat:
cat_compta_obj = win32com.client.CastTo(
persist_cat, "IBOCategorieCompta3"
)
cat_compta_obj.Read()
client.CatCompta = cat_compta_obj
champs_modifies.append("categorie_comptable")
logger.info(f" CatCompta = {cat_id} [OK]")
except Exception as e:
logger.warning(f" CatCompta erreur: {e}")
taux_modifies = False
for i in range(1, 5):
key = f"taux{i:02d}"
if key in client_data:
if not taux_modifies:
logger.info("[ETAPE 7] TAUX")
taux_modifies = True
val = safe_float(client_data[key])
if try_set_attribute(client, f"CT_Taux{i:02d}", val):
champs_modifies.append(key)
stat_modifies = False
stat01 = client_data.get("statistique01") or client_data.get("secteur")
if stat01:
if not stat_modifies:
logger.info("[ETAPE 8] STATISTIQUES")
stat_modifies = True
if try_set_attribute(
client, "CT_Statistique01", clean_str(stat01, 21)
):
champs_modifies.append("statistique01")
for i in range(2, 11):
key = f"statistique{i:02d}"
if key in client_data:
if not stat_modifies:
logger.info("[ETAPE 8] STATISTIQUES")
stat_modifies = True
if try_set_attribute(
client,
f"CT_Statistique{i:02d}",
clean_str(client_data[key], 21),
):
champs_modifies.append(key)
commercial_keys = [
"encours_autorise",
"assurance_credit",
"langue",
"commercial_code",
]
if any(k in client_data for k in commercial_keys):
logger.info("[ETAPE 9] COMMERCIAL")
if "encours_autorise" in client_data:
if try_set_attribute(
client,
"CT_Encours",
safe_float(client_data["encours_autorise"]),
):
champs_modifies.append("encours_autorise")
if "assurance_credit" in client_data:
if try_set_attribute(
client,
"CT_Assurance",
safe_float(client_data["assurance_credit"]),
):
champs_modifies.append("assurance_credit")
if "langue" in client_data:
if try_set_attribute(
client, "CT_Langue", safe_int(client_data["langue"])
):
champs_modifies.append("langue")
if "commercial_code" in client_data:
co_no = safe_int(client_data["commercial_code"])
if not try_set_attribute(client, "CO_No", co_no):
try:
factory_collab = (
self.cial.CptaApplication.FactoryCollaborateur
)
persist_collab = factory_collab.ReadIntitule(str(co_no))
if persist_collab:
collab_obj = win32com.client.CastTo(
persist_collab, "IBOCollaborateur3"
)
collab_obj.Read()
client.Collaborateur = collab_obj
champs_modifies.append("commercial_code")
logger.info(f" Collaborateur = {co_no} [OK]")
except Exception as e:
logger.warning(f" Collaborateur erreur: {e}")
facturation_keys = [
"lettrage_auto",
"est_actif",
"type_facture",
"est_prospect",
"bl_en_facture",
"saut_page",
"validation_echeance",
"controle_encours",
"exclure_relance",
"exclure_penalites",
"bon_a_payer",
]
if any(k in client_data for k in facturation_keys):
logger.info("[ETAPE 10] FACTURATION")
if "lettrage_auto" in client_data:
if try_set_attribute(
client,
"CT_Lettrage",
1 if client_data["lettrage_auto"] else 0,
):
champs_modifies.append("lettrage_auto")
if "est_actif" in client_data:
if try_set_attribute(
client, "CT_Sommeil", 0 if client_data["est_actif"] else 1
):
champs_modifies.append("est_actif")
if "type_facture" in client_data:
if try_set_attribute(
client, "CT_Facture", safe_int(client_data["type_facture"])
):
champs_modifies.append("type_facture")
if "est_prospect" in client_data:
if try_set_attribute(
client,
"CT_Prospect",
1 if client_data["est_prospect"] else 0,
):
champs_modifies.append("est_prospect")
factu_map = {
"CT_BLFact": "bl_en_facture",
"CT_Saut": "saut_page",
"CT_ValidEch": "validation_echeance",
"CT_ControlEnc": "controle_encours",
"CT_NotRappel": "exclure_relance",
"CT_NotPenal": "exclure_penalites",
"CT_BonAPayer": "bon_a_payer",
}
for attr, key in factu_map.items():
if key in client_data:
if try_set_attribute(
client, attr, safe_int(client_data[key])
):
champs_modifies.append(key)
logistique_keys = [
"priorite_livraison",
"livraison_partielle",
"delai_transport",
"delai_appro",
]
if any(k in client_data for k in logistique_keys):
logger.info("[ETAPE 11] LOGISTIQUE")
logistique_map = {
"CT_PrioriteLivr": "priorite_livraison",
"CT_LivrPartielle": "livraison_partielle",
"CT_DelaiTransport": "delai_transport",
"CT_DelaiAppro": "delai_appro",
}
for attr, key in logistique_map.items():
if key in client_data:
if try_set_attribute(
client, attr, safe_int(client_data[key])
):
champs_modifies.append(key)
if "commentaire" in client_data:
logger.info("[ETAPE 12] COMMENTAIRE")
if try_set_attribute(
client,
"CT_Commentaire",
clean_str(client_data["commentaire"], 35),
):
champs_modifies.append("commentaire")
if "section_analytique" in client_data:
logger.info("[ETAPE 13] ANALYTIQUE")
if try_set_attribute(
client,
"CA_Num",
clean_str(client_data["section_analytique"], 13),
):
champs_modifies.append("section_analytique")
organisation_keys = [
"mode_reglement_code",
"surveillance_active",
"coface",
"forme_juridique",
"effectif",
"sv_regularite",
"sv_cotation",
"sv_objet_maj",
"ca_annuel",
"sv_chiffre_affaires",
"sv_resultat",
]
if any(k in client_data for k in organisation_keys):
logger.info("[ETAPE 14] ORGANISATION & SURVEILLANCE")
if "mode_reglement_code" in client_data:
mr_no = safe_int(client_data["mode_reglement_code"])
if not try_set_attribute(client, "MR_No", mr_no):
try:
factory_mr = self.cial.CptaApplication.FactoryModeRegl
persist_mr = factory_mr.ReadIntitule(str(mr_no))
if persist_mr:
mr_obj = win32com.client.CastTo(
persist_mr, "IBOModeRegl3"
)
mr_obj.Read()
client.ModeRegl = mr_obj
champs_modifies.append("mode_reglement_code")
logger.info(f" ModeRegl = {mr_no} [OK]")
except Exception as e:
logger.warning(f" ModeRegl erreur: {e}")
if "surveillance_active" in client_data:
surveillance = 1 if client_data["surveillance_active"] else 0
try:
client.CT_Surveillance = surveillance
champs_modifies.append("surveillance_active")
logger.info(f" CT_Surveillance = {surveillance} [OK]")
except Exception as e:
logger.warning(f" CT_Surveillance [ECHEC: {e}]")
if "coface" in client_data:
coface = clean_str(client_data["coface"], 25)
try:
client.CT_Coface = coface
champs_modifies.append("coface")
logger.info(f" CT_Coface = {coface} [OK]")
except Exception as e:
logger.warning(f" CT_Coface [ECHEC: {e}]")
if "forme_juridique" in client_data:
if try_set_attribute(
client,
"CT_SvFormeJuri",
clean_str(client_data["forme_juridique"], 33),
):
champs_modifies.append("forme_juridique")
if "effectif" in client_data:
if try_set_attribute(
client,
"CT_SvEffectif",
clean_str(client_data["effectif"], 11),
):
champs_modifies.append("effectif")
if "sv_regularite" in client_data:
if try_set_attribute(
client,
"CT_SvRegul",
clean_str(client_data["sv_regularite"], 3),
):
champs_modifies.append("sv_regularite")
if "sv_cotation" in client_data:
if try_set_attribute(
client,
"CT_SvCotation",
clean_str(client_data["sv_cotation"], 5),
):
champs_modifies.append("sv_cotation")
if "sv_objet_maj" in client_data:
if try_set_attribute(
client,
"CT_SvObjetMaj",
clean_str(client_data["sv_objet_maj"], 61),
):
champs_modifies.append("sv_objet_maj")
ca = client_data.get("ca_annuel") or client_data.get(
"sv_chiffre_affaires"
)
if ca:
if try_set_attribute(client, "CT_SvCA", safe_float(ca)):
champs_modifies.append("ca_annuel/sv_chiffre_affaires")
if "sv_resultat" in client_data:
if try_set_attribute(
client,
"CT_SvResultat",
safe_float(client_data["sv_resultat"]),
):
champs_modifies.append("sv_resultat")
if not champs_modifies:
logger.warning("Aucun champ à modifier")
if locked:
try:
if hasattr(client, "ReadUnlock"):
client.ReadUnlock()
elif hasattr(client, "Unlock"):
client.Unlock()
except Exception:
pass
return _extraire_client(client)
logger.info("=" * 80)
logger.info(f"[WRITE] {len(champs_modifies)} champs modifiés:")
for i, champ in enumerate(champs_modifies, 1):
logger.info(f" {i}. {champ}")
logger.info("=" * 80)
try:
client.Write()
logger.info("[OK] Write réussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f"[ERREUR] {error_detail}")
raise RuntimeError(f"Echec Write(): {error_detail}")
finally:
if locked:
try:
if hasattr(client, "ReadUnlock"):
client.ReadUnlock()
logger.debug(" ReadUnlock() [OK]")
elif hasattr(client, "Unlock"):
client.Unlock()
logger.debug(" Unlock() [OK]")
elif hasattr(client, "UnlockRecord"):
client.UnlockRecord()
logger.debug(" UnlockRecord() [OK]")
except Exception as unlock_err:
logger.warning(f" Déverrouillage ignoré: {unlock_err}")
client.Read()
logger.info("=" * 80)
logger.info(
f"[SUCCES] CLIENT MODIFIÉ: {code} ({len(champs_modifies)} champs)"
)
logger.info("=" * 80)
return _extraire_client(client)
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def creer_commande_enrichi(self, commande_data: dict) -> Dict:
"""Crée une commande"""
return creer_document_vente(self, commande_data, TypeDocumentVente.COMMANDE)
def modifier_commande(self, numero: str, commande_data: dict) -> Dict:
"""Modifie une commande"""
return modifier_document_vente(
self, numero, commande_data, TypeDocumentVente.COMMANDE
)
def creer_livraison_enrichi(self, livraison_data: dict) -> Dict:
"""Crée un bon de livraison"""
return creer_document_vente(self, livraison_data, TypeDocumentVente.LIVRAISON)
def modifier_livraison(self, numero: str, livraison_data: dict) -> Dict:
"""Modifie un bon de livraison"""
return modifier_document_vente(
self, numero, livraison_data, TypeDocumentVente.LIVRAISON
)
def creer_avoir_enrichi(self, avoir_data: dict) -> Dict:
"""Crée un avoir"""
return creer_document_vente(self, avoir_data, TypeDocumentVente.AVOIR)
def modifier_avoir(self, numero: str, avoir_data: dict) -> Dict:
"""Modifie un avoir"""
return modifier_document_vente(
self, numero, avoir_data, TypeDocumentVente.AVOIR
)
def creer_facture_enrichi(self, facture_data: dict) -> Dict:
"""Crée une facture"""
return creer_document_vente(self, facture_data, TypeDocumentVente.FACTURE)
def modifier_facture(self, numero: str, facture_data: dict) -> Dict:
"""Modifie une facture"""
return modifier_document_vente(
self, numero, facture_data, TypeDocumentVente.FACTURE
)
def creer_article(self, article_data: dict) -> dict:
"""Crée un article dans Sage 100 - Version fusionnée complète"""
with self._com_context(), self._lock_com:
try:
logger.info("[ARTICLE] === CREATION ARTICLE ===")
valide, erreur = valider_donnees_creation(article_data)
if not valide:
raise ValueError(erreur)
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug("Transaction Sage démarrée")
except Exception as e:
logger.debug(f"BeginTrans non disponible : {e}")
try:
depots_disponibles = []
depot_a_utiliser = None
depot_code_demande = article_data.get("depot_code")
try:
factory_depot = self.cial.FactoryDepot
index = 1
while index <= 100:
try:
persist = factory_depot.List(index)
if persist is None:
break
depot_obj = win32com.client.CastTo(persist, "IBODepot3")
depot_obj.Read()
code = getattr(depot_obj, "DE_Code", "").strip()
if not code:
index += 1
continue
numero = int(getattr(depot_obj, "Compteur", 0))
intitule = getattr(
depot_obj, "DE_Intitule", f"Depot {code}"
)
depot_info = {
"code": code,
"numero": numero,
"intitule": intitule,
"objet": depot_obj,
}
depots_disponibles.append(depot_info)
if depot_code_demande and code == depot_code_demande:
depot_a_utiliser = depot_info
elif not depot_code_demande and not depot_a_utiliser:
depot_a_utiliser = depot_info
index += 1
except Exception as e:
if "Acces refuse" in str(e):
break
index += 1
except Exception as e:
logger.warning(f"[DEPOT] Erreur découverte dépôts : {e}")
if not depots_disponibles:
raise ValueError(
"Aucun dépôt trouvé dans Sage. Créez d'abord un dépôt."
)
if not depot_a_utiliser:
depot_a_utiliser = depots_disponibles[0]
logger.info(
f"[DEPOT] Utilisation : '{depot_a_utiliser['code']}' ({depot_a_utiliser['intitule']})"
)
reference = article_data.get("reference", "").upper().strip()
designation = article_data.get("designation", "").strip()
if len(designation) > 69:
designation = designation[:69]
stock_reel = article_data.get("stock_reel", 0.0)
stock_mini = article_data.get("stock_mini", 0.0)
stock_maxi = article_data.get("stock_maxi", 0.0)
logger.info(f"[ARTICLE] Référence : {reference}")
logger.info(f"[ARTICLE] Désignation : {designation}")
factory = self.cial.FactoryArticle
try:
article_existant = factory.ReadReference(reference)
if article_existant:
raise ValueError(f"L'article {reference} existe déjà")
except Exception as e:
error_msg = str(e)
if (
"Enregistrement non trouve" not in error_msg
and "-2607" not in error_msg
):
raise
persist = factory.Create()
article = win32com.client.CastTo(persist, "IBOArticle3")
article.SetDefault()
article.AR_Ref = reference
article.AR_Design = designation
logger.info("[MODELE] Recherche article modèle...")
article_modele_ref = None
article_modele = None
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT TOP 1 AR_Ref
FROM F_ARTICLE
WHERE AR_Sommeil = 0
ORDER BY AR_Ref
""")
row = cursor.fetchone()
if row:
article_modele_ref = _safe_strip(row.AR_Ref)
logger.info(
f" [SQL] Article modèle : {article_modele_ref}"
)
except Exception as e:
logger.warning(f" [SQL] Erreur recherche : {e}")
if article_modele_ref:
try:
persist_modele = factory.ReadReference(article_modele_ref)
if persist_modele:
article_modele = win32com.client.CastTo(
persist_modele, "IBOArticle3"
)
article_modele.Read()
logger.info(
f" [OK] Modèle chargé : {article_modele_ref}"
)
except Exception as e:
logger.warning(f" [WARN] Erreur chargement : {e}")
article_modele = None
if not article_modele:
raise ValueError(
"Aucun article modèle trouvé. Créez au moins un article dans Sage."
)
logger.info("[UNITE] Copie Unite depuis modèle...")
unite_trouvee = False
try:
unite_obj = getattr(article_modele, "Unite", None)
if unite_obj:
article.Unite = unite_obj
logger.info(" [OK] Unite copiée")
unite_trouvee = True
except Exception as e:
logger.warning(f" Unite non copiable : {e}")
if not unite_trouvee:
raise ValueError(
"Impossible de copier l'unité depuis le modèle"
)
famille_trouvee = False
famille_code_personnalise = article_data.get("famille")
if famille_code_personnalise:
logger.info(
f" [FAMILLE] Code demandé : {famille_code_personnalise}"
)
try:
famille_code_exact = None
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT FA_CodeFamille, FA_Type
FROM F_FAMILLE
WHERE UPPER(FA_CodeFamille) = ?
""",
(famille_code_personnalise.upper(),),
)
row = cursor.fetchone()
if row:
famille_code_exact = _safe_strip(row.FA_CodeFamille)
if row.FA_Type == 1:
raise ValueError(
f"Famille '{famille_code_personnalise}' est de type Total"
)
logger.info(
f" [SQL] Famille trouvée : {famille_code_exact}"
)
else:
raise ValueError(
f"Famille '{famille_code_personnalise}' introuvable"
)
if famille_code_exact:
factory_famille = self.cial.FactoryFamille
famille_obj = None
index = 1
while index <= 1000:
try:
persist_test = factory_famille.List(index)
if persist_test is None:
break
fam_test = win32com.client.CastTo(
persist_test, "IBOFamille3"
)
fam_test.Read()
code_test = (
getattr(fam_test, "FA_CodeFamille", "")
.strip()
.upper()
)
if code_test == famille_code_exact.upper():
famille_obj = fam_test
famille_trouvee = True
logger.info(
f" [OK] Famille trouvée à index {index}"
)
break
index += 1
except Exception:
break
if famille_obj:
famille_obj.Read()
article.Famille = famille_obj
else:
raise ValueError(
f"Famille '{famille_code_personnalise}' inaccessible via COM"
)
except Exception as e:
logger.error(f" [ERREUR] Famille : {e}")
raise
if not famille_trouvee:
try:
famille_obj = getattr(article_modele, "Famille", None)
if famille_obj:
article.Famille = famille_obj
logger.info(" [OK] Famille copiée depuis modèle")
famille_trouvee = True
except Exception as e:
logger.debug(f" Famille non copiable : {e}")
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))
article.AR_Nomencl = int(getattr(article_modele, "AR_Nomencl", 0))
article.AR_SuiviStock = 2
logger.info(" [OK] Champs de base copiés (AR_SuiviStock=2)")
logger.info("[CHAMPS] Application champs fournis...")
champs_appliques = []
champs_echoues = []
if "prix_vente" in article_data:
try:
article.AR_PrixVen = float(article_data["prix_vente"])
champs_appliques.append("prix_vente")
logger.info(
f" prix_vente = {article_data['prix_vente']}"
)
except Exception as e:
champs_echoues.append(f"prix_vente: {e}")
if "prix_achat" in article_data:
try:
article.AR_PrixAchat = float(article_data["prix_achat"])
champs_appliques.append("prix_achat")
logger.info(
f" prix_achat = {article_data['prix_achat']}"
)
except Exception as e:
champs_echoues.append(f"prix_achat: {e}")
if "coef" in article_data:
try:
article.AR_Coef = float(article_data["coef"])
champs_appliques.append("coef")
logger.info(f" coef = {article_data['coef']}")
except Exception as e:
champs_echoues.append(f"coef: {e}")
if "code_ean" in article_data:
try:
article.AR_CodeBarre = str(article_data["code_ean"])
champs_appliques.append("code_ean")
logger.info(f" code_ean = {article_data['code_ean']}")
except Exception as e:
champs_echoues.append(f"code_ean: {e}")
if "description" in article_data:
try:
article.AR_Langue1 = str(article_data["description"])[:255]
champs_appliques.append("description")
logger.info(" description définie (AR_Langue1)")
except Exception as e:
champs_echoues.append(f"description: {e}")
if "pays" in article_data:
try:
article.AR_Pays = str(article_data["pays"])[:3].upper()
champs_appliques.append("pays")
logger.info(f" pays = {article_data['pays']}")
except Exception as e:
champs_echoues.append(f"pays: {e}")
if "garantie" in article_data:
try:
article.AR_Garantie = int(article_data["garantie"])
champs_appliques.append("garantie")
logger.info(f" garantie = {article_data['garantie']}")
except Exception as e:
champs_echoues.append(f"garantie: {e}")
if "delai" in article_data:
try:
article.AR_Delai = int(article_data["delai"])
champs_appliques.append("delai")
logger.info(f" delai = {article_data['delai']}")
except Exception as e:
champs_echoues.append(f"delai: {e}")
if "poids_net" in article_data:
try:
article.AR_PoidsNet = float(article_data["poids_net"])
champs_appliques.append("poids_net")
logger.info(f" poids_net = {article_data['poids_net']}")
except Exception as e:
champs_echoues.append(f"poids_net: {e}")
if "poids_brut" in article_data:
try:
article.AR_PoidsBrut = float(article_data["poids_brut"])
champs_appliques.append("poids_brut")
logger.info(
f" poids_brut = {article_data['poids_brut']}"
)
except Exception as e:
champs_echoues.append(f"poids_brut: {e}")
if "code_fiscal" in article_data:
try:
article.AR_CodeFiscal = str(article_data["code_fiscal"])[
:10
]
champs_appliques.append("code_fiscal")
logger.info(
f" code_fiscal = {article_data['code_fiscal']}"
)
except Exception as e:
champs_echoues.append(f"code_fiscal: {e}")
if "soumis_escompte" in article_data:
try:
article.AR_Escompte = (
1 if article_data["soumis_escompte"] else 0
)
champs_appliques.append("soumis_escompte")
logger.info(
f" soumis_escompte = {article_data['soumis_escompte']}"
)
except Exception as e:
champs_echoues.append(f"soumis_escompte: {e}")
if "publie" in article_data:
try:
article.AR_Publie = 1 if article_data["publie"] else 0
champs_appliques.append("publie")
logger.info(f" publie = {article_data['publie']}")
except Exception as e:
champs_echoues.append(f"publie: {e}")
if "en_sommeil" in article_data:
try:
article.AR_Sommeil = 1 if article_data["en_sommeil"] else 0
champs_appliques.append("en_sommeil")
logger.info(
f" en_sommeil = {article_data['en_sommeil']}"
)
except Exception as e:
champs_echoues.append(f"en_sommeil: {e}")
if champs_echoues:
logger.warning(
f"[WARN] Champs échoués : {', '.join(champs_echoues)}"
)
logger.info(
f"[CHAMPS] Appliqués : {len(champs_appliques)} / Échoués : {len(champs_echoues)}"
)
logger.info("[ARTICLE] Écriture dans Sage...")
try:
article.Write()
logger.info(" [OK] Write() réussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
except Exception:
pass
logger.error(f" [ERREUR] Write() : {error_detail}")
raise RuntimeError(f"Échec création : {error_detail}")
stats_a_definir = []
for i in range(1, 6):
stat_key = f"stat_0{i}"
if stat_key in article_data and article_data[stat_key]:
stats_a_definir.append(
(i - 1, str(article_data[stat_key])[:20])
)
if stats_a_definir:
logger.info("[STATS] Définition statistiques via AR_Stat()...")
try:
for index, value in stats_a_definir:
article.AR_Stat(index, value)
logger.info(f" stat_0{index + 1} = {value}")
champs_appliques.append(f"stat_0{index + 1}")
article.Write()
logger.info(" [OK] Statistiques sauvegardées")
except Exception as e:
logger.warning(f" Statistiques : {e}")
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.info("[COMMIT] Transaction committée")
except Exception as e:
logger.warning(f"[COMMIT] Erreur : {e}")
stock_defini = False
has_stock_values = stock_reel or stock_mini or stock_maxi
if has_stock_values:
logger.info(
f"[STOCK] Définition stock (dépôt '{depot_a_utiliser['code']}')..."
)
if stock_reel:
try:
depot_obj = depot_a_utiliser["objet"]
factory_stock = None
for factory_name in [
"FactoryArticleStock",
"FactoryDepotStock",
]:
try:
factory_stock = getattr(
depot_obj, factory_name, None
)
if factory_stock:
logger.info(f" Factory : {factory_name}")
break
except Exception:
continue
if factory_stock:
stock_persist = factory_stock.Create()
stock_obj = win32com.client.CastTo(
stock_persist, "IBODepotStock3"
)
stock_obj.SetDefault()
stock_obj.AR_Ref = reference
stock_obj.AS_QteSto = float(stock_reel)
if stock_mini:
try:
stock_obj.AS_QteMini = float(stock_mini)
except Exception as e:
logger.warning(f" AS_QteMini : {e}")
if stock_maxi:
try:
stock_obj.AS_QteMaxi = float(stock_maxi)
except Exception as e:
logger.warning(f" AS_QteMaxi : {e}")
stock_obj.Write()
stock_defini = True
logger.info(
f" [OK] Stock défini via COM : réel={stock_reel}, min={stock_mini}, max={stock_maxi}"
)
except Exception as e:
logger.warning(f" [WARN] Stock COM : {e}")
if (stock_mini or stock_maxi) and not stock_defini:
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT TOP 1 DE_No FROM F_DEPOT ORDER BY DE_No"
)
row = cursor.fetchone()
if row:
depot_no = row.DE_No
cursor.execute(
"""
SELECT COUNT(*)
FROM F_ARTSTOCK
WHERE AR_Ref = ? AND DE_No = ?
""",
(reference.upper(), depot_no),
)
count = cursor.fetchone()[0]
if count > 0:
update_parts = []
params = []
if stock_mini:
update_parts.append("AS_QteMini = ?")
params.append(float(stock_mini))
if stock_maxi:
update_parts.append("AS_QteMaxi = ?")
params.append(float(stock_maxi))
if update_parts:
params.extend(
[reference.upper(), depot_no]
)
cursor.execute(
f"""
UPDATE F_ARTSTOCK
SET {", ".join(update_parts)}
WHERE AR_Ref = ? AND DE_No = ?
""",
params,
)
conn.commit()
logger.info(
" [SQL] Stocks mini/maxi mis à jour"
)
else:
cursor.execute(
"""
INSERT INTO F_ARTSTOCK (AR_Ref, DE_No, AS_QteSto, AS_QteMini, AS_QteMaxi)
VALUES (?, ?, ?, ?, ?)
""",
(
reference.upper(),
depot_no,
float(stock_reel)
if stock_reel
else 0.0,
float(stock_mini)
if stock_mini
else 0.0,
float(stock_maxi)
if stock_maxi
else 0.0,
),
)
conn.commit()
logger.info(" [SQL] Ligne stock créée")
except Exception as e:
logger.error(f"[STOCK] Erreur SQL : {e}")
logger.info("[RESPONSE] Construction réponse depuis SQL...")
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
a.AR_Ref, a.AR_Design, a.AR_PrixVen, a.AR_PrixAch, a.AR_Coef,
a.AR_CodeBarre, a.AR_CodeFiscal, a.AR_Pays, a.AR_Garantie, a.AR_Delai,
a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_Escompte, a.AR_Publie, a.AR_Sommeil,
a.FA_CodeFamille, f.FA_Intitule,
a.AR_Type, a.AR_UniteVen, a.AR_Langue1,
a.AR_Stat01, a.AR_Stat02, a.AR_Stat03, a.AR_Stat04, a.AR_Stat05
FROM F_ARTICLE a
LEFT JOIN F_FAMILLE f ON a.FA_CodeFamille = f.FA_CodeFamille
WHERE a.AR_Ref = ?
""",
(reference.upper(),),
)
row = cursor.fetchone()
if row:
resultat = {
"reference": _safe_strip(row[0]),
"designation": _safe_strip(row[1]),
"prix_vente": float(row[2]) if row[2] else 0.0,
"prix_achat": float(row[3]) if row[3] else 0.0,
"coef": float(row[4]) if row[4] else None,
"code_ean": _safe_strip(row[5]),
"code_barre": _safe_strip(row[5]),
"code_fiscal": _safe_strip(row[6]),
"pays": _safe_strip(row[7]),
"garantie": int(row[8]) if row[8] else None,
"delai": int(row[9]) if row[9] else None,
"poids_net": float(row[10]) if row[10] else None,
"poids_brut": float(row[11]) if row[11] else None,
"soumis_escompte": bool(row[12])
if row[12] is not None
else None,
"publie": bool(row[13])
if row[13] is not None
else None,
"en_sommeil": bool(row[14]) if row[14] else False,
"est_actif": not bool(row[14])
if row[14] is not None
else True,
"famille_code": _safe_strip(row[15]),
"famille_libelle": _safe_strip(row[16])
if row[16]
else "",
"type_article": int(row[17])
if row[17] is not None
else 0,
"type_article_libelle": "Article"
if not row[17]
else None,
"unite_vente": _safe_strip(row[18])
if row[18]
else None,
"description": _safe_strip(row[19])
if row[19]
else None,
"stat_01": _safe_strip(row[20])
if row[20]
else None,
"stat_02": _safe_strip(row[21])
if row[21]
else None,
"stat_03": _safe_strip(row[22])
if row[22]
else None,
"stat_04": _safe_strip(row[23])
if row[23]
else None,
"stat_05": _safe_strip(row[24])
if row[24]
else None,
}
cursor.execute(
"""
SELECT s.AS_QteSto, s.AS_QteMini, s.AS_QteMaxi, s.AS_QteRes, s.AS_QteCom
FROM F_ARTSTOCK s
WHERE s.AR_Ref = ?
""",
(reference.upper(),),
)
stock_total = 0.0
stock_mini_val = 0.0
stock_maxi_val = 0.0
stock_reserve_val = 0.0
stock_commande_val = 0.0
stock_row = cursor.fetchone()
if stock_row:
stock_total = (
float(stock_row[0]) if stock_row[0] else 0.0
)
stock_mini_val = (
float(stock_row[1]) if stock_row[1] else 0.0
)
stock_maxi_val = (
float(stock_row[2]) if stock_row[2] else 0.0
)
stock_reserve_val = (
float(stock_row[3]) if stock_row[3] else 0.0
)
stock_commande_val = (
float(stock_row[4]) if stock_row[4] else 0.0
)
resultat["stock_reel"] = stock_total
resultat["stock_mini"] = stock_mini_val
resultat["stock_maxi"] = stock_maxi_val
resultat["stock_disponible"] = (
stock_total - stock_reserve_val
)
resultat["stock_reserve"] = stock_reserve_val
resultat["stock_commande"] = stock_commande_val
logger.info(
f"[OK] ARTICLE CREE : {reference} - Stock : {stock_total}"
)
return resultat
except Exception as e:
logger.warning(f"[RESPONSE] Erreur lecture SQL : {e}")
logger.info("[FALLBACK] Extraction COM...")
article_cree_persist = factory.ReadReference(reference)
if not article_cree_persist:
raise RuntimeError("Article créé mais introuvable")
article_cree = win32com.client.CastTo(
article_cree_persist, "IBOArticle3"
)
article_cree.Read()
resultat = _extraire_article(article_cree)
if not resultat:
resultat = {"reference": reference, "designation": designation}
for key in [
"prix_vente",
"prix_achat",
"coef",
"stock_mini",
"stock_maxi",
"code_ean",
"code_fiscal",
"pays",
"garantie",
"delai",
"poids_net",
"poids_brut",
"soumis_escompte",
"publie",
]:
if key in article_data and article_data[key] is not None:
resultat[key] = article_data[key]
return resultat
except ValueError:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except Exception:
pass
raise
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except Exception:
pass
logger.error(f"Erreur création : {e}", exc_info=True)
raise RuntimeError(f"Erreur création : {str(e)}")
except Exception as e:
logger.error(f"Erreur globale : {e}", exc_info=True)
raise
def modifier_article(self, reference: str, article_data: Dict) -> Dict:
"""Modifie un article existant dans Sage 100 - Version fusionnée"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
valide, erreur = valider_donnees_modification(article_data)
if not valide:
raise ValueError(erreur)
with self._com_context(), self._lock_com:
logger.info(f"[ARTICLE] === MODIFICATION {reference} ===")
factory_article = self.cial.FactoryArticle
persist = factory_article.ReadReference(reference.upper())
if not persist:
raise ValueError(f"Article {reference} introuvable")
article = win32com.client.CastTo(persist, "IBOArticle3")
article.Read()
logger.info(f"[ARTICLE] Trouvé : {reference}")
champs_modifies = []
champs_echoues = []
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}")
try:
famille_code_exact = None
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT FA_CodeFamille, FA_Type
FROM F_FAMILLE
WHERE UPPER(FA_CodeFamille) = ?
""",
(famille_code_demande,),
)
row = cursor.fetchone()
if row:
famille_code_exact = _safe_strip(row.FA_CodeFamille)
if row.FA_Type == 1:
raise ValueError(
f"Famille '{famille_code_demande}' est de type Total"
)
logger.info(
f" [SQL] Famille trouvée : {famille_code_exact}"
)
else:
raise ValueError(
f"Famille '{famille_code_demande}' introuvable"
)
if famille_code_exact:
factory_famille = self.cial.FactoryFamille
famille_obj = None
index = 1
while index <= 1000:
try:
persist_test = factory_famille.List(index)
if persist_test is None:
break
fam_test = win32com.client.CastTo(
persist_test, "IBOFamille3"
)
fam_test.Read()
code_test = (
getattr(fam_test, "FA_CodeFamille", "")
.strip()
.upper()
)
if code_test == famille_code_exact.upper():
famille_obj = fam_test
logger.info(f" [OK] Famille à index {index}")
break
index += 1
except Exception:
break
if famille_obj:
famille_obj.Read()
article.Famille = famille_obj
champs_modifies.append("famille")
logger.info(
f" [OK] Famille changée : {famille_code_exact}"
)
else:
raise ValueError(
f"Famille '{famille_code_demande}' inaccessible via COM"
)
except Exception as e:
logger.error(f" [ERREUR] Famille : {e}")
champs_echoues.append(f"famille: {e}")
if "designation" in article_data:
try:
designation = str(article_data["designation"])[:69].strip()
article.AR_Design = designation
champs_modifies.append("designation")
logger.info(f" designation = {designation}")
except Exception as e:
champs_echoues.append(f"designation: {e}")
if "prix_vente" in article_data:
try:
article.AR_PrixVen = float(article_data["prix_vente"])
champs_modifies.append("prix_vente")
logger.info(f" prix_vente = {article_data['prix_vente']}")
except Exception as e:
champs_echoues.append(f"prix_vente: {e}")
if "prix_achat" in article_data:
try:
article.AR_PrixAchat = float(article_data["prix_achat"])
champs_modifies.append("prix_achat")
logger.info(f" prix_achat = {article_data['prix_achat']}")
except Exception as e:
champs_echoues.append(f"prix_achat: {e}")
if "coef" in article_data:
try:
article.AR_Coef = float(article_data["coef"])
champs_modifies.append("coef")
logger.info(f" coef = {article_data['coef']}")
except Exception as e:
champs_echoues.append(f"coef: {e}")
if "code_ean" in article_data:
try:
article.AR_CodeBarre = str(article_data["code_ean"])[
:13
].strip()
champs_modifies.append("code_ean")
logger.info(f" code_ean = {article_data['code_ean']}")
except Exception as e:
champs_echoues.append(f"code_ean: {e}")
if "description" in article_data:
try:
article.AR_Langue1 = str(article_data["description"])[
:255
].strip()
champs_modifies.append("description")
logger.info(" description définie (AR_Langue1)")
except Exception as e:
champs_echoues.append(f"description: {e}")
if "pays" in article_data:
try:
article.AR_Pays = str(article_data["pays"])[:3].upper()
champs_modifies.append("pays")
logger.info(f" pays = {article_data['pays']}")
except Exception as e:
champs_echoues.append(f"pays: {e}")
if "garantie" in article_data:
try:
article.AR_Garantie = int(article_data["garantie"])
champs_modifies.append("garantie")
logger.info(f" garantie = {article_data['garantie']}")
except Exception as e:
champs_echoues.append(f"garantie: {e}")
if "delai" in article_data:
try:
article.AR_Delai = int(article_data["delai"])
champs_modifies.append("delai")
logger.info(f" delai = {article_data['delai']}")
except Exception as e:
champs_echoues.append(f"delai: {e}")
if "poids_net" in article_data:
try:
article.AR_PoidsNet = float(article_data["poids_net"])
champs_modifies.append("poids_net")
logger.info(f" poids_net = {article_data['poids_net']}")
except Exception as e:
champs_echoues.append(f"poids_net: {e}")
if "poids_brut" in article_data:
try:
article.AR_PoidsBrut = float(article_data["poids_brut"])
champs_modifies.append("poids_brut")
logger.info(f" poids_brut = {article_data['poids_brut']}")
except Exception as e:
champs_echoues.append(f"poids_brut: {e}")
if "code_fiscal" in article_data:
try:
article.AR_CodeFiscal = str(article_data["code_fiscal"])[:10]
champs_modifies.append("code_fiscal")
logger.info(f" code_fiscal = {article_data['code_fiscal']}")
except Exception as e:
champs_echoues.append(f"code_fiscal: {e}")
if "soumis_escompte" in article_data:
try:
article.AR_Escompte = (
1 if article_data["soumis_escompte"] else 0
)
champs_modifies.append("soumis_escompte")
logger.info(
f" soumis_escompte = {article_data['soumis_escompte']}"
)
except Exception as e:
champs_echoues.append(f"soumis_escompte: {e}")
if "publie" in article_data:
try:
article.AR_Publie = 1 if article_data["publie"] else 0
champs_modifies.append("publie")
logger.info(f" publie = {article_data['publie']}")
except Exception as e:
champs_echoues.append(f"publie: {e}")
if "en_sommeil" in article_data:
try:
article.AR_Sommeil = 1 if article_data["en_sommeil"] else 0
champs_modifies.append("en_sommeil")
logger.info(f" en_sommeil = {article_data['en_sommeil']}")
except Exception as e:
champs_echoues.append(f"en_sommeil: {e}")
if champs_echoues:
logger.warning(
f"[WARN] Champs échoués : {', '.join(champs_echoues)}"
)
if not champs_modifies:
logger.warning("[ARTICLE] Aucun champ modifié")
return _extraire_article(article)
logger.info(f"[ARTICLE] Champs modifiés : {', '.join(champs_modifies)}")
logger.info("[ARTICLE] Écriture...")
try:
article.Write()
logger.info("[ARTICLE] Write() réussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f"[ARTICLE] Erreur Write() : {error_detail}")
raise RuntimeError(f"Échec modification : {error_detail}")
stats_a_modifier = []
for i in range(1, 6):
stat_key = f"stat_0{i}"
if stat_key in article_data:
stats_a_modifier.append(
(
i - 1,
str(article_data[stat_key])[:20]
if article_data[stat_key]
else "",
)
)
if stats_a_modifier:
logger.info("[STATS] Modification statistiques via AR_Stat()...")
try:
for index, value in stats_a_modifier:
article.AR_Stat(index, value)
logger.info(f" stat_0{index + 1} = {value}")
champs_modifies.append(f"stat_0{index + 1}")
article.Write()
logger.info(" [OK] Statistiques sauvegardées")
except Exception as e:
logger.warning(f" Statistiques : {e}")
if "stock_mini" in article_data or "stock_maxi" in article_data:
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT TOP 1 DE_No FROM F_DEPOT ORDER BY DE_No"
)
row = cursor.fetchone()
if row:
depot_no = row.DE_No
update_parts = []
params = []
if "stock_mini" in article_data:
update_parts.append("AS_QteMini = ?")
params.append(float(article_data["stock_mini"]))
champs_modifies.append("stock_mini")
if "stock_maxi" in article_data:
update_parts.append("AS_QteMaxi = ?")
params.append(float(article_data["stock_maxi"]))
champs_modifies.append("stock_maxi")
if update_parts:
params.extend([reference.upper(), depot_no])
cursor.execute(
f"""
UPDATE F_ARTSTOCK
SET {", ".join(update_parts)}
WHERE AR_Ref = ? AND DE_No = ?
""",
params,
)
conn.commit()
logger.info(" [SQL] Stocks mini/maxi mis à jour")
except Exception as e:
logger.error(f"[STOCK] Erreur SQL : {e}")
champs_echoues.append(f"stocks: {e}")
logger.info("[RESPONSE] Construction réponse depuis SQL...")
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
a.AR_Ref, a.AR_Design, a.AR_PrixVen, a.AR_PrixAch, a.AR_Coef,
a.AR_CodeBarre, a.AR_CodeFiscal, a.AR_Pays, a.AR_Garantie, a.AR_Delai,
a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_Escompte, a.AR_Publie, a.AR_Sommeil,
a.FA_CodeFamille, f.FA_Intitule,
a.AR_Type, a.AR_UniteVen, a.AR_Langue1,
a.AR_Stat01, a.AR_Stat02, a.AR_Stat03, a.AR_Stat04, a.AR_Stat05
FROM F_ARTICLE a
LEFT JOIN F_FAMILLE f ON a.FA_CodeFamille = f.FA_CodeFamille
WHERE a.AR_Ref = ?
""",
(reference.upper(),),
)
row = cursor.fetchone()
if row:
resultat = {
"reference": _safe_strip(row[0]),
"designation": _safe_strip(row[1]),
"prix_vente": float(row[2]) if row[2] else 0.0,
"prix_achat": float(row[3]) if row[3] else 0.0,
"coef": float(row[4]) if row[4] else None,
"code_ean": _safe_strip(row[5]),
"code_barre": _safe_strip(row[5]),
"code_fiscal": _safe_strip(row[6]),
"pays": _safe_strip(row[7]),
"garantie": int(row[8]) if row[8] else None,
"delai": int(row[9]) if row[9] else None,
"poids_net": float(row[10]) if row[10] else None,
"poids_brut": float(row[11]) if row[11] else None,
"soumis_escompte": bool(row[12])
if row[12] is not None
else None,
"publie": bool(row[13])
if row[13] is not None
else None,
"en_sommeil": bool(row[14]) if row[14] else False,
"est_actif": not bool(row[14])
if row[14] is not None
else True,
"famille_code": _safe_strip(row[15]),
"famille_libelle": _safe_strip(row[16])
if row[16]
else "",
"type_article": int(row[17])
if row[17] is not None
else 0,
"type_article_libelle": "Article"
if not row[17]
else None,
"unite_vente": _safe_strip(row[18])
if row[18]
else None,
"description": _safe_strip(row[19])
if row[19]
else None,
"stat_01": _safe_strip(row[20]) if row[20] else None,
"stat_02": _safe_strip(row[21]) if row[21] else None,
"stat_03": _safe_strip(row[22]) if row[22] else None,
"stat_04": _safe_strip(row[23]) if row[23] else None,
"stat_05": _safe_strip(row[24]) if row[24] else None,
}
cursor.execute(
"""
SELECT s.AS_QteSto, s.AS_QteMini, s.AS_QteMaxi, s.AS_QteRes, s.AS_QteCom
FROM F_ARTSTOCK s
WHERE s.AR_Ref = ?
""",
(reference.upper(),),
)
stock_total = 0.0
stock_mini_val = 0.0
stock_maxi_val = 0.0
stock_reserve_val = 0.0
stock_commande_val = 0.0
stock_row = cursor.fetchone()
if stock_row:
stock_total = (
float(stock_row[0]) if stock_row[0] else 0.0
)
stock_mini_val = (
float(stock_row[1]) if stock_row[1] else 0.0
)
stock_maxi_val = (
float(stock_row[2]) if stock_row[2] else 0.0
)
stock_reserve_val = (
float(stock_row[3]) if stock_row[3] else 0.0
)
stock_commande_val = (
float(stock_row[4]) if stock_row[4] else 0.0
)
resultat["stock_reel"] = stock_total
resultat["stock_mini"] = stock_mini_val
resultat["stock_maxi"] = stock_maxi_val
resultat["stock_disponible"] = (
stock_total - stock_reserve_val
)
resultat["stock_reserve"] = stock_reserve_val
resultat["stock_commande"] = stock_commande_val
logger.info(
f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)"
)
return resultat
except Exception as e:
logger.warning(f"[RESPONSE] Erreur lecture SQL : {e}")
article.Read()
logger.info(
f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)"
)
resultat = _extraire_article(article)
if not resultat:
resultat = {"reference": reference}
return resultat
except ValueError as e:
logger.error(f"[ARTICLE] Erreur métier : {e}")
raise
except Exception as e:
logger.error(f"[ARTICLE] Erreur technique : {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = f"Erreur Sage: {err.Description}"
except Exception:
pass
raise RuntimeError(f"Erreur technique Sage : {error_message}")
def creer_famille(self, famille_data: dict) -> dict:
with self._com_context(), self._lock_com:
try:
logger.info("[FAMILLE] === CRÉATION FAMILLE (TYPE DÉTAIL) ===")
code = famille_data.get("code", "").upper().strip()
if not code:
raise ValueError("Le code famille est obligatoire")
if len(code) > 18:
raise ValueError(
"Le code famille ne peut pas dépasser 18 caractères"
)
intitule = famille_data.get("intitule", "").strip()
if not intitule:
raise ValueError("L'intitulé est obligatoire")
if len(intitule) > 69:
intitule = intitule[:69]
logger.info(f"[FAMILLE] Code : {code}")
logger.info(f"[FAMILLE] Intitulé : {intitule}")
type_demande = famille_data.get("type", 0)
if type_demande == 1:
logger.warning(
"[FAMILLE] Type 'Total' demandé mais IGNORÉ - Création en type Détail uniquement"
)
factory_famille = self.cial.FactoryFamille
try:
index = 1
while index <= 1000:
try:
persist_test = factory_famille.List(index)
if persist_test is None:
break
fam_test = win32com.client.CastTo(
persist_test, "IBOFamille3"
)
fam_test.Read()
code_existant = (
getattr(fam_test, "FA_CodeFamille", "").strip().upper()
)
if code_existant == code:
raise ValueError(f"La famille {code} existe déjà")
index += 1
except ValueError:
raise
except Exception:
index += 1
except ValueError:
raise
persist = factory_famille.Create()
famille = win32com.client.CastTo(persist, "IBOFamille3")
famille.SetDefault()
famille.FA_CodeFamille = code
famille.FA_Intitule = intitule
try:
famille.FA_Type = 0
logger.info("[FAMILLE] Type : 0 (Détail)")
except Exception as e:
logger.warning(f"[FAMILLE] FA_Type non défini : {e}")
compte_achat = famille_data.get("compte_achat")
if compte_achat:
try:
factory_compte = self.cial.CptaApplication.FactoryCompteG
persist_compte = factory_compte.ReadNumero(compte_achat)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
famille.CompteGAchat = compte_obj
logger.info(f"[FAMILLE] Compte achat : {compte_achat}")
except Exception as e:
logger.warning(f"[FAMILLE] Compte achat non défini : {e}")
compte_vente = famille_data.get("compte_vente")
if compte_vente:
try:
factory_compte = self.cial.CptaApplication.FactoryCompteG
persist_compte = factory_compte.ReadNumero(compte_vente)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
famille.CompteGVente = compte_obj
logger.info(f"[FAMILLE] Compte vente : {compte_vente}")
except Exception as e:
logger.warning(f"[FAMILLE] Compte vente non défini : {e}")
logger.info("[FAMILLE] Écriture dans Sage...")
try:
famille.Write()
logger.info("[FAMILLE] Write() réussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f"[FAMILLE] Erreur Write() : {error_detail}")
raise RuntimeError(f"Échec création famille : {error_detail}")
famille.Read()
resultat = {
"code": getattr(famille, "FA_CodeFamille", "").strip(),
"intitule": getattr(famille, "FA_Intitule", "").strip(),
"type": 0,
"type_libelle": "Détail",
}
logger.info(f"[FAMILLE] FAMILLE CRÉÉE : {code} - {intitule} (Détail)")
return resultat
except ValueError as e:
logger.error(f"[FAMILLE] Erreur métier : {e}")
raise
except Exception as e:
logger.error(f"[FAMILLE] Erreur technique : {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = f"Erreur Sage: {err.Description}"
except Exception:
pass
raise RuntimeError(f"Erreur technique Sage : {error_message}")
def lister_toutes_familles(
self, filtre: str = "", inclure_totaux: bool = True
) -> List[Dict]:
"""Liste toutes les familles avec leurs comptes comptables et fournisseur principal"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
logger.info(
"[SQL] Chargement des familles avec F_FAMCOMPTA et F_FAMFOURNISS..."
)
query = """
SELECT
-- F_FAMILLE - Identification
f.FA_CodeFamille,
f.FA_Type,
f.FA_Intitule,
f.FA_UniteVen,
f.FA_Coef,
f.FA_SuiviStock,
f.FA_Garantie,
f.FA_Central,
-- F_FAMILLE - Statistiques
f.FA_Stat01,
f.FA_Stat02,
f.FA_Stat03,
f.FA_Stat04,
f.FA_Stat05,
-- F_FAMILLE - Fiscal et gestion
f.FA_CodeFiscal,
f.FA_Pays,
f.FA_UnitePoids,
f.FA_Escompte,
f.FA_Delai,
f.FA_HorsStat,
f.FA_VteDebit,
f.FA_NotImp,
-- F_FAMILLE - Frais annexes (3 frais avec 3 remises chacun)
f.FA_Frais01FR_Denomination,
f.FA_Frais01FR_Rem01REM_Valeur,
f.FA_Frais01FR_Rem01REM_Type,
f.FA_Frais01FR_Rem02REM_Valeur,
f.FA_Frais01FR_Rem02REM_Type,
f.FA_Frais01FR_Rem03REM_Valeur,
f.FA_Frais01FR_Rem03REM_Type,
f.FA_Frais02FR_Denomination,
f.FA_Frais02FR_Rem01REM_Valeur,
f.FA_Frais02FR_Rem01REM_Type,
f.FA_Frais02FR_Rem02REM_Valeur,
f.FA_Frais02FR_Rem02REM_Type,
f.FA_Frais02FR_Rem03REM_Valeur,
f.FA_Frais02FR_Rem03REM_Type,
f.FA_Frais03FR_Denomination,
f.FA_Frais03FR_Rem01REM_Valeur,
f.FA_Frais03FR_Rem01REM_Type,
f.FA_Frais03FR_Rem02REM_Valeur,
f.FA_Frais03FR_Rem02REM_Type,
f.FA_Frais03FR_Rem03REM_Valeur,
f.FA_Frais03FR_Rem03REM_Type,
-- F_FAMILLE - Options diverses
f.FA_Contremarque,
f.FA_FactPoids,
f.FA_FactForfait,
f.FA_Publie,
f.FA_RacineRef,
f.FA_RacineCB,
-- F_FAMILLE - Catégories
f.CL_No1,
f.CL_No2,
f.CL_No3,
f.CL_No4,
-- F_FAMILLE - Gestion avancée
f.FA_Nature,
f.FA_NbColis,
f.FA_SousTraitance,
f.FA_Fictif,
f.FA_Criticite,
-- F_FAMILLE - Métadonnées système
f.cbMarq,
f.cbCreateur,
f.cbModification,
f.cbCreation,
f.cbCreationUser,
-- F_FAMCOMPTA Vente (FCP_Type = 0)
vte.FCP_ComptaCPT_CompteG,
vte.FCP_ComptaCPT_CompteA,
vte.FCP_ComptaCPT_Taxe1,
vte.FCP_ComptaCPT_Taxe2,
vte.FCP_ComptaCPT_Taxe3,
vte.FCP_ComptaCPT_Date1,
vte.FCP_ComptaCPT_Date2,
vte.FCP_ComptaCPT_Date3,
vte.FCP_TypeFacture,
-- F_FAMCOMPTA Achat (FCP_Type = 1)
ach.FCP_ComptaCPT_CompteG,
ach.FCP_ComptaCPT_CompteA,
ach.FCP_ComptaCPT_Taxe1,
ach.FCP_ComptaCPT_Taxe2,
ach.FCP_ComptaCPT_Taxe3,
ach.FCP_ComptaCPT_Date1,
ach.FCP_ComptaCPT_Date2,
ach.FCP_ComptaCPT_Date3,
ach.FCP_TypeFacture,
-- F_FAMCOMPTA Stock (FCP_Type = 2)
sto.FCP_ComptaCPT_CompteG,
sto.FCP_ComptaCPT_CompteA,
-- F_FAMFOURNISS (fournisseur principal FF_Principal=1)
ff.CT_Num,
ff.FF_Unite,
ff.FF_Conversion,
ff.FF_DelaiAppro,
ff.FF_Garantie,
ff.FF_Colisage,
ff.FF_QteMini,
ff.FF_QteMont,
ff.EG_Champ,
ff.FF_Devise,
ff.FF_Remise,
ff.FF_ConvDiv,
ff.FF_TypeRem,
-- Nombre d'articles
ISNULL(COUNT(DISTINCT a.AR_Ref), 0) as nb_articles
FROM F_FAMILLE f
-- Jointures comptables
LEFT JOIN F_FAMCOMPTA vte ON f.FA_CodeFamille = vte.FA_CodeFamille
AND vte.FCP_Type = 0 -- Vente
AND vte.FCP_Champ = 1 -- Compte principal
LEFT JOIN F_FAMCOMPTA ach ON f.FA_CodeFamille = ach.FA_CodeFamille
AND ach.FCP_Type = 1 -- Achat
AND ach.FCP_Champ = 1
LEFT JOIN F_FAMCOMPTA sto ON f.FA_CodeFamille = sto.FA_CodeFamille
AND sto.FCP_Type = 2 -- Stock
AND sto.FCP_Champ = 1
-- Fournisseur principal
LEFT JOIN F_FAMFOURNISS ff ON f.FA_CodeFamille = ff.FA_CodeFamille
AND ff.FF_Principal = 1
-- Nombre d'articles
LEFT JOIN F_ARTICLE a ON f.FA_CodeFamille = a.FA_CodeFamille
WHERE 1=1
"""
params = []
if not inclure_totaux:
query += " AND f.FA_Type = 0"
logger.info("[SQL] Filtre : FA_Type = 0 (Détail uniquement)")
if filtre:
query += """
AND (
f.FA_CodeFamille LIKE ?
OR f.FA_Intitule LIKE ?
)
"""
params.extend([f"%{filtre}%", f"%{filtre}%"])
query += """
GROUP BY
f.FA_CodeFamille, f.FA_Type, f.FA_Intitule, f.FA_UniteVen, f.FA_Coef,
f.FA_SuiviStock, f.FA_Garantie, f.FA_Central,
f.FA_Stat01, f.FA_Stat02, f.FA_Stat03, f.FA_Stat04, f.FA_Stat05,
f.FA_CodeFiscal, f.FA_Pays, f.FA_UnitePoids, f.FA_Escompte, f.FA_Delai,
f.FA_HorsStat, f.FA_VteDebit, f.FA_NotImp,
f.FA_Frais01FR_Denomination, f.FA_Frais01FR_Rem01REM_Valeur, f.FA_Frais01FR_Rem01REM_Type,
f.FA_Frais01FR_Rem02REM_Valeur, f.FA_Frais01FR_Rem02REM_Type,
f.FA_Frais01FR_Rem03REM_Valeur, f.FA_Frais01FR_Rem03REM_Type,
f.FA_Frais02FR_Denomination, f.FA_Frais02FR_Rem01REM_Valeur, f.FA_Frais02FR_Rem01REM_Type,
f.FA_Frais02FR_Rem02REM_Valeur, f.FA_Frais02FR_Rem02REM_Type,
f.FA_Frais02FR_Rem03REM_Valeur, f.FA_Frais02FR_Rem03REM_Type,
f.FA_Frais03FR_Denomination, f.FA_Frais03FR_Rem01REM_Valeur, f.FA_Frais03FR_Rem01REM_Type,
f.FA_Frais03FR_Rem02REM_Valeur, f.FA_Frais03FR_Rem02REM_Type,
f.FA_Frais03FR_Rem03REM_Valeur, f.FA_Frais03FR_Rem03REM_Type,
f.FA_Contremarque, f.FA_FactPoids, f.FA_FactForfait, f.FA_Publie,
f.FA_RacineRef, f.FA_RacineCB,
f.CL_No1, f.CL_No2, f.CL_No3, f.CL_No4,
f.FA_Nature, f.FA_NbColis, f.FA_SousTraitance, f.FA_Fictif, f.FA_Criticite,
f.cbMarq, f.cbCreateur, f.cbModification, f.cbCreation, f.cbCreationUser,
vte.FCP_ComptaCPT_CompteG, vte.FCP_ComptaCPT_CompteA,
vte.FCP_ComptaCPT_Taxe1, vte.FCP_ComptaCPT_Taxe2, vte.FCP_ComptaCPT_Taxe3,
vte.FCP_ComptaCPT_Date1, vte.FCP_ComptaCPT_Date2, vte.FCP_ComptaCPT_Date3,
vte.FCP_TypeFacture,
ach.FCP_ComptaCPT_CompteG, ach.FCP_ComptaCPT_CompteA,
ach.FCP_ComptaCPT_Taxe1, ach.FCP_ComptaCPT_Taxe2, ach.FCP_ComptaCPT_Taxe3,
ach.FCP_ComptaCPT_Date1, ach.FCP_ComptaCPT_Date2, ach.FCP_ComptaCPT_Date3,
ach.FCP_TypeFacture,
sto.FCP_ComptaCPT_CompteG, sto.FCP_ComptaCPT_CompteA,
ff.CT_Num, ff.FF_Unite, ff.FF_Conversion, ff.FF_DelaiAppro,
ff.FF_Garantie, ff.FF_Colisage, ff.FF_QteMini, ff.FF_QteMont,
ff.EG_Champ, ff.FF_Devise, ff.FF_Remise, ff.FF_ConvDiv, ff.FF_TypeRem
ORDER BY f.FA_Intitule
"""
cursor.execute(query, params)
rows = cursor.fetchall()
def to_str(val):
"""Convertit en string, gère None et int"""
if val is None:
return ""
return str(val).strip() if isinstance(val, str) else str(val)
def to_float(val):
"""Convertit en float, gère None"""
if val is None or val == "":
return 0.0
try:
return float(val)
except (ValueError, TypeError):
return 0.0
def to_int(val):
"""Convertit en int, gère None"""
if val is None or val == "":
return 0
try:
return int(val)
except (ValueError, TypeError):
return 0
def to_bool(val):
"""Convertit en bool"""
if val is None:
return False
if isinstance(val, bool):
return val
if isinstance(val, int):
return val != 0
return bool(val)
familles = []
for row in rows:
idx = 0
famille = {
"code": to_str(row[idx]),
"type": to_int(row[idx + 1]),
"intitule": to_str(row[idx + 2]),
"unite_vente": to_str(row[idx + 3]),
"coef": to_float(row[idx + 4]),
"suivi_stock": to_bool(row[idx + 5]),
"garantie": to_int(row[idx + 6]),
"est_centrale": to_bool(row[idx + 7]),
}
idx += 8
famille.update(
{
"stat_01": to_str(row[idx]),
"stat_02": to_str(row[idx + 1]),
"stat_03": to_str(row[idx + 2]),
"stat_04": to_str(row[idx + 3]),
"stat_05": to_str(row[idx + 4]),
}
)
idx += 5
famille.update(
{
"code_fiscal": to_str(row[idx]),
"pays": to_str(row[idx + 1]),
"unite_poids": to_str(row[idx + 2]),
"escompte": to_bool(row[idx + 3]),
"delai": to_int(row[idx + 4]),
"hors_statistique": to_bool(row[idx + 5]),
"vente_debit": to_bool(row[idx + 6]),
"non_imprimable": to_bool(row[idx + 7]),
}
)
idx += 8
famille.update(
{
"frais_01_libelle": to_str(row[idx]),
"frais_01_remise_1_valeur": to_float(row[idx + 1]),
"frais_01_remise_1_type": to_int(row[idx + 2]),
"frais_01_remise_2_valeur": to_float(row[idx + 3]),
"frais_01_remise_2_type": to_int(row[idx + 4]),
"frais_01_remise_3_valeur": to_float(row[idx + 5]),
"frais_01_remise_3_type": to_int(row[idx + 6]),
"frais_02_libelle": to_str(row[idx + 7]),
"frais_02_remise_1_valeur": to_float(row[idx + 8]),
"frais_02_remise_1_type": to_int(row[idx + 9]),
"frais_02_remise_2_valeur": to_float(row[idx + 10]),
"frais_02_remise_2_type": to_int(row[idx + 11]),
"frais_02_remise_3_valeur": to_float(row[idx + 12]),
"frais_02_remise_3_type": to_int(row[idx + 13]),
"frais_03_libelle": to_str(row[idx + 14]),
"frais_03_remise_1_valeur": to_float(row[idx + 15]),
"frais_03_remise_1_type": to_int(row[idx + 16]),
"frais_03_remise_2_valeur": to_float(row[idx + 17]),
"frais_03_remise_2_type": to_int(row[idx + 18]),
"frais_03_remise_3_valeur": to_float(row[idx + 19]),
"frais_03_remise_3_type": to_int(row[idx + 20]),
}
)
idx += 21
famille.update(
{
"contremarque": to_bool(row[idx]),
"fact_poids": to_bool(row[idx + 1]),
"fact_forfait": to_bool(row[idx + 2]),
"publie": to_bool(row[idx + 3]),
"racine_reference": to_str(row[idx + 4]),
"racine_code_barre": to_str(row[idx + 5]),
}
)
idx += 6
famille.update(
{
"categorie_1": to_int(row[idx]),
"categorie_2": to_int(row[idx + 1]),
"categorie_3": to_int(row[idx + 2]),
"categorie_4": to_int(row[idx + 3]),
}
)
idx += 4
famille.update(
{
"nature": to_int(row[idx]),
"nb_colis": to_int(row[idx + 1]),
"sous_traitance": to_bool(row[idx + 2]),
"fictif": to_bool(row[idx + 3]),
"criticite": to_int(row[idx + 4]),
}
)
idx += 5
famille.update(
{
"cb_marq": to_int(row[idx]),
"cb_createur": to_str(row[idx + 1]),
"cb_modification": row[idx + 2],
"cb_creation": row[idx + 3],
"cb_creation_user": to_str(row[idx + 4]),
}
)
idx += 5
famille.update(
{
"compte_vente": to_str(row[idx]),
"compte_auxiliaire_vente": to_str(row[idx + 1]),
"tva_vente_1": to_str(row[idx + 2]),
"tva_vente_2": to_str(row[idx + 3]),
"tva_vente_3": to_str(row[idx + 4]),
"tva_vente_date_1": row[idx + 5],
"tva_vente_date_2": row[idx + 6],
"tva_vente_date_3": row[idx + 7],
"type_facture_vente": to_int(row[idx + 8]),
}
)
idx += 9
famille.update(
{
"compte_achat": to_str(row[idx]),
"compte_auxiliaire_achat": to_str(row[idx + 1]),
"tva_achat_1": to_str(row[idx + 2]),
"tva_achat_2": to_str(row[idx + 3]),
"tva_achat_3": to_str(row[idx + 4]),
"tva_achat_date_1": row[idx + 5],
"tva_achat_date_2": row[idx + 6],
"tva_achat_date_3": row[idx + 7],
"type_facture_achat": to_int(row[idx + 8]),
}
)
idx += 9
famille.update(
{
"compte_stock": to_str(row[idx]),
"compte_auxiliaire_stock": to_str(row[idx + 1]),
}
)
idx += 2
famille.update(
{
"fournisseur_principal": to_str(row[idx]),
"fournisseur_unite": to_str(row[idx + 1]),
"fournisseur_conversion": to_float(row[idx + 2]),
"fournisseur_delai_appro": to_int(row[idx + 3]),
"fournisseur_garantie": to_int(row[idx + 4]),
"fournisseur_colisage": to_int(row[idx + 5]),
"fournisseur_qte_mini": to_float(row[idx + 6]),
"fournisseur_qte_mont": to_float(row[idx + 7]),
"fournisseur_enumere_gamme": to_int(row[idx + 8]),
"fournisseur_devise": to_int(row[idx + 9]),
"fournisseur_remise": to_float(row[idx + 10]),
"fournisseur_conv_div": to_float(row[idx + 11]),
"fournisseur_type_remise": to_int(row[idx + 12]),
}
)
idx += 13
famille["nb_articles"] = to_int(row[idx])
famille["type_libelle"] = (
"Total" if famille["type"] == 1 else "Détail"
)
famille["est_total"] = famille["type"] == 1
famille["est_detail"] = famille["type"] == 0
famille["FA_CodeFamille"] = famille["code"]
famille["FA_Intitule"] = famille["intitule"]
famille["FA_Type"] = famille["type"]
famille["CG_NumVte"] = famille["compte_vente"]
famille["CG_NumAch"] = famille["compte_achat"]
familles.append(famille)
type_msg = "DÉTAIL uniquement" if not inclure_totaux else "TOUS types"
logger.info(f" {len(familles)} familles chargées ({type_msg})")
return familles
except Exception as e:
logger.error(f"Erreur SQL familles: {e}", exc_info=True)
raise RuntimeError(f"Erreur lecture familles: {str(e)}")
def lire_famille(self, code: str) -> Dict:
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
logger.info(f"[SQL] Lecture famille : {code}")
cursor.execute("SELECT TOP 1 * FROM F_FAMILLE")
colonnes_disponibles = [column[0] for column in cursor.description]
logger.info(f"[SQL] Colonnes trouvées : {len(colonnes_disponibles)}")
colonnes_souhaitees = [
"FA_CodeFamille",
"FA_Intitule",
"FA_Type",
"FA_UniteVen",
"FA_Coef",
"FA_SuiviStock",
"FA_Garantie",
"FA_UnitePoids",
"FA_Delai",
"FA_NbColis",
"CG_NumAch",
"CG_NumVte",
"FA_CodeFiscal",
"FA_Escompte",
"FA_Central",
"FA_Nature",
"CL_No1",
"CL_No2",
"CL_No3",
"CL_No4",
"FA_Stat01",
"FA_Stat02",
"FA_Stat03",
"FA_Stat04",
"FA_Stat05",
"FA_HorsStat",
"FA_Pays",
"FA_VteDebit",
"FA_NotImp",
"FA_Contremarque",
"FA_FactPoids",
"FA_FactForfait",
"FA_Publie",
"FA_RacineRef",
"FA_RacineCB",
"FA_Raccourci",
"FA_SousTraitance",
"FA_Fictif",
"FA_Criticite",
]
colonnes_a_lire = [
col for col in colonnes_souhaitees if col in colonnes_disponibles
]
if not colonnes_a_lire:
colonnes_a_lire = colonnes_disponibles
logger.info(f"[SQL] Colonnes sélectionnées : {len(colonnes_a_lire)}")
colonnes_str = ", ".join([f"f.{col}" for col in colonnes_a_lire])
query = f"""
SELECT {colonnes_str},
ISNULL(COUNT(a.AR_Ref), 0) as nb_articles
FROM F_FAMILLE f
LEFT JOIN F_ARTICLE a ON f.FA_CodeFamille = a.FA_CodeFamille
WHERE UPPER(f.FA_CodeFamille) = ?
GROUP BY {colonnes_str}
"""
cursor.execute(query, (code.upper().strip(),))
row = cursor.fetchone()
if not row:
raise ValueError(f"Famille '{code}' introuvable dans Sage")
famille = {}
for idx, colonne in enumerate(colonnes_a_lire):
valeur = row[idx]
if isinstance(valeur, str):
valeur = valeur.strip()
famille[colonne] = valeur
famille["nb_articles"] = row[-1]
if "FA_CodeFamille" in famille:
famille["code"] = famille["FA_CodeFamille"]
if "FA_Intitule" in famille:
famille["intitule"] = famille["FA_Intitule"]
if "FA_Type" in famille:
type_val = famille["FA_Type"]
famille["type"] = type_val
famille["type_libelle"] = "Total" if type_val == 1 else "Détail"
famille["est_total"] = type_val == 1
else:
famille["type"] = 0
famille["type_libelle"] = "Détail"
famille["est_total"] = False
famille["unite_vente"] = str(famille.get("FA_UniteVen", ""))
famille["unite_poids"] = str(famille.get("FA_UnitePoids", ""))
famille["coef"] = (
float(famille.get("FA_Coef", 0.0))
if famille.get("FA_Coef") is not None
else 0.0
)
famille["suivi_stock"] = bool(famille.get("FA_SuiviStock", 0))
famille["garantie"] = int(famille.get("FA_Garantie", 0))
famille["delai"] = int(famille.get("FA_Delai", 0))
famille["nb_colis"] = int(famille.get("FA_NbColis", 0))
famille["compte_achat"] = famille.get("CG_NumAch", "")
famille["compte_vente"] = famille.get("CG_NumVte", "")
famille["code_fiscal"] = famille.get("FA_CodeFiscal", "")
famille["escompte"] = bool(famille.get("FA_Escompte", 0))
famille["est_centrale"] = bool(famille.get("FA_Central", 0))
famille["nature"] = famille.get("FA_Nature", 0)
famille["pays"] = famille.get("FA_Pays", "")
famille["categorie_1"] = famille.get("CL_No1", 0)
famille["categorie_2"] = famille.get("CL_No2", 0)
famille["categorie_3"] = famille.get("CL_No3", 0)
famille["categorie_4"] = famille.get("CL_No4", 0)
famille["stat_01"] = famille.get("FA_Stat01", "")
famille["stat_02"] = famille.get("FA_Stat02", "")
famille["stat_03"] = famille.get("FA_Stat03", "")
famille["stat_04"] = famille.get("FA_Stat04", "")
famille["stat_05"] = famille.get("FA_Stat05", "")
famille["hors_statistique"] = bool(famille.get("FA_HorsStat", 0))
famille["vente_debit"] = bool(famille.get("FA_VteDebit", 0))
famille["non_imprimable"] = bool(famille.get("FA_NotImp", 0))
famille["contremarque"] = bool(famille.get("FA_Contremarque", 0))
famille["fact_poids"] = bool(famille.get("FA_FactPoids", 0))
famille["fact_forfait"] = bool(famille.get("FA_FactForfait", 0))
famille["publie"] = bool(famille.get("FA_Publie", 0))
famille["racine_reference"] = famille.get("FA_RacineRef", "")
famille["racine_code_barre"] = famille.get("FA_RacineCB", "")
famille["raccourci"] = famille.get("FA_Raccourci", "")
famille["sous_traitance"] = bool(famille.get("FA_SousTraitance", 0))
famille["fictif"] = bool(famille.get("FA_Fictif", 0))
famille["criticite"] = int(famille.get("FA_Criticite", 0))
logger.info(
f"SQL: Famille '{famille['code']}' chargée ({famille['nb_articles']} articles)"
)
return famille
except ValueError as e:
logger.error(f"Erreur famille: {e}")
raise
except Exception as e:
logger.error(f"Erreur SQL famille: {e}", exc_info=True)
raise RuntimeError(f"Erreur lecture famille: {str(e)}")
def creer_entree_stock(self, entree_data: Dict) -> Dict:
try:
with self._com_context(), self._lock_com:
logger.info("[STOCK] === CRÉATION ENTRÉE STOCK (COM pur) ===")
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug("Transaction démarrée")
except Exception:
pass
try:
factory_doc = self.cial.FactoryDocumentStock
persist_doc = factory_doc.CreateType(180)
doc = win32com.client.CastTo(persist_doc, "IBODocumentStock3")
doc.SetDefault()
date_mouv = entree_data.get("date_mouvement")
if isinstance(date_mouv, date):
doc.DO_Date = pywintypes.Time(
datetime.combine(date_mouv, datetime.min.time())
)
else:
doc.DO_Date = pywintypes.Time(datetime.now())
if entree_data.get("reference"):
doc.DO_Ref = entree_data["reference"]
doc.Write()
logger.info("[STOCK] Document créé")
factory_article = self.cial.FactoryArticle
factory_depot = self.cial.FactoryDepot
stocks_mis_a_jour = []
depot_principal = None
try:
persist_depot = factory_depot.List(1)
if persist_depot:
depot_principal = win32com.client.CastTo(
persist_depot, "IBODepot3"
)
depot_principal.Read()
logger.info(
f"Dépôt principal: {getattr(depot_principal, 'DE_Code', '?')}"
)
except Exception as e:
logger.warning(f"Erreur chargement dépôt: {e}")
try:
factory_lignes = doc.FactoryDocumentLigne
except Exception:
factory_lignes = doc.FactoryDocumentStockLigne
for idx, ligne_data in enumerate(entree_data["lignes"], 1):
article_ref = ligne_data["article_ref"].upper()
quantite = ligne_data["quantite"]
stock_mini = ligne_data.get("stock_mini")
stock_maxi = ligne_data.get("stock_maxi")
logger.info(f"[STOCK] Ligne {idx}: {article_ref} x {quantite}")
persist_article = factory_article.ReadReference(article_ref)
if not persist_article:
raise ValueError(f"Article {article_ref} introuvable")
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentStockLigne3"
)
ligne_obj.SetDefault()
try:
ligne_obj.SetDefaultArticleReference(
article_ref, float(quantite)
)
except Exception:
try:
ligne_obj.SetDefaultArticle(
article_obj, float(quantite)
)
except Exception:
raise ValueError(
f"Impossible de lier l'article {article_ref}"
)
prix = ligne_data.get("prix_unitaire")
if prix:
try:
ligne_obj.DL_PrixUnitaire = float(prix)
except Exception:
pass
ligne_obj.Write()
if stock_mini is not None or stock_maxi is not None:
logger.info(
f"[STOCK] Ajustement stock pour {article_ref}..."
)
try:
logger.info(
" [COM] Méthode A : Article.FactoryArticleStock"
)
factory_article = self.cial.FactoryArticle
persist_article_full = factory_article.ReadReference(
article_ref
)
article_full = win32com.client.CastTo(
persist_article_full, "IBOArticle3"
)
article_full.Read()
factory_article_stock = None
try:
factory_article_stock = (
article_full.FactoryArticleStock
)
logger.info(" FactoryArticleStock trouvée")
except AttributeError:
logger.warning(
" FactoryArticleStock non disponible"
)
if factory_article_stock:
stock_trouve = None
index_stock = 1
while index_stock <= 100:
try:
stock_persist = factory_article_stock.List(
index_stock
)
if stock_persist is None:
break
stock_obj = win32com.client.CastTo(
stock_persist, "IBOArticleStock3"
)
stock_obj.Read()
depot_stock = None
try:
depot_stock = getattr(
stock_obj, "Depot", None
)
if depot_stock:
depot_stock.Read()
depot_code = getattr(
depot_stock, "DE_Code", ""
).strip()
logger.debug(
f" Dépôt {index_stock}: {depot_code}"
)
if (
not stock_trouve
or depot_code
== getattr(
depot_principal,
"DE_Code",
"",
)
):
stock_trouve = stock_obj
logger.info(
f" Stock trouvé pour dépôt {depot_code}"
)
except Exception:
pass
index_stock += 1
except Exception as e:
logger.debug(
f" Erreur stock {index_stock}: {e}"
)
index_stock += 1
if not stock_trouve:
try:
stock_persist = (
factory_article_stock.Create()
)
stock_trouve = win32com.client.CastTo(
stock_persist, "IBOArticleStock3"
)
stock_trouve.SetDefault()
if depot_principal:
try:
stock_trouve.Depot = depot_principal
logger.info(
" Dépôt principal lié"
)
except Exception:
pass
logger.info(" Nouvel ArticleStock créé")
except Exception as e:
logger.error(
f" Impossible de créer ArticleStock: {e}"
)
raise
if stock_trouve:
try:
stock_trouve.Read()
except Exception:
pass
if stock_mini is not None:
try:
for prop_name in [
"AS_QteMini",
"AS_Mini",
"AR_StockMini",
"StockMini",
]:
try:
setattr(
stock_trouve,
prop_name,
float(stock_mini),
)
logger.info(
f" Stock mini défini via {prop_name}: {stock_mini}"
)
break
except AttributeError:
continue
except Exception as e:
logger.debug(
f" {prop_name} échoué: {e}"
)
continue
except Exception as e:
logger.warning(
f" Stock mini non défini: {e}"
)
if stock_maxi is not None:
try:
for prop_name in [
"AS_QteMaxi",
"AS_Maxi",
"AR_StockMaxi",
"StockMaxi",
]:
try:
setattr(
stock_trouve,
prop_name,
float(stock_maxi),
)
logger.info(
f" Stock maxi défini via {prop_name}: {stock_maxi}"
)
break
except AttributeError:
continue
except Exception as e:
logger.debug(
f" {prop_name} échoué: {e}"
)
continue
except Exception as e:
logger.warning(
f" Stock maxi non défini: {e}"
)
try:
stock_trouve.Write()
logger.info(" ArticleStock sauvegardé")
except Exception as e:
logger.error(
f" Erreur Write() ArticleStock: {e}"
)
raise
if depot_principal and (
stock_mini is not None or stock_maxi is not None
):
logger.info(
" [COM] Méthode B : Depot.FactoryDepotStock (alternative)"
)
try:
factory_depot_stock = None
for factory_name in [
"FactoryDepotStock",
"FactoryArticleStock",
]:
try:
factory_depot_stock = getattr(
depot_principal, factory_name, None
)
if factory_depot_stock:
logger.info(
f" Factory trouvée: {factory_name}"
)
break
except Exception:
continue
if factory_depot_stock:
stock_depot_trouve = None
index_ds = 1
while index_ds <= 100:
try:
stock_ds_persist = (
factory_depot_stock.List(
index_ds
)
)
if stock_ds_persist is None:
break
stock_ds = win32com.client.CastTo(
stock_ds_persist,
"IBODepotStock3",
)
stock_ds.Read()
ar_ref_ds = (
getattr(stock_ds, "AR_Ref", "")
.strip()
.upper()
)
if ar_ref_ds == article_ref:
stock_depot_trouve = stock_ds
break
index_ds += 1
except Exception:
index_ds += 1
if not stock_depot_trouve:
try:
stock_ds_persist = (
factory_depot_stock.Create()
)
stock_depot_trouve = (
win32com.client.CastTo(
stock_ds_persist,
"IBODepotStock3",
)
)
stock_depot_trouve.SetDefault()
stock_depot_trouve.AR_Ref = (
article_ref
)
logger.info(
" Nouveau DepotStock créé"
)
except Exception as e:
logger.error(
f" Impossible de créer DepotStock: {e}"
)
if stock_depot_trouve:
if stock_mini is not None:
try:
stock_depot_trouve.AS_QteMini = float(
stock_mini
)
logger.info(
f" DepotStock.AS_QteMini = {stock_mini}"
)
except Exception as e:
logger.warning(
f" DepotStock mini échoué: {e}"
)
if stock_maxi is not None:
try:
stock_depot_trouve.AS_QteMaxi = float(
stock_maxi
)
logger.info(
f" DepotStock.AS_QteMaxi = {stock_maxi}"
)
except Exception as e:
logger.warning(
f" DepotStock maxi échoué: {e}"
)
try:
stock_depot_trouve.Write()
logger.info(
" DepotStock sauvegardé"
)
except Exception as e:
logger.error(
f" DepotStock Write() échoué: {e}"
)
except Exception as e:
logger.warning(f" Méthode B échouée: {e}")
except Exception as e:
logger.error(
f"[STOCK] Erreur ajustement stock: {e}",
exc_info=True,
)
stocks_mis_a_jour.append(
{
"article_ref": article_ref,
"quantite_ajoutee": quantite,
"stock_mini_defini": stock_mini,
"stock_maxi_defini": stock_maxi,
}
)
doc.Write()
doc.Read()
numero = getattr(doc, "DO_Piece", "")
logger.info(f"[STOCK] Document finalisé: {numero}")
logger.info("[STOCK] Vérification finale via COM...")
for stock_info in stocks_mis_a_jour:
article_ref = stock_info["article_ref"]
try:
persist_article = factory_article.ReadReference(article_ref)
article_verif = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_verif.Read()
stock_total = 0.0
stock_mini_lu = 0.0
stock_maxi_lu = 0.0
for attr in ["AR_Stock", "AS_QteSto", "Stock"]:
try:
val = getattr(article_verif, attr, None)
if val is not None:
stock_total = float(val)
break
except Exception:
pass
for attr in ["AR_StockMini", "AS_QteMini", "StockMini"]:
try:
val = getattr(article_verif, attr, None)
if val is not None:
stock_mini_lu = float(val)
break
except Exception:
pass
for attr in ["AR_StockMaxi", "AS_QteMaxi", "StockMaxi"]:
try:
val = getattr(article_verif, attr, None)
if val is not None:
stock_maxi_lu = float(val)
break
except Exception:
pass
logger.info(
f"[VERIF] {article_ref}: "
f"Total={stock_total}, "
f"Mini={stock_mini_lu}, "
f"Maxi={stock_maxi_lu}"
)
stock_info["stock_total_verifie"] = stock_total
stock_info["stock_mini_verifie"] = stock_mini_lu
stock_info["stock_maxi_verifie"] = stock_maxi_lu
except Exception as e:
logger.warning(
f"[VERIF] Erreur vérification {article_ref}: {e}"
)
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.info("[STOCK] Transaction committée")
except Exception:
logger.info("[STOCK] Changements sauvegardés")
return {
"article_ref": article_ref,
"numero": numero,
"type": 180,
"type_libelle": "Entrée en stock",
"date": str(getattr(doc, "DO_Date", "")),
"nb_lignes": len(stocks_mis_a_jour),
"stocks_mis_a_jour": stocks_mis_a_jour,
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.info("[STOCK] Transaction annulée")
except Exception:
pass
logger.error(f"[STOCK] ERREUR : {e}", exc_info=True)
raise ValueError(f"Erreur création entrée stock : {str(e)}")
except Exception as e:
logger.error(f"[STOCK] ERREUR GLOBALE : {e}", exc_info=True)
raise ValueError(f"Erreur création entrée stock : {str(e)}")
def creer_sortie_stock(self, sortie_data: Dict) -> Dict:
try:
with (
self._com_context(),
self._lock_com,
self._get_sql_connection() as conn,
):
cursor = conn.cursor()
logger.info("[STOCK] === CRÉATION SORTIE STOCK ===")
logger.info(f"[STOCK] {len(sortie_data.get('lignes', []))} ligne(s)")
try:
self.cial.CptaApplication.BeginTrans()
except Exception:
pass
try:
factory = self.cial.FactoryDocumentStock
persist = factory.CreateType(181)
doc = win32com.client.CastTo(persist, "IBODocumentStock3")
doc.SetDefault()
date_mouv = sortie_data.get("date_mouvement")
if isinstance(date_mouv, date):
doc.DO_Date = pywintypes.Time(
datetime.combine(date_mouv, datetime.min.time())
)
else:
doc.DO_Date = pywintypes.Time(datetime.now())
if sortie_data.get("reference"):
doc.DO_Ref = sortie_data["reference"]
doc.Write()
logger.info(
f"[STOCK] Document créé : {getattr(doc, 'DO_Piece', '?')}"
)
try:
factory_lignes = doc.FactoryDocumentLigne
except Exception:
factory_lignes = doc.FactoryDocumentStockLigne
factory_article = self.cial.FactoryArticle
stocks_mis_a_jour = []
for idx, ligne_data in enumerate(sortie_data["lignes"], 1):
article_ref = ligne_data["article_ref"].upper()
quantite = ligne_data["quantite"]
logger.info(
f"[STOCK] ======== LIGNE {idx} : {article_ref} x {quantite} ========"
)
persist_article = factory_article.ReadReference(article_ref)
if not persist_article:
raise ValueError(f"Article {article_ref} introuvable")
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ar_suivi = getattr(article_obj, "AR_SuiviStock", 0)
ar_design = getattr(article_obj, "AR_Design", article_ref)
logger.info(f"[STOCK] Article : {ar_design}")
logger.info(f"[STOCK] AR_SuiviStock : {ar_suivi}")
stock_dispo = verifier_stock_suffisant(
article_ref, quantite, cursor, None
)
if not stock_dispo["suffisant"]:
raise ValueError(
f"Stock insuffisant pour {article_ref} : "
f"disponible={stock_dispo['stock_disponible']}, "
f"demandé={quantite}"
)
logger.info(
f"[STOCK] Stock disponible : {stock_dispo['stock_disponible']}"
)
numero_lot = ligne_data.get("numero_lot")
if ar_suivi == 1:
if numero_lot:
logger.warning("[STOCK] CMUP : Suppression du lot")
numero_lot = None
elif ar_suivi == 2:
if not numero_lot:
import uuid
numero_lot = f"AUTO-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6].upper()}"
logger.info(
f"[STOCK] FIFO/LIFO : Lot auto-généré '{numero_lot}'"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentStockLigne3"
)
ligne_obj.SetDefault()
article_lie = False
try:
ligne_obj.SetDefaultArticleReference(
article_ref, float(quantite)
)
article_lie = True
logger.info("[STOCK] SetDefaultArticleReference()")
except Exception:
try:
ligne_obj.SetDefaultArticle(
article_obj, float(quantite)
)
article_lie = True
logger.info("[STOCK] SetDefaultArticle()")
except Exception:
pass
if not article_lie:
raise ValueError(
f"Impossible de lier l'article {article_ref}"
)
if numero_lot and ar_suivi == 2:
try:
ligne_obj.SetDefaultLot(numero_lot)
logger.info("[STOCK] Lot défini")
except Exception:
try:
ligne_obj.LS_NoSerie = numero_lot
logger.info("[STOCK] Lot via LS_NoSerie")
except Exception:
pass
prix = ligne_data.get("prix_unitaire")
if prix:
try:
ligne_obj.DL_PrixUnitaire = float(prix)
except Exception:
pass
ligne_obj.Write()
logger.info("[STOCK] Write() réussi")
ligne_obj.Read()
ref_verifiee = article_ref
try:
article_lie_obj = getattr(ligne_obj, "Article", None)
if article_lie_obj:
article_lie_obj.Read()
ref_verifiee = (
getattr(article_lie_obj, "AR_Ref", "").strip()
or article_ref
)
except Exception:
pass
logger.info(f"[STOCK] LIGNE {idx} COMPLÈTE")
stocks_mis_a_jour.append(
{
"article_ref": article_ref,
"quantite_retiree": quantite,
"reference_verifiee": ref_verifiee,
"stock_avant": stock_dispo["stock_disponible"],
"stock_apres": stock_dispo["stock_apres"],
"numero_lot": numero_lot if ar_suivi == 2 else None,
}
)
doc.Write()
doc.Read()
numero = getattr(doc, "DO_Piece", "")
logger.info(f"[STOCK] Document finalisé : {numero}")
try:
self.cial.CptaApplication.CommitTrans()
logger.info("[STOCK] Transaction committée")
except Exception:
pass
return {
"numero": numero,
"type": 1,
"date": str(getattr(doc, "DO_Date", "")),
"nb_lignes": len(stocks_mis_a_jour),
"reference": sortie_data.get("reference"),
"stocks_mis_a_jour": stocks_mis_a_jour,
}
except Exception as e:
logger.error(f"[STOCK] ERREUR : {e}", exc_info=True)
try:
self.cial.CptaApplication.RollbackTrans()
except Exception:
pass
raise ValueError(f"Erreur création sortie stock : {str(e)}")
except Exception as e:
logger.error(f"[STOCK] ERREUR GLOBALE : {e}", exc_info=True)
raise ValueError(f"Erreur création sortie stock : {str(e)}")
def lire_mouvement_stock(self, numero: str) -> Dict:
try:
with self._com_context(), self._lock_com:
factory = self.cial.FactoryDocumentStock
persist = None
index = 1
logger.info(f"[MOUVEMENT] Recherche de {numero}...")
while index < 10000:
try:
persist_test = factory.List(index)
if persist_test is None:
break
doc_test = win32com.client.CastTo(
persist_test, "IBODocumentStock3"
)
doc_test.Read()
if getattr(doc_test, "DO_Piece", "") == numero:
persist = persist_test
logger.info(f"[MOUVEMENT] Trouvé à l'index {index}")
break
index += 1
except Exception:
index += 1
if not persist:
raise ValueError(f"Mouvement {numero} introuvable")
doc = win32com.client.CastTo(persist, "IBODocumentStock3")
doc.Read()
do_type = getattr(doc, "DO_Type", -1)
types_mouvements = {
180: "Entrée",
181: "Sortie",
182: "Transfert",
183: "Inventaire",
}
mouvement = {
"numero": numero,
"type": do_type,
"type_libelle": types_mouvements.get(do_type, f"Type {do_type}"),
"date": str(getattr(doc, "DO_Date", "")),
"reference": getattr(doc, "DO_Ref", ""),
"lignes": [],
}
try:
factory_lignes = getattr(
doc, "FactoryDocumentLigne", None
) or getattr(doc, "FactoryDocumentStockLigne", None)
if factory_lignes:
idx = 1
while idx <= 100:
try:
ligne_p = factory_lignes.List(idx)
if ligne_p is None:
break
try:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
except Exception:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentStockLigne3"
)
ligne.Read()
article_ref = ""
try:
article_obj = getattr(ligne, "Article", None)
if article_obj:
article_obj.Read()
article_ref = getattr(
article_obj, "AR_Ref", ""
).strip()
except Exception:
pass
ligne_info = {
"article_ref": article_ref,
"designation": getattr(ligne, "DL_Design", ""),
"quantite": float(getattr(ligne, "DL_Qte", 0.0)),
"prix_unitaire": float(
getattr(ligne, "DL_PrixUnitaire", 0.0)
),
"montant_ht": float(
getattr(ligne, "DL_MontantHT", 0.0)
),
"numero_lot": getattr(ligne, "LS_NoSerie", ""),
}
mouvement["lignes"].append(ligne_info)
idx += 1
except Exception:
break
except Exception as e:
logger.warning(f"[MOUVEMENT] Erreur lecture lignes : {e}")
mouvement["nb_lignes"] = len(mouvement["lignes"])
logger.info(
f"[MOUVEMENT] Lu : {numero} - {mouvement['nb_lignes']} ligne(s)"
)
return mouvement
except ValueError:
raise
except Exception as e:
logger.error(f"[MOUVEMENT] Erreur : {e}", exc_info=True)
raise ValueError(f"Erreur lecture mouvement : {str(e)}")
def lister_tous_tiers(
self, type_tiers: Optional[str] = None, filtre: str = ""
) -> List[Dict]:
"""Liste tous les tiers avec jointure sur le collaborateur/commercial"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = _build_tiers_select_query()
query += """
FROM F_COMPTET t
LEFT JOIN F_COLLABORATEUR c ON t.CO_No = c.CO_No
WHERE 1=1
"""
params = []
if type_tiers and type_tiers != "all":
if type_tiers == "prospect":
query += " AND t.CT_Type = 0 AND t.CT_Prospect = 1"
elif type_tiers == "client":
query += " AND t.CT_Type = 0 AND t.CT_Prospect = 0"
elif type_tiers == "fournisseur":
query += " AND t.CT_Type = 1"
if filtre:
query += " AND (t.CT_Num LIKE ? OR t.CT_Intitule LIKE ?)"
params.extend([f"%{filtre}%", f"%{filtre}%"])
query += " ORDER BY t.CT_Intitule"
cursor.execute(query, params)
rows = cursor.fetchall()
tiers_list = []
for row in rows:
tiers = tiers_to_dict(row)
tiers["contacts"] = _get_contacts(row.CT_Num, conn)
tiers_list.append(tiers)
logger.info(
f" SQL: {len(tiers_list)} tiers retournés (type={type_tiers}, filtre={filtre})"
)
return tiers_list
except Exception as e:
logger.error(f" Erreur SQL tiers: {e}")
raise RuntimeError(f"Erreur lecture tiers: {str(e)}")
def lire_tiers(self, code: str) -> Optional[Dict]:
"""Lit un tiers (client/fournisseur/prospect) par code avec son commercial"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = _build_tiers_select_query()
query += """
FROM F_COMPTET t
LEFT JOIN F_COLLABORATEUR c ON t.CO_No = c.CO_No
WHERE t.CT_Num = ?
"""
cursor.execute(query, (code.upper(),))
row = cursor.fetchone()
if not row:
return None
tiers = tiers_to_dict(row)
tiers["contacts"] = _get_contacts(row.CT_Num, conn)
logger.info(f" SQL: Tiers {code} lu avec succès")
return tiers
except Exception as e:
logger.error(f" Erreur SQL tiers {code}: {e}")
return None
def lister_tous_collaborateurs(self, filtre="", actifs_seulement=True):
"""Liste tous les collaborateurs"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT
CO_No, CO_Nom, CO_Prenom, CO_Fonction,
CO_Adresse, CO_Complement, CO_CodePostal, CO_Ville,
CO_CodeRegion, CO_Pays, CO_Service,
CO_Vendeur, CO_Caissier, CO_Acheteur,
CO_Telephone, CO_Telecopie, CO_EMail, CO_TelPortable,
CO_Matricule, CO_Facebook, CO_LinkedIn, CO_Skype,
CO_Sommeil, CO_ChefVentes, CO_NoChefVentes
FROM F_COLLABORATEUR
WHERE 1=1
"""
params = []
if actifs_seulement:
query += " AND CO_Sommeil = 0"
if filtre:
query += " AND (CO_Nom LIKE ? OR CO_Prenom LIKE ?)"
params.extend([f"%{filtre}%", f"%{filtre}%"])
query += " ORDER BY CO_Nom, CO_Prenom"
cursor.execute(query, params)
rows = cursor.fetchall()
collaborateurs = [collaborators_to_dict(row) for row in rows]
logger.info(f" SQL: {len(collaborateurs)} collaborateurs")
return collaborateurs
except Exception as e:
logger.error(f" Erreur SQL collaborateurs: {e}")
raise RuntimeError(f"Erreur lecture collaborateurs: {str(e)}")
def lire_collaborateur(self, numero):
"""Lit un collaborateur par son numéro"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT
CO_No, CO_Nom, CO_Prenom, CO_Fonction,
CO_Adresse, CO_Complement, CO_CodePostal, CO_Ville,
CO_CodeRegion, CO_Pays, CO_Service,
CO_Vendeur, CO_Caissier, CO_Acheteur,
CO_Telephone, CO_Telecopie, CO_EMail, CO_TelPortable,
CO_Matricule, CO_Facebook, CO_LinkedIn, CO_Skype,
CO_Sommeil, CO_ChefVentes, CO_NoChefVentes
FROM F_COLLABORATEUR
WHERE CO_No = ?
"""
cursor.execute(query, (numero,))
row = cursor.fetchone()
if not row:
return None
collaborateur = collaborators_to_dict(row)
logger.info(
f" SQL: Collaborateur {numero} avec {len(collaborateur)} champs"
)
return collaborateur
except Exception as e:
logger.error(f" Erreur SQL collaborateur {numero}: {e}")
return None
def creer_collaborateur(self, data: dict) -> dict:
"""Crée un nouveau collaborateur via COM Sage (BOI)"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
if not data.get("nom"):
raise ValueError("Le champ 'nom' est obligatoire")
nom_upper = str(data["nom"]).upper().strip()[:35]
prenom = str(data.get("prenom", "")).strip()[:35] if data.get("prenom") else ""
logger.info(f"\n{'=' * 70}")
logger.info(f" CRÉATION COLLABORATEUR: {nom_upper} {prenom}")
logger.info(f"{'=' * 70}")
try:
with self._com_context(), self._lock_com:
logger.info(" Vérification doublon...")
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT CO_No FROM F_COLLABORATEUR WHERE CO_Nom = ? AND CO_Prenom = ?",
(nom_upper, prenom),
)
existing = cursor.fetchone()
if existing:
raise ValueError(
f"Le collaborateur '{nom_upper} {prenom}' existe déjà (N°{existing[0]})"
)
logger.info(" Pas de doublon")
try:
factory = self.cial.FactoryCollaborateur
except AttributeError:
factory = self.cial.CptaApplication.FactoryCollaborateur
persist = factory.Create()
collab = None
for iface in [
"IBOCollaborateur3",
"IBOCollaborateur2",
"IBOCollaborateur",
]:
try:
collab = win32com.client.CastTo(persist, iface)
logger.info(f" Cast vers {iface}")
break
except Exception:
pass
if not collab:
collab = persist
try:
collab.SetDefault()
logger.info(" SetDefault()")
except Exception as e:
logger.warning(f"SetDefault() ignoré: {e}")
def safe_set(obj, attr, value, max_len=None):
"""Affecte une valeur de manière sécurisée"""
if value is None or value == "":
return False
try:
val = str(value)
if max_len:
val = val[:max_len]
setattr(obj, attr, val)
logger.debug(f" {attr} = '{val}'")
return True
except Exception as e:
logger.warning(f" {attr}: {e}")
return False
logger.info(" Champs directs...")
safe_set(collab, "Nom", nom_upper, 35)
safe_set(collab, "Prenom", prenom, 35)
safe_set(collab, "Fonction", data.get("fonction"), 35)
safe_set(collab, "Service", data.get("service"), 35)
safe_set(collab, "Matricule", data.get("matricule"), 10)
safe_set(collab, "Facebook", data.get("facebook"), 35)
safe_set(collab, "LinkedIn", data.get("linkedin"), 35)
safe_set(collab, "Skype", data.get("skype"), 35)
logger.info(" Adresse...")
try:
adresse_obj = collab.Adresse
safe_set(adresse_obj, "Adresse", data.get("adresse"), 35)
safe_set(adresse_obj, "Complement", data.get("complement"), 35)
safe_set(adresse_obj, "CodePostal", data.get("code_postal"), 9)
safe_set(adresse_obj, "Ville", data.get("ville"), 35)
safe_set(adresse_obj, "CodeRegion", data.get("code_region"), 25)
safe_set(adresse_obj, "Pays", data.get("pays"), 35)
except Exception as e:
logger.warning(f" Erreur Adresse: {e}")
logger.info(" Telecom...")
try:
telecom_obj = collab.Telecom
safe_set(telecom_obj, "Telephone", data.get("telephone"), 21)
safe_set(telecom_obj, "Telecopie", data.get("telecopie"), 21)
safe_set(telecom_obj, "EMail", data.get("email"), 69)
safe_set(telecom_obj, "Portable", data.get("tel_portable"), 21)
except Exception as e:
logger.warning(f" Erreur Telecom: {e}")
logger.info(" Booléens...")
if data.get("vendeur") is True:
try:
collab.Vendeur = True
logger.debug(" Vendeur = True")
except Exception:
pass
if data.get("caissier") is True:
try:
collab.Caissier = True
except Exception:
pass
if data.get("acheteur") is True:
try:
collab.Acheteur = True
except Exception:
pass
if data.get("sommeil") is True:
try:
collab.Sommeil = True
except Exception:
pass
if data.get("chef_ventes") is True:
try:
collab.ChefVentes = True
except Exception:
pass
logger.info(" Write()...")
try:
collab.Write()
logger.info(" Write() RÉUSSI!")
except Exception as e:
logger.error(f" Write() échoué: {e}")
raise RuntimeError(f"Échec Write(): {e}")
numero_cree = None
try:
collab.Read()
for attr in ["No", "CO_No", "Numero"]:
try:
val = getattr(collab, attr)
if val and isinstance(val, int):
numero_cree = val
break
except Exception:
pass
except Exception:
pass
if not numero_cree:
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = f"SELECT CO_No FROM F_COLLABORATEUR WHERE CO_Nom = '{nom_upper}'"
if prenom:
query += f" AND CO_Prenom = '{prenom}'"
cursor.execute(query)
row = cursor.fetchone()
if row:
numero_cree = row[0]
except Exception as e:
logger.warning(f"SQL récup numéro: {e}")
logger.info(f"\n{'=' * 70}")
logger.info(
f" COLLABORATEUR CRÉÉ: N°{numero_cree} - {nom_upper} {prenom}"
)
logger.info(f"{'=' * 70}")
if numero_cree:
return self.lire_collaborateur(numero_cree)
else:
return {"nom": nom_upper, "prenom": prenom, "status": "créé"}
except ValueError as e:
logger.warning(f" Validation: {e}")
raise
except Exception as e:
logger.error(f" Erreur création collaborateur: {e}", exc_info=True)
raise RuntimeError(f"Échec création collaborateur: {str(e)}")
def modifier_collaborateur(self, numero: int, data: dict) -> dict:
"""Modifie un collaborateur existant via COM Sage (BOI)"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(f"\n{'=' * 70}")
logger.info(f" MODIFICATION COLLABORATEUR N°{numero}")
logger.info(f"{'=' * 70}")
try:
with self._com_context(), self._lock_com:
try:
factory = self.cial.FactoryCollaborateur
except AttributeError:
factory = self.cial.CptaApplication.FactoryCollaborateur
try:
persist = factory.ReadNumero(numero)
except Exception as e:
raise ValueError(f"Collaborateur {numero} introuvable: {e}")
if not persist:
raise ValueError(f"Collaborateur {numero} introuvable")
collab = None
for iface in [
"IBOCollaborateur3",
"IBOCollaborateur2",
"IBOCollaborateur",
]:
try:
collab = win32com.client.CastTo(persist, iface)
logger.info(f" Cast vers {iface}")
break
except Exception:
pass
if not collab:
collab = persist
try:
collab.Read()
logger.info(f" Collaborateur {numero} chargé")
except Exception as e:
logger.warning(f"Read() ignoré: {e}")
def safe_set(obj, attr, value, max_len=None):
"""Affecte une valeur de manière sécurisée"""
if value is None:
return False
try:
val = str(value) if not isinstance(value, bool) else value
if max_len and isinstance(val, str):
val = val[:max_len]
setattr(obj, attr, val)
logger.debug(f" {attr} = '{val}'")
return True
except Exception as e:
logger.warning(f" {attr}: {e}")
return False
champs_modifies = []
logger.info(" Champs directs...")
champs_directs = {
"nom": ("Nom", 35),
"prenom": ("Prenom", 35),
"fonction": ("Fonction", 35),
"service": ("Service", 35),
"matricule": ("Matricule", 10),
"facebook": ("Facebook", 35),
"linkedin": ("LinkedIn", 35),
"skype": ("Skype", 35),
}
for py_field, (sage_attr, max_len) in champs_directs.items():
if py_field in data:
val = data[py_field]
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)
logger.info(" Adresse...")
try:
adresse_obj = collab.Adresse
champs_adresse = {
"adresse": ("Adresse", 35),
"complement": ("Complement", 35),
"code_postal": ("CodePostal", 9),
"ville": ("Ville", 35),
"code_region": ("CodeRegion", 25),
"pays": ("Pays", 35),
}
for py_field, (sage_attr, max_len) in champs_adresse.items():
if py_field in data:
if safe_set(
adresse_obj, sage_attr, data[py_field], max_len
):
champs_modifies.append(f"Adresse.{sage_attr}")
except Exception as e:
logger.warning(f" Erreur accès Adresse: {e}")
logger.info(" Telecom...")
try:
telecom_obj = collab.Telecom
champs_telecom = {
"telephone": ("Telephone", 21),
"telecopie": ("Telecopie", 21),
"email": ("EMail", 69),
"tel_portable": ("Portable", 21),
}
for py_field, (sage_attr, max_len) in champs_telecom.items():
if py_field in data:
if safe_set(
telecom_obj, sage_attr, data[py_field], max_len
):
champs_modifies.append(f"Telecom.{sage_attr}")
except Exception as e:
logger.warning(f" Erreur accès Telecom: {e}")
logger.info(" Booléens...")
champs_bool = {
"vendeur": "Vendeur",
"caissier": "Caissier",
"acheteur": "Acheteur",
"sommeil": "Sommeil",
"chef_ventes": "ChefVentes",
}
for py_field, sage_attr in champs_bool.items():
if py_field in data and data[py_field] is not None:
try:
val = bool(data[py_field])
setattr(collab, sage_attr, val)
champs_modifies.append(sage_attr)
logger.debug(f" {sage_attr} = {val}")
except Exception as e:
logger.warning(f" {sage_attr}: {e}")
if not champs_modifies:
logger.info(" Aucun champ à modifier")
return self.lire_collaborateur(numero)
logger.info(
f" {len(champs_modifies)} champ(s) à modifier: {champs_modifies}"
)
logger.info(" Write()...")
try:
collab.Write()
logger.info(" Write() RÉUSSI!")
except Exception as e:
logger.error(f" Write() échoué: {e}")
raise RuntimeError(f"Échec Write(): {e}")
logger.info(f"\n{'=' * 70}")
logger.info(f" COLLABORATEUR MODIFIÉ: N°{numero}")
logger.info(f"{'=' * 70}")
return self.lire_collaborateur(numero)
except ValueError as e:
logger.warning(f" Validation: {e}")
raise
except Exception as e:
logger.error(f" Erreur modification collaborateur: {e}", exc_info=True)
raise RuntimeError(f"Échec modification collaborateur: {str(e)}")
def lire_informations_societe(self):
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
row = get_societe_row(cursor)
if not row:
logger.warning("Aucune donnée dans P_DOSSIER")
return None
societe = society_to_dict(row)
societe["exercices"] = build_exercices(row)
self._numero_dossier = societe.get("numero_dossier")
add_logo(societe)
if not societe.get("logo_base64"):
logo_com = recuperer_logo_com(self)
societe.update(logo_com)
logger.info(
f" Informations société '{societe['raison_sociale']}' lues"
)
return societe
except Exception as e:
logger.error(f" Erreur lecture P_DOSSIER: {e}", exc_info=True)
raise RuntimeError(f"Erreur lecture informations société: {str(e)}")
def regler_facture(
self,
numero_facture: str,
montant: float,
mode_reglement: int = 0,
date_reglement: datetime = None,
reference: str = "",
libelle: str = "",
code_journal: str = None,
devise_code: int = 0,
cours_devise: float = 1.0,
tva_encaissement: bool = False,
compte_general: str = None,
):
"""Règle une facture"""
return _regler_facture(
self,
numero_facture=numero_facture,
montant=montant,
mode_reglement=mode_reglement,
date_reglement=date_reglement,
reference=reference,
libelle=libelle,
code_journal=code_journal,
devise_code=devise_code,
cours_devise=cours_devise,
tva_encaissement=tva_encaissement,
compte_general=compte_general,
)
def regler_factures_client(
self,
client_code: str,
montant_total: float,
mode_reglement: int = 0,
date_reglement: datetime = None,
reference: str = "",
libelle: str = "",
code_journal: str = None,
numeros_factures: List[str] = None,
devise_code: int = 0,
cours_devise: float = 1.0,
tva_encaissement: bool = False,
):
"""Règle plusieurs factures d'un client"""
return _regler_factures_client(
self,
client_code=client_code,
montant_total=montant_total,
mode_reglement=mode_reglement,
date_reglement=date_reglement,
reference=reference,
libelle=libelle,
code_journal=code_journal,
numeros_factures=numeros_factures,
devise_code=devise_code,
cours_devise=cours_devise,
tva_encaissement=tva_encaissement,
)
def lire_reglements_facture(self, numero_facture: str):
"""Récupère les règlements d'une facture"""
return _lire_reglements_facture(self, numero_facture)
def lire_reglements_client(
self,
client_code: str,
date_debut: datetime = None,
date_fin: datetime = None,
inclure_soldees: bool = True,
):
"""Récupère les règlements d'un client"""
return _lire_reglements_client(
self,
client_code=client_code,
date_debut=date_debut,
date_fin=date_fin,
inclure_soldees=inclure_soldees,
)
def lire_journaux_banque(self):
return _lire_journaux(self)
def lire_tous_journaux(self):
return _lire(self)
def introspecter_reglement(self):
return _intro(self)
def valider_facture(self, numero_facture: str) -> dict:
return _valider(self, numero_facture)
def devalider_facture(self, numero_facture: str) -> dict:
return _devalider(self, numero_facture)
def get_statut_validation(self, numero_facture: str) -> dict:
return _get_statut(self, numero_facture)
def introspecter_validation(self, numero_facture: str = None) -> dict:
return _introspect(self, numero_facture)
def introspecter_document_complet(self, numero_facture: str) -> dict:
return _introspect_doc(self, numero_facture)
def lire_modes_reglement(self) -> dict:
return lire_modes_reglement(self)
def lire_devises(self) -> dict:
return lire_devises(self)
def lire_journaux_tresorerie(self) -> dict:
return lire_journaux_tresorerie(self)
def lire_comptes_generaux(self, prefixe, type_compte) -> dict:
return lire_comptes_generaux(self, prefixe, type_compte)
def lire_tva_taux(self) -> dict:
return lire_tva_taux(self)
def lire_parametres_encaissement(self) -> dict:
return lire_parametres_encaissement(self)
def lire_tous_reglements(
self,
date_debut,
date_fin,
client_code,
type_reglement,
) -> dict:
return lire_tous_reglements(
self,
date_debut,
date_fin,
client_code,
type_reglement,
)
def lire_facture_reglement_detail(self, facture_no) -> dict:
return lire_facture_reglement_detail(self, facture_no)
def lire_reglement_detail(self, rg_no) -> dict:
return lire_reglement_detail(self, rg_no)