8104 lines
350 KiB
Python
8104 lines
350 KiB
Python
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 (
|
||
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()
|
||
|
||
# === DÉTECTION DES COLONNES ===
|
||
logger.info(f"[SQL] Lecture article {reference}...")
|
||
cursor.execute("SELECT TOP 1 * FROM F_ARTICLE")
|
||
colonnes_disponibles = [column[0] for column in cursor.description]
|
||
|
||
# Configuration du mapping complet
|
||
colonnes_config = {
|
||
"AR_Ref": "reference",
|
||
"AR_Design": "designation",
|
||
"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",
|
||
}
|
||
|
||
# Sélection des colonnes disponibles
|
||
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
|
||
|
||
# Construction de la requête SQL avec échappement des noms de colonnes
|
||
colonnes_sql = []
|
||
for col in colonnes_a_lire:
|
||
if " " in col or "/" in col or "è" in col:
|
||
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
|
||
|
||
# Construction du dictionnaire row_data
|
||
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
|
||
|
||
# Mapping de l'article
|
||
article = _mapper_article_depuis_row(row_data, colonnes_config)
|
||
|
||
# Enrichissements
|
||
articles = [
|
||
article
|
||
] # Liste d'un seul article pour les fonctions d'enrichissement
|
||
|
||
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
|
||
|
||
# Tentative 1 : Client
|
||
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}")
|
||
|
||
# Tentative 2 : Fournisseur (si pas trouvé comme client)
|
||
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}")
|
||
|
||
# Vérification finale
|
||
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)."
|
||
)
|
||
|
||
# Cast et lecture
|
||
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
|
||
|
||
# Tentative 1 : Client
|
||
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}")
|
||
|
||
# Tentative 2 : Fournisseur (si pas trouvé comme client)
|
||
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}")
|
||
|
||
# Vérification finale
|
||
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)."
|
||
)
|
||
|
||
# Cast et lecture
|
||
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")
|
||
|
||
# ============================================================
|
||
# DEBUG TEMPORAIRE : Lister les méthodes disponibles
|
||
# ============================================================
|
||
methodes_client = [m for m in dir(client) if not m.startswith("_")]
|
||
logger.info(
|
||
f" [DEBUG] Méthodes disponibles sur client: {methodes_client}"
|
||
)
|
||
|
||
# Chercher spécifiquement les méthodes de verrouillage
|
||
lock_methods = [
|
||
m
|
||
for m in methodes_client
|
||
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}")
|
||
|
||
# ============================================================
|
||
# VERROUILLAGE : Tenter plusieurs approches
|
||
# ============================================================
|
||
import time
|
||
|
||
max_retries = 3
|
||
retry_delay = 1.0
|
||
locked = False
|
||
lock_method_used = None
|
||
|
||
for attempt in range(max_retries):
|
||
try:
|
||
# Approche 1: ReadLock (méthode préférée)
|
||
if hasattr(client, "ReadLock"):
|
||
client.ReadLock()
|
||
locked = True
|
||
lock_method_used = "ReadLock"
|
||
logger.info(" Verrouillage via ReadLock() [OK]")
|
||
break
|
||
|
||
# Approche 2: Lock
|
||
elif hasattr(client, "Lock"):
|
||
client.Lock()
|
||
locked = True
|
||
lock_method_used = "Lock"
|
||
logger.info(" Verrouillage via Lock() [OK]")
|
||
break
|
||
|
||
# Approche 3: LockRecord
|
||
elif hasattr(client, "LockRecord"):
|
||
client.LockRecord()
|
||
locked = True
|
||
lock_method_used = "LockRecord"
|
||
logger.info(" Verrouillage via LockRecord() [OK]")
|
||
break
|
||
|
||
# Approche 4: Read avec paramètre mode écriture
|
||
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:
|
||
# Autre erreur, propager
|
||
raise
|
||
|
||
logger.info(
|
||
f" Client chargé: {getattr(client, 'CT_Intitule', '')} (via {lock_method_used})"
|
||
)
|
||
|
||
# ============================================================
|
||
# ETAPE 2: IDENTIFICATION
|
||
# ============================================================
|
||
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")
|
||
|
||
# ============================================================
|
||
# ETAPE 3: ADRESSE
|
||
# ============================================================
|
||
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}")
|
||
|
||
# ============================================================
|
||
# ETAPE 4: TELECOM
|
||
# ============================================================
|
||
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}")
|
||
|
||
# ============================================================
|
||
# ETAPE 5: COMPTE GENERAL
|
||
# ============================================================
|
||
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}")
|
||
|
||
# ============================================================
|
||
# ETAPE 6: CATEGORIES
|
||
# ============================================================
|
||
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}")
|
||
|
||
# ============================================================
|
||
# ETAPE 7: TAUX
|
||
# ============================================================
|
||
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)
|
||
|
||
# ============================================================
|
||
# ETAPE 8: STATISTIQUES
|
||
# ============================================================
|
||
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)
|
||
|
||
# ============================================================
|
||
# ETAPE 9: COMMERCIAL
|
||
# ============================================================
|
||
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}")
|
||
|
||
# ============================================================
|
||
# ETAPE 10: FACTURATION
|
||
# ============================================================
|
||
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)
|
||
|
||
# ============================================================
|
||
# ETAPE 11: LOGISTIQUE
|
||
# ============================================================
|
||
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)
|
||
|
||
# ============================================================
|
||
# ETAPE 12: COMMENTAIRE
|
||
# ============================================================
|
||
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")
|
||
|
||
# ============================================================
|
||
# ETAPE 13: ANALYTIQUE
|
||
# ============================================================
|
||
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")
|
||
|
||
# ============================================================
|
||
# ETAPE 14: ORGANISATION & SURVEILLANCE
|
||
# ============================================================
|
||
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")
|
||
|
||
# ============================================================
|
||
# VERIFICATION AVANT WRITE
|
||
# ============================================================
|
||
if not champs_modifies:
|
||
logger.warning("Aucun champ à modifier")
|
||
# Déverrouiller si nécessaire
|
||
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)
|
||
|
||
# ============================================================
|
||
# WRITE AVEC GESTION DU DEVERROUILLAGE
|
||
# ============================================================
|
||
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:
|
||
# Toujours déverrouiller après Write (succès ou échec)
|
||
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}")
|
||
|
||
# Relire après Write pour retourner les données à jour
|
||
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 ===")
|
||
|
||
# === Validation données ===
|
||
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:
|
||
# === Découverte dépôts ===
|
||
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']})"
|
||
)
|
||
|
||
# === Extraction et validation des données ===
|
||
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}")
|
||
|
||
# === Vérifier si article existe ===
|
||
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
|
||
|
||
# === Créer l'article ===
|
||
persist = factory.Create()
|
||
article = win32com.client.CastTo(persist, "IBOArticle3")
|
||
article.SetDefault()
|
||
|
||
article.AR_Ref = reference
|
||
article.AR_Design = designation
|
||
|
||
# === Recherche article modèle ===
|
||
logger.info("[MODELE] Recherche article modèle...")
|
||
article_modele_ref = None
|
||
article_modele = None
|
||
|
||
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."
|
||
)
|
||
|
||
# === Copie Unite depuis modèle ===
|
||
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"
|
||
)
|
||
|
||
# === Gestion famille ===
|
||
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}")
|
||
|
||
# === Champs obligatoires depuis modèle ===
|
||
logger.info("[CHAMPS] Copie champs obligatoires...")
|
||
article.AR_Type = int(getattr(article_modele, "AR_Type", 0))
|
||
article.AR_Nature = int(getattr(article_modele, "AR_Nature", 0))
|
||
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)")
|
||
|
||
# === Application des champs fournis ===
|
||
logger.info("[CHAMPS] Application champs fournis...")
|
||
champs_appliques = []
|
||
champs_echoues = []
|
||
|
||
# Prix de vente
|
||
if "prix_vente" in article_data:
|
||
try:
|
||
article.AR_PrixVen = float(article_data["prix_vente"])
|
||
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}")
|
||
|
||
# Prix d'achat
|
||
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}")
|
||
|
||
# Coefficient
|
||
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}")
|
||
|
||
# Code EAN
|
||
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}")
|
||
|
||
# Description -> AR_Langue1
|
||
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}")
|
||
|
||
# Pays
|
||
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}")
|
||
|
||
# Garantie
|
||
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}")
|
||
|
||
# Délai
|
||
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}")
|
||
|
||
# Poids net
|
||
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}")
|
||
|
||
# Poids brut
|
||
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}")
|
||
|
||
# Code fiscal
|
||
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}")
|
||
|
||
# Soumis escompte
|
||
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}")
|
||
|
||
# Publié
|
||
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}")
|
||
|
||
# En sommeil
|
||
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)}"
|
||
)
|
||
|
||
# === Écriture dans Sage ===
|
||
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}")
|
||
|
||
# === Statistiques (AR_Stat après Write) ===
|
||
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}")
|
||
|
||
# === Commit transaction ===
|
||
if transaction_active:
|
||
try:
|
||
self.cial.CptaApplication.CommitTrans()
|
||
logger.info("[COMMIT] Transaction committée")
|
||
except Exception as e:
|
||
logger.warning(f"[COMMIT] Erreur : {e}")
|
||
|
||
# === Gestion stocks ===
|
||
stock_defini = False
|
||
has_stock_values = stock_reel or stock_mini or stock_maxi
|
||
|
||
if has_stock_values:
|
||
logger.info(
|
||
f"[STOCK] Définition stock (dépôt '{depot_a_utiliser['code']}')..."
|
||
)
|
||
|
||
# Méthode 1 : Créer via COM
|
||
if stock_reel:
|
||
try:
|
||
depot_obj = depot_a_utiliser["objet"]
|
||
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}")
|
||
|
||
# Méthode 2 : Mise à jour SQL si COM échoue ou pour mini/maxi seulement
|
||
if (stock_mini or stock_maxi) and not stock_defini:
|
||
try:
|
||
with self._get_sql_connection() as conn:
|
||
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}")
|
||
|
||
# === Construction réponse depuis SQL ===
|
||
logger.info("[RESPONSE] Construction réponse depuis SQL...")
|
||
try:
|
||
with self._get_sql_connection() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Lecture complète article
|
||
cursor.execute(
|
||
"""
|
||
SELECT
|
||
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,
|
||
}
|
||
|
||
# Lecture stocks
|
||
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}")
|
||
|
||
# Fallback sur extraction COM si SQL échoue
|
||
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}
|
||
|
||
# Forcer les valeurs connues
|
||
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 = []
|
||
|
||
# === Gestion famille ===
|
||
if "famille" in article_data and article_data["famille"]:
|
||
famille_code_demande = article_data["famille"].upper().strip()
|
||
logger.info(f"[FAMILLE] Changement : {famille_code_demande}")
|
||
|
||
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}")
|
||
|
||
# === Traitement explicite des champs ===
|
||
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...")
|
||
|
||
# === Écriture COM ===
|
||
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}")
|
||
|
||
# === Statistiques (AR_Stat après Write) ===
|
||
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}")
|
||
|
||
# === Gestion stocks mini/maxi via SQL ===
|
||
if "stock_mini" in article_data or "stock_maxi" in article_data:
|
||
try:
|
||
with self._get_sql_connection() as conn:
|
||
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}")
|
||
|
||
# === Construction réponse depuis SQL ===
|
||
logger.info("[RESPONSE] Construction réponse depuis SQL...")
|
||
try:
|
||
with self._get_sql_connection() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# Lecture complète article
|
||
cursor.execute(
|
||
"""
|
||
SELECT
|
||
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,
|
||
}
|
||
|
||
# Lecture stocks
|
||
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}")
|
||
|
||
# Fallback sur extraction COM
|
||
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()
|
||
|
||
# ⚠️⚠️⚠️ VÉRIFIE CETTE LIGNE ⚠️⚠️⚠️
|
||
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
|
||
|
||
# ⚠️ UTILISER LA FONCTION DE CLASSE EXISTANTE
|
||
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")
|
||
|
||
# Validation préalable
|
||
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:
|
||
# ===== VÉRIFICATION DOUBLON VIA SQL =====
|
||
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")
|
||
|
||
# ===== FACTORY + CREATE =====
|
||
try:
|
||
factory = self.cial.FactoryCollaborateur
|
||
except AttributeError:
|
||
factory = self.cial.CptaApplication.FactoryCollaborateur
|
||
|
||
persist = factory.Create()
|
||
|
||
# Cast vers interface
|
||
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
|
||
|
||
# ===== SETDEFAULT =====
|
||
try:
|
||
collab.SetDefault()
|
||
logger.info("✓ SetDefault()")
|
||
except Exception as e:
|
||
logger.warning(f"SetDefault() ignoré: {e}")
|
||
|
||
# ===== HELPER =====
|
||
def safe_set(obj, attr, value, max_len=None):
|
||
"""Affecte une valeur de manière sécurisée"""
|
||
if value is None or value == "":
|
||
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
|
||
|
||
# ===== CHAMPS DIRECTS SUR COLLABORATEUR =====
|
||
logger.info("📝 Champs directs...")
|
||
|
||
# Obligatoire
|
||
safe_set(collab, "Nom", nom_upper, 35)
|
||
|
||
# Optionnels
|
||
safe_set(collab, "Prenom", prenom, 35)
|
||
safe_set(collab, "Fonction", data.get("fonction"), 35)
|
||
safe_set(collab, "Service", data.get("service"), 35)
|
||
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)
|
||
|
||
# ===== SOUS-OBJET ADRESSE =====
|
||
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}")
|
||
|
||
# ===== SOUS-OBJET TELECOM =====
|
||
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}")
|
||
|
||
# ===== CHAMPS BOOLÉENS (seulement si True) =====
|
||
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
|
||
|
||
# ===== WRITE =====
|
||
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}")
|
||
|
||
# ===== RÉCUPÉRATION DU NUMÉRO =====
|
||
numero_cree = None
|
||
|
||
# Via Read()
|
||
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
|
||
|
||
# Via SQL si pas trouvé
|
||
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}")
|
||
|
||
# Retourner le collaborateur
|
||
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:
|
||
# ===== LECTURE DU COLLABORATEUR EXISTANT =====
|
||
try:
|
||
factory = self.cial.FactoryCollaborateur
|
||
except AttributeError:
|
||
factory = self.cial.CptaApplication.FactoryCollaborateur
|
||
|
||
# Lire par numéro
|
||
try:
|
||
persist = factory.ReadNumero(numero)
|
||
except Exception as e:
|
||
raise ValueError(f"Collaborateur {numero} introuvable: {e}")
|
||
|
||
if not persist:
|
||
raise ValueError(f"Collaborateur {numero} introuvable")
|
||
|
||
# Cast vers interface
|
||
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
|
||
|
||
# Charger les données actuelles
|
||
try:
|
||
collab.Read()
|
||
logger.info(f"✓ Collaborateur {numero} chargé")
|
||
except Exception as e:
|
||
logger.warning(f"Read() ignoré: {e}")
|
||
|
||
# ===== HELPER =====
|
||
def safe_set(obj, attr, value, max_len=None):
|
||
"""Affecte une valeur de manière sécurisée"""
|
||
if value is None:
|
||
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 = []
|
||
|
||
# ===== CHAMPS DIRECTS SUR COLLABORATEUR =====
|
||
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]
|
||
# Cas spécial: nom en majuscules
|
||
if py_field == "nom" and val:
|
||
val = str(val).upper().strip()
|
||
if safe_set(collab, sage_attr, val, max_len):
|
||
champs_modifies.append(sage_attr)
|
||
|
||
# ===== SOUS-OBJET ADRESSE =====
|
||
logger.info("📍 Adresse...")
|
||
try:
|
||
adresse_obj = collab.Adresse
|
||
|
||
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}")
|
||
|
||
# ===== SOUS-OBJET TELECOM =====
|
||
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}")
|
||
|
||
# ===== CHAMPS BOOLÉENS =====
|
||
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}")
|
||
|
||
# ===== VÉRIFICATION =====
|
||
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}"
|
||
)
|
||
|
||
# ===== WRITE =====
|
||
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}")
|
||
|
||
# ===== RETOUR =====
|
||
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)
|
||
|
||
# Stocker le numéro de dossier pour la recherche du logo
|
||
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,
|
||
montant,
|
||
mode_reglement=2,
|
||
date_reglement=None,
|
||
reference="",
|
||
libelle="",
|
||
):
|
||
return _regler_facture(
|
||
self,
|
||
numero_facture,
|
||
montant,
|
||
mode_reglement,
|
||
date_reglement,
|
||
reference,
|
||
libelle,
|
||
)
|
||
|
||
def regler_factures_client(
|
||
self,
|
||
client_code: str,
|
||
montant_total: float,
|
||
mode_reglement: int = 2,
|
||
date_reglement: datetime = None,
|
||
reference: str = "",
|
||
libelle: str = "",
|
||
code_journal: str = "BEU",
|
||
numeros_factures: List[str] = None,
|
||
):
|
||
"""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,
|
||
)
|
||
|
||
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)
|