Sage100-ws/sage_connector.py

14411 lines
624 KiB
Python

import win32com.client
import pythoncom # AJOUT CRITIQUE
from datetime import datetime, timedelta, date
from typing import Dict, List, Optional, Any
import threading
import time
import logging
from config import settings, validate_settings
import pyodbc
from contextlib import contextmanager
import pywintypes
import os
import glob
import tempfile
import logging
from dataclasses import dataclass, field
import zlib
import struct
logger = logging.getLogger(__name__)
class SageConnector:
def __init__(self, chemin_base, 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 = "OV-FDDDC6\\SAGE100"
self.sql_database = "BIJOU"
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:
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:
cursor = 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:
pass
def lister_tous_fournisseurs(self, filtre=""):
"""
Liste tous les fournisseurs avec TOUS les champs
Symétrie complète avec lister_tous_clients
"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT
-- IDENTIFICATION (9)
CT_Num, CT_Intitule, CT_Type, CT_Qualite,
CT_Classement, CT_Raccourci, CT_Siret, CT_Identifiant,
CT_Ape,
-- ADRESSE (7)
CT_Contact, CT_Adresse, CT_Complement,
CT_CodePostal, CT_Ville, CT_CodeRegion, CT_Pays,
-- TELECOM (6)
CT_Telephone, CT_Telecopie, CT_EMail, CT_Site,
CT_Facebook, CT_LinkedIn,
-- TAUX (4)
CT_Taux01, CT_Taux02, CT_Taux03, CT_Taux04,
-- STATISTIQUES (10)
CT_Statistique01, CT_Statistique02, CT_Statistique03,
CT_Statistique04, CT_Statistique05, CT_Statistique06,
CT_Statistique07, CT_Statistique08, CT_Statistique09,
CT_Statistique10,
-- COMMERCIAL (4)
CT_Encours, CT_Assurance, CT_Langue, CO_No,
-- FACTURATION (11)
CT_Lettrage, CT_Sommeil, CT_Facture, CT_Prospect,
CT_BLFact, CT_Saut, CT_ValidEch, CT_ControlEnc,
CT_NotRappel, CT_NotPenal, CT_BonAPayer,
-- LOGISTIQUE (4)
CT_PrioriteLivr, CT_LivrPartielle,
CT_DelaiTransport, CT_DelaiAppro,
-- COMMENTAIRE (1)
CT_Commentaire,
-- ANALYTIQUE (1)
CA_Num,
-- ORGANISATION / SURVEILLANCE (10)
MR_No, CT_Surveillance, CT_Coface,
CT_SvFormeJuri, CT_SvEffectif, CT_SvRegul,
CT_SvCotation, CT_SvObjetMaj, CT_SvCA, CT_SvResultat,
-- COMPTE GENERAL ET CATEGORIES (3)
CG_NumPrinc, N_CatTarif, N_CatCompta
FROM F_COMPTET
WHERE CT_Type = 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()
fournisseurs = []
for row in rows:
fournisseur = {
"numero": self._safe_strip(row.CT_Num),
"intitule": self._safe_strip(row.CT_Intitule),
"type_tiers": row.CT_Type,
"qualite": self._safe_strip(row.CT_Qualite),
"classement": self._safe_strip(row.CT_Classement),
"raccourci": self._safe_strip(row.CT_Raccourci),
"siret": self._safe_strip(row.CT_Siret),
"tva_intra": self._safe_strip(row.CT_Identifiant),
"code_naf": self._safe_strip(row.CT_Ape),
"contact": self._safe_strip(row.CT_Contact),
"adresse": self._safe_strip(row.CT_Adresse),
"complement": self._safe_strip(row.CT_Complement),
"code_postal": self._safe_strip(row.CT_CodePostal),
"ville": self._safe_strip(row.CT_Ville),
"region": self._safe_strip(row.CT_CodeRegion),
"pays": self._safe_strip(row.CT_Pays),
"telephone": self._safe_strip(row.CT_Telephone),
"telecopie": self._safe_strip(row.CT_Telecopie),
"email": self._safe_strip(row.CT_EMail),
"site_web": self._safe_strip(row.CT_Site),
"facebook": self._safe_strip(row.CT_Facebook),
"linkedin": self._safe_strip(row.CT_LinkedIn),
"taux01": row.CT_Taux01,
"taux02": row.CT_Taux02,
"taux03": row.CT_Taux03,
"taux04": row.CT_Taux04,
"statistique01": self._safe_strip(row.CT_Statistique01),
"statistique02": self._safe_strip(row.CT_Statistique02),
"statistique03": self._safe_strip(row.CT_Statistique03),
"statistique04": self._safe_strip(row.CT_Statistique04),
"statistique05": self._safe_strip(row.CT_Statistique05),
"statistique06": self._safe_strip(row.CT_Statistique06),
"statistique07": self._safe_strip(row.CT_Statistique07),
"statistique08": self._safe_strip(row.CT_Statistique08),
"statistique09": self._safe_strip(row.CT_Statistique09),
"statistique10": self._safe_strip(row.CT_Statistique10),
"encours_autorise": row.CT_Encours,
"assurance_credit": row.CT_Assurance,
"langue": row.CT_Langue,
"commercial_code": row.CO_No,
"lettrage_auto": (row.CT_Lettrage == 1),
"est_actif": (row.CT_Sommeil == 0),
"type_facture": row.CT_Facture,
"est_prospect": (row.CT_Prospect == 1),
"bl_en_facture": row.CT_BLFact,
"saut_page": row.CT_Saut,
"validation_echeance": row.CT_ValidEch,
"controle_encours": row.CT_ControlEnc,
"exclure_relance": (row.CT_NotRappel == 1),
"exclure_penalites": (row.CT_NotPenal == 1),
"bon_a_payer": row.CT_BonAPayer,
"priorite_livraison": row.CT_PrioriteLivr,
"livraison_partielle": row.CT_LivrPartielle,
"delai_transport": row.CT_DelaiTransport,
"delai_appro": row.CT_DelaiAppro,
"commentaire": self._safe_strip(row.CT_Commentaire),
"section_analytique": self._safe_strip(row.CA_Num),
"mode_reglement_code": row.MR_No,
"surveillance_active": (row.CT_Surveillance == 1),
"coface": self._safe_strip(row.CT_Coface),
"forme_juridique": self._safe_strip(row.CT_SvFormeJuri),
"effectif": self._safe_strip(row.CT_SvEffectif),
"sv_regularite": self._safe_strip(row.CT_SvRegul),
"sv_cotation": self._safe_strip(row.CT_SvCotation),
"sv_objet_maj": self._safe_strip(row.CT_SvObjetMaj),
"sv_chiffre_affaires": row.CT_SvCA,
"sv_resultat": row.CT_SvResultat,
"compte_general": self._safe_strip(row.CG_NumPrinc),
"categorie_tarif": row.N_CatTarif,
"categorie_compta": row.N_CatCompta,
}
fournisseur["contacts"] = self._get_contacts_client(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 TOUS les champs (identique à lister_tous_fournisseurs)
Symétrie complète GET/POST
"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT
-- IDENTIFICATION (9)
CT_Num, CT_Intitule, CT_Type, CT_Qualite,
CT_Classement, CT_Raccourci, CT_Siret, CT_Identifiant,
CT_Ape,
-- ADRESSE (7)
CT_Contact, CT_Adresse, CT_Complement,
CT_CodePostal, CT_Ville, CT_CodeRegion, CT_Pays,
-- TELECOM (6)
CT_Telephone, CT_Telecopie, CT_EMail, CT_Site,
CT_Facebook, CT_LinkedIn,
-- TAUX (4)
CT_Taux01, CT_Taux02, CT_Taux03, CT_Taux04,
-- STATISTIQUES (10)
CT_Statistique01, CT_Statistique02, CT_Statistique03,
CT_Statistique04, CT_Statistique05, CT_Statistique06,
CT_Statistique07, CT_Statistique08, CT_Statistique09,
CT_Statistique10,
-- COMMERCIAL (4)
CT_Encours, CT_Assurance, CT_Langue, CO_No,
-- FACTURATION (11)
CT_Lettrage, CT_Sommeil, CT_Facture, CT_Prospect,
CT_BLFact, CT_Saut, CT_ValidEch, CT_ControlEnc,
CT_NotRappel, CT_NotPenal, CT_BonAPayer,
-- LOGISTIQUE (4)
CT_PrioriteLivr, CT_LivrPartielle,
CT_DelaiTransport, CT_DelaiAppro,
-- COMMENTAIRE (1)
CT_Commentaire,
-- ANALYTIQUE (1)
CA_Num,
-- ORGANISATION / SURVEILLANCE (10)
MR_No, CT_Surveillance, CT_Coface,
CT_SvFormeJuri, CT_SvEffectif, CT_SvRegul,
CT_SvCotation, CT_SvObjetMaj, CT_SvCA, CT_SvResultat,
-- COMPTE GENERAL ET CATEGORIES (3)
CG_NumPrinc, N_CatTarif, N_CatCompta
FROM F_COMPTET
WHERE CT_Num = ? AND CT_Type = 1
"""
cursor.execute(query, (code_fournisseur.upper(),))
row = cursor.fetchone()
if not row:
return None
fournisseur = {
"numero": self._safe_strip(row.CT_Num),
"intitule": self._safe_strip(row.CT_Intitule),
"type_tiers": row.CT_Type,
"qualite": self._safe_strip(row.CT_Qualite),
"classement": self._safe_strip(row.CT_Classement),
"raccourci": self._safe_strip(row.CT_Raccourci),
"siret": self._safe_strip(row.CT_Siret),
"tva_intra": self._safe_strip(row.CT_Identifiant),
"code_naf": self._safe_strip(row.CT_Ape),
"contact": self._safe_strip(row.CT_Contact),
"adresse": self._safe_strip(row.CT_Adresse),
"complement": self._safe_strip(row.CT_Complement),
"code_postal": self._safe_strip(row.CT_CodePostal),
"ville": self._safe_strip(row.CT_Ville),
"region": self._safe_strip(row.CT_CodeRegion),
"pays": self._safe_strip(row.CT_Pays),
"telephone": self._safe_strip(row.CT_Telephone),
"telecopie": self._safe_strip(row.CT_Telecopie),
"email": self._safe_strip(row.CT_EMail),
"site_web": self._safe_strip(row.CT_Site),
"facebook": self._safe_strip(row.CT_Facebook),
"linkedin": self._safe_strip(row.CT_LinkedIn),
"taux01": row.CT_Taux01,
"taux02": row.CT_Taux02,
"taux03": row.CT_Taux03,
"taux04": row.CT_Taux04,
"statistique01": self._safe_strip(row.CT_Statistique01),
"statistique02": self._safe_strip(row.CT_Statistique02),
"statistique03": self._safe_strip(row.CT_Statistique03),
"statistique04": self._safe_strip(row.CT_Statistique04),
"statistique05": self._safe_strip(row.CT_Statistique05),
"statistique06": self._safe_strip(row.CT_Statistique06),
"statistique07": self._safe_strip(row.CT_Statistique07),
"statistique08": self._safe_strip(row.CT_Statistique08),
"statistique09": self._safe_strip(row.CT_Statistique09),
"statistique10": self._safe_strip(row.CT_Statistique10),
"encours_autorise": row.CT_Encours,
"assurance_credit": row.CT_Assurance,
"langue": row.CT_Langue,
"commercial_code": row.CO_No,
"lettrage_auto": (row.CT_Lettrage == 1),
"est_actif": (row.CT_Sommeil == 0),
"type_facture": row.CT_Facture,
"est_prospect": (row.CT_Prospect == 1),
"bl_en_facture": row.CT_BLFact,
"saut_page": row.CT_Saut,
"validation_echeance": row.CT_ValidEch,
"controle_encours": row.CT_ControlEnc,
"exclure_relance": (row.CT_NotRappel == 1),
"exclure_penalites": (row.CT_NotPenal == 1),
"bon_a_payer": row.CT_BonAPayer,
"priorite_livraison": row.CT_PrioriteLivr,
"livraison_partielle": row.CT_LivrPartielle,
"delai_transport": row.CT_DelaiTransport,
"delai_appro": row.CT_DelaiAppro,
"commentaire": self._safe_strip(row.CT_Commentaire),
"section_analytique": self._safe_strip(row.CA_Num),
"mode_reglement_code": row.MR_No,
"surveillance_active": (row.CT_Surveillance == 1),
"coface": self._safe_strip(row.CT_Coface),
"forme_juridique": self._safe_strip(row.CT_SvFormeJuri),
"effectif": self._safe_strip(row.CT_SvEffectif),
"sv_regularite": self._safe_strip(row.CT_SvRegul),
"sv_cotation": self._safe_strip(row.CT_SvCotation),
"sv_objet_maj": self._safe_strip(row.CT_SvObjetMaj),
"sv_chiffre_affaires": row.CT_SvCA,
"sv_resultat": row.CT_SvResultat,
"compte_general": self._safe_strip(row.CG_NumPrinc),
"categorie_tarif": row.N_CatTarif,
"categorie_compta": row.N_CatCompta,
}
fournisseur["contacts"] = self._get_contacts_client(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 # 1 = Fournisseur
logger.debug(" CT_Type: 1 (Fournisseur)")
except:
logger.debug(" CT_Type non défini (géré par FactoryFournisseur)")
try:
fournisseur.CT_Qualite = "FOU"
logger.debug(" CT_Qualite: 'FOU'")
except:
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:
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, # Fournisseur
"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:
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 = self._cast_client(persist) # Réutiliser _cast_client
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:
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:
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:
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:
pass
raise RuntimeError(f"Erreur technique Sage: {error_message}")
def _get_contacts_client(self, numero: str, conn) -> list:
"""
Récupère tous les contacts d'un client avec indication du contact par défaut
"""
try:
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 = ?
ORDER BY N_Contact, CT_Nom, CT_Prenom
"""
cursor.execute(query, [numero])
rows = cursor.fetchall()
query_client = """
SELECT CT_Contact
FROM F_COMPTET
WHERE CT_Num = ?
"""
cursor.execute(query_client, [numero])
client_row = cursor.fetchone()
nom_contact_defaut = None
if client_row:
nom_contact_defaut = self._safe_strip(client_row.CT_Contact)
contacts = []
for row in rows:
contact = self._row_to_contact_dict(row)
if nom_contact_defaut:
nom_complet = f"{contact.get('prenom', '')} {contact['nom']}".strip()
contact["est_defaut"] = (
nom_complet == nom_contact_defaut or
contact['nom'] == nom_contact_defaut
)
else:
contact["est_defaut"] = False
contacts.append(contact)
return contacts
except Exception as e:
logger.warning(f"⚠️ Impossible de récupérer contacts pour {numero}: {e}")
return []
def lister_tous_clients(self, filtre=""):
"""
Liste tous les clients avec TOUS les champs gérés par creer_client
Symétrie complète GET/POST
"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT
-- IDENTIFICATION (8)
CT_Num, CT_Intitule, CT_Type, CT_Qualite,
CT_Classement, CT_Raccourci, CT_Siret, CT_Identifiant,
CT_Ape,
-- ADRESSE (7)
CT_Contact, CT_Adresse, CT_Complement,
CT_CodePostal, CT_Ville, CT_CodeRegion, CT_Pays,
-- TELECOM (7)
CT_Telephone, CT_Telecopie, CT_EMail, CT_Site,
CT_Facebook, CT_LinkedIn,
-- TAUX (4)
CT_Taux01, CT_Taux02, CT_Taux03, CT_Taux04,
-- STATISTIQUES (10)
CT_Statistique01, CT_Statistique02, CT_Statistique03,
CT_Statistique04, CT_Statistique05, CT_Statistique06,
CT_Statistique07, CT_Statistique08, CT_Statistique09,
CT_Statistique10,
-- COMMERCIAL (4)
CT_Encours, CT_Assurance, CT_Langue, CO_No,
-- FACTURATION (11)
CT_Lettrage, CT_Sommeil, CT_Facture, CT_Prospect,
CT_BLFact, CT_Saut, CT_ValidEch, CT_ControlEnc,
CT_NotRappel, CT_NotPenal, CT_BonAPayer,
-- LOGISTIQUE (4)
CT_PrioriteLivr, CT_LivrPartielle,
CT_DelaiTransport, CT_DelaiAppro,
-- COMMENTAIRE (1)
CT_Commentaire,
-- ANALYTIQUE (1)
CA_Num,
-- ORGANISATION / SURVEILLANCE (10)
MR_No, CT_Surveillance, CT_Coface,
CT_SvFormeJuri, CT_SvEffectif, CT_SvRegul,
CT_SvCotation, CT_SvObjetMaj, CT_SvCA, CT_SvResultat,
-- COMPTE GENERAL ET CATEGORIES (3)
CG_NumPrinc, N_CatTarif, N_CatCompta
FROM F_COMPTET
WHERE CT_Type = 0
"""
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()
clients = []
for row in rows:
client = {
"numero": self._safe_strip(row.CT_Num),
"intitule": self._safe_strip(row.CT_Intitule),
"type_tiers": row.CT_Type,
"qualite": self._safe_strip(row.CT_Qualite),
"classement": self._safe_strip(row.CT_Classement),
"raccourci": self._safe_strip(row.CT_Raccourci),
"siret": self._safe_strip(row.CT_Siret),
"tva_intra": self._safe_strip(row.CT_Identifiant),
"code_naf": self._safe_strip(row.CT_Ape),
"contact": self._safe_strip(row.CT_Contact),
"adresse": self._safe_strip(row.CT_Adresse),
"complement": self._safe_strip(row.CT_Complement),
"code_postal": self._safe_strip(row.CT_CodePostal),
"ville": self._safe_strip(row.CT_Ville),
"region": self._safe_strip(row.CT_CodeRegion),
"pays": self._safe_strip(row.CT_Pays),
"telephone": self._safe_strip(row.CT_Telephone),
"telecopie": self._safe_strip(row.CT_Telecopie),
"email": self._safe_strip(row.CT_EMail),
"site_web": self._safe_strip(row.CT_Site),
"facebook": self._safe_strip(row.CT_Facebook),
"linkedin": self._safe_strip(row.CT_LinkedIn),
"taux01": row.CT_Taux01,
"taux02": row.CT_Taux02,
"taux03": row.CT_Taux03,
"taux04": row.CT_Taux04,
"statistique01": self._safe_strip(row.CT_Statistique01),
"statistique02": self._safe_strip(row.CT_Statistique02),
"statistique03": self._safe_strip(row.CT_Statistique03),
"statistique04": self._safe_strip(row.CT_Statistique04),
"statistique05": self._safe_strip(row.CT_Statistique05),
"statistique06": self._safe_strip(row.CT_Statistique06),
"statistique07": self._safe_strip(row.CT_Statistique07),
"statistique08": self._safe_strip(row.CT_Statistique08),
"statistique09": self._safe_strip(row.CT_Statistique09),
"statistique10": self._safe_strip(row.CT_Statistique10),
"encours_autorise": row.CT_Encours,
"assurance_credit": row.CT_Assurance,
"langue": row.CT_Langue,
"commercial_code": row.CO_No,
"lettrage_auto": (row.CT_Lettrage == 1),
"est_actif": (row.CT_Sommeil == 0),
"type_facture": row.CT_Facture,
"est_prospect": (row.CT_Prospect == 1),
"bl_en_facture": row.CT_BLFact,
"saut_page": row.CT_Saut,
"validation_echeance": row.CT_ValidEch,
"controle_encours": row.CT_ControlEnc,
"exclure_relance": (row.CT_NotRappel == 1),
"exclure_penalites": (row.CT_NotPenal == 1),
"bon_a_payer": row.CT_BonAPayer,
"priorite_livraison": row.CT_PrioriteLivr,
"livraison_partielle": row.CT_LivrPartielle,
"delai_transport": row.CT_DelaiTransport,
"delai_appro": row.CT_DelaiAppro,
"commentaire": self._safe_strip(row.CT_Commentaire),
"section_analytique": self._safe_strip(row.CA_Num),
"mode_reglement_code": row.MR_No,
"surveillance_active": (row.CT_Surveillance == 1),
"coface": self._safe_strip(row.CT_Coface),
"forme_juridique": self._safe_strip(row.CT_SvFormeJuri),
"effectif": self._safe_strip(row.CT_SvEffectif),
"sv_regularite": self._safe_strip(row.CT_SvRegul),
"sv_cotation": self._safe_strip(row.CT_SvCotation),
"sv_objet_maj": self._safe_strip(row.CT_SvObjetMaj),
"sv_chiffre_affaires": row.CT_SvCA,
"sv_resultat": row.CT_SvResultat,
"compte_general": self._safe_strip(row.CG_NumPrinc),
"categorie_tarif": row.N_CatTarif,
"categorie_compta": row.N_CatCompta,
}
client["contacts"] = self._get_contacts_client(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 TOUS les champs (identique à lister_tous_clients)
Symétrie complète GET/POST
"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT
-- IDENTIFICATION (8)
CT_Num, CT_Intitule, CT_Type, CT_Qualite,
CT_Classement, CT_Raccourci, CT_Siret, CT_Identifiant,
CT_Ape,
-- ADRESSE (7)
CT_Contact, CT_Adresse, CT_Complement,
CT_CodePostal, CT_Ville, CT_CodeRegion, CT_Pays,
-- TELECOM (7)
CT_Telephone, CT_Telecopie, CT_EMail, CT_Site,
CT_Facebook, CT_LinkedIn,
-- TAUX (4)
CT_Taux01, CT_Taux02, CT_Taux03, CT_Taux04,
-- STATISTIQUES (10)
CT_Statistique01, CT_Statistique02, CT_Statistique03,
CT_Statistique04, CT_Statistique05, CT_Statistique06,
CT_Statistique07, CT_Statistique08, CT_Statistique09,
CT_Statistique10,
-- COMMERCIAL (4)
CT_Encours, CT_Assurance, CT_Langue, CO_No,
-- FACTURATION (11)
CT_Lettrage, CT_Sommeil, CT_Facture, CT_Prospect,
CT_BLFact, CT_Saut, CT_ValidEch, CT_ControlEnc,
CT_NotRappel, CT_NotPenal, CT_BonAPayer,
-- LOGISTIQUE (4)
CT_PrioriteLivr, CT_LivrPartielle,
CT_DelaiTransport, CT_DelaiAppro,
-- COMMENTAIRE (1)
CT_Commentaire,
-- ANALYTIQUE (1)
CA_Num,
-- ORGANISATION / SURVEILLANCE (10)
MR_No, CT_Surveillance, CT_Coface,
CT_SvFormeJuri, CT_SvEffectif, CT_SvRegul,
CT_SvCotation, CT_SvObjetMaj, CT_SvCA, CT_SvResultat,
-- COMPTE GENERAL ET CATEGORIES (3)
CG_NumPrinc, N_CatTarif, N_CatCompta
FROM F_COMPTET
WHERE CT_Num = ? AND CT_Type = 0
"""
cursor.execute(query, (code_client.upper(),))
row = cursor.fetchone()
if not row:
return None
client = {
"numero": self._safe_strip(row.CT_Num),
"intitule": self._safe_strip(row.CT_Intitule),
"type_tiers": row.CT_Type,
"qualite": self._safe_strip(row.CT_Qualite),
"classement": self._safe_strip(row.CT_Classement),
"raccourci": self._safe_strip(row.CT_Raccourci),
"siret": self._safe_strip(row.CT_Siret),
"tva_intra": self._safe_strip(row.CT_Identifiant),
"code_naf": self._safe_strip(row.CT_Ape),
"contact": self._safe_strip(row.CT_Contact),
"adresse": self._safe_strip(row.CT_Adresse),
"complement": self._safe_strip(row.CT_Complement),
"code_postal": self._safe_strip(row.CT_CodePostal),
"ville": self._safe_strip(row.CT_Ville),
"region": self._safe_strip(row.CT_CodeRegion),
"pays": self._safe_strip(row.CT_Pays),
"telephone": self._safe_strip(row.CT_Telephone),
"telecopie": self._safe_strip(row.CT_Telecopie),
"email": self._safe_strip(row.CT_EMail),
"site_web": self._safe_strip(row.CT_Site),
"facebook": self._safe_strip(row.CT_Facebook),
"linkedin": self._safe_strip(row.CT_LinkedIn),
"taux01": row.CT_Taux01,
"taux02": row.CT_Taux02,
"taux03": row.CT_Taux03,
"taux04": row.CT_Taux04,
"statistique01": self._safe_strip(row.CT_Statistique01),
"statistique02": self._safe_strip(row.CT_Statistique02),
"statistique03": self._safe_strip(row.CT_Statistique03),
"statistique04": self._safe_strip(row.CT_Statistique04),
"statistique05": self._safe_strip(row.CT_Statistique05),
"statistique06": self._safe_strip(row.CT_Statistique06),
"statistique07": self._safe_strip(row.CT_Statistique07),
"statistique08": self._safe_strip(row.CT_Statistique08),
"statistique09": self._safe_strip(row.CT_Statistique09),
"statistique10": self._safe_strip(row.CT_Statistique10),
"encours_autorise": row.CT_Encours,
"assurance_credit": row.CT_Assurance,
"langue": row.CT_Langue,
"commercial_code": row.CO_No,
"lettrage_auto": (row.CT_Lettrage == 1),
"est_actif": (row.CT_Sommeil == 0),
"type_facture": row.CT_Facture,
"est_prospect": (row.CT_Prospect == 1),
"bl_en_facture": row.CT_BLFact,
"saut_page": row.CT_Saut,
"validation_echeance": row.CT_ValidEch,
"controle_encours": row.CT_ControlEnc,
"exclure_relance": (row.CT_NotRappel == 1),
"exclure_penalites": (row.CT_NotPenal == 1),
"bon_a_payer": row.CT_BonAPayer,
"priorite_livraison": row.CT_PrioriteLivr,
"livraison_partielle": row.CT_LivrPartielle,
"delai_transport": row.CT_DelaiTransport,
"delai_appro": row.CT_DelaiAppro,
"commentaire": self._safe_strip(row.CT_Commentaire),
"section_analytique": self._safe_strip(row.CA_Num),
"mode_reglement_code": row.MR_No,
"surveillance_active": (row.CT_Surveillance == 1),
"coface": self._safe_strip(row.CT_Coface),
"forme_juridique": self._safe_strip(row.CT_SvFormeJuri),
"effectif": self._safe_strip(row.CT_SvEffectif),
"sv_regularite": self._safe_strip(row.CT_SvRegul),
"sv_cotation": self._safe_strip(row.CT_SvCotation),
"sv_objet_maj": self._safe_strip(row.CT_SvObjetMaj),
"sv_chiffre_affaires": row.CT_SvCA,
"sv_resultat": row.CT_SvResultat,
"compte_general": self._safe_strip(row.CG_NumPrinc),
"categorie_tarif": row.N_CatTarif,
"categorie_compta": row.N_CatCompta,
}
client["contacts"] = self._get_contacts_client(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 = self._mapper_article_depuis_row(row_data, colonnes_config)
articles.append(article_data)
articles = self._enrichir_stocks_articles(articles, cursor)
articles = self._enrichir_familles_articles(articles, cursor)
articles = self._enrichir_fournisseurs_articles(articles, cursor)
articles = self._enrichir_tva_articles(articles, cursor)
articles = self._enrichir_stock_emplacements(articles, cursor)
articles = self._enrichir_gammes_articles(articles, cursor)
articles = self._enrichir_tarifs_clients(articles, cursor)
articles = self._enrichir_nomenclature(articles, cursor)
articles = self._enrichir_compta_articles(articles, cursor)
articles = self._enrichir_fournisseurs_multiples(articles, cursor)
articles = self._enrichir_depots_details(articles, cursor)
articles = self._enrichir_emplacements_details(articles, cursor)
articles = self._enrichir_gammes_enumeres(articles, cursor)
articles = self._enrichir_references_enumerees(articles, cursor)
articles = self._enrichir_medias_articles(articles, cursor)
articles = self._enrichir_prix_gammes(articles, cursor)
articles = self._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 _enrichir_stock_emplacements(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit avec le détail du stock par emplacement
Structure: articles[i]["emplacements"] = [{"depot": "01", "emplacement": "A1", "qte": 10}, ...]
"""
try:
logger.info(f" → Enrichissement stock emplacements...")
references = [a["reference"] for a in articles if a["reference"]]
if not references:
return articles
placeholders = ",".join(["?"] * len(references))
query = f"""
SELECT
AR_Ref,
DE_No,
DP_No,
AE_QteSto,
AE_QtePrepa,
AE_QteAControler,
cbCreation,
cbModification
FROM F_ARTSTOCKEMPL
WHERE AR_Ref IN ({placeholders})
ORDER BY AR_Ref, DE_No, DP_No
"""
cursor.execute(query, references)
rows = cursor.fetchall()
emplacements_map = {}
for row in rows:
ref = self._safe_strip(row[0])
if not ref:
continue
if ref not in emplacements_map:
emplacements_map[ref] = []
emplacements_map[ref].append({
"depot": self._safe_strip(row[1]),
"emplacement": self._safe_strip(row[2]),
"qte_stockee": float(row[3]) if row[3] else 0.0,
"qte_preparee": float(row[4]) if row[4] else 0.0,
"qte_a_controler": float(row[5]) if row[5] else 0.0,
"date_creation": row[6],
"date_modification": row[7],
})
for article in articles:
article["emplacements"] = emplacements_map.get(article["reference"], [])
article["nb_emplacements"] = len(article["emplacements"])
logger.info(f"{len(emplacements_map)} articles avec emplacements")
return articles
except Exception as e:
logger.error(f" ✗ Erreur stock emplacements: {e}")
for article in articles:
article["emplacements"] = []
article["nb_emplacements"] = 0
return articles
def _enrichir_gammes_articles(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit avec les gammes (taille, couleur, etc.)
Structure: articles[i]["gammes"] = [{"numero": 1, "enumere": "001", "type": 0}, ...]
"""
try:
logger.info(f" → Enrichissement gammes articles...")
references = [a["reference"] for a in articles if a["reference"]]
if not references:
return articles
placeholders = ",".join(["?"] * len(references))
query = f"""
SELECT
AR_Ref,
AG_No,
EG_Enumere,
AG_Type,
cbCreation,
cbModification
FROM F_ARTGAMME
WHERE AR_Ref IN ({placeholders})
ORDER BY AR_Ref, AG_No, EG_Enumere
"""
cursor.execute(query, references)
rows = cursor.fetchall()
gammes_map = {}
for row in rows:
ref = self._safe_strip(row[0])
if not ref:
continue
if ref not in gammes_map:
gammes_map[ref] = []
gammes_map[ref].append({
"numero_gamme": int(row[1]) if row[1] else 0,
"enumere": self._safe_strip(row[2]),
"type_gamme": int(row[3]) if row[3] else 0,
"date_creation": row[4],
"date_modification": row[5],
})
for article in articles:
article["gammes"] = gammes_map.get(article["reference"], [])
article["nb_gammes"] = len(article["gammes"])
logger.info(f"{len(gammes_map)} articles avec gammes")
return articles
except Exception as e:
logger.error(f" ✗ Erreur gammes: {e}")
for article in articles:
article["gammes"] = []
article["nb_gammes"] = 0
return articles
def _enrichir_tarifs_clients(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit avec les tarifs spécifiques par client/catégorie tarifaire
Structure: articles[i]["tarifs_clients"] = [{"client": "CLI001", "prix": 125.5}, ...]
"""
try:
logger.info(f" → Enrichissement tarifs clients...")
references = [a["reference"] for a in articles if a["reference"]]
if not references:
return articles
placeholders = ",".join(["?"] * len(references))
query = f"""
SELECT
AR_Ref,
AC_Categorie,
CT_Num,
AC_PrixVen,
AC_Coef,
AC_PrixTTC,
AC_Arrondi,
AC_QteMont,
EG_Champ,
AC_PrixDev,
AC_Devise,
AC_Remise,
AC_Calcul,
AC_TypeRem,
AC_RefClient,
AC_CoefNouv,
AC_PrixVenNouv,
AC_PrixDevNouv,
AC_RemiseNouv,
AC_DateApplication,
cbCreation,
cbModification
FROM F_ARTCLIENT
WHERE AR_Ref IN ({placeholders})
ORDER BY AR_Ref, AC_Categorie, CT_Num
"""
cursor.execute(query, references)
rows = cursor.fetchall()
tarifs_map = {}
for row in rows:
ref = self._safe_strip(row[0])
if not ref:
continue
if ref not in tarifs_map:
tarifs_map[ref] = []
tarifs_map[ref].append({
"categorie": int(row[1]) if row[1] else 0,
"client_num": self._safe_strip(row[2]),
"prix_vente": float(row[3]) if row[3] else 0.0,
"coefficient": float(row[4]) if row[4] else 0.0,
"prix_ttc": float(row[5]) if row[5] else 0.0,
"arrondi": float(row[6]) if row[6] else 0.0,
"qte_montant": float(row[7]) if row[7] else 0.0,
"enumere_gamme": int(row[8]) if row[8] else 0,
"prix_devise": float(row[9]) if row[9] else 0.0,
"devise": int(row[10]) if row[10] else 0,
"remise": float(row[11]) if row[11] else 0.0,
"mode_calcul": int(row[12]) if row[12] else 0,
"type_remise": int(row[13]) if row[13] else 0,
"ref_client": self._safe_strip(row[14]),
"coef_nouveau": float(row[15]) if row[15] else 0.0,
"prix_vente_nouveau": float(row[16]) if row[16] else 0.0,
"prix_devise_nouveau": float(row[17]) if row[17] else 0.0,
"remise_nouvelle": float(row[18]) if row[18] else 0.0,
"date_application": row[19],
"date_creation": row[20],
"date_modification": row[21],
})
for article in articles:
article["tarifs_clients"] = tarifs_map.get(article["reference"], [])
article["nb_tarifs_clients"] = len(article["tarifs_clients"])
logger.info(f"{len(tarifs_map)} articles avec tarifs clients")
return articles
except Exception as e:
logger.error(f" ✗ Erreur tarifs clients: {e}")
for article in articles:
article["tarifs_clients"] = []
article["nb_tarifs_clients"] = 0
return articles
def _enrichir_nomenclature(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit avec la nomenclature de production (composants, opérations)
Structure: articles[i]["composants"] = [{"operation": "OP10", "ressource": "RES01"}, ...]
"""
try:
logger.info(f" → Enrichissement nomenclature...")
references = [a["reference"] for a in articles if a["reference"]]
if not references:
return articles
placeholders = ",".join(["?"] * len(references))
query = f"""
SELECT
AR_Ref,
AT_Operation,
RP_Code,
AT_Temps,
AT_Type,
AT_Description,
AT_Ordre,
AG_No1Comp,
AG_No2Comp,
AT_TypeRessource,
AT_Chevauche,
AT_Demarre,
AT_OperationChevauche,
AT_ValeurChevauche,
AT_TypeChevauche,
cbCreation,
cbModification
FROM F_ARTCOMPO
WHERE AR_Ref IN ({placeholders})
ORDER BY AR_Ref, AT_Ordre, AT_Operation
"""
cursor.execute(query, references)
rows = cursor.fetchall()
composants_map = {}
for row in rows:
ref = self._safe_strip(row[0])
if not ref:
continue
if ref not in composants_map:
composants_map[ref] = []
composants_map[ref].append({
"operation": self._safe_strip(row[1]),
"code_ressource": self._safe_strip(row[2]),
"temps": float(row[3]) if row[3] else 0.0,
"type": int(row[4]) if row[4] else 0,
"description": self._safe_strip(row[5]),
"ordre": int(row[6]) if row[6] else 0,
"gamme_1_comp": int(row[7]) if row[7] else 0,
"gamme_2_comp": int(row[8]) if row[8] else 0,
"type_ressource": int(row[9]) if row[9] else 0,
"chevauche": int(row[10]) if row[10] else 0,
"demarre": int(row[11]) if row[11] else 0,
"operation_chevauche": self._safe_strip(row[12]),
"valeur_chevauche": float(row[13]) if row[13] else 0.0,
"type_chevauche": int(row[14]) if row[14] else 0,
"date_creation": row[15],
"date_modification": row[16],
})
for article in articles:
article["composants"] = composants_map.get(article["reference"], [])
article["nb_composants"] = len(article["composants"])
logger.info(f"{len(composants_map)} articles avec nomenclature")
return articles
except Exception as e:
logger.error(f" ✗ Erreur nomenclature: {e}")
for article in articles:
article["composants"] = []
article["nb_composants"] = 0
return articles
def _enrichir_compta_articles(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit avec les comptes comptables spécifiques par article
Structure: articles[i]["compta_vente/achat/stock"] = {...}
"""
try:
logger.info(f" → Enrichissement comptabilité articles...")
references = [a["reference"] for a in articles if a["reference"]]
if not references:
return articles
placeholders = ",".join(["?"] * len(references))
query = f"""
SELECT
AR_Ref,
ACP_Type,
ACP_Champ,
ACP_ComptaCPT_CompteG,
ACP_ComptaCPT_CompteA,
ACP_ComptaCPT_Taxe1,
ACP_ComptaCPT_Taxe2,
ACP_ComptaCPT_Taxe3,
ACP_ComptaCPT_Date1,
ACP_ComptaCPT_Date2,
ACP_ComptaCPT_Date3,
ACP_ComptaCPT_TaxeAnc1,
ACP_ComptaCPT_TaxeAnc2,
ACP_ComptaCPT_TaxeAnc3,
ACP_TypeFacture,
cbCreation,
cbModification
FROM F_ARTCOMPTA
WHERE AR_Ref IN ({placeholders})
ORDER BY AR_Ref, ACP_Type, ACP_Champ
"""
cursor.execute(query, references)
rows = cursor.fetchall()
compta_map = {}
for row in rows:
ref = self._safe_strip(row[0])
if not ref:
continue
if ref not in compta_map:
compta_map[ref] = {"vente": [], "achat": [], "stock": []}
type_compta = int(row[1]) if row[1] else 0
type_key = {0: "vente", 1: "achat", 2: "stock"}.get(type_compta, "autre")
compta_entry = {
"champ": int(row[2]) if row[2] else 0,
"compte_general": self._safe_strip(row[3]),
"compte_auxiliaire": self._safe_strip(row[4]),
"taxe_1": self._safe_strip(row[5]),
"taxe_2": self._safe_strip(row[6]),
"taxe_3": self._safe_strip(row[7]),
"taxe_date_1": row[8],
"taxe_date_2": row[9],
"taxe_date_3": row[10],
"taxe_anc_1": self._safe_strip(row[11]),
"taxe_anc_2": self._safe_strip(row[12]),
"taxe_anc_3": self._safe_strip(row[13]),
"type_facture": int(row[14]) if row[14] else 0,
"date_creation": row[15],
"date_modification": row[16],
}
if type_key in compta_map[ref]:
compta_map[ref][type_key].append(compta_entry)
for article in articles:
compta = compta_map.get(article["reference"], {"vente": [], "achat": [], "stock": []})
article["compta_vente"] = compta["vente"]
article["compta_achat"] = compta["achat"]
article["compta_stock"] = compta["stock"]
logger.info(f"{len(compta_map)} articles avec compta spécifique")
return articles
except Exception as e:
logger.error(f" ✗ Erreur comptabilité articles: {e}")
for article in articles:
article["compta_vente"] = []
article["compta_achat"] = []
article["compta_stock"] = []
return articles
def _enrichir_fournisseurs_multiples(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit avec TOUS les fournisseurs (pas seulement le principal)
Structure: articles[i]["fournisseurs"] = [{"num": "F001", "ref": "REF123", "prix": 45.5}, ...]
"""
try:
logger.info(f" → Enrichissement fournisseurs multiples...")
references = [a["reference"] for a in articles if a["reference"]]
if not references:
return articles
placeholders = ",".join(["?"] * len(references))
query = f"""
SELECT
AR_Ref,
CT_Num,
AF_RefFourniss,
AF_PrixAch,
AF_Unite,
AF_Conversion,
AF_DelaiAppro,
AF_Garantie,
AF_Colisage,
AF_QteMini,
AF_QteMont,
EG_Champ,
AF_Principal,
AF_PrixDev,
AF_Devise,
AF_Remise,
AF_ConvDiv,
AF_TypeRem,
AF_CodeBarre,
AF_PrixAchNouv,
AF_PrixDevNouv,
AF_RemiseNouv,
AF_DateApplication,
cbCreation,
cbModification
FROM F_ARTFOURNISS
WHERE AR_Ref IN ({placeholders})
ORDER BY AR_Ref, AF_Principal DESC, CT_Num
"""
cursor.execute(query, references)
rows = cursor.fetchall()
fournisseurs_map = {}
for row in rows:
ref = self._safe_strip(row[0])
if not ref:
continue
if ref not in fournisseurs_map:
fournisseurs_map[ref] = []
fournisseurs_map[ref].append({
"fournisseur_num": self._safe_strip(row[1]),
"ref_fournisseur": self._safe_strip(row[2]),
"prix_achat": float(row[3]) if row[3] else 0.0,
"unite": self._safe_strip(row[4]),
"conversion": float(row[5]) if row[5] else 0.0,
"delai_appro": int(row[6]) if row[6] else 0,
"garantie": int(row[7]) if row[7] else 0,
"colisage": int(row[8]) if row[8] else 0,
"qte_mini": float(row[9]) if row[9] else 0.0,
"qte_montant": float(row[10]) if row[10] else 0.0,
"enumere_gamme": int(row[11]) if row[11] else 0,
"est_principal": bool(row[12]),
"prix_devise": float(row[13]) if row[13] else 0.0,
"devise": int(row[14]) if row[14] else 0,
"remise": float(row[15]) if row[15] else 0.0,
"conversion_devise": float(row[16]) if row[16] else 0.0,
"type_remise": int(row[17]) if row[17] else 0,
"code_barre_fournisseur": self._safe_strip(row[18]),
"prix_achat_nouveau": float(row[19]) if row[19] else 0.0,
"prix_devise_nouveau": float(row[20]) if row[20] else 0.0,
"remise_nouvelle": float(row[21]) if row[21] else 0.0,
"date_application": row[22],
"date_creation": row[23],
"date_modification": row[24],
})
for article in articles:
article["fournisseurs"] = fournisseurs_map.get(article["reference"], [])
article["nb_fournisseurs"] = len(article["fournisseurs"])
logger.info(f"{len(fournisseurs_map)} articles avec fournisseurs multiples")
return articles
except Exception as e:
logger.error(f" ✗ Erreur fournisseurs multiples: {e}")
for article in articles:
article["fournisseurs"] = []
article["nb_fournisseurs"] = 0
return articles
def _enrichir_depots_details(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit les stocks avec les informations détaillées des dépôts
Ajoute le nom du dépôt à chaque ligne de stock
"""
try:
logger.info(f" → Enrichissement détails dépôts...")
query = """
SELECT
DE_No,
DE_Intitule,
DE_Code,
DE_Adresse,
DE_Complement,
DE_CodePostal,
DE_Ville,
DE_Contact,
DE_Principal,
DE_CatCompta,
DE_Region,
DE_Pays,
DE_EMail,
DE_Telephone,
DE_Telecopie,
DP_NoDefaut,
DE_Exclure
FROM F_DEPOT
"""
cursor.execute(query)
rows = cursor.fetchall()
depots_map = {}
for row in rows:
de_no = self._safe_strip(row[0])
if not de_no:
continue
depots_map[de_no] = {
"depot_num": de_no,
"depot_nom": self._safe_strip(row[1]),
"depot_code": self._safe_strip(row[2]),
"depot_adresse": self._safe_strip(row[3]),
"depot_complement": self._safe_strip(row[4]),
"depot_code_postal": self._safe_strip(row[5]),
"depot_ville": self._safe_strip(row[6]),
"depot_contact": self._safe_strip(row[7]),
"depot_est_principal": bool(row[8]),
"depot_categorie_compta": int(row[9]) if row[9] else 0,
"depot_region": self._safe_strip(row[10]),
"depot_pays": self._safe_strip(row[11]),
"depot_email": self._safe_strip(row[12]),
"depot_telephone": self._safe_strip(row[13]),
"depot_fax": self._safe_strip(row[14]),
"depot_emplacement_defaut": self._safe_strip(row[15]),
"depot_exclu": bool(row[16]),
}
logger.info(f"{len(depots_map)} dépôts chargés")
for article in articles:
for empl in article.get("emplacements", []):
depot_num = empl.get("depot")
if depot_num and depot_num in depots_map:
empl.update(depots_map[depot_num])
logger.info(f" ✓ Emplacements enrichis avec détails dépôts")
return articles
except Exception as e:
logger.error(f" ✗ Erreur détails dépôts: {e}")
return articles
def _enrichir_emplacements_details(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit les emplacements avec leurs détails (zone, type, etc.)
"""
try:
logger.info(f" → Enrichissement détails emplacements...")
query = """
SELECT
DE_No,
DP_No,
DP_Code,
DP_Intitule,
DP_Zone,
DP_Type
FROM F_DEPOTEMPL
"""
cursor.execute(query)
rows = cursor.fetchall()
emplacements_map = {}
for row in rows:
de_no = self._safe_strip(row[0])
dp_no = self._safe_strip(row[1])
if not de_no or not dp_no:
continue
key = f"{de_no}_{dp_no}"
emplacements_map[key] = {
"emplacement_code": self._safe_strip(row[2]),
"emplacement_libelle": self._safe_strip(row[3]),
"emplacement_zone": self._safe_strip(row[4]),
"emplacement_type": int(row[5]) if row[5] else 0,
}
logger.info(f"{len(emplacements_map)} emplacements détaillés chargés")
for article in articles:
for empl in article.get("emplacements", []):
depot = empl.get("depot")
emplacement = empl.get("emplacement")
if depot and emplacement:
key = f"{depot}_{emplacement}"
if key in emplacements_map:
empl.update(emplacements_map[key])
logger.info(f" ✓ Emplacements enrichis avec détails")
return articles
except Exception as e:
logger.error(f" ✗ Erreur détails emplacements: {e}")
return articles
def _enrichir_gammes_enumeres(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit les gammes avec leurs libellés depuis F_ENUMGAMME et P_GAMME
"""
try:
logger.info(f" → Enrichissement énumérés gammes...")
query_pgamme = "SELECT G_Intitule, G_Type FROM P_GAMME ORDER BY G_Type"
cursor.execute(query_pgamme)
pgamme_rows = cursor.fetchall()
gammes_config = {}
for idx, row in enumerate(pgamme_rows):
gammes_config[idx + 1] = {
"nom": self._safe_strip(row[0]),
"type": int(row[1]) if row[1] else 0,
}
logger.info(f" → Configuration gammes: {gammes_config}")
query_enum = """
SELECT
EG_Champ,
EG_Ligne,
EG_Enumere,
EG_BorneSup
FROM F_ENUMGAMME
ORDER BY EG_Champ, EG_Ligne
"""
cursor.execute(query_enum)
enum_rows = cursor.fetchall()
enumeres_map = {}
for row in enum_rows:
champ = int(row[0]) if row[0] else 0
enumere = self._safe_strip(row[2])
if not enumere:
continue
key = f"{champ}_{enumere}"
enumeres_map[key] = {
"ligne": int(row[1]) if row[1] else 0,
"enumere": enumere,
"borne_sup": float(row[3]) if row[3] else 0.0,
"gamme_nom": gammes_config.get(champ, {}).get("nom", f"Gamme {champ}"),
}
logger.info(f"{len(enumeres_map)} énumérés chargés")
for article in articles:
for gamme in article.get("gammes", []):
num_gamme = gamme.get("numero_gamme")
enumere = gamme.get("enumere")
if num_gamme and enumere:
key = f"{num_gamme}_{enumere}"
if key in enumeres_map:
gamme.update(enumeres_map[key])
logger.info(f" ✓ Gammes enrichies avec énumérés")
return articles
except Exception as e:
logger.error(f" ✗ Erreur énumérés gammes: {e}")
return articles
def _enrichir_references_enumerees(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit avec les références énumérées (articles avec gammes)
Structure: articles[i]["refs_enumerees"] = [{"gamme1": 1, "gamme2": 3, "ref": "ART-R-B"}, ...]
"""
try:
logger.info(f" → Enrichissement références énumérées...")
references = [a["reference"] for a in articles if a["reference"]]
if not references:
return articles
placeholders = ",".join(["?"] * len(references))
query = f"""
SELECT
AR_Ref,
AG_No1,
AG_No2,
AE_Ref,
AE_PrixAch,
AE_CodeBarre,
AE_PrixAchNouv,
AE_EdiCode,
AE_Sommeil,
cbCreation,
cbModification
FROM F_ARTENUMREF
WHERE AR_Ref IN ({placeholders})
ORDER BY AR_Ref, AG_No1, AG_No2
"""
cursor.execute(query, references)
rows = cursor.fetchall()
refs_enum_map = {}
for row in rows:
ref = self._safe_strip(row[0])
if not ref:
continue
if ref not in refs_enum_map:
refs_enum_map[ref] = []
refs_enum_map[ref].append({
"gamme_1": int(row[1]) if row[1] else 0,
"gamme_2": int(row[2]) if row[2] else 0,
"reference_enumeree": self._safe_strip(row[3]),
"prix_achat": float(row[4]) if row[4] else 0.0,
"code_barre": self._safe_strip(row[5]),
"prix_achat_nouveau": float(row[6]) if row[6] else 0.0,
"edi_code": self._safe_strip(row[7]),
"en_sommeil": bool(row[8]),
"date_creation": row[9],
"date_modification": row[10],
})
for article in articles:
article["refs_enumerees"] = refs_enum_map.get(article["reference"], [])
article["nb_refs_enumerees"] = len(article["refs_enumerees"])
logger.info(f"{len(refs_enum_map)} articles avec références énumérées")
return articles
except Exception as e:
logger.error(f" ✗ Erreur références énumérées: {e}")
for article in articles:
article["refs_enumerees"] = []
article["nb_refs_enumerees"] = 0
return articles
def _enrichir_medias_articles(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit avec les médias attachés (photos, documents, etc.)
Structure: articles[i]["medias"] = [{"fichier": "photo.jpg", "type": "image/jpeg"}, ...]
"""
try:
logger.info(f" → Enrichissement médias articles...")
references = [a["reference"] for a in articles if a["reference"]]
if not references:
return articles
placeholders = ",".join(["?"] * len(references))
query = f"""
SELECT
AR_Ref,
ME_Commentaire,
ME_Fichier,
ME_TypeMIME,
ME_Origine,
ME_GedId,
cbCreation,
cbModification
FROM F_ARTICLEMEDIA
WHERE AR_Ref IN ({placeholders})
ORDER BY AR_Ref, cbCreation
"""
cursor.execute(query, references)
rows = cursor.fetchall()
medias_map = {}
for row in rows:
ref = self._safe_strip(row[0])
if not ref:
continue
if ref not in medias_map:
medias_map[ref] = []
medias_map[ref].append({
"commentaire": self._safe_strip(row[1]),
"fichier": self._safe_strip(row[2]),
"type_mime": self._safe_strip(row[3]),
"origine": int(row[4]) if row[4] else 0,
"ged_id": self._safe_strip(row[5]),
"date_creation": row[6],
"date_modification": row[7],
})
for article in articles:
article["medias"] = medias_map.get(article["reference"], [])
article["nb_medias"] = len(article["medias"])
logger.info(f"{len(medias_map)} articles avec médias")
return articles
except Exception as e:
logger.error(f" ✗ Erreur médias: {e}")
for article in articles:
article["medias"] = []
article["nb_medias"] = 0
return articles
def _enrichir_prix_gammes(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit avec les prix spécifiques par combinaison de gammes
Structure: articles[i]["prix_gammes"] = [{"gamme1": 1, "gamme2": 3, "prix_net": 125.5}, ...]
"""
try:
logger.info(f" → Enrichissement prix par gammes...")
references = [a["reference"] for a in articles if a["reference"]]
if not references:
return articles
placeholders = ",".join(["?"] * len(references))
query = f"""
SELECT
AR_Ref,
AG_No1,
AG_No2,
AR_PUNet,
AR_CoutStd,
cbCreation,
cbModification
FROM F_ARTPRIX
WHERE AR_Ref IN ({placeholders})
ORDER BY AR_Ref, AG_No1, AG_No2
"""
cursor.execute(query, references)
rows = cursor.fetchall()
prix_gammes_map = {}
for row in rows:
ref = self._safe_strip(row[0])
if not ref:
continue
if ref not in prix_gammes_map:
prix_gammes_map[ref] = []
prix_gammes_map[ref].append({
"gamme_1": int(row[1]) if row[1] else 0,
"gamme_2": int(row[2]) if row[2] else 0,
"prix_net": float(row[3]) if row[3] else 0.0,
"cout_standard": float(row[4]) if row[4] else 0.0,
"date_creation": row[5],
"date_modification": row[6],
})
for article in articles:
article["prix_gammes"] = prix_gammes_map.get(article["reference"], [])
article["nb_prix_gammes"] = len(article["prix_gammes"])
logger.info(f"{len(prix_gammes_map)} articles avec prix par gammes")
return articles
except Exception as e:
logger.error(f" ✗ Erreur prix gammes: {e}")
for article in articles:
article["prix_gammes"] = []
article["nb_prix_gammes"] = 0
return articles
def _enrichir_conditionnements(self, articles: List[Dict], cursor) -> List[Dict]:
"""
Enrichit avec les conditionnements disponibles
"""
try:
logger.info(f" → Enrichissement conditionnements...")
query = """
SELECT
EC_Champ,
EC_Enumere,
EC_Quantite,
EC_EdiCode
FROM F_ENUMCOND
ORDER BY EC_Champ, EC_Enumere
"""
cursor.execute(query)
rows = cursor.fetchall()
cond_map = {}
for row in rows:
champ = int(row[0]) if row[0] else 0
enumere = self._safe_strip(row[1])
if not enumere:
continue
key = f"{champ}_{enumere}"
cond_map[key] = {
"champ": champ,
"enumere": enumere,
"quantite": float(row[2]) if row[2] else 0.0,
"edi_code": self._safe_strip(row[3]),
}
logger.info(f"{len(cond_map)} conditionnements chargés")
for article in articles:
conditionnement = article.get("conditionnement")
if conditionnement:
for key, cond_data in cond_map.items():
if cond_data["enumere"] == conditionnement:
article["conditionnement_qte"] = cond_data["quantite"]
article["conditionnement_edi"] = cond_data["edi_code"]
break
logger.info(f" ✓ Conditionnements enrichis")
return articles
except Exception as e:
logger.error(f" ✗ Erreur conditionnements: {e}")
return articles
def _mapper_article_depuis_row(self, row_data: Dict, colonnes_config: Dict) -> Dict:
"""
Mappe une ligne SQL vers un dictionnaire article normalisé
Args:
row_data: Dictionnaire avec noms de colonnes SQL comme clés
colonnes_config: Mapping SQL -> noms normalisés
Returns:
Dictionnaire article avec noms normalisés
"""
article = {}
def get_val(sql_col, default=None, convert_type=None):
val = row_data.get(sql_col, default)
if val is None:
return default
if convert_type == float:
return float(val) if val not in (None, "") else (default or 0.0)
elif convert_type == int:
return int(val) if val not in (None, "") else (default or 0)
elif convert_type == bool:
return bool(val) if val not in (None, "") else (default or False)
elif convert_type == str:
return self._safe_strip(val)
return val
article["reference"] = get_val("AR_Ref", convert_type=str)
article["designation"] = get_val("AR_Design", convert_type=str)
article["code_ean"] = get_val("AR_CodeBarre", convert_type=str)
article["code_barre"] = get_val("AR_CodeBarre", convert_type=str)
article["edi_code"] = get_val("AR_EdiCode", convert_type=str)
article["raccourci"] = get_val("AR_Raccourci", convert_type=str)
article["prix_vente"] = get_val("AR_PrixVen", 0.0, float)
article["prix_achat"] = get_val("AR_PrixAch", 0.0, float)
article["coef"] = get_val("AR_Coef", 0.0, float)
article["prix_net"] = get_val("AR_PUNet", 0.0, float)
article["prix_achat_nouveau"] = get_val("AR_PrixAchNouv", 0.0, float)
article["coef_nouveau"] = get_val("AR_CoefNouv", 0.0, float)
article["prix_vente_nouveau"] = get_val("AR_PrixVenNouv", 0.0, float)
date_app = get_val("AR_DateApplication")
article["date_application_prix"] = str(date_app) if date_app else None
article["cout_standard"] = get_val("AR_CoutStd", 0.0, float)
article["unite_vente"] = get_val("AR_UniteVen", convert_type=str)
article["unite_poids"] = get_val("AR_UnitePoids", convert_type=str)
article["poids_net"] = get_val("AR_PoidsNet", 0.0, float)
article["poids_brut"] = get_val("AR_PoidsBrut", 0.0, float)
article["gamme_1"] = get_val("AR_Gamme1", convert_type=str)
article["gamme_2"] = get_val("AR_Gamme2", convert_type=str)
type_val = get_val("AR_Type", 0, int)
article["type_article"] = type_val
article["type_article_libelle"] = self._get_type_article_libelle(type_val)
article["famille_code"] = get_val("FA_CodeFamille", convert_type=str)
article["nature"] = get_val("AR_Nature", 0, int)
article["garantie"] = get_val("AR_Garantie", 0, int)
article["code_fiscal"] = get_val("AR_CodeFiscal", convert_type=str)
article["pays"] = get_val("AR_Pays", convert_type=str)
article["fournisseur_principal"] = get_val("CO_No", 0, int)
article["conditionnement"] = get_val("AR_Condition", convert_type=str)
article["nb_colis"] = get_val("AR_NbColis", 0, int)
article["prevision"] = get_val("AR_Prevision", False, bool)
article["suivi_stock"] = get_val("AR_SuiviStock", False, bool)
article["nomenclature"] = get_val("AR_Nomencl", False, bool)
article["qte_composant"] = get_val("AR_QteComp", 0.0, float)
article["qte_operatoire"] = get_val("AR_QteOperatoire", 0.0, float)
sommeil = get_val("AR_Sommeil", 0, int)
article["est_actif"] = (sommeil == 0)
article["en_sommeil"] = (sommeil == 1)
article["article_substitut"] = get_val("AR_Substitut", convert_type=str)
article["soumis_escompte"] = get_val("AR_Escompte", False, bool)
article["delai"] = get_val("AR_Delai", 0, int)
article["stat_01"] = get_val("AR_Stat01", convert_type=str)
article["stat_02"] = get_val("AR_Stat02", convert_type=str)
article["stat_03"] = get_val("AR_Stat03", convert_type=str)
article["stat_04"] = get_val("AR_Stat04", convert_type=str)
article["stat_05"] = get_val("AR_Stat05", convert_type=str)
article["hors_statistique"] = get_val("AR_HorsStat", False, bool)
article["categorie_1"] = get_val("CL_No1", 0, int)
article["categorie_2"] = get_val("CL_No2", 0, int)
article["categorie_3"] = get_val("CL_No3", 0, int)
article["categorie_4"] = get_val("CL_No4", 0, int)
date_modif = get_val("AR_DateModif")
article["date_modification"] = str(date_modif) if date_modif else None
article["vente_debit"] = get_val("AR_VteDebit", False, bool)
article["non_imprimable"] = get_val("AR_NotImp", False, bool)
article["transfere"] = get_val("AR_Transfere", False, bool)
article["publie"] = get_val("AR_Publie", False, bool)
article["contremarque"] = get_val("AR_Contremarque", False, bool)
article["fact_poids"] = get_val("AR_FactPoids", False, bool)
article["fact_forfait"] = get_val("AR_FactForfait", False, bool)
article["saisie_variable"] = get_val("AR_SaisieVar", False, bool)
article["fictif"] = get_val("AR_Fictif", False, bool)
article["sous_traitance"] = get_val("AR_SousTraitance", False, bool)
article["criticite"] = get_val("AR_Criticite", 0, int)
article["reprise_code_defaut"] = get_val("RP_CodeDefaut", convert_type=str)
article["delai_fabrication"] = get_val("AR_DelaiFabrication", 0, int)
article["delai_peremption"] = get_val("AR_DelaiPeremption", 0, int)
article["delai_securite"] = get_val("AR_DelaiSecurite", 0, int)
article["type_lancement"] = get_val("AR_TypeLancement", 0, int)
article["cycle"] = get_val("AR_Cycle", 1, int)
article["photo"] = get_val("AR_Photo", convert_type=str)
article["langue_1"] = get_val("AR_Langue1", convert_type=str)
article["langue_2"] = get_val("AR_Langue2", convert_type=str)
article["frais_01_denomination"] = get_val("AR_Frais01FR_Denomination", convert_type=str)
article["frais_02_denomination"] = get_val("AR_Frais02FR_Denomination", convert_type=str)
article["frais_03_denomination"] = get_val("AR_Frais03FR_Denomination", convert_type=str)
article["marque_commerciale"] = get_val("Marque commerciale", convert_type=str)
objectif_val = get_val("Objectif / Qtés vendues")
if objectif_val is not None:
article["objectif_qtes_vendues"] = str(float(objectif_val)) if objectif_val not in ("", 0, 0.0) else None
else:
article["objectif_qtes_vendues"] = None
pourcentage_val = get_val("Pourcentage teneur en or")
if pourcentage_val is not None:
article["pourcentage_or"] = str(float(pourcentage_val)) if pourcentage_val not in ("", 0, 0.0) else None
else:
article["pourcentage_or"] = None
date_com = get_val("1ère commercialisation")
article["premiere_commercialisation"] = str(date_com) if date_com else None
article["interdire_commande"] = get_val("AR_InterdireCommande", False, bool)
article["exclure"] = get_val("AR_Exclure", False, bool)
article["stock_reel"] = 0.0
article["stock_mini"] = 0.0
article["stock_maxi"] = 0.0
article["stock_reserve"] = 0.0
article["stock_commande"] = 0.0
article["stock_disponible"] = 0.0
article["famille_libelle"] = None
article["famille_type"] = None
article["famille_unite_vente"] = None
article["famille_coef"] = None
article["famille_suivi_stock"] = None
article["famille_garantie"] = None
article["famille_unite_poids"] = None
article["famille_delai"] = None
article["famille_nb_colis"] = None
article["famille_code_fiscal"] = None
article["famille_escompte"] = None
article["famille_centrale"] = None
article["famille_nature"] = None
article["famille_hors_stat"] = None
article["famille_pays"] = None
article["fournisseur_nom"] = None
article["tva_code"] = None
article["tva_taux"] = None
return article
def _enrichir_stocks_articles(self, articles: List[Dict], cursor) -> List[Dict]:
"""Enrichit les articles avec les données de stock depuis F_ARTSTOCK"""
try:
logger.info(f" → Enrichissement stocks pour {len(articles)} articles...")
references = [a["reference"] for a in articles if a["reference"]]
if not references:
return articles
placeholders = ",".join(["?"] * len(references))
stock_query = f"""
SELECT
AR_Ref,
SUM(ISNULL(AS_QteSto, 0)) as Stock_Total,
MIN(ISNULL(AS_QteMini, 0)) as Stock_Mini,
MAX(ISNULL(AS_QteMaxi, 0)) as Stock_Maxi,
SUM(ISNULL(AS_QteRes, 0)) as Stock_Reserve,
SUM(ISNULL(AS_QteCom, 0)) as Stock_Commande
FROM F_ARTSTOCK
WHERE AR_Ref IN ({placeholders})
GROUP BY AR_Ref
"""
cursor.execute(stock_query, references)
stock_rows = cursor.fetchall()
stock_map = {}
for stock_row in stock_rows:
ref = self._safe_strip(stock_row[0])
if ref:
stock_map[ref] = {
"stock_reel": float(stock_row[1]) if stock_row[1] else 0.0,
"stock_mini": float(stock_row[2]) if stock_row[2] else 0.0,
"stock_maxi": float(stock_row[3]) if stock_row[3] else 0.0,
"stock_reserve": float(stock_row[4]) if stock_row[4] else 0.0,
"stock_commande": float(stock_row[5]) if stock_row[5] else 0.0,
}
logger.info(f"{len(stock_map)} articles avec stock trouvés dans F_ARTSTOCK")
for article in articles:
if article["reference"] in stock_map:
stock_data = stock_map[article["reference"]]
article.update(stock_data)
article["stock_disponible"] = (
article["stock_reel"] - article["stock_reserve"]
)
return articles
except Exception as e:
logger.error(f" ✗ Erreur enrichissement stocks: {e}", exc_info=True)
return articles
def _enrichir_fournisseurs_articles(self, articles: List[Dict], cursor) -> List[Dict]:
"""Enrichit les articles avec le nom du fournisseur principal"""
try:
logger.info(f" → Enrichissement fournisseurs...")
nums_fournisseurs = list(set([
a["fournisseur_principal"] for a in articles
if a.get("fournisseur_principal") and a["fournisseur_principal"] > 0
]))
if not nums_fournisseurs:
logger.warning(" ⚠ Aucun numéro de fournisseur trouvé dans les articles")
for article in articles:
article["fournisseur_nom"] = None
return articles
logger.info(f"{len(nums_fournisseurs)} fournisseurs uniques à chercher")
logger.info(f" → Exemples CO_No : {nums_fournisseurs[:5]}")
placeholders = ",".join(["?"] * len(nums_fournisseurs))
fournisseur_query = f"""
SELECT
CT_Num,
CT_Intitule,
CT_Type
FROM F_COMPTET
WHERE CT_Num IN ({placeholders})
AND CT_Type = 1
"""
cursor.execute(fournisseur_query, nums_fournisseurs)
fournisseur_rows = cursor.fetchall()
logger.info(f"{len(fournisseur_rows)} fournisseurs trouvés dans F_COMPTET")
if len(fournisseur_rows) == 0:
logger.warning(f" ⚠ Aucun fournisseur trouvé pour CT_Type=1 et CT_Num IN {nums_fournisseurs[:5]}")
cursor.execute(f"SELECT CT_Num, CT_Type FROM F_COMPTET WHERE CT_Num IN ({placeholders})", nums_fournisseurs)
tous_types = cursor.fetchall()
if tous_types:
logger.info(f" → Trouvé {len(tous_types)} comptes (tous types) : {[(r[0], r[1]) for r in tous_types[:5]]}")
fournisseur_map = {}
for fourn_row in fournisseur_rows:
num = int(fourn_row[0]) # CT_Num
nom = self._safe_strip(fourn_row[1]) # CT_Intitule
type_ct = int(fourn_row[2]) # CT_Type
fournisseur_map[num] = nom
logger.debug(f" → Fournisseur mappé : {num} = {nom} (Type={type_ct})")
nb_enrichis = 0
for article in articles:
num_fourn = article.get("fournisseur_principal")
if num_fourn and num_fourn in fournisseur_map:
article["fournisseur_nom"] = fournisseur_map[num_fourn]
nb_enrichis += 1
else:
article["fournisseur_nom"] = None
logger.info(f"{nb_enrichis} articles enrichis avec nom fournisseur")
return articles
except Exception as e:
logger.error(f" ✗ Erreur enrichissement fournisseurs: {e}", exc_info=True)
for article in articles:
article["fournisseur_nom"] = None
return articles
def _enrichir_familles_articles(self, articles: List[Dict], cursor) -> List[Dict]:
"""Enrichit les articles avec les informations de famille depuis F_FAMILLE"""
try:
logger.info(f" → Enrichissement familles pour {len(articles)} articles...")
codes_familles_bruts = [
a.get("famille_code") for a in articles
if a.get("famille_code") not in (None, "", " ")
]
if codes_familles_bruts:
logger.info(f" → Exemples de codes familles : {codes_familles_bruts[:5]}")
codes_familles = list(set([
str(code).strip() for code in codes_familles_bruts if code
]))
if not codes_familles:
logger.warning(" ⚠ Aucun code famille trouvé dans les articles")
for article in articles:
self._init_champs_famille_vides(article)
return articles
logger.info(f"{len(codes_familles)} codes famille uniques")
cursor.execute("SELECT TOP 1 * FROM F_FAMILLE")
colonnes_disponibles = [column[0] for column in cursor.description]
colonnes_souhaitees = [
"FA_CodeFamille",
"FA_Intitule",
"FA_Type",
"FA_UniteVen",
"FA_Coef",
"FA_SuiviStock",
"FA_Garantie",
"FA_UnitePoids",
"FA_Delai",
"FA_NbColis",
"FA_CodeFiscal",
"FA_Escompte",
"FA_Central",
"FA_Nature",
"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 "FA_CodeFamille" not in colonnes_a_lire or "FA_Intitule" not in colonnes_a_lire:
logger.error(" ✗ Colonnes essentielles manquantes !")
return articles
logger.info(f" → Colonnes disponibles : {len(colonnes_a_lire)}")
colonnes_str = ", ".join(colonnes_a_lire)
placeholders = ",".join(["?"] * len(codes_familles))
famille_query = f"""
SELECT {colonnes_str}
FROM F_FAMILLE
WHERE FA_CodeFamille IN ({placeholders})
"""
cursor.execute(famille_query, codes_familles)
famille_rows = cursor.fetchall()
logger.info(f"{len(famille_rows)} familles trouvées")
famille_map = {}
for fam_row in famille_rows:
famille_data = {}
for idx, col in enumerate(colonnes_a_lire):
famille_data[col] = fam_row[idx]
code = self._safe_strip(famille_data.get("FA_CodeFamille"))
if not code:
continue
famille_map[code] = {
"famille_libelle": self._safe_strip(famille_data.get("FA_Intitule")),
"famille_type": int(famille_data.get("FA_Type", 0) or 0),
"famille_unite_vente": self._safe_strip(famille_data.get("FA_UniteVen")),
"famille_coef": float(famille_data.get("FA_Coef", 0) or 0),
"famille_suivi_stock": bool(famille_data.get("FA_SuiviStock", 0)),
"famille_garantie": int(famille_data.get("FA_Garantie", 0) or 0),
"famille_unite_poids": self._safe_strip(famille_data.get("FA_UnitePoids")),
"famille_delai": int(famille_data.get("FA_Delai", 0) or 0),
"famille_nb_colis": int(famille_data.get("FA_NbColis", 0) or 0),
"famille_code_fiscal": self._safe_strip(famille_data.get("FA_CodeFiscal")),
"famille_escompte": bool(famille_data.get("FA_Escompte", 0)),
"famille_centrale": bool(famille_data.get("FA_Central", 0)),
"famille_nature": int(famille_data.get("FA_Nature", 0) or 0),
"famille_hors_stat": bool(famille_data.get("FA_HorsStat", 0)),
"famille_pays": self._safe_strip(famille_data.get("FA_Pays")),
}
logger.info(f"{len(famille_map)} familles mappées")
nb_enrichis = 0
for article in articles:
code_fam = str(article.get("famille_code", "")).strip()
if code_fam and code_fam in famille_map:
article.update(famille_map[code_fam])
nb_enrichis += 1
else:
self._init_champs_famille_vides(article)
logger.info(f"{nb_enrichis} articles enrichis avec infos famille")
return articles
except Exception as e:
logger.error(f" Erreur enrichissement familles: {e}", exc_info=True)
for article in articles:
self._init_champs_famille_vides(article)
return articles
def _init_champs_famille_vides(self, article: Dict):
"""Initialise les champs famille à None/0"""
article["famille_libelle"] = None
article["famille_type"] = None
article["famille_unite_vente"] = None
article["famille_coef"] = None
article["famille_suivi_stock"] = None
article["famille_garantie"] = None
article["famille_unite_poids"] = None
article["famille_delai"] = None
article["famille_nb_colis"] = None
article["famille_code_fiscal"] = None
article["famille_escompte"] = None
article["famille_centrale"] = None
article["famille_nature"] = None
article["famille_hors_stat"] = None
article["famille_pays"] = None
def _enrichir_tva_articles(self, articles: List[Dict], cursor) -> List[Dict]:
"""Enrichit les articles avec le taux de TVA"""
try:
logger.info(f" → Enrichissement TVA...")
codes_tva = list(set([
a["code_fiscal"] for a in articles
if a.get("code_fiscal")
]))
if not codes_tva:
for article in articles:
article["tva_code"] = None
article["tva_taux"] = None
return articles
placeholders = ",".join(["?"] * len(codes_tva))
tva_query = f"""
SELECT
TA_Code,
TA_Taux
FROM F_TAXE
WHERE TA_Code IN ({placeholders})
"""
cursor.execute(tva_query, codes_tva)
tva_rows = cursor.fetchall()
tva_map = {}
for tva_row in tva_rows:
code = self._safe_strip(tva_row[0])
tva_map[code] = float(tva_row[1]) if tva_row[1] else 0.0
logger.info(f"{len(tva_map)} codes TVA trouvés")
for article in articles:
code_tva = article.get("code_fiscal")
if code_tva and code_tva in tva_map:
article["tva_code"] = code_tva
article["tva_taux"] = tva_map[code_tva]
else:
article["tva_code"] = code_tva
article["tva_taux"] = None
return articles
except Exception as e:
logger.error(f" ✗ Erreur enrichissement TVA: {e}", exc_info=True)
for article in articles:
article["tva_code"] = article.get("code_fiscal")
article["tva_taux"] = None
return articles
def _get_type_article_libelle(self, type_val: int) -> str:
"""Retourne le libellé du type d'article"""
types = {
0: "Article",
1: "Prestation",
2: "Divers / Frais",
3: "Nomenclature"
}
return types.get(type_val, f"Type {type_val}")
def _safe_strip(self, value) -> Optional[str]:
"""Nettoie une valeur string en toute sécurité"""
if value is None:
return None
if isinstance(value, str):
stripped = value.strip()
return stripped if stripped else None
return str(value).strip() or None
def _convertir_type_pour_sql(self, type_doc: int) -> int:
"""COM → SQL : 0, 10, 20, 30... → 0, 1, 2, 3..."""
mapping = {0: 0, 10: 1, 20: 2, 30: 3, 40: 4, 50: 5, 60: 6}
return mapping.get(type_doc, type_doc)
def _convertir_type_depuis_sql(self, type_sql: int) -> int:
"""SQL → COM : 0, 1, 2, 3... → 0, 10, 20, 30..."""
mapping = {0: 0, 1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 6: 60}
return mapping.get(type_sql, type_sql)
def _lire_document_sql(self, numero: str, type_doc: int):
"""
Lit un document spécifique par son numéro.
PAS de filtre par préfixe car on cherche un document précis.
"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT
d.DO_Piece, d.DO_Date, d.DO_Ref, d.DO_TotalHT, d.DO_TotalTTC,
d.DO_Statut, d.DO_Tiers, d.DO_DateLivr, d.DO_DateExpedition,
d.DO_Contact, d.DO_TotalHTNet, d.DO_NetAPayer,
d.DO_MontantRegle, d.DO_Reliquat, d.DO_TxEscompte, d.DO_Escompte,
d.DO_Taxe1, d.DO_Taxe2, d.DO_Taxe3,
d.DO_CodeTaxe1, d.DO_CodeTaxe2, d.DO_CodeTaxe3,
d.DO_EStatut, d.DO_Imprim, d.DO_Valide, d.DO_Cloture,
d.DO_Transfere, d.DO_Souche, d.DO_PieceOrig, d.DO_GUID,
d.CA_Num, d.CG_Num, d.DO_Expedit, d.DO_Condition,
d.DO_Tarif, d.DO_TypeFrais, d.DO_ValFrais,
d.DO_TypeFranco, d.DO_ValFranco,
c.CT_Intitule, c.CT_Adresse, c.CT_CodePostal,
c.CT_Ville, c.CT_Telephone, c.CT_EMail
FROM F_DOCENTETE d
LEFT JOIN F_COMPTET c ON d.DO_Tiers = c.CT_Num
WHERE d.DO_Piece = ? AND d.DO_Type = ?
"""
logger.info(f"[SQL READ] Lecture directe de {numero} (type={type_doc})")
cursor.execute(query, (numero, type_doc))
row = cursor.fetchone()
if not row:
logger.warning(
f"[SQL READ] Document {numero} (type={type_doc}) INTROUVABLE dans F_DOCENTETE"
)
return None
numero_piece = self._safe_strip(row[0])
logger.info(f"[SQL READ] Document trouvé: {numero_piece}")
doc = {
"numero": numero_piece,
"reference": self._safe_strip(row[2]), # DO_Ref
"date": str(row[1]) if row[1] else "", # DO_Date
"date_livraison": (str(row[7]) if row[7] else ""), # DO_DateLivr
"date_expedition": (
str(row[8]) if row[8] else ""
), # DO_DateExpedition
"client_code": self._safe_strip(row[6]), # DO_Tiers
"client_intitule": self._safe_strip(row[39]), # CT_Intitule
"client_adresse": self._safe_strip(row[40]), # CT_Adresse
"client_code_postal": self._safe_strip(row[41]), # CT_CodePostal
"client_ville": self._safe_strip(row[42]), # CT_Ville
"client_telephone": self._safe_strip(row[43]), # CT_Telephone
"client_email": self._safe_strip(row[44]), # CT_EMail
"contact": self._safe_strip(row[9]), # DO_Contact
"total_ht": float(row[3]) if row[3] else 0.0, # DO_TotalHT
"total_ht_net": float(row[10]) if row[10] else 0.0, # DO_TotalHTNet
"total_ttc": float(row[4]) if row[4] else 0.0, # DO_TotalTTC
"net_a_payer": float(row[11]) if row[11] else 0.0, # DO_NetAPayer
"montant_regle": (
float(row[12]) if row[12] else 0.0
), # DO_MontantRegle
"reliquat": float(row[13]) if row[13] else 0.0, # DO_Reliquat
"taux_escompte": (
float(row[14]) if row[14] else 0.0
), # DO_TxEscompte
"escompte": float(row[15]) if row[15] else 0.0, # DO_Escompte
"taxe1": float(row[16]) if row[16] else 0.0, # DO_Taxe1
"taxe2": float(row[17]) if row[17] else 0.0, # DO_Taxe2
"taxe3": float(row[18]) if row[18] else 0.0, # DO_Taxe3
"code_taxe1": self._safe_strip(row[19]), # DO_CodeTaxe1
"code_taxe2": self._safe_strip(row[20]), # DO_CodeTaxe2
"code_taxe3": self._safe_strip(row[21]), # DO_CodeTaxe3
"statut": int(row[5]) if row[5] is not None else 0, # DO_Statut
"statut_estatut": (
int(row[22]) if row[22] is not None else 0
), # DO_EStatut
"imprime": int(row[23]) if row[23] is not None else 0, # DO_Imprim
"valide": int(row[24]) if row[24] is not None else 0, # DO_Valide
"cloture": int(row[25]) if row[25] is not None else 0, # DO_Cloture
"transfere": (
int(row[26]) if row[26] is not None else 0
), # DO_Transfere
"souche": int(row[27]) if row[27] is not None else 0, # DO_Souche
"piece_origine": self._safe_strip(row[28]), # DO_PieceOrig
"guid": self._safe_strip(row[29]), # DO_GUID
"ca_num": self._safe_strip(row[30]), # CA_Num
"cg_num": self._safe_strip(row[31]), # CG_Num
"expedition": (
int(row[32]) if row[32] is not None else 1
), # DO_Expedit
"condition": (
int(row[33]) if row[33] is not None else 1
), # DO_Condition
"tarif": int(row[34]) if row[34] is not None else 1, # DO_Tarif
"type_frais": (
int(row[35]) if row[35] is not None else 0
), # DO_TypeFrais
"valeur_frais": float(row[36]) if row[36] else 0.0, # DO_ValFrais
"type_franco": (
int(row[37]) if row[37] is not None else 0
), # DO_TypeFranco
"valeur_franco": float(row[38]) if row[38] else 0.0, # DO_ValFranco
}
cursor.execute(
"""
SELECT
dl.*,
a.AR_Design, a.FA_CodeFamille, a.AR_PrixTTC, a.AR_PrixVen, a.AR_PrixAch,
a.AR_Gamme1, a.AR_Gamme2, a.AR_CodeBarre, a.AR_CoutStd,
a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_UniteVen,
a.AR_Type, a.AR_Nature, a.AR_Escompte, a.AR_Garantie
FROM F_DOCLIGNE dl
LEFT JOIN F_ARTICLE a ON dl.AR_Ref = a.AR_Ref
WHERE dl.DO_Piece = ? AND dl.DO_Type = ?
ORDER BY dl.DL_Ligne
""",
(numero, type_doc),
)
lignes = []
for ligne_row in cursor.fetchall():
montant_ht = (
float(ligne_row.DL_MontantHT) if ligne_row.DL_MontantHT else 0.0
)
montant_net = (
float(ligne_row.DL_MontantNet)
if hasattr(ligne_row, "DL_MontantNet")
and ligne_row.DL_MontantNet
else montant_ht
)
taux_taxe1 = (
float(ligne_row.DL_Taxe1)
if hasattr(ligne_row, "DL_Taxe1") and ligne_row.DL_Taxe1
else 0.0
)
taux_taxe2 = (
float(ligne_row.DL_Taxe2)
if hasattr(ligne_row, "DL_Taxe2") and ligne_row.DL_Taxe2
else 0.0
)
taux_taxe3 = (
float(ligne_row.DL_Taxe3)
if hasattr(ligne_row, "DL_Taxe3") and ligne_row.DL_Taxe3
else 0.0
)
total_taux_taxes = taux_taxe1 + taux_taxe2 + taux_taxe3
montant_ttc = montant_net * (1 + total_taux_taxes / 100)
montant_taxe1 = montant_net * (taux_taxe1 / 100)
montant_taxe2 = montant_net * (taux_taxe2 / 100)
montant_taxe3 = montant_net * (taux_taxe3 / 100)
ligne = {
"numero_ligne": (
int(ligne_row.DL_Ligne) if ligne_row.DL_Ligne else 0
),
"article_code": self._safe_strip(ligne_row.AR_Ref),
"designation": self._safe_strip(ligne_row.DL_Design),
"designation_article": self._safe_strip(ligne_row.AR_Design),
"quantite": (
float(ligne_row.DL_Qte) if ligne_row.DL_Qte else 0.0
),
"quantite_livree": (
float(ligne_row.DL_QteLiv)
if hasattr(ligne_row, "DL_QteLiv") and ligne_row.DL_QteLiv
else 0.0
),
"quantite_reservee": (
float(ligne_row.DL_QteRes)
if hasattr(ligne_row, "DL_QteRes") and ligne_row.DL_QteRes
else 0.0
),
"unite": (
self._safe_strip(ligne_row.DL_Unite)
if hasattr(ligne_row, "DL_Unite")
else ""
),
"prix_unitaire_ht": (
float(ligne_row.DL_PrixUnitaire)
if ligne_row.DL_PrixUnitaire
else 0.0
),
"prix_unitaire_achat": (
float(ligne_row.AR_PrixAch) if ligne_row.AR_PrixAch else 0.0
),
"prix_unitaire_vente": (
float(ligne_row.AR_PrixVen) if ligne_row.AR_PrixVen else 0.0
),
"prix_unitaire_ttc": (
float(ligne_row.AR_PrixTTC) if ligne_row.AR_PrixTTC else 0.0
),
"montant_ligne_ht": montant_ht,
"montant_ligne_net": montant_net,
"montant_ligne_ttc": montant_ttc,
"remise_valeur1": (
float(ligne_row.DL_Remise01REM_Valeur)
if hasattr(ligne_row, "DL_Remise01REM_Valeur")
and ligne_row.DL_Remise01REM_Valeur
else 0.0
),
"remise_type1": (
int(ligne_row.DL_Remise01REM_Type)
if hasattr(ligne_row, "DL_Remise01REM_Type")
and ligne_row.DL_Remise01REM_Type
else 0
),
"remise_valeur2": (
float(ligne_row.DL_Remise02REM_Valeur)
if hasattr(ligne_row, "DL_Remise02REM_Valeur")
and ligne_row.DL_Remise02REM_Valeur
else 0.0
),
"remise_type2": (
int(ligne_row.DL_Remise02REM_Type)
if hasattr(ligne_row, "DL_Remise02REM_Type")
and ligne_row.DL_Remise02REM_Type
else 0
),
"remise_article": (
float(ligne_row.AR_Escompte)
if ligne_row.AR_Escompte
else 0.0
),
"taux_taxe1": taux_taxe1,
"montant_taxe1": montant_taxe1,
"taux_taxe2": taux_taxe2,
"montant_taxe2": montant_taxe2,
"taux_taxe3": taux_taxe3,
"montant_taxe3": montant_taxe3,
"total_taxes": montant_taxe1 + montant_taxe2 + montant_taxe3,
"famille_article": self._safe_strip(ligne_row.FA_CodeFamille),
"gamme1": self._safe_strip(ligne_row.AR_Gamme1),
"gamme2": self._safe_strip(ligne_row.AR_Gamme2),
"code_barre": self._safe_strip(ligne_row.AR_CodeBarre),
"type_article": self._safe_strip(ligne_row.AR_Type),
"nature_article": self._safe_strip(ligne_row.AR_Nature),
"garantie": self._safe_strip(ligne_row.AR_Garantie),
"cout_standard": (
float(ligne_row.AR_CoutStd) if ligne_row.AR_CoutStd else 0.0
),
"poids_net": (
float(ligne_row.AR_PoidsNet)
if ligne_row.AR_PoidsNet
else 0.0
),
"poids_brut": (
float(ligne_row.AR_PoidsBrut)
if ligne_row.AR_PoidsBrut
else 0.0
),
"unite_vente": self._safe_strip(ligne_row.AR_UniteVen),
"date_livraison_ligne": (
str(ligne_row.DL_DateLivr)
if hasattr(ligne_row, "DL_DateLivr")
and ligne_row.DL_DateLivr
else ""
),
"statut_ligne": (
int(ligne_row.DL_Statut)
if hasattr(ligne_row, "DL_Statut")
and ligne_row.DL_Statut is not None
else 0
),
"depot": (
self._safe_strip(ligne_row.DE_No)
if hasattr(ligne_row, "DE_No")
else ""
),
"numero_commande": (
self._safe_strip(ligne_row.DL_NoColis)
if hasattr(ligne_row, "DL_NoColis")
else ""
),
"num_colis": (
self._safe_strip(ligne_row.DL_Colis)
if hasattr(ligne_row, "DL_Colis")
else ""
),
}
lignes.append(ligne)
doc["lignes"] = lignes
doc["nb_lignes"] = len(lignes)
total_ht_calcule = sum(l.get("montant_ligne_ht", 0) for l in lignes)
total_ttc_calcule = sum(l.get("montant_ligne_ttc", 0) for l in lignes)
total_taxes_calcule = sum(l.get("total_taxes", 0) for l in lignes)
doc["total_ht_calcule"] = total_ht_calcule
doc["total_ttc_calcule"] = total_ttc_calcule
doc["total_taxes_calcule"] = total_taxes_calcule
return doc
except Exception as e:
logger.error(f" Erreur SQL lecture document {numero}: {e}", exc_info=True)
return None
def _lister_documents_avec_lignes_sql(
self,
type_doc: int,
filtre: str = "",
limit: int = None,
inclure_liaisons: bool = False,
calculer_transformations: bool = True,
):
"""Liste les documents avec leurs lignes."""
try:
type_doc_sql = self._convertir_type_pour_sql(type_doc)
logger.info(f"[SQL LIST] ═══ Type COM {type_doc} → SQL {type_doc_sql} ═══")
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT DISTINCT
d.DO_Piece, d.DO_Type, d.DO_Date, d.DO_Ref, d.DO_Tiers,
d.DO_TotalHT, d.DO_TotalTTC, d.DO_NetAPayer, d.DO_Statut,
d.DO_DateLivr, d.DO_DateExpedition, d.DO_Contact, d.DO_TotalHTNet,
d.DO_MontantRegle, d.DO_Reliquat, d.DO_TxEscompte, d.DO_Escompte,
d.DO_Taxe1, d.DO_Taxe2, d.DO_Taxe3,
d.DO_CodeTaxe1, d.DO_CodeTaxe2, d.DO_CodeTaxe3,
d.DO_EStatut, d.DO_Imprim, d.DO_Valide, d.DO_Cloture, d.DO_Transfere,
d.DO_Souche, d.DO_PieceOrig, d.DO_GUID,
d.CA_Num, d.CG_Num, d.DO_Expedit, d.DO_Condition, d.DO_Tarif,
d.DO_TypeFrais, d.DO_ValFrais, d.DO_TypeFranco, d.DO_ValFranco,
c.CT_Intitule, c.CT_Adresse, c.CT_CodePostal,
c.CT_Ville, c.CT_Telephone, c.CT_EMail
FROM F_DOCENTETE d
LEFT JOIN F_COMPTET c ON d.DO_Tiers = c.CT_Num
WHERE d.DO_Type = ?
"""
params = [type_doc_sql]
if filtre:
query += " AND (d.DO_Piece LIKE ? OR c.CT_Intitule LIKE ? OR d.DO_Ref LIKE ?)"
params.extend([f"%{filtre}%", f"%{filtre}%", f"%{filtre}%"])
query += " ORDER BY d.DO_Date DESC"
if limit:
query = f"SELECT TOP ({limit}) * FROM ({query}) AS subquery"
cursor.execute(query, params)
entetes = cursor.fetchall()
logger.info(f"[SQL LIST] {len(entetes)} documents SQL")
documents = []
stats = {
"total": len(entetes),
"exclus_prefixe": 0,
"erreur_construction": 0,
"erreur_lignes": 0,
"erreur_transformations": 0,
"erreur_liaisons": 0,
"succes": 0,
}
for idx, entete in enumerate(entetes):
numero = self._safe_strip(entete.DO_Piece)
logger.info(
f"[SQL LIST] [{idx+1}/{len(entetes)}] Traitement {numero}..."
)
try:
prefixes_vente = {
0: ["DE"],
10: ["BC"],
30: ["BL"],
50: ["AV", "AR"],
60: ["FA", "FC"],
}
prefixes_acceptes = prefixes_vente.get(type_doc, [])
if prefixes_acceptes:
est_vente = any(
numero.upper().startswith(p) for p in prefixes_acceptes
)
if not est_vente:
logger.info(
f"[SQL LIST] {numero} : exclu (préfixe achat)"
)
stats["exclus_prefixe"] += 1
continue
logger.debug(f"[SQL LIST] {numero} : préfixe OK")
try:
type_doc_depuis_sql = self._convertir_type_depuis_sql(
int(entete.DO_Type)
)
doc = {
"numero": numero,
"type": type_doc_depuis_sql,
"reference": self._safe_strip(entete.DO_Ref),
"date": str(entete.DO_Date) if entete.DO_Date else "",
"date_livraison": (
str(entete.DO_DateLivr)
if entete.DO_DateLivr
else ""
),
"date_expedition": (
str(entete.DO_DateExpedition)
if entete.DO_DateExpedition
else ""
),
"client_code": self._safe_strip(entete.DO_Tiers),
"client_intitule": self._safe_strip(entete.CT_Intitule),
"client_adresse": self._safe_strip(entete.CT_Adresse),
"client_code_postal": self._safe_strip(
entete.CT_CodePostal
),
"client_ville": self._safe_strip(entete.CT_Ville),
"client_telephone": self._safe_strip(
entete.CT_Telephone
),
"client_email": self._safe_strip(entete.CT_EMail),
"contact": self._safe_strip(entete.DO_Contact),
"total_ht": (
float(entete.DO_TotalHT)
if entete.DO_TotalHT
else 0.0
),
"total_ht_net": (
float(entete.DO_TotalHTNet)
if entete.DO_TotalHTNet
else 0.0
),
"total_ttc": (
float(entete.DO_TotalTTC)
if entete.DO_TotalTTC
else 0.0
),
"net_a_payer": (
float(entete.DO_NetAPayer)
if entete.DO_NetAPayer
else 0.0
),
"montant_regle": (
float(entete.DO_MontantRegle)
if entete.DO_MontantRegle
else 0.0
),
"reliquat": (
float(entete.DO_Reliquat)
if entete.DO_Reliquat
else 0.0
),
"taux_escompte": (
float(entete.DO_TxEscompte)
if entete.DO_TxEscompte
else 0.0
),
"escompte": (
float(entete.DO_Escompte)
if entete.DO_Escompte
else 0.0
),
"taxe1": (
float(entete.DO_Taxe1) if entete.DO_Taxe1 else 0.0
),
"taxe2": (
float(entete.DO_Taxe2) if entete.DO_Taxe2 else 0.0
),
"taxe3": (
float(entete.DO_Taxe3) if entete.DO_Taxe3 else 0.0
),
"code_taxe1": self._safe_strip(entete.DO_CodeTaxe1),
"code_taxe2": self._safe_strip(entete.DO_CodeTaxe2),
"code_taxe3": self._safe_strip(entete.DO_CodeTaxe3),
"statut": (
int(entete.DO_Statut)
if entete.DO_Statut is not None
else 0
),
"statut_estatut": (
int(entete.DO_EStatut)
if entete.DO_EStatut is not None
else 0
),
"imprime": (
int(entete.DO_Imprim)
if entete.DO_Imprim is not None
else 0
),
"valide": (
int(entete.DO_Valide)
if entete.DO_Valide is not None
else 0
),
"cloture": (
int(entete.DO_Cloture)
if entete.DO_Cloture is not None
else 0
),
"transfere": (
int(entete.DO_Transfere)
if entete.DO_Transfere is not None
else 0
),
"souche": self._safe_strip(entete.DO_Souche),
"piece_origine": self._safe_strip(entete.DO_PieceOrig),
"guid": self._safe_strip(entete.DO_GUID),
"ca_num": self._safe_strip(entete.CA_Num),
"cg_num": self._safe_strip(entete.CG_Num),
"expedition": self._safe_strip(entete.DO_Expedit),
"condition": self._safe_strip(entete.DO_Condition),
"tarif": self._safe_strip(entete.DO_Tarif),
"type_frais": (
int(entete.DO_TypeFrais)
if entete.DO_TypeFrais is not None
else 0
),
"valeur_frais": (
float(entete.DO_ValFrais)
if entete.DO_ValFrais
else 0.0
),
"type_franco": (
int(entete.DO_TypeFranco)
if entete.DO_TypeFranco is not None
else 0
),
"valeur_franco": (
float(entete.DO_ValFranco)
if entete.DO_ValFranco
else 0.0
),
"lignes": [],
}
logger.debug(
f"[SQL LIST] {numero} : document de base créé"
)
except Exception as e:
logger.error(
f"[SQL LIST] {numero} : ERREUR construction base: {e}",
exc_info=True,
)
stats["erreur_construction"] += 1
continue
try:
cursor.execute(
"""
SELECT dl.*,
a.AR_Design, a.FA_CodeFamille, a.AR_PrixTTC, a.AR_PrixVen, a.AR_PrixAch,
a.AR_Gamme1, a.AR_Gamme2, a.AR_CodeBarre, a.AR_CoutStd,
a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_UniteVen,
a.AR_Type, a.AR_Nature, a.AR_Escompte, a.AR_Garantie
FROM F_DOCLIGNE dl
LEFT JOIN F_ARTICLE a ON dl.AR_Ref = a.AR_Ref
WHERE dl.DO_Piece = ? AND dl.DO_Type = ?
ORDER BY dl.DL_Ligne
""",
(numero, type_doc_sql),
)
for ligne_row in cursor.fetchall():
montant_ht = (
float(ligne_row.DL_MontantHT)
if ligne_row.DL_MontantHT
else 0.0
)
montant_net = (
float(ligne_row.DL_MontantNet)
if hasattr(ligne_row, "DL_MontantNet")
and ligne_row.DL_MontantNet
else montant_ht
)
taux_taxe1 = (
float(ligne_row.DL_Taxe1)
if hasattr(ligne_row, "DL_Taxe1")
and ligne_row.DL_Taxe1
else 0.0
)
taux_taxe2 = (
float(ligne_row.DL_Taxe2)
if hasattr(ligne_row, "DL_Taxe2")
and ligne_row.DL_Taxe2
else 0.0
)
taux_taxe3 = (
float(ligne_row.DL_Taxe3)
if hasattr(ligne_row, "DL_Taxe3")
and ligne_row.DL_Taxe3
else 0.0
)
total_taux_taxes = taux_taxe1 + taux_taxe2 + taux_taxe3
montant_ttc = montant_net * (1 + total_taux_taxes / 100)
montant_taxe1 = montant_net * (taux_taxe1 / 100)
montant_taxe2 = montant_net * (taux_taxe2 / 100)
montant_taxe3 = montant_net * (taux_taxe3 / 100)
ligne = {
"numero_ligne": (
int(ligne_row.DL_Ligne)
if ligne_row.DL_Ligne
else 0
),
"article_code": self._safe_strip(ligne_row.AR_Ref),
"designation": self._safe_strip(
ligne_row.DL_Design
),
"designation_article": self._safe_strip(
ligne_row.AR_Design
),
"quantite": (
float(ligne_row.DL_Qte)
if ligne_row.DL_Qte
else 0.0
),
"quantite_livree": (
float(ligne_row.DL_QteLiv)
if hasattr(ligne_row, "DL_QteLiv")
and ligne_row.DL_QteLiv
else 0.0
),
"quantite_reservee": (
float(ligne_row.DL_QteRes)
if hasattr(ligne_row, "DL_QteRes")
and ligne_row.DL_QteRes
else 0.0
),
"unite": (
self._safe_strip(ligne_row.DL_Unite)
if hasattr(ligne_row, "DL_Unite")
else ""
),
"prix_unitaire_ht": (
float(ligne_row.DL_PrixUnitaire)
if ligne_row.DL_PrixUnitaire
else 0.0
),
"prix_unitaire_achat": (
float(ligne_row.AR_PrixAch)
if ligne_row.AR_PrixAch
else 0.0
),
"prix_unitaire_vente": (
float(ligne_row.AR_PrixVen)
if ligne_row.AR_PrixVen
else 0.0
),
"prix_unitaire_ttc": (
float(ligne_row.AR_PrixTTC)
if ligne_row.AR_PrixTTC
else 0.0
),
"montant_ligne_ht": montant_ht,
"montant_ligne_net": montant_net,
"montant_ligne_ttc": montant_ttc,
"remise_valeur1": (
float(ligne_row.DL_Remise01REM_Valeur)
if hasattr(ligne_row, "DL_Remise01REM_Valeur")
and ligne_row.DL_Remise01REM_Valeur
else 0.0
),
"remise_type1": (
int(ligne_row.DL_Remise01REM_Type)
if hasattr(ligne_row, "DL_Remise01REM_Type")
and ligne_row.DL_Remise01REM_Type
else 0
),
"remise_valeur2": (
float(ligne_row.DL_Remise02REM_Valeur)
if hasattr(ligne_row, "DL_Remise02REM_Valeur")
and ligne_row.DL_Remise02REM_Valeur
else 0.0
),
"remise_type2": (
int(ligne_row.DL_Remise02REM_Type)
if hasattr(ligne_row, "DL_Remise02REM_Type")
and ligne_row.DL_Remise02REM_Type
else 0
),
"remise_article": (
float(ligne_row.AR_Escompte)
if ligne_row.AR_Escompte
else 0.0
),
"taux_taxe1": taux_taxe1,
"montant_taxe1": montant_taxe1,
"taux_taxe2": taux_taxe2,
"montant_taxe2": montant_taxe2,
"taux_taxe3": taux_taxe3,
"montant_taxe3": montant_taxe3,
"total_taxes": montant_taxe1
+ montant_taxe2
+ montant_taxe3,
"famille_article": self._safe_strip(
ligne_row.FA_CodeFamille
),
"gamme1": self._safe_strip(ligne_row.AR_Gamme1),
"gamme2": self._safe_strip(ligne_row.AR_Gamme2),
"code_barre": self._safe_strip(
ligne_row.AR_CodeBarre
),
"type_article": self._safe_strip(ligne_row.AR_Type),
"nature_article": self._safe_strip(
ligne_row.AR_Nature
),
"garantie": self._safe_strip(ligne_row.AR_Garantie),
"cout_standard": (
float(ligne_row.AR_CoutStd)
if ligne_row.AR_CoutStd
else 0.0
),
"poids_net": (
float(ligne_row.AR_PoidsNet)
if ligne_row.AR_PoidsNet
else 0.0
),
"poids_brut": (
float(ligne_row.AR_PoidsBrut)
if ligne_row.AR_PoidsBrut
else 0.0
),
"unite_vente": self._safe_strip(
ligne_row.AR_UniteVen
),
"date_livraison_ligne": (
str(ligne_row.DL_DateLivr)
if hasattr(ligne_row, "DL_DateLivr")
and ligne_row.DL_DateLivr
else ""
),
"statut_ligne": (
int(ligne_row.DL_Statut)
if hasattr(ligne_row, "DL_Statut")
and ligne_row.DL_Statut is not None
else 0
),
"depot": (
self._safe_strip(ligne_row.DE_No)
if hasattr(ligne_row, "DE_No")
else ""
),
"numero_commande": (
self._safe_strip(ligne_row.DL_NoColis)
if hasattr(ligne_row, "DL_NoColis")
else ""
),
"num_colis": (
self._safe_strip(ligne_row.DL_Colis)
if hasattr(ligne_row, "DL_Colis")
else ""
),
}
doc["lignes"].append(ligne)
doc["nb_lignes"] = len(doc["lignes"])
doc["total_ht_calcule"] = sum(
l.get("montant_ligne_ht", 0) for l in doc["lignes"]
)
doc["total_ttc_calcule"] = sum(
l.get("montant_ligne_ttc", 0) for l in doc["lignes"]
)
doc["total_taxes_calcule"] = sum(
l.get("total_taxes", 0) for l in doc["lignes"]
)
logger.debug(
f"[SQL LIST] {numero} : {doc['nb_lignes']} lignes chargées"
)
except Exception as e:
logger.error(
f"[SQL LIST] {numero} : ERREUR lignes: {e}",
exc_info=True,
)
stats["erreur_lignes"] += 1
documents.append(doc)
stats["succes"] += 1
logger.info(
f"[SQL LIST] {numero} : AJOUTÉ à la liste (total: {len(documents)})"
)
except Exception as e:
logger.error(
f"[SQL LIST] {numero} : EXCEPTION GLOBALE - DOCUMENT EXCLU: {e}",
exc_info=True,
)
continue
logger.info(f"[SQL LIST] ═══════════════════════════")
logger.info(f"[SQL LIST] STATISTIQUES FINALES:")
logger.info(f"[SQL LIST] Total SQL: {stats['total']}")
logger.info(f"[SQL LIST] Exclus préfixe: {stats['exclus_prefixe']}")
logger.info(
f"[SQL LIST] Erreur construction: {stats['erreur_construction']}"
)
logger.info(f"[SQL LIST] Erreur lignes: {stats['erreur_lignes']}")
logger.info(
f"[SQL LIST] Erreur transformations: {stats['erreur_transformations']}"
)
logger.info(f"[SQL LIST] Erreur liaisons: {stats['erreur_liaisons']}")
logger.info(f"[SQL LIST] SUCCÈS: {stats['succes']}")
logger.info(f"[SQL LIST] Documents retournés: {len(documents)}")
logger.info(f"[SQL LIST] ═══════════════════════════")
return documents
except Exception as e:
logger.error(f" Erreur GLOBALE listage: {e}", exc_info=True)
return []
def lister_tous_devis_cache(self, filtre=""):
return self._lister_documents_avec_lignes_sql(type_doc=0, filtre=filtre)
def lire_devis_cache(self, numero):
return self._lire_document_sql(numero, type_doc=0)
def lister_toutes_commandes_cache(self, filtre=""):
return self._lister_documents_avec_lignes_sql(type_doc=1, filtre=filtre)
def lire_commande_cache(self, numero):
return self._lire_document_sql(numero, type_doc=1)
def lister_toutes_factures_cache(self, filtre=""):
return self._lister_documents_avec_lignes_sql(type_doc=6, filtre=filtre)
def lire_facture_cache(self, numero):
return self._lire_document_sql(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=""):
return self._lister_documents_avec_lignes_sql(type_doc=3, filtre=filtre)
def lire_livraison_cache(self, numero):
return self._lire_document_sql(numero, type_doc=3)
def lister_tous_avoirs_cache(self, filtre=""):
return self._lister_documents_avec_lignes_sql(type_doc=5, filtre=filtre)
def lire_avoir_cache(self, numero):
return self._lire_document_sql(numero, type_doc=5)
def _cast_client(self, persist_obj):
try:
obj = win32com.client.CastTo(persist_obj, "IBOClient3")
obj.Read()
return obj
except Exception as e:
logger.debug(f" _cast_client échoue: {e}") # AJOUTER CE LOG
return None
def _cast_article(self, persist_obj):
try:
obj = win32com.client.CastTo(persist_obj, "IBOArticle3")
obj.Read()
return obj
except:
return None
def _extraire_client(self, client_obj):
try:
try:
numero = getattr(client_obj, "CT_Num", "").strip()
if not numero:
logger.debug("Objet sans CT_Num, skip")
return None
except Exception as e:
logger.debug(f" Erreur lecture CT_Num: {e}")
return None
try:
intitule = getattr(client_obj, "CT_Intitule", "").strip()
if not intitule:
logger.debug(f"{numero} sans CT_Intitule")
except Exception as e:
logger.debug(f"Erreur CT_Intitule sur {numero}: {e}")
intitule = ""
data = {
"numero": numero,
"intitule": intitule,
}
try:
qualite_code = getattr(client_obj, "CT_Type", None)
qualite_map = {
0: "CLI", # Client
1: "FOU", # Fournisseur
2: "CLIFOU", # Client + Fournisseur
3: "SAL", # Salarié
4: "PRO", # Prospect
}
data["qualite"] = qualite_map.get(qualite_code, "CLI")
data["est_fournisseur"] = qualite_code in [1, 2]
except:
data["qualite"] = "CLI"
data["est_fournisseur"] = False
try:
data["est_prospect"] = getattr(client_obj, "CT_Prospect", 0) == 1
except:
data["est_prospect"] = False
if data["est_prospect"]:
data["type_tiers"] = "prospect"
elif data["est_fournisseur"] and data["qualite"] != "CLIFOU":
data["type_tiers"] = "fournisseur"
elif data["qualite"] == "CLIFOU":
data["type_tiers"] = "client_fournisseur"
else:
data["type_tiers"] = "client"
try:
sommeil = getattr(client_obj, "CT_Sommeil", 0)
data["est_actif"] = sommeil == 0
data["est_en_sommeil"] = sommeil == 1
except:
data["est_actif"] = True
data["est_en_sommeil"] = False
try:
forme_juridique = getattr(client_obj, "CT_FormeJuridique", "").strip()
data["forme_juridique"] = forme_juridique
data["est_entreprise"] = bool(forme_juridique)
data["est_particulier"] = not bool(forme_juridique)
except:
data["forme_juridique"] = ""
data["est_entreprise"] = False
data["est_particulier"] = True
try:
data["civilite"] = getattr(client_obj, "CT_Civilite", "").strip()
except:
data["civilite"] = ""
try:
data["nom"] = getattr(client_obj, "CT_Nom", "").strip()
except:
data["nom"] = ""
try:
data["prenom"] = getattr(client_obj, "CT_Prenom", "").strip()
except:
data["prenom"] = ""
if data.get("nom") or data.get("prenom"):
parts = []
if data.get("civilite"):
parts.append(data["civilite"])
if data.get("prenom"):
parts.append(data["prenom"])
if data.get("nom"):
parts.append(data["nom"])
data["nom_complet"] = " ".join(parts)
else:
data["nom_complet"] = ""
try:
data["contact"] = getattr(client_obj, "CT_Contact", "").strip()
except:
data["contact"] = ""
try:
adresse_obj = getattr(client_obj, "Adresse", None)
if adresse_obj:
try:
data["adresse"] = getattr(adresse_obj, "Adresse", "").strip()
except:
data["adresse"] = ""
try:
data["complement"] = getattr(
adresse_obj, "Complement", ""
).strip()
except:
data["complement"] = ""
try:
data["code_postal"] = getattr(
adresse_obj, "CodePostal", ""
).strip()
except:
data["code_postal"] = ""
try:
data["ville"] = getattr(adresse_obj, "Ville", "").strip()
except:
data["ville"] = ""
try:
data["region"] = getattr(adresse_obj, "Region", "").strip()
except:
data["region"] = ""
try:
data["pays"] = getattr(adresse_obj, "Pays", "").strip()
except:
data["pays"] = ""
else:
data["adresse"] = ""
data["complement"] = ""
data["code_postal"] = ""
data["ville"] = ""
data["region"] = ""
data["pays"] = ""
except Exception as e:
logger.debug(f"Erreur adresse sur {numero}: {e}")
data["adresse"] = ""
data["complement"] = ""
data["code_postal"] = ""
data["ville"] = ""
data["region"] = ""
data["pays"] = ""
try:
telecom = getattr(client_obj, "Telecom", None)
if telecom:
try:
data["telephone"] = getattr(telecom, "Telephone", "").strip()
except:
data["telephone"] = ""
try:
data["portable"] = getattr(telecom, "Portable", "").strip()
except:
data["portable"] = ""
try:
data["telecopie"] = getattr(telecom, "Telecopie", "").strip()
except:
data["telecopie"] = ""
try:
data["email"] = getattr(telecom, "EMail", "").strip()
except:
data["email"] = ""
try:
site = (
getattr(telecom, "Site", None)
or getattr(telecom, "Web", None)
or getattr(telecom, "SiteWeb", "")
)
data["site_web"] = str(site).strip() if site else ""
except:
data["site_web"] = ""
else:
data["telephone"] = ""
data["portable"] = ""
data["telecopie"] = ""
data["email"] = ""
data["site_web"] = ""
except Exception as e:
logger.debug(f"Erreur telecom sur {numero}: {e}")
data["telephone"] = ""
data["portable"] = ""
data["telecopie"] = ""
data["email"] = ""
data["site_web"] = ""
try:
data["siret"] = getattr(client_obj, "CT_Siret", "").strip()
except:
data["siret"] = ""
try:
data["siren"] = getattr(client_obj, "CT_Siren", "").strip()
except:
data["siren"] = ""
try:
data["tva_intra"] = getattr(client_obj, "CT_Identifiant", "").strip()
except:
data["tva_intra"] = ""
try:
data["code_naf"] = (
getattr(client_obj, "CT_CodeNAF", "").strip()
or getattr(client_obj, "CT_APE", "").strip()
)
except:
data["code_naf"] = ""
try:
data["secteur"] = getattr(client_obj, "CT_Secteur", "").strip()
except:
data["secteur"] = ""
try:
effectif = getattr(client_obj, "CT_Effectif", None)
data["effectif"] = int(effectif) if effectif is not None else None
except:
data["effectif"] = None
try:
ca = getattr(client_obj, "CT_ChiffreAffaire", None)
data["ca_annuel"] = float(ca) if ca is not None else None
except:
data["ca_annuel"] = None
try:
data["commercial_code"] = getattr(client_obj, "CO_No", "").strip()
except:
try:
data["commercial_code"] = getattr(
client_obj, "CT_Commercial", ""
).strip()
except:
data["commercial_code"] = ""
if data.get("commercial_code"):
try:
commercial_obj = getattr(client_obj, "Commercial", None)
if commercial_obj:
commercial_obj.Read()
data["commercial_nom"] = getattr(
commercial_obj, "CO_Nom", ""
).strip()
else:
data["commercial_nom"] = ""
except:
data["commercial_nom"] = ""
else:
data["commercial_nom"] = ""
try:
data["categorie_tarifaire"] = getattr(client_obj, "N_CatTarif", None)
except:
data["categorie_tarifaire"] = None
try:
data["categorie_comptable"] = getattr(client_obj, "N_CatCompta", None)
except:
data["categorie_comptable"] = None
try:
data["encours_autorise"] = float(getattr(client_obj, "CT_Encours", 0.0))
except:
data["encours_autorise"] = 0.0
try:
data["assurance_credit"] = float(
getattr(client_obj, "CT_Assurance", 0.0)
)
except:
data["assurance_credit"] = 0.0
try:
data["compte_general"] = getattr(client_obj, "CG_Num", "").strip()
except:
data["compte_general"] = ""
try:
date_creation = getattr(client_obj, "CT_DateCreate", None)
data["date_creation"] = str(date_creation) if date_creation else ""
except:
data["date_creation"] = ""
try:
date_modif = getattr(client_obj, "CT_DateModif", None)
data["date_modification"] = str(date_modif) if date_modif else ""
except:
data["date_modification"] = ""
return data
except Exception as e:
logger.error(f" ERREUR GLOBALE _extraire_client: {e}", exc_info=True)
return None
def _extraire_article(self, article_obj):
try:
data = {
"reference": getattr(article_obj, "AR_Ref", "").strip(),
"designation": getattr(article_obj, "AR_Design", "").strip(),
}
data["code_ean"] = ""
data["code_barre"] = ""
try:
code_barre = getattr(article_obj, "AR_CodeBarre", "").strip()
if code_barre:
data["code_ean"] = code_barre
data["code_barre"] = code_barre
if not data["code_ean"]:
code_barre1 = getattr(article_obj, "AR_CodeBarre1", "").strip()
if code_barre1:
data["code_ean"] = code_barre1
data["code_barre"] = code_barre1
except:
pass
try:
data["prix_vente"] = float(getattr(article_obj, "AR_PrixVen", 0.0))
except:
data["prix_vente"] = 0.0
try:
data["prix_achat"] = float(getattr(article_obj, "AR_PrixAch", 0.0))
except:
data["prix_achat"] = 0.0
try:
data["prix_revient"] = float(
getattr(article_obj, "AR_PrixRevient", 0.0)
)
except:
data["prix_revient"] = 0.0
try:
data["stock_reel"] = float(getattr(article_obj, "AR_Stock", 0.0))
except:
data["stock_reel"] = 0.0
try:
data["stock_mini"] = float(getattr(article_obj, "AR_StockMini", 0.0))
except:
data["stock_mini"] = 0.0
try:
data["stock_maxi"] = float(getattr(article_obj, "AR_StockMaxi", 0.0))
except:
data["stock_maxi"] = 0.0
try:
data["stock_reserve"] = float(getattr(article_obj, "AR_QteCom", 0.0))
except:
data["stock_reserve"] = 0.0
try:
data["stock_commande"] = float(
getattr(article_obj, "AR_QteComFou", 0.0)
)
except:
data["stock_commande"] = 0.0
try:
data["stock_disponible"] = data["stock_reel"] - data["stock_reserve"]
except:
data["stock_disponible"] = data["stock_reel"]
try:
commentaire = getattr(article_obj, "AR_Commentaire", "").strip()
data["description"] = commentaire
except:
data["description"] = ""
try:
design2 = getattr(article_obj, "AR_Design2", "").strip()
data["designation_complementaire"] = design2
except:
data["designation_complementaire"] = ""
try:
type_art = getattr(article_obj, "AR_Type", 0)
data["type_article"] = type_art
data["type_article_libelle"] = {
0: "Article",
1: "Prestation",
2: "Divers",
}.get(type_art, "Inconnu")
except:
data["type_article"] = 0
data["type_article_libelle"] = "Article"
try:
famille_code = getattr(article_obj, "FA_CodeFamille", "").strip()
data["famille_code"] = famille_code
if famille_code:
try:
famille_obj = getattr(article_obj, "Famille", None)
if famille_obj:
famille_obj.Read()
data["famille_libelle"] = getattr(
famille_obj, "FA_Intitule", ""
).strip()
else:
data["famille_libelle"] = ""
except:
data["famille_libelle"] = ""
else:
data["famille_libelle"] = ""
except:
data["famille_code"] = ""
data["famille_libelle"] = ""
try:
fournisseur_code = getattr(article_obj, "CT_Num", "").strip()
data["fournisseur_principal"] = fournisseur_code
if fournisseur_code:
try:
fourn_obj = getattr(article_obj, "Fournisseur", None)
if fourn_obj:
fourn_obj.Read()
data["fournisseur_nom"] = getattr(
fourn_obj, "CT_Intitule", ""
).strip()
else:
data["fournisseur_nom"] = ""
except:
data["fournisseur_nom"] = ""
else:
data["fournisseur_nom"] = ""
except:
data["fournisseur_principal"] = ""
data["fournisseur_nom"] = ""
try:
data["unite_vente"] = getattr(article_obj, "AR_UniteVen", "").strip()
except:
data["unite_vente"] = ""
try:
data["unite_achat"] = getattr(article_obj, "AR_UniteAch", "").strip()
except:
data["unite_achat"] = ""
try:
data["poids"] = float(getattr(article_obj, "AR_Poids", 0.0))
except:
data["poids"] = 0.0
try:
data["volume"] = float(getattr(article_obj, "AR_Volume", 0.0))
except:
data["volume"] = 0.0
try:
sommeil = getattr(article_obj, "AR_Sommeil", 0)
data["est_actif"] = sommeil == 0
data["en_sommeil"] = sommeil == 1
except:
data["est_actif"] = True
data["en_sommeil"] = False
try:
tva_code = getattr(article_obj, "TA_Code", "").strip()
data["tva_code"] = tva_code
try:
tva_obj = getattr(article_obj, "Taxe1", None)
if tva_obj:
tva_obj.Read()
data["tva_taux"] = float(getattr(tva_obj, "TA_Taux", 20.0))
else:
data["tva_taux"] = 20.0
except:
data["tva_taux"] = 20.0
except:
data["tva_code"] = ""
data["tva_taux"] = 20.0
try:
date_creation = getattr(article_obj, "AR_DateCreate", None)
data["date_creation"] = str(date_creation) if date_creation else ""
except:
data["date_creation"] = ""
try:
date_modif = getattr(article_obj, "AR_DateModif", None)
data["date_modification"] = str(date_modif) if date_modif else ""
except:
data["date_modification"] = ""
return data
except Exception as e:
logger.error(f" Erreur extraction article: {e}", exc_info=True)
return {
"reference": getattr(article_obj, "AR_Ref", "").strip(),
"designation": getattr(article_obj, "AR_Design", "").strip(),
"prix_vente": 0.0,
"stock_reel": 0.0,
"code_ean": "",
"description": "",
"designation_complementaire": "",
"prix_achat": 0.0,
"prix_revient": 0.0,
"stock_mini": 0.0,
"stock_maxi": 0.0,
"stock_reserve": 0.0,
"stock_commande": 0.0,
"stock_disponible": 0.0,
"code_barre": "",
"type_article": 0,
"type_article_libelle": "Article",
"famille_code": "",
"famille_libelle": "",
"fournisseur_principal": "",
"fournisseur_nom": "",
"unite_vente": "",
"unite_achat": "",
"poids": 0.0,
"volume": 0.0,
"est_actif": True,
"en_sommeil": False,
"tva_code": "",
"tva_taux": 20.0,
"date_creation": "",
"date_modification": "",
}
def _extraire_fournisseur_enrichi(self, fourn_obj):
try:
numero = getattr(fourn_obj, "CT_Num", "").strip()
if not numero:
return None
intitule = getattr(fourn_obj, "CT_Intitule", "").strip()
data = {
"numero": numero,
"intitule": intitule,
"type": 1, # Fournisseur
"est_fournisseur": True,
}
try:
sommeil = getattr(fourn_obj, "CT_Sommeil", 0)
data["est_actif"] = sommeil == 0
data["en_sommeil"] = sommeil == 1
except:
data["est_actif"] = True
data["en_sommeil"] = False
try:
adresse_obj = getattr(fourn_obj, "Adresse", None)
if adresse_obj:
data["adresse"] = getattr(adresse_obj, "Adresse", "").strip()
data["complement"] = getattr(adresse_obj, "Complement", "").strip()
data["code_postal"] = getattr(adresse_obj, "CodePostal", "").strip()
data["ville"] = getattr(adresse_obj, "Ville", "").strip()
data["region"] = getattr(adresse_obj, "Region", "").strip()
data["pays"] = getattr(adresse_obj, "Pays", "").strip()
parties_adresse = []
if data["adresse"]:
parties_adresse.append(data["adresse"])
if data["complement"]:
parties_adresse.append(data["complement"])
if data["code_postal"] or data["ville"]:
ville_cp = f"{data['code_postal']} {data['ville']}".strip()
if ville_cp:
parties_adresse.append(ville_cp)
if data["pays"]:
parties_adresse.append(data["pays"])
data["adresse_complete"] = ", ".join(parties_adresse)
else:
data["adresse"] = ""
data["complement"] = ""
data["code_postal"] = ""
data["ville"] = ""
data["region"] = ""
data["pays"] = ""
data["adresse_complete"] = ""
except Exception as e:
logger.debug(f"Erreur adresse fournisseur {numero}: {e}")
data["adresse"] = ""
data["complement"] = ""
data["code_postal"] = ""
data["ville"] = ""
data["region"] = ""
data["pays"] = ""
data["adresse_complete"] = ""
try:
telecom_obj = getattr(fourn_obj, "Telecom", None)
if telecom_obj:
data["telephone"] = getattr(telecom_obj, "Telephone", "").strip()
data["portable"] = getattr(telecom_obj, "Portable", "").strip()
data["telecopie"] = getattr(telecom_obj, "Telecopie", "").strip()
data["email"] = getattr(telecom_obj, "EMail", "").strip()
try:
site = (
getattr(telecom_obj, "Site", None)
or getattr(telecom_obj, "Web", None)
or getattr(telecom_obj, "SiteWeb", "")
)
data["site_web"] = str(site).strip() if site else ""
except:
data["site_web"] = ""
else:
data["telephone"] = ""
data["portable"] = ""
data["telecopie"] = ""
data["email"] = ""
data["site_web"] = ""
except Exception as e:
logger.debug(f"Erreur telecom fournisseur {numero}: {e}")
data["telephone"] = ""
data["portable"] = ""
data["telecopie"] = ""
data["email"] = ""
data["site_web"] = ""
try:
data["siret"] = getattr(fourn_obj, "CT_Siret", "").strip()
except:
data["siret"] = ""
try:
if data["siret"] and len(data["siret"]) >= 9:
data["siren"] = data["siret"][:9]
else:
data["siren"] = getattr(fourn_obj, "CT_Siren", "").strip()
except:
data["siren"] = ""
try:
data["tva_intra"] = getattr(fourn_obj, "CT_Identifiant", "").strip()
except:
data["tva_intra"] = ""
try:
data["code_naf"] = (
getattr(fourn_obj, "CT_CodeNAF", "").strip()
or getattr(fourn_obj, "CT_APE", "").strip()
)
except:
data["code_naf"] = ""
try:
data["forme_juridique"] = getattr(
fourn_obj, "CT_FormeJuridique", ""
).strip()
except:
data["forme_juridique"] = ""
try:
cat_tarif = getattr(fourn_obj, "N_CatTarif", None)
data["categorie_tarifaire"] = (
int(cat_tarif) if cat_tarif is not None else None
)
except:
data["categorie_tarifaire"] = None
try:
cat_compta = getattr(fourn_obj, "N_CatCompta", None)
data["categorie_comptable"] = (
int(cat_compta) if cat_compta is not None else None
)
except:
data["categorie_comptable"] = None
try:
cond_regl = getattr(fourn_obj, "CT_CondRegl", "").strip()
data["conditions_reglement_code"] = cond_regl
if cond_regl:
try:
cond_obj = getattr(fourn_obj, "ConditionReglement", None)
if cond_obj:
cond_obj.Read()
data["conditions_reglement_libelle"] = getattr(
cond_obj, "C_Intitule", ""
).strip()
else:
data["conditions_reglement_libelle"] = ""
except:
data["conditions_reglement_libelle"] = ""
else:
data["conditions_reglement_libelle"] = ""
except:
data["conditions_reglement_code"] = ""
data["conditions_reglement_libelle"] = ""
try:
mode_regl = getattr(fourn_obj, "CT_ModeRegl", "").strip()
data["mode_reglement_code"] = mode_regl
if mode_regl:
try:
mode_obj = getattr(fourn_obj, "ModeReglement", None)
if mode_obj:
mode_obj.Read()
data["mode_reglement_libelle"] = getattr(
mode_obj, "M_Intitule", ""
).strip()
else:
data["mode_reglement_libelle"] = ""
except:
data["mode_reglement_libelle"] = ""
else:
data["mode_reglement_libelle"] = ""
except:
data["mode_reglement_code"] = ""
data["mode_reglement_libelle"] = ""
data["coordonnees_bancaires"] = []
try:
factory_banque = getattr(fourn_obj, "FactoryBanque", None)
if factory_banque:
index = 1
while index <= 5: # Max 5 comptes bancaires
try:
banque_persist = factory_banque.List(index)
if banque_persist is None:
break
banque = win32com.client.CastTo(
banque_persist, "IBOBanque3"
)
banque.Read()
compte_bancaire = {
"banque_nom": getattr(
banque, "BI_Intitule", ""
).strip(),
"iban": getattr(banque, "RIB_Iban", "").strip(),
"bic": getattr(banque, "RIB_Bic", "").strip(),
"code_banque": getattr(
banque, "RIB_Banque", ""
).strip(),
"code_guichet": getattr(
banque, "RIB_Guichet", ""
).strip(),
"numero_compte": getattr(
banque, "RIB_Compte", ""
).strip(),
"cle_rib": getattr(banque, "RIB_Cle", "").strip(),
}
if (
compte_bancaire["iban"]
or compte_bancaire["numero_compte"]
):
data["coordonnees_bancaires"].append(compte_bancaire)
index += 1
except:
break
except Exception as e:
logger.debug(
f"Erreur coordonnées bancaires fournisseur {numero}: {e}"
)
if data["coordonnees_bancaires"]:
data["iban_principal"] = data["coordonnees_bancaires"][0].get(
"iban", ""
)
data["bic_principal"] = data["coordonnees_bancaires"][0].get("bic", "")
else:
data["iban_principal"] = ""
data["bic_principal"] = ""
data["contacts"] = []
try:
factory_contact = getattr(fourn_obj, "FactoryContact", None)
if factory_contact:
index = 1
while index <= 20: # Max 20 contacts
try:
contact_persist = factory_contact.List(index)
if contact_persist is None:
break
contact = win32com.client.CastTo(
contact_persist, "IBOContact3"
)
contact.Read()
contact_data = {
"nom": getattr(contact, "CO_Nom", "").strip(),
"prenom": getattr(contact, "CO_Prenom", "").strip(),
"fonction": getattr(contact, "CO_Fonction", "").strip(),
"service": getattr(contact, "CO_Service", "").strip(),
"telephone": getattr(
contact, "CO_Telephone", ""
).strip(),
"portable": getattr(contact, "CO_Portable", "").strip(),
"email": getattr(contact, "CO_EMail", "").strip(),
}
nom_complet = f"{contact_data['prenom']} {contact_data['nom']}".strip()
if nom_complet:
contact_data["nom_complet"] = nom_complet
else:
contact_data["nom_complet"] = contact_data["nom"]
if contact_data["nom"]:
data["contacts"].append(contact_data)
index += 1
except:
break
except Exception as e:
logger.debug(f"Erreur contacts fournisseur {numero}: {e}")
data["nb_contacts"] = len(data["contacts"])
if data["contacts"]:
data["contact_principal"] = data["contacts"][0]
else:
data["contact_principal"] = None
try:
data["encours_autorise"] = float(getattr(fourn_obj, "CT_Encours", 0.0))
except:
data["encours_autorise"] = 0.0
try:
data["ca_annuel"] = float(getattr(fourn_obj, "CT_ChiffreAffaire", 0.0))
except:
data["ca_annuel"] = 0.0
try:
data["compte_general"] = getattr(fourn_obj, "CG_Num", "").strip()
except:
data["compte_general"] = ""
try:
date_creation = getattr(fourn_obj, "CT_DateCreate", None)
data["date_creation"] = str(date_creation) if date_creation else ""
except:
data["date_creation"] = ""
try:
date_modif = getattr(fourn_obj, "CT_DateModif", None)
data["date_modification"] = str(date_modif) if date_modif else ""
except:
data["date_modification"] = ""
return data
except Exception as e:
logger.error(f" Erreur extraction fournisseur: {e}", exc_info=True)
return {
"numero": getattr(fourn_obj, "CT_Num", "").strip(),
"intitule": getattr(fourn_obj, "CT_Intitule", "").strip(),
"type": 1,
"est_fournisseur": True,
"est_actif": True,
"en_sommeil": False,
"adresse": "",
"complement": "",
"code_postal": "",
"ville": "",
"region": "",
"pays": "",
"adresse_complete": "",
"telephone": "",
"portable": "",
"telecopie": "",
"email": "",
"site_web": "",
"siret": "",
"siren": "",
"tva_intra": "",
"code_naf": "",
"forme_juridique": "",
"categorie_tarifaire": None,
"categorie_comptable": None,
"conditions_reglement_code": "",
"conditions_reglement_libelle": "",
"mode_reglement_code": "",
"mode_reglement_libelle": "",
"iban_principal": "",
"bic_principal": "",
"coordonnees_bancaires": [],
"contacts": [],
"nb_contacts": 0,
"contact_principal": None,
"encours_autorise": 0.0,
"ca_annuel": 0.0,
"compte_general": "",
"date_creation": "",
"date_modification": "",
}
def normaliser_date(self, valeur):
if isinstance(valeur, str):
try:
return datetime.fromisoformat(valeur)
except ValueError:
return datetime.now()
elif isinstance(valeur, date):
return datetime.combine(valeur, datetime.min.time())
elif isinstance(valeur, datetime):
return valeur
else:
return datetime.now()
def creer_devis_enrichi(self, devis_data: dict, forcer_brouillon: bool = False):
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(
f" Début création devis pour client {devis_data['client']['code']} "
f"(brouillon={forcer_brouillon})"
)
try:
with self._com_context(), self._lock_com:
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug(" Transaction Sage démarrée")
except Exception as e:
logger.warning(f"BeginTrans échoué: {e}")
try:
process = self.cial.CreateProcess_Document(0)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
logger.info(" Document devis créé")
doc.DO_Date = pywintypes.Time(
self.normaliser_date(devis_data.get("date_devis"))
)
if "date_livraison" in devis_data and devis_data["date_livraison"]:
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(devis_data["date_livraison"])
)
factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(
devis_data["client"]["code"]
)
if not persist_client:
raise ValueError(
f" Client {devis_data['client']['code']} introuvable"
)
client_obj = self._cast_client(persist_client)
if not client_obj:
raise ValueError(
f" Impossible de charger le client {devis_data['client']['code']}"
)
doc.SetDefaultClient(client_obj)
logger.info(f" Client {devis_data['client']['code']} associé")
if forcer_brouillon:
doc.DO_Statut = 0
logger.info(" Statut défini: 0 (Brouillon)")
else:
doc.DO_Statut = 2
logger.info(" Statut défini: 2 (Accepté)")
doc.Write()
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
logger.info(f" Ajout de {len(devis_data['lignes'])} lignes...")
for idx, ligne_data in enumerate(devis_data["lignes"], 1):
logger.debug(
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f" Article {ligne_data['article_code']} introuvable"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
designation_sage = getattr(article_obj, "AR_Design", "")
if prix_sage == 0:
logger.warning(
f"Article {ligne_data['article_code']} a un prix = 0€"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Design = (
designation_sage
or ligne_data.get("designation", "")
)
ligne_obj.DL_Qte = quantite
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
if prix_a_utiliser is not None and prix_a_utiliser > 0:
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
elif prix_auto == 0:
if prix_sage == 0:
raise ValueError(
f"Prix nul pour article {ligne_data['article_code']}"
)
ligne_obj.DL_PrixUnitaire = float(prix_sage)
remise = ligne_data.get("remise_pourcentage", 0)
if remise > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(remise)
ligne_obj.DL_Remise01REM_Type = 0
except Exception as e:
logger.warning(f"Remise non appliquée: {e}")
ligne_obj.Write()
logger.info(f" {len(devis_data['lignes'])} lignes écrites")
doc.Write()
if not forcer_brouillon:
logger.info(" Lancement Process()...")
process.Process()
else:
try:
process.Process()
logger.info(" Process() appelé (brouillon)")
except:
logger.debug("Process() ignoré pour brouillon")
numero_devis = self._recuperer_numero_devis(process, doc)
if not numero_devis:
raise RuntimeError(" Numéro devis vide après création")
logger.info(f" Numéro: {numero_devis}")
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.info(" Transaction committée")
except:
pass
import time
time.sleep(0.5)
if "reference" in devis_data and devis_data["reference"]:
try:
logger.info(
f" Application de la référence: {devis_data['reference']}"
)
doc_reload = self._charger_devis(numero_devis)
nouvelle_reference = devis_data["reference"]
doc_reload.DO_Ref = (
str(nouvelle_reference) if nouvelle_reference else ""
)
doc_reload.Write()
time.sleep(0.5)
doc_reload.Read()
logger.info(f" Référence définie: {nouvelle_reference}")
except Exception as e:
logger.warning(
f"Impossible de définir la référence: {e}",
exc_info=True,
)
time.sleep(0.5)
doc_final_data = self._relire_devis(
numero_devis, devis_data, forcer_brouillon
)
logger.info(
f" DEVIS CRÉÉ: {numero_devis} - {doc_final_data['total_ttc']}€ TTC "
)
return doc_final_data
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.error(" Transaction annulée (rollback)")
except:
pass
raise
except Exception as e:
logger.error(f" ERREUR CRÉATION DEVIS: {e}", exc_info=True)
raise RuntimeError(f"Échec création devis: {str(e)}")
def _recuperer_numero_devis(self, process, doc):
"""Récupère le numéro du devis créé via plusieurs méthodes."""
numero_devis = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(doc_result, "IBODocumentVente3")
doc_result.Read()
numero_devis = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_devis:
numero_devis = getattr(doc, "DO_Piece", "")
if not numero_devis:
try:
doc.SetDefaultNumPiece()
doc.Write()
doc.Read()
numero_devis = getattr(doc, "DO_Piece", "")
except:
pass
return numero_devis
def _relire_devis(self, numero_devis, devis_data, forcer_brouillon):
"""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 = self._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:
pass
else:
total_calcule = sum(
l.get("montant_ligne_ht", 0) for l in devis_data["lignes"]
)
total_ht = total_calcule
total_ttc = round(total_calcule * 1.20, 2)
statut_final = 0 if forcer_brouillon else 2
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 _rechercher_devis_dans_liste(self, numero_devis, factory_doc):
"""Recherche un devis dans les 100 premiers éléments de la liste."""
index = 1
while index < 100:
try:
persist_test = factory_doc.List(index)
if persist_test is None:
break
doc_test = win32com.client.CastTo(persist_test, "IBODocumentVente3")
doc_test.Read()
if (
getattr(doc_test, "DO_Type", -1) == 0
and getattr(doc_test, "DO_Piece", "") == numero_devis
):
logger.info(f" Document trouvé à l'index {index}")
return persist_test
index += 1
except:
index += 1
return None
def modifier_devis(self, numero: str, devis_data: Dict) -> Dict:
"""
Modifie un devis existant dans Sage - VERSION COMPLÈTE.
Args:
numero: Numéro du devis
devis_data: dict contenant les champs à modifier:
- date_devis: str ou date (optionnel)
- date_livraison: str ou date (optionnel)
- reference: str (optionnel)
- statut: int (optionnel)
- lignes: list[dict] (optionnel)
"""
logger.info("=" * 100)
logger.info("=" * 100)
logger.info(f" NOUVELLE MÉTHODE modifier_devis() APPELÉE POUR {numero} ")
logger.info(f" Données reçues: {devis_data}")
logger.info("=" * 100)
if not self.cial:
logger.error(" Connexion Sage non établie")
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info("")
logger.info("=" * 80)
logger.info(f" [ÉTAPE 1] CHARGEMENT DU DEVIS {numero}")
logger.info("=" * 80)
doc = self._charger_devis(numero)
logger.info(f" Devis {numero} chargé avec succès")
logger.info("")
self._afficher_etat_document(doc, "📸 ÉTAT INITIAL")
logger.info(" Vérification statut transformation...")
self._verifier_devis_non_transforme(numero, doc)
logger.info(" Devis non transformé - modification autorisée")
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 2] ANALYSE DOCUMENT ACTUEL")
logger.info("=" * 80)
client_code_initial = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code_initial = getattr(client_obj, "CT_Num", "").strip()
logger.info(f" Client: {client_code_initial}")
else:
logger.warning(" Objet Client non trouvé")
except Exception as e:
logger.warning(f" Impossible de lire le client: {e}")
nb_lignes_initial = self._compter_lignes_document(doc)
logger.info(f" Nombre de lignes actuelles: {nb_lignes_initial}")
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 3] ANALYSE MODIFICATIONS DEMANDÉES")
logger.info("=" * 80)
modif_date = "date_devis" in devis_data
modif_date_livraison = "date_livraison" in devis_data
modif_statut = "statut" in devis_data
modif_ref = "reference" in devis_data
modif_lignes = "lignes" in devis_data and devis_data["lignes"] is not None
logger.info(f" Date devis: {modif_date}")
if modif_date:
logger.info(f" → Valeur: {devis_data['date_devis']}")
logger.info(f" Date livraison: {modif_date_livraison}")
if modif_date_livraison:
logger.info(f" → Valeur: {devis_data['date_livraison']}")
logger.info(f" Référence: {modif_ref}")
if modif_ref:
logger.info(f" → Valeur: '{devis_data['reference']}'")
logger.info(f" Statut: {modif_statut}")
if modif_statut:
logger.info(f" → Valeur: {devis_data['statut']}")
logger.info(f" Lignes: {modif_lignes}")
if modif_lignes:
logger.info(f" → Nombre: {len(devis_data['lignes'])}")
for i, ligne in enumerate(devis_data['lignes'], 1):
logger.info(f" → Ligne {i}: {ligne.get('article_code')} (Qté: {ligne.get('quantite')})")
devis_data_temp = devis_data.copy()
reference_a_modifier = None
statut_a_modifier = None
if modif_lignes:
logger.info("")
logger.info(" STRATÉGIE: Report référence/statut APRÈS modification lignes")
if modif_ref:
reference_a_modifier = devis_data_temp.pop("reference")
logger.info(f" Référence '{reference_a_modifier}' reportée")
modif_ref = False
if modif_statut:
statut_a_modifier = devis_data_temp.pop("statut")
logger.info(f" Statut {statut_a_modifier} reporté")
modif_statut = False
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 4] TEST WRITE() BASIQUE")
logger.info("=" * 80)
logger.info("Test sans modification pour vérifier le verrouillage...")
try:
doc.Write()
logger.info(" Write() basique OK - Document NON verrouillé")
time.sleep(0.3)
doc.Read()
logger.info(" Read() après Write() OK")
except Exception as e:
logger.error(f" Write() basique ÉCHOUE: {e}")
logger.error(" ABANDON: Document VERROUILLÉ ou problème COM")
raise ValueError(f"Document verrouillé: {e}")
champs_modifies = []
if not modif_lignes and (modif_date or modif_date_livraison or modif_statut or modif_ref):
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 5A] MODIFICATIONS SIMPLES (sans lignes)")
logger.info("=" * 80)
if modif_date:
logger.info("")
logger.info(" Modification DATE_DEVIS...")
try:
ancienne_date = getattr(doc, "DO_Date", None)
ancienne_date_str = ancienne_date.strftime("%Y-%m-%d") if ancienne_date else "None"
logger.info(f" Actuelle: {ancienne_date_str}")
nouvelle_date = self.normaliser_date(devis_data_temp["date_devis"])
nouvelle_date_str = nouvelle_date.strftime("%Y-%m-%d")
logger.info(f" Cible: {nouvelle_date_str}")
doc.DO_Date = pywintypes.Time(nouvelle_date)
logger.info(" ✓ doc.DO_Date affecté")
champs_modifies.append("date_devis")
logger.info(f" Date devis sera modifiée: {ancienne_date_str}{nouvelle_date_str}")
except Exception as e:
logger.error(f" Erreur date devis: {e}", exc_info=True)
if modif_date_livraison:
logger.info("")
logger.info(" Modification DATE_LIVRAISON...")
try:
ancienne_date_livr = getattr(doc, "DO_DateLivr", None)
ancienne_date_livr_str = ancienne_date_livr.strftime("%Y-%m-%d") if ancienne_date_livr else "None"
logger.info(f" Actuelle: {ancienne_date_livr_str}")
if devis_data_temp["date_livraison"]:
nouvelle_date_livr = self.normaliser_date(devis_data_temp["date_livraison"])
nouvelle_date_livr_str = nouvelle_date_livr.strftime("%Y-%m-%d")
logger.info(f" Cible: {nouvelle_date_livr_str}")
doc.DO_DateLivr = pywintypes.Time(nouvelle_date_livr)
logger.info(" ✓ doc.DO_DateLivr affecté")
else:
logger.info(" Cible: Effacement (None)")
doc.DO_DateLivr = None
logger.info(" ✓ doc.DO_DateLivr = None")
champs_modifies.append("date_livraison")
logger.info(" Date livraison sera modifiée")
except Exception as e:
logger.error(f" Erreur date livraison: {e}", exc_info=True)
if modif_ref:
logger.info("")
logger.info(" Modification RÉFÉRENCE...")
try:
ancienne_ref = getattr(doc, "DO_Ref", "")
logger.info(f" Actuelle: '{ancienne_ref}'")
nouvelle_ref = str(devis_data_temp["reference"]) if devis_data_temp["reference"] else ""
logger.info(f" Cible: '{nouvelle_ref}'")
doc.DO_Ref = nouvelle_ref
logger.info(" ✓ doc.DO_Ref affecté")
champs_modifies.append("reference")
logger.info(f" Référence sera modifiée: '{ancienne_ref}''{nouvelle_ref}'")
except Exception as e:
logger.error(f" Erreur référence: {e}", exc_info=True)
if modif_statut:
logger.info("")
logger.info(" Modification STATUT...")
try:
statut_actuel = getattr(doc, "DO_Statut", 0)
logger.info(f" Actuel: {statut_actuel}")
nouveau_statut = int(devis_data_temp["statut"])
logger.info(f" Cible: {nouveau_statut}")
if nouveau_statut in [0, 1, 2, 3]:
doc.DO_Statut = nouveau_statut
logger.info(" ✓ doc.DO_Statut affecté")
champs_modifies.append("statut")
logger.info(f" Statut sera modifié: {statut_actuel}{nouveau_statut}")
else:
logger.warning(f" Statut {nouveau_statut} invalide (doit être 0,1,2 ou 3)")
except Exception as e:
logger.error(f" Erreur statut: {e}", exc_info=True)
logger.info("")
logger.info(" Write() modifications simples...")
try:
doc.Write()
logger.info(" Write() réussi")
time.sleep(0.5)
doc.Read()
logger.info(" Read() après Write() OK")
except Exception as e:
logger.error(f" Write() a échoué: {e}", exc_info=True)
raise
elif modif_lignes:
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 5B] REMPLACEMENT COMPLET DES LIGNES")
logger.info("=" * 80)
if modif_date:
logger.info(" Modification date devis (avant lignes)...")
try:
nouvelle_date = self.normaliser_date(devis_data_temp["date_devis"])
doc.DO_Date = pywintypes.Time(nouvelle_date)
champs_modifies.append("date_devis")
logger.info(f" Date: {nouvelle_date.strftime('%Y-%m-%d')}")
except Exception as e:
logger.error(f" Erreur: {e}")
if modif_date_livraison:
logger.info(" Modification date livraison (avant lignes)...")
try:
if devis_data_temp["date_livraison"]:
nouvelle_date_livr = self.normaliser_date(devis_data_temp["date_livraison"])
doc.DO_DateLivr = pywintypes.Time(nouvelle_date_livr)
logger.info(f" Date livraison: {nouvelle_date_livr.strftime('%Y-%m-%d')}")
else:
doc.DO_DateLivr = None
logger.info(" Date livraison effacée")
champs_modifies.append("date_livraison")
except Exception as e:
logger.error(f" Erreur: {e}")
nouvelles_lignes = devis_data["lignes"]
nb_nouvelles = len(nouvelles_lignes)
logger.info("")
logger.info(f" Remplacement: {nb_lignes_initial} lignes → {nb_nouvelles} lignes")
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
if nb_lignes_initial > 0:
logger.info("")
logger.info(f" Suppression de {nb_lignes_initial} lignes existantes...")
for idx in range(nb_lignes_initial, 0, -1):
try:
ligne_p = factory_lignes.List(idx)
if ligne_p:
try:
ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3")
except:
ligne = win32com.client.CastTo(ligne_p, "IBODocumentVenteLigne3")
ligne.Read()
ligne.Remove()
logger.debug(f" Ligne {idx} supprimée")
except Exception as e:
logger.warning(f" Ligne {idx} non supprimée: {e}")
logger.info(f" {nb_lignes_initial} lignes supprimées")
logger.info("")
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
article_code = ligne_data["article_code"]
quantite = float(ligne_data["quantite"])
logger.info("")
logger.info(f" Ligne {idx}/{nb_nouvelles}: {article_code}")
logger.info(f" Quantité: {quantite}")
if ligne_data.get("prix_unitaire_ht"):
logger.info(f" Prix HT: {ligne_data['prix_unitaire_ht']}")
if ligne_data.get("remise_pourcentage"):
logger.info(f" Remise: {ligne_data['remise_pourcentage']}%")
try:
persist_article = factory_article.ReadReference(article_code)
if not persist_article:
raise ValueError(f"Article {article_code} INTROUVABLE")
article_obj = win32com.client.CastTo(persist_article, "IBOArticle3")
article_obj.Read()
logger.info(f" ✓ Article chargé")
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3")
except:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3")
try:
ligne_obj.SetDefaultArticleReference(article_code, quantite)
logger.info(f" ✓ Article associé via SetDefaultArticleReference")
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(f" ✓ Article associé via SetDefaultArticle")
except:
ligne_obj.DL_Design = ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
logger.info(f" ✓ Article associé manuellement")
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"])
logger.info(f" ✓ Prix unitaire défini")
if ligne_data.get("remise_pourcentage", 0) > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(ligne_data["remise_pourcentage"])
ligne_obj.DL_Remise01REM_Type = 0
logger.info(f" ✓ Remise définie")
except:
logger.debug(f" Remise non supportée")
ligne_obj.Write()
logger.info(f" Ligne {idx} créée avec succès")
except Exception as e:
logger.error(f" ERREUR ligne {idx}: {e}", exc_info=True)
raise
logger.info("")
logger.info(f" {nb_nouvelles} lignes créées")
logger.info("")
logger.info(" Write() après remplacement lignes...")
try:
doc.Write()
logger.info(" Write() réussi")
time.sleep(0.5)
doc.Read()
logger.info(" Read() après Write() OK")
except Exception as e:
logger.error(f" Write() a échoué: {e}", exc_info=True)
raise
champs_modifies.append("lignes")
if reference_a_modifier is not None:
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 6] MODIFICATION RÉFÉRENCE (après lignes)")
logger.info("=" * 80)
try:
ancienne_ref = getattr(doc, "DO_Ref", "")
nouvelle_ref = str(reference_a_modifier) if reference_a_modifier else ""
logger.info(f" Actuelle: '{ancienne_ref}'")
logger.info(f" Cible: '{nouvelle_ref}'")
doc.DO_Ref = nouvelle_ref
logger.info(" ✓ doc.DO_Ref affecté")
doc.Write()
logger.info(" ✓ Write()")
time.sleep(0.5)
doc.Read()
logger.info(" ✓ Read()")
champs_modifies.append("reference")
logger.info(f" Référence modifiée: '{ancienne_ref}''{nouvelle_ref}'")
except Exception as e:
logger.error(f" Erreur référence: {e}", exc_info=True)
if statut_a_modifier is not None:
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 7] MODIFICATION STATUT (en dernier)")
logger.info("=" * 80)
try:
statut_actuel = getattr(doc, "DO_Statut", 0)
nouveau_statut = int(statut_a_modifier)
logger.info(f" Actuel: {statut_actuel}")
logger.info(f" Cible: {nouveau_statut}")
if nouveau_statut != statut_actuel and nouveau_statut in [0, 1, 2, 3]:
doc.DO_Statut = nouveau_statut
logger.info(" ✓ doc.DO_Statut affecté")
doc.Write()
logger.info(" ✓ Write()")
time.sleep(0.5)
doc.Read()
logger.info(" ✓ Read()")
champs_modifies.append("statut")
logger.info(f" Statut modifié: {statut_actuel}{nouveau_statut}")
else:
logger.info(f" Pas de modification (identique ou invalide)")
except Exception as e:
logger.error(f" Erreur statut: {e}", exc_info=True)
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 8] VALIDATION FINALE")
logger.info("=" * 80)
try:
doc.Write()
logger.info(" Write() final")
except Exception as e:
logger.warning(f" Write() final: {e}")
time.sleep(0.5)
doc.Read()
logger.info(" Read() final")
logger.info("")
self._afficher_etat_document(doc, "📸 ÉTAT FINAL")
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 9] EXTRACTION RÉSULTAT")
logger.info("=" * 80)
resultat = self._extraire_infos_devis(doc, numero, champs_modifies)
logger.info(f" Résultat extrait:")
logger.info(f" Numéro: {resultat['numero']}")
logger.info(f" Référence: '{resultat['reference']}'")
logger.info(f" Date devis: {resultat['date_devis']}")
logger.info(f" Date livraison: {resultat['date_livraison']}")
logger.info(f" Statut: {resultat['statut']}")
logger.info(f" Total HT: {resultat['total_ht']}")
logger.info(f" Total TTC: {resultat['total_ttc']}")
logger.info(f" Champs modifiés: {resultat['champs_modifies']}")
logger.info("")
logger.info("=" * 100)
logger.info(f" MODIFICATION DEVIS {numero} TERMINÉE AVEC SUCCÈS ")
logger.info("=" * 100)
return resultat
except ValueError as e:
logger.error(f" ERREUR MÉTIER: {e}")
raise
except Exception as e:
logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True)
raise RuntimeError(f"Erreur technique Sage: {str(e)}")
def _afficher_etat_document(self, doc, titre: str):
"""Affiche l'état complet d'un document."""
logger.info("-" * 80)
logger.info(titre)
logger.info("-" * 80)
try:
logger.info(f" DO_Piece: {getattr(doc, 'DO_Piece', 'N/A')}")
logger.info(f" DO_Ref: '{getattr(doc, 'DO_Ref', 'N/A')}'")
logger.info(f" DO_Statut: {getattr(doc, 'DO_Statut', 'N/A')}")
date_doc = getattr(doc, 'DO_Date', None)
date_str = date_doc.strftime('%Y-%m-%d') if date_doc else 'None'
logger.info(f" DO_Date: {date_str}")
date_livr = getattr(doc, 'DO_DateLivr', None)
date_livr_str = date_livr.strftime('%Y-%m-%d') if date_livr else 'None'
logger.info(f" DO_DateLivr: {date_livr_str}")
logger.info(f" DO_TotalHT: {getattr(doc, 'DO_TotalHT', 0)}")
logger.info(f" DO_TotalTTC: {getattr(doc, 'DO_TotalTTC', 0)}")
except Exception as e:
logger.error(f" Erreur affichage état: {e}")
logger.info("-" * 80)
def _compter_lignes_document(self, doc) -> int:
"""Compte les lignes d'un document."""
try:
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
count = 0
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
count += 1
index += 1
except:
break
return count
except Exception as e:
logger.warning(f" Erreur comptage lignes: {e}")
return 0
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 = self._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 _rechercher_devis_par_numero(self, numero: str, factory):
"""Recherche un devis par numéro dans la liste."""
logger.info(f" Recherche de {numero} dans la liste...")
index = 1
while index < 10000:
try:
persist_test = factory.List(index)
if persist_test is None:
break
doc_test = win32com.client.CastTo(persist_test, "IBODocumentVente3")
doc_test.Read()
if (
getattr(doc_test, "DO_Type", -1) == 0
and getattr(doc_test, "DO_Piece", "") == numero
):
logger.info(f" Trouvé à l'index {index}")
return persist_test
index += 1
except:
index += 1
logger.error(f" Devis {numero} non trouvé dans la liste")
return None
def _verifier_devis_non_transforme(self, numero: str, doc):
"""Vérifie que le devis n'est pas transformé."""
verification = self.verifier_si_deja_transforme_sql(numero, 0)
if verification["deja_transforme"]:
docs_cibles = verification["documents_cibles"]
nums = [d["numero"] for d in docs_cibles]
raise ValueError(
f" Devis {numero} déjà transformé en {len(docs_cibles)} document(s): {', '.join(nums)}"
)
statut_actuel = getattr(doc, "DO_Statut", 0)
if statut_actuel == 5:
raise ValueError(f" Devis {numero} déjà transformé (statut=5)")
def _extraire_infos_devis(self, doc, numero: str, champs_modifies: list) -> Dict:
"""Extrait les informations complètes du devis."""
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
statut = getattr(doc, "DO_Statut", 0)
reference = getattr(doc, "DO_Ref", "")
date_devis = None
try:
date_doc = getattr(doc, "DO_Date", None)
if date_doc:
date_devis = date_doc.strftime("%Y-%m-%d")
except:
pass
date_livraison = None
try:
date_livr = getattr(doc, "DO_DateLivr", None)
if date_livr:
date_livraison = date_livr.strftime("%Y-%m-%d")
except:
pass
client_code = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code = getattr(client_obj, "CT_Num", "")
except:
pass
return {
"numero": numero,
"total_ht": total_ht,
"total_ttc": total_ttc,
"reference": reference,
"date_devis": date_devis,
"date_livraison": date_livraison,
"champs_modifies": champs_modifies,
"statut": statut,
"client_code": client_code,
}
def lire_devis(self, numero_devis):
try:
devis = self._lire_document_sql(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):
return self._lire_document_sql(numero, type_doc)
def verifier_si_deja_transforme_sql(self, numero_source, type_source):
"""Version corrigée avec normalisation des types"""
logger.info(
f"[VERIF] Vérification transformations de {numero_source} (type {type_source})"
)
logger.info(
f"[VERIF] Vérification transformations de {numero_source} (type {type_source})"
)
logger.info(f"[DEBUG] Type source brut: {type_source}")
logger.info(
f"[DEBUG] Type source après normalisation: {self._normaliser_type_document(type_source)}"
)
logger.info(
f"[DEBUG] Type source après normalisation SQL: {self._convertir_type_pour_sql(type_source)}"
)
type_source = self._convertir_type_pour_sql(type_source)
champ_liaison_mapping = {
0: "DL_PieceDE",
1: "DL_PieceBC",
3: "DL_PieceBL",
}
champ_liaison = champ_liaison_mapping.get(type_source)
if not champ_liaison:
logger.warning(f"[VERIF] Type source {type_source} non géré")
return {"deja_transforme": False, "documents_cibles": []}
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = f"""
SELECT DISTINCT
dc.DO_Piece,
dc.DO_Type,
dc.DO_Statut,
(SELECT COUNT(*) FROM F_DOCLIGNE
WHERE DO_Piece = dc.DO_Piece AND DO_Type = dc.DO_Type) as NbLignes
FROM F_DOCENTETE dc
INNER JOIN F_DOCLIGNE dl ON dc.DO_Piece = dl.DO_Piece AND dc.DO_Type = dl.DO_Type
WHERE dl.{champ_liaison} = ?
ORDER BY dc.DO_Type, dc.DO_Piece
"""
cursor.execute(query, (numero_source,))
resultats = cursor.fetchall()
documents_cibles = []
for row in resultats:
type_brut = int(row.DO_Type)
type_normalise = self._convertir_type_depuis_sql(type_brut)
doc = {
"numero": row.DO_Piece.strip() if row.DO_Piece else "",
"type": type_normalise, # ← TYPE NORMALISÉ
"type_brut": type_brut, # Garder aussi le type original
"type_libelle": self._get_type_libelle(type_brut),
"statut": int(row.DO_Statut) if row.DO_Statut else 0,
"nb_lignes": int(row.NbLignes) if row.NbLignes else 0,
}
documents_cibles.append(doc)
logger.info(
f"[VERIF] Trouvé: {doc['numero']} "
f"(type {type_brut}{type_normalise} - {doc['type_libelle']}) "
f"- {doc['nb_lignes']} lignes"
)
deja_transforme = len(documents_cibles) > 0
if deja_transforme:
logger.info(
f"[VERIF] Document {numero_source} a {len(documents_cibles)} transformation(s)"
)
else:
logger.info(
f"[VERIF] Document {numero_source} pas encore transformé"
)
return {
"deja_transforme": deja_transforme,
"documents_cibles": documents_cibles,
}
except Exception as e:
logger.error(f"[VERIF] Erreur vérification: {e}")
return {"deja_transforme": False, "documents_cibles": []}
def peut_etre_transforme(self, numero_source, type_source, type_cible):
"""Version corrigée avec normalisation"""
type_source = self._normaliser_type_document(type_source)
type_cible = self._normaliser_type_document(type_cible)
logger.info(
f"[VERIF_TRANSFO] {numero_source} "
f"(type {type_source}) → type {type_cible}"
)
verif = self.verifier_si_deja_transforme_sql(numero_source, type_source)
docs_meme_type = [
d for d in verif["documents_cibles"] if d["type"] == type_cible
]
if docs_meme_type:
nums = [d["numero"] for d in docs_meme_type]
return {
"possible": False,
"raison": f"Document déjà transformé en {self._get_type_libelle(type_cible)}",
"documents_existants": docs_meme_type,
"message_detaille": f"Document(s) existant(s): {', '.join(nums)}",
}
return {
"possible": True,
"raison": "Transformation possible",
"documents_existants": [],
}
def _get_type_libelle(self, type_doc: int) -> str:
"""
Retourne le libellé d'un type de document.
Gère les 2 formats : valeur réelle Sage (0,10,20...) ET valeur affichée (0,1,2...)
"""
types_officiels = {
0: "Devis",
10: "Bon de commande",
20: "Préparation",
30: "Bon de livraison",
40: "Bon de retour",
50: "Bon d'avoir",
60: "Facture",
}
types_alternatifs = {
1: "Bon de commande",
2: "Préparation",
3: "Bon de livraison",
4: "Bon de retour",
5: "Bon d'avoir",
6: "Facture",
}
if type_doc in types_officiels:
return types_officiels[type_doc]
if type_doc in types_alternatifs:
return types_alternatifs[type_doc]
return f"Type {type_doc}"
def _normaliser_type_document(self, type_doc: int) -> int:
"""
Normalise le type de document vers la valeur officielle Sage.
Convertit 1→10, 2→20, etc. si nécessaire
"""
logger.info(f"[INFO] TYPE RECU{type_doc}")
if type_doc in [0, 10, 20, 30, 40, 50, 60]:
return type_doc
mapping_normalisation = {
1: 10, # Commande
2: 20, # Préparation
3: 30, # BL
4: 40, # Retour
5: 50, # Avoir
6: 60, # Facture
}
return mapping_normalisation.get(type_doc, type_doc)
def transformer_document(
self,
numero_source,
type_source,
type_cible,
ignorer_controle_stock=False,
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"{self._get_type_libelle(type_source)}{self._get_type_libelle(type_cible)}"
)
module, methode = transformations_valides[(type_source, type_cible)]
logger.info(f"[TRANSFORM] Méthode: Transformation.{module}.{methode}()")
if verifier_doublons:
logger.info("[TRANSFORM] Vérification des doublons...")
verif = self.peut_etre_transforme(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é")
try:
with self._com_context(), self._lock_com:
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:
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:
can_process = True
if not can_process:
erreurs = self.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:
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 = self.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:
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 as e:
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:
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 as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.error("[TRANSFORM] Transaction annulée (rollback)")
except:
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 lire_erreurs_sage(self, obj, nom_obj=""):
"""
Lit toutes les erreurs d'un objet Sage COM.
Utilisé pour diagnostiquer les échecs de Process().
"""
erreurs = []
try:
if not hasattr(obj, "Errors") or obj.Errors is None:
return erreurs
nb_erreurs = 0
try:
nb_erreurs = obj.Errors.Count
except:
return erreurs
if nb_erreurs == 0:
return erreurs
for i in range(1, nb_erreurs + 1):
try:
err = None
try:
err = obj.Errors.Item(i)
except:
try:
err = obj.Errors(i)
except:
try:
err = obj.Errors.Item(i - 1)
except:
pass
if err is not None:
description = ""
field = ""
number = ""
for attr in ["Description", "Descr", "Message", "Text"]:
try:
val = getattr(err, attr, None)
if val:
description = str(val)
break
except:
pass
for attr in ["Field", "FieldName", "Champ", "Property"]:
try:
val = getattr(err, attr, None)
if val:
field = str(val)
break
except:
pass
for attr in ["Number", "Code", "ErrorCode", "Numero"]:
try:
val = getattr(err, attr, None)
if val is not None:
number = str(val)
break
except:
pass
if description or field or number:
erreurs.append(
{
"source": nom_obj,
"index": i,
"description": description or "Erreur inconnue",
"field": field or "?",
"number": number or "?",
}
)
except Exception as e:
logger.debug(f"Erreur lecture erreur {i}: {e}")
continue
except Exception as e:
logger.debug(f"Erreur globale lecture erreurs {nom_obj}: {e}")
return erreurs
def _find_document_in_list(self, numero, type_doc):
"""Cherche un document dans List() si ReadPiece échoue"""
try:
factory = self.cial.FactoryDocumentVente
index = 1
while index < 10000:
try:
persist = factory.List(index)
if persist is None:
break
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
if (
getattr(doc, "DO_Type", -1) == type_doc
and getattr(doc, "DO_Piece", "") == numero
):
logger.info(f"[TRANSFORM] Document trouve a l'index {index}")
return persist
index += 1
except:
index += 1
continue
return None
except Exception as e:
logger.error(f"[TRANSFORM] Erreur recherche document: {e}")
return None
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 self._cast_client(persist)
except:
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 = self._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:
pass
try:
contact_info["nom"] = (
getattr(client, "CT_Contact", "")
or contact_info["client_intitule"]
)
except:
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": self._safe_strip(row.CT_Num),
"intitule": self._safe_strip(row.CT_Intitule),
"adresse": self._safe_strip(row.CT_Adresse),
"ville": self._safe_strip(row.CT_Ville),
"code_postal": self._safe_strip(row.CT_CodePostal),
"telephone": self._safe_strip(row.CT_Telephone),
"email": self._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": self._safe_strip(row.CT_Num),
"intitule": self._safe_strip(row.CT_Intitule),
"type": 0,
"qualite": self._safe_strip(row.CT_Qualite),
"est_prospect": True,
"adresse": self._safe_strip(row.CT_Adresse),
"complement": self._safe_strip(row.CT_Complement),
"ville": self._safe_strip(row.CT_Ville),
"code_postal": self._safe_strip(row.CT_CodePostal),
"pays": self._safe_strip(row.CT_Pays),
"telephone": self._safe_strip(row.CT_Telephone),
"portable": self._safe_strip(row.CT_Portable),
"email": self._safe_strip(row.CT_EMail),
"telecopie": self._safe_strip(row.CT_Telecopie),
"siret": self._safe_strip(row.CT_Siret),
"tva_intra": self._safe_strip(row.CT_Identifiant),
"est_actif": (row.CT_Sommeil == 0),
"contact": self._safe_strip(row.CT_Contact),
"forme_juridique": self._safe_strip(row.CT_FormeJuridique),
"secteur": self._safe_strip(row.CT_Secteur),
}
except Exception as e:
logger.error(f" Erreur SQL prospect {code_prospect}: {e}")
return None
def lister_avoirs(self, limit=100, statut=None):
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = f"""
SELECT TOP ({limit})
d.DO_Piece, d.DO_Date, d.DO_Ref, d.DO_TotalHT, d.DO_TotalTTC,
d.DO_Statut, d.CT_Num, c.CT_Intitule
FROM F_DOCENTETE d
LEFT JOIN F_COMPTET c ON d.CT_Num = c.CT_Num
WHERE d.DO_Type = 50
"""
params = []
if statut is not None:
query += " AND d.DO_Statut = ?"
params.append(statut)
query += " ORDER BY d.DO_Date DESC"
cursor.execute(query, params)
rows = cursor.fetchall()
avoirs = []
for row in rows:
avoirs.append(
{
"numero": self._safe_strip(row.DO_Piece),
"reference": self._safe_strip(row.DO_Ref),
"date": str(row.DO_Date) if row.DO_Date else "",
"client_code": self._safe_strip(row.CT_Num),
"client_intitule": self._safe_strip(row.CT_Intitule),
"total_ht": (
float(row.DO_TotalHT) if row.DO_TotalHT else 0.0
),
"total_ttc": (
float(row.DO_TotalTTC) if row.DO_TotalTTC else 0.0
),
"statut": row.DO_Statut if row.DO_Statut is not None else 0,
}
)
return avoirs
except Exception as e:
logger.error(f" Erreur SQL avoirs: {e}")
return []
def lire_avoir(self, numero):
return self._lire_document_sql(numero, type_doc=50)
def lister_livraisons(self, limit=100, statut=None):
""" Liste les livraisons via SQL (méthode legacy)"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = f"""
SELECT TOP ({limit})
d.DO_Piece, d.DO_Date, d.DO_Ref, d.DO_TotalHT, d.DO_TotalTTC,
d.DO_Statut, d.CT_Num, c.CT_Intitule
FROM F_DOCENTETE d
LEFT JOIN F_COMPTET c ON d.CT_Num = c.CT_Num
WHERE d.DO_Type = 30
"""
params = []
if statut is not None:
query += " AND d.DO_Statut = ?"
params.append(statut)
query += " ORDER BY d.DO_Date DESC"
cursor.execute(query, params)
rows = cursor.fetchall()
livraisons = []
for row in rows:
livraisons.append(
{
"numero": self._safe_strip(row.DO_Piece),
"reference": self._safe_strip(row.DO_Ref),
"date": str(row.DO_Date) if row.DO_Date else "",
"client_code": self._safe_strip(row.CT_Num),
"client_intitule": self._safe_strip(row.CT_Intitule),
"total_ht": (
float(row.DO_TotalHT) if row.DO_TotalHT else 0.0
),
"total_ttc": (
float(row.DO_TotalTTC) if row.DO_TotalTTC else 0.0
),
"statut": row.DO_Statut if row.DO_Statut is not None else 0,
}
)
return livraisons
except Exception as e:
logger.error(f" Erreur SQL livraisons: {e}")
return []
def lire_livraison(self, numero):
""" Lit UNE livraison via SQL (avec lignes)"""
return self._lire_document_sql(numero, type_doc=30)
def creer_contact(self, contact_data: Dict) -> Dict:
"""
Crée un nouveau contact dans F_CONTACTT via COM
VERSION FINALE COMPLÈTE
"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info("[CREATION CONTACT F_CONTACTT]")
logger.info("=" * 80)
# Validation
if not contact_data.get("numero"):
raise ValueError("numero (code client) obligatoire")
if not contact_data.get("nom"):
raise ValueError("nom obligatoire")
numero_client = self._clean_str(contact_data["numero"], 17).upper()
nom = self._clean_str(contact_data["nom"], 35)
prenom = self._clean_str(contact_data.get("prenom", ""), 35)
logger.info(f" CLIENT: {numero_client}")
logger.info(f" CONTACT: {prenom} {nom}")
# Charger le client
logger.info(f"[1] Chargement du client: {numero_client}")
factory_client = self.cial.CptaApplication.FactoryClient
try:
persist_client = factory_client.ReadNumero(numero_client)
if not persist_client:
raise ValueError(f"Client {numero_client} non trouve")
client_obj = win32com.client.CastTo(persist_client, "IBOClient3")
client_obj.Read()
logger.info(f" OK Client charge")
except Exception as e:
raise ValueError(f"Client {numero_client} introuvable: {e}")
# Via FactoryTiersContact du client
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__}")
# Créer l'objet
persist = factory_contact.Create()
logger.info(f" Objet cree: {type(persist).__name__}")
# Cast vers IBOTiersContact3
contact = None
interfaces_a_tester = [
"IBOTiersContact3",
"IBOTiersContact",
"IBOContactT3",
"IBOContactT",
]
for interface_name in interfaces_a_tester:
try:
temp = win32com.client.CastTo(persist, interface_name)
# Vérifier si Nom existe
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")
# Configuration du contact
logger.info("[3] Configuration du contact")
# Vérifier les propriétés disponibles
if hasattr(contact, '_prop_map_put_'):
props = list(contact._prop_map_put_.keys())
logger.info(f" Proprietes disponibles: {props}")
# Nom (obligatoire)
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}")
# Prénom
if prenom:
try:
contact.Prenom = prenom
logger.info(f" OK Prenom = {prenom}")
except Exception as e:
logger.warning(f" WARN Prenom: {e}")
# Civilité
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}")
# Fonction
if contact_data.get("fonction"):
fonction = self._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}")
# Service
if contact_data.get("service_code") is not None:
try:
service = self._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}")
# Telecom
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 = self._clean_str(contact_data["telephone"], 21)
if self._try_set_attribute(telecom, "Telephone", telephone):
logger.info(f" Telephone = {telephone}")
if contact_data.get("portable"):
portable = self._clean_str(contact_data["portable"], 21)
if self._try_set_attribute(telecom, "Portable", portable):
logger.info(f" Portable = {portable}")
if contact_data.get("email"):
email = self._clean_str(contact_data["email"], 69)
if self._try_set_attribute(telecom, "EMail", email):
logger.info(f" EMail = {email}")
if contact_data.get("telecopie"):
fax = self._clean_str(contact_data["telecopie"], 21)
if self._try_set_attribute(telecom, "Telecopie", fax):
logger.info(f" Telecopie = {fax}")
except Exception as e:
logger.warning(f" WARN Erreur Telecom: {e}")
# Réseaux sociaux
logger.info("[5] Reseaux sociaux")
if contact_data.get("facebook"):
facebook = self._clean_str(contact_data["facebook"], 69)
try:
contact.Facebook = facebook
logger.info(f" Facebook = {facebook}")
except:
pass
if contact_data.get("linkedin"):
linkedin = self._clean_str(contact_data["linkedin"], 69)
try:
contact.LinkedIn = linkedin
logger.info(f" LinkedIn = {linkedin}")
except:
pass
if contact_data.get("skype"):
skype = self._clean_str(contact_data["skype"], 69)
try:
contact.Skype = skype
logger.info(f" Skype = {skype}")
except:
pass
# SetDefault
try:
contact.SetDefault()
logger.info(" OK SetDefault() applique")
except Exception as e:
logger.warning(f" WARN SetDefault(): {e}")
# Enregistrer
logger.info("[6] Enregistrement du contact")
try:
contact.Write()
logger.info(" OK Write() reussi")
contact.Read()
logger.info(" OK Read() 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:
pass
logger.error(f" ERROR Write: {error_detail}")
raise RuntimeError(f"Echec enregistrement: {error_detail}")
# Récupérer les IDs
contact_no = None
n_contact = None
try:
contact_no = getattr(contact, 'CT_No', None)
n_contact = getattr(contact, 'N_Contact', None)
logger.info(f" Contact CT_No={contact_no}, N_Contact={n_contact}")
except:
pass
# Contact par défaut
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:
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)
logger.info(f"[SUCCES] Contact cree: {prenom} {nom}")
logger.info(f" Lie au client {numero_client}")
if contact_no:
logger.info(f" CT_No={contact_no}")
logger.info("=" * 80)
# Utiliser _contact_to_dict
contact_dict = self._contact_to_dict(
contact,
numero_client=numero_client,
contact_numero=contact_no,
n_contact=n_contact
)
contact_dict["est_defaut"] = est_defaut
logger.info("=" * 80)
logger.info("[DEBUG RETOUR]")
logger.info(f" numero_client = {numero_client}")
logger.info(f" contact_no = {contact_no}")
logger.info(f" n_contact = {n_contact}")
logger.info(f" contact_dict COMPLET = {contact_dict}")
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 modifier_contact(self, numero: str, contact_numero: int, updates: Dict) -> Dict:
"""
Modifie un contact existant via COM
VERSION COMPLÈTE REFACTORISÉE
"""
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)
# Charger le client
logger.info("[1] 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_obj = win32com.client.CastTo(persist_client, "IBOClient3")
client_obj.Read()
logger.info(f" OK Client charge")
except Exception as e:
raise ValueError(f"Client {numero} introuvable: {e}")
# Charger le contact via SQL puis DossierContact
logger.info("[2] Chargement du contact")
nom_recherche = None
prenom_recherche = None
try:
# Récupérer nom/prénom via SQL
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_recherche = row.CT_Nom.strip() if row.CT_Nom else ""
prenom_recherche = row.CT_Prenom.strip() if row.CT_Prenom else ""
logger.info(f" Contact trouve en SQL: {prenom_recherche} {nom_recherche}")
except Exception as e:
raise ValueError(f"Contact introuvable: {e}")
# Charger via FactoryDossierContact
factory_dossier = self.cial.CptaApplication.FactoryDossierContact
persist = factory_dossier.ReadNomPrenom(nom_recherche, prenom_recherche)
if not persist:
raise ValueError(f"Contact non trouvable via ReadNomPrenom")
contact = win32com.client.CastTo(persist, "IBOTiersContact3")
contact.Read()
logger.info(f" OK Contact charge: {contact.Nom}")
# Appliquer les modifications
logger.info("[3] Application des modifications")
modifications_appliquees = []
# Identité
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:
pass
if "nom" in updates:
nom = self._clean_str(updates["nom"], 35)
if nom:
try:
contact.Nom = nom
logger.info(f" Nom = {nom}")
modifications_appliquees.append("nom")
except:
pass
if "prenom" in updates:
prenom = self._clean_str(updates["prenom"], 35)
try:
contact.Prenom = prenom
logger.info(f" Prenom = {prenom}")
modifications_appliquees.append("prenom")
except:
pass
if "fonction" in updates:
fonction = self._clean_str(updates["fonction"], 35)
try:
contact.Fonction = fonction
logger.info(f" Fonction = {fonction}")
modifications_appliquees.append("fonction")
except:
pass
# Service
if "service_code" in updates:
service = self._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:
pass
# Coordonnées via Telecom
if hasattr(contact, 'Telecom'):
try:
telecom = contact.Telecom
if "telephone" in updates:
telephone = self._clean_str(updates["telephone"], 21)
if self._try_set_attribute(telecom, "Telephone", telephone):
logger.info(f" Telephone = {telephone}")
modifications_appliquees.append("telephone")
if "portable" in updates:
portable = self._clean_str(updates["portable"], 21)
if self._try_set_attribute(telecom, "Portable", portable):
logger.info(f" Portable = {portable}")
modifications_appliquees.append("portable")
if "email" in updates:
email = self._clean_str(updates["email"], 69)
if self._try_set_attribute(telecom, "EMail", email):
logger.info(f" EMail = {email}")
modifications_appliquees.append("email")
if "telecopie" in updates:
fax = self._clean_str(updates["telecopie"], 21)
if self._try_set_attribute(telecom, "Telecopie", fax):
logger.info(f" Telecopie = {fax}")
modifications_appliquees.append("telecopie")
except:
pass
# Réseaux sociaux
if "facebook" in updates:
facebook = self._clean_str(updates["facebook"], 69)
try:
contact.Facebook = facebook
logger.info(f" Facebook = {facebook}")
modifications_appliquees.append("facebook")
except:
pass
if "linkedin" in updates:
linkedin = self._clean_str(updates["linkedin"], 69)
try:
contact.LinkedIn = linkedin
logger.info(f" LinkedIn = {linkedin}")
modifications_appliquees.append("linkedin")
except:
pass
if "skype" in updates:
skype = self._clean_str(updates["skype"], 69)
try:
contact.Skype = skype
logger.info(f" Skype = {skype}")
modifications_appliquees.append("skype")
except:
pass
# Enregistrement du contact
logger.info("[4] Enregistrement")
try:
contact.Write()
contact.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:
pass
logger.error(f" ERROR Write: {error_detail}")
raise RuntimeError(f"Echec modification contact: {error_detail}")
logger.info(f" Modifications appliquees: {', '.join(modifications_appliquees)}")
# Gestion du contact par défaut
est_defaut_demande = updates.get("est_defaut")
est_actuellement_defaut = False
if est_defaut_demande is not None and est_defaut_demande:
logger.info("[5] Gestion 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:
pass
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: {e}")
logger.info("=" * 80)
logger.info(f"[SUCCES] Contact modifie: CT_No={contact_numero}")
logger.info("=" * 80)
# Utiliser _contact_to_dict
contact_dict = self._contact_to_dict(
contact,
numero_client=numero,
contact_numero=contact_numero,
n_contact=None
)
contact_dict["est_defaut"] = est_actuellement_defaut
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:
"""
Définit un contact comme contact par défaut du client
VERSION COMPLÈTE REFACTORISÉE
"""
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)
# Récupérer le nom du contact via SQL
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}")
# Charger le client
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}")
# Définir le contact par défaut
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:
pass
# Enregistrement
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:
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]:
"""
Liste tous les contacts d'un client
"""
try:
with self._get_sql_connection() as conn:
return self._get_contacts_client(numero, conn)
except Exception as e:
logger.error(f"Erreur liste contacts: {e}")
raise RuntimeError(f"Erreur lecture contacts: {str(e)}")
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 self._row_to_contact_dict(row)
except Exception as e:
logger.error(f"Erreur obtention contact: {e}")
raise RuntimeError(f"Erreur lecture contact: {str(e)}")
def obtenir_contact_defaut(self, numero: str) -> Optional[Dict]:
"""
Récupère le contact par défaut d'un client
Returns:
Dictionnaire avec les infos du contact par défaut, ou None si non défini
"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(numero)
if not persist_client:
raise ValueError(f"Client {numero} non trouvé")
client = win32com.client.CastTo(persist_client, "IBOClient3")
client.Read()
ct_no_defaut = None
try:
ct_no_defaut = getattr(client, "CT_NoContact", None)
if ct_no_defaut:
logger.info(f"Contact par défaut via CT_NoContact: {ct_no_defaut}")
except:
pass
nom_contact_defaut = None
try:
nom_contact_defaut = getattr(client, "CT_Contact", None)
if nom_contact_defaut:
logger.info(f"Contact par défaut via CT_Contact: {nom_contact_defaut}")
except:
pass
if ct_no_defaut:
return self.obtenir_contact(numero, ct_no_defaut)
if nom_contact_defaut:
contacts = self.lister_contacts(numero)
for contact in contacts:
nom_complet = f"{contact.get('prenom', '')} {contact['nom']}".strip()
if nom_complet == nom_contact_defaut or contact['nom'] == nom_contact_defaut:
return {**contact, "est_defaut": True}
return None
except Exception as e:
logger.error(f"Erreur obtention contact par défaut: {e}")
return None
def supprimer_contact(self, numero: str, contact_numero: int) -> Dict:
"""
Supprime un contact via COM
VERSION COMPLÈTE REFACTORISÉE
"""
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)
# Récupérer le nom du contact via SQL
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}")
# Charger le contact via FactoryDossierContact
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(f"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}")
# Supprimer
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:
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 _contact_to_dict(self, contact, numero_client=None, contact_numero=None, n_contact=None) -> Dict:
"""
Convertit un objet COM Contact (IBOTiersContact3) en dictionnaire
Args:
contact: Objet COM contact
numero_client: Code du client (optionnel)
contact_numero: CT_No du contact (optionnel)
n_contact: N_Contact du contact (optionnel)
"""
try:
# Civilité
civilite_code = getattr(contact, "Civilite", None)
civilite_map = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"}
civilite = civilite_map.get(civilite_code) if civilite_code is not None else None
# Coordonnées via Telecom
telephone = None
portable = None
telecopie = None
email = None
if hasattr(contact, 'Telecom'):
try:
telecom = contact.Telecom
telephone = self._safe_strip(getattr(telecom, "Telephone", None))
portable = self._safe_strip(getattr(telecom, "Portable", None))
telecopie = self._safe_strip(getattr(telecom, "Telecopie", None))
email = self._safe_strip(getattr(telecom, "EMail", None))
except:
pass
return {
"numero": numero_client,
"contact_numero": contact_numero,
"n_contact": n_contact or contact_numero,
"civilite": civilite,
"nom": self._safe_strip(getattr(contact, "Nom", None)),
"prenom": self._safe_strip(getattr(contact, "Prenom", None)),
"fonction": self._safe_strip(getattr(contact, "Fonction", None)),
"service_code": getattr(contact, "ServiceContact", None),
"telephone": telephone,
"portable": portable,
"telecopie": telecopie,
"email": email,
"facebook": self._safe_strip(getattr(contact, "Facebook", None)),
"linkedin": self._safe_strip(getattr(contact, "LinkedIn", None)),
"skype": self._safe_strip(getattr(contact, "Skype", None)),
}
except Exception as e:
logger.warning(f"Erreur conversion contact: {e}")
return {}
def _row_to_contact_dict(self, row) -> Dict:
"""Convertit une ligne SQL en dictionnaire contact"""
civilite_code = row.CT_Civilite
civilite_map = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"}
return {
"numero": self._safe_strip(row.CT_Num),
"contact_numero": row.CT_No,
"n_contact": row.N_Contact,
"civilite": civilite_map.get(civilite_code) if civilite_code is not None else None,
"nom": self._safe_strip(row.CT_Nom),
"prenom": self._safe_strip(row.CT_Prenom),
"fonction": self._safe_strip(row.CT_Fonction),
"service_code": row.N_Service,
"telephone": self._safe_strip(row.CT_Telephone),
"portable": self._safe_strip(row.CT_TelPortable),
"telecopie": self._safe_strip(row.CT_Telecopie),
"email": self._safe_strip(row.CT_EMail),
"facebook": self._safe_strip(row.CT_Facebook),
"linkedin": self._safe_strip(row.CT_LinkedIn),
"skype": self._safe_strip(row.CT_Skype),
}
def _clean_str(self, value, max_len: int) -> str:
"""Nettoie et tronque une chaîne"""
if value is None or str(value).lower() in ('none', 'null', ''):
return ""
return str(value)[:max_len].strip()
def _safe_int(self, value, default=None):
"""Conversion sécurisée en entier"""
if value is None:
return default
try:
return int(value)
except (ValueError, TypeError):
return default
def _try_set_attribute(self, obj, attr_name, value, variants=None):
"""Essaie de définir un attribut avec plusieurs variantes"""
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)
return True
except Exception as e:
logger.debug(f" {variant} échec: {str(e)[:50]}")
return False
def creer_client(self, client_data: Dict) -> Dict:
"""
Creation client Sage - Version corrigée pour erreur cohérence
"""
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 as e:
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:
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", # Clients
"4010000", "401000", "401", # Fournisseurs
]
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:
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:
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) # URL ou @username
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) # URL ou profil
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:
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:
"""
Modification client Sage - Version complète alignée sur creer_client
"""
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")
client.Read()
logger.info(f" Client chargé: {getattr(client, 'CT_Intitule', '')}")
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:
if try_set_attribute(client, "CT_Raccourci", raccourci):
champs_modifies.append("raccourci")
if "siret" in client_data:
if try_set_attribute(client, "CT_Siret", clean_str(client_data["siret"], 15)):
champs_modifies.append("siret")
if "tva_intra" in client_data:
if try_set_attribute(client, "CT_Identifiant", clean_str(client_data["tva_intra"], 25)):
champs_modifies.append("tva_intra")
if "code_naf" in client_data:
if try_set_attribute(client, "CT_Ape", clean_str(client_data["code_naf"], 7)):
champs_modifies.append("code_naf")
adresse_keys = ["contact", "adresse", "complement", "code_postal", "ville", "region", "pays"]
if any(k in client_data for k in adresse_keys):
logger.info("[ETAPE 3] ADRESSE")
try:
if "contact" in client_data:
contact_nom = clean_str(client_data["contact"], 35)
try:
client.CT_Contact = contact_nom
champs_modifies.append("contact (client)")
logger.info(f" CT_Contact (client) = {contact_nom} [OK]")
except Exception as e:
logger.warning(f" CT_Contact (client) [ECHEC: {e}]")
try:
adresse_obj = client.Adresse
adresse_obj.Contact = contact_nom
champs_modifies.append("contact (adresse)")
logger.info(f" Contact (adresse) = {contact_nom} [OK]")
except Exception as e:
logger.warning(f" Contact (adresse) [ECHEC: {e}]")
adresse_obj = client.Adresse
if "adresse" in client_data:
adresse_obj.Adresse = clean_str(client_data["adresse"], 35)
champs_modifies.append("adresse")
if "complement" in client_data:
adresse_obj.Complement = clean_str(client_data["complement"], 35)
champs_modifies.append("complement")
if "code_postal" in client_data:
adresse_obj.CodePostal = clean_str(client_data["code_postal"], 9)
champs_modifies.append("code_postal")
if "ville" in client_data:
adresse_obj.Ville = clean_str(client_data["ville"], 35)
champs_modifies.append("ville")
if "region" in client_data:
adresse_obj.CodeRegion = clean_str(client_data["region"], 25)
champs_modifies.append("region")
if "pays" in client_data:
adresse_obj.Pays = clean_str(client_data["pays"], 35)
champs_modifies.append("pays")
logger.info(f" Adresse mise à jour ({len([k for k in adresse_keys if k in client_data])} champs)")
except Exception as e:
logger.error(f" Adresse erreur: {e}")
telecom_keys = ["telephone", "telecopie", "email", "site_web", "portable", "facebook", "linkedin"]
if any(k in client_data for k in telecom_keys):
logger.info("[ETAPE 4] TELECOM")
try:
telecom_obj = client.Telecom
if "telephone" in client_data:
telecom_obj.Telephone = clean_str(client_data["telephone"], 21)
champs_modifies.append("telephone")
if "telecopie" in client_data:
telecom_obj.Telecopie = clean_str(client_data["telecopie"], 21)
champs_modifies.append("telecopie")
if "email" in client_data:
telecom_obj.EMail = clean_str(client_data["email"], 69)
champs_modifies.append("email")
if "site_web" in client_data:
telecom_obj.Site = clean_str(client_data["site_web"], 69)
champs_modifies.append("site_web")
if "portable" in client_data:
portable = clean_str(client_data["portable"], 21)
if try_set_attribute(telecom_obj, "Portable", portable):
champs_modifies.append("portable")
if "facebook" in client_data:
facebook = clean_str(client_data["facebook"], 69)
if not try_set_attribute(telecom_obj, "Facebook", facebook):
try_set_attribute(client, "CT_Facebook", facebook)
champs_modifies.append("facebook")
if "linkedin" in client_data:
linkedin = clean_str(client_data["linkedin"], 69)
if not try_set_attribute(telecom_obj, "LinkedIn", linkedin):
try_set_attribute(client, "CT_LinkedIn", linkedin)
champs_modifies.append("linkedin")
logger.info(f" Telecom mis à jour ({len([k for k in telecom_keys if k in client_data])} champs)")
except Exception as e:
logger.error(f" Telecom erreur: {e}")
if "compte_general" in client_data:
logger.info("[ETAPE 5] COMPTE GENERAL")
compte = clean_str(client_data["compte_general"], 13)
factory_compte = self.cial.CptaApplication.FactoryCompteG
try:
persist_compte = factory_compte.ReadNumero(compte)
if persist_compte:
compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3")
compte_obj.Read()
type_compte = getattr(compte_obj, 'CG_Type', None)
if type_compte == 0:
client.CompteGPrinc = compte_obj
champs_modifies.append("compte_general")
logger.info(f" CompteGPrinc = {compte} [OK]")
else:
logger.warning(f" Compte {compte} - Type {type_compte} incompatible")
except Exception as e:
logger.warning(f" CompteGPrinc erreur: {e}")
if "categorie_tarifaire" in client_data or "categorie_comptable" in client_data:
logger.info("[ETAPE 6] CATEGORIES")
if "categorie_tarifaire" in client_data:
try:
cat_id = str(client_data["categorie_tarifaire"])
factory_cat_tarif = self.cial.CptaApplication.FactoryCategorieTarif
persist_cat = factory_cat_tarif.ReadIntitule(cat_id)
if persist_cat:
cat_tarif_obj = win32com.client.CastTo(persist_cat, "IBOCategorieTarif3")
cat_tarif_obj.Read()
client.CatTarif = cat_tarif_obj
champs_modifies.append("categorie_tarifaire")
logger.info(f" CatTarif = {cat_id} [OK]")
except Exception as e:
logger.warning(f" CatTarif erreur: {e}")
if "categorie_comptable" in client_data:
try:
cat_id = str(client_data["categorie_comptable"])
factory_cat_compta = self.cial.CptaApplication.FactoryCategorieCompta
persist_cat = factory_cat_compta.ReadIntitule(cat_id)
if persist_cat:
cat_compta_obj = win32com.client.CastTo(persist_cat, "IBOCategorieCompta3")
cat_compta_obj.Read()
client.CatCompta = cat_compta_obj
champs_modifies.append("categorie_comptable")
logger.info(f" CatCompta = {cat_id} [OK]")
except Exception as e:
logger.warning(f" CatCompta erreur: {e}")
taux_modifies = False
for i in range(1, 5):
key = f"taux{i:02d}"
if key in client_data:
if not taux_modifies:
logger.info("[ETAPE 7] TAUX")
taux_modifies = True
val = safe_float(client_data[key])
if try_set_attribute(client, f"CT_Taux{i:02d}", val):
champs_modifies.append(key)
stat_keys = ["statistique01", "secteur"] + [f"statistique{i:02d}" for i in range(2, 11)]
stat_modifies = False
stat01 = client_data.get("statistique01") or client_data.get("secteur")
if stat01:
if not stat_modifies:
logger.info("[ETAPE 8] STATISTIQUES")
stat_modifies = True
if try_set_attribute(client, "CT_Statistique01", clean_str(stat01, 21)):
champs_modifies.append("statistique01")
for i in range(2, 11):
key = f"statistique{i:02d}"
if key in client_data:
if not stat_modifies:
logger.info("[ETAPE 8] STATISTIQUES")
stat_modifies = True
if try_set_attribute(client, f"CT_Statistique{i:02d}", clean_str(client_data[key], 21)):
champs_modifies.append(key)
commercial_keys = ["encours_autorise", "assurance_credit", "langue", "commercial_code"]
if any(k in client_data for k in commercial_keys):
logger.info("[ETAPE 9] COMMERCIAL")
if "encours_autorise" in client_data:
if try_set_attribute(client, "CT_Encours", safe_float(client_data["encours_autorise"])):
champs_modifies.append("encours_autorise")
if "assurance_credit" in client_data:
if try_set_attribute(client, "CT_Assurance", safe_float(client_data["assurance_credit"])):
champs_modifies.append("assurance_credit")
if "langue" in client_data:
if try_set_attribute(client, "CT_Langue", safe_int(client_data["langue"])):
champs_modifies.append("langue")
if "commercial_code" in client_data:
co_no = safe_int(client_data["commercial_code"])
if not try_set_attribute(client, "CO_No", co_no):
try:
factory_collab = self.cial.CptaApplication.FactoryCollaborateur
persist_collab = factory_collab.ReadIntitule(str(co_no))
if persist_collab:
collab_obj = win32com.client.CastTo(persist_collab, "IBOCollaborateur3")
collab_obj.Read()
client.Collaborateur = collab_obj
champs_modifies.append("commercial_code")
logger.info(f" Collaborateur = {co_no} [OK]")
except Exception as e:
logger.warning(f" Collaborateur erreur: {e}")
facturation_keys = [
"lettrage_auto", "est_actif", "type_facture", "est_prospect",
"bl_en_facture", "saut_page", "validation_echeance", "controle_encours",
"exclure_relance", "exclure_penalites", "bon_a_payer"
]
if any(k in client_data for k in facturation_keys):
logger.info("[ETAPE 10] FACTURATION")
if "lettrage_auto" in client_data:
if try_set_attribute(client, "CT_Lettrage", 1 if client_data["lettrage_auto"] else 0):
champs_modifies.append("lettrage_auto")
if "est_actif" in client_data:
if try_set_attribute(client, "CT_Sommeil", 0 if client_data["est_actif"] else 1):
champs_modifies.append("est_actif")
if "type_facture" in client_data:
if try_set_attribute(client, "CT_Facture", safe_int(client_data["type_facture"])):
champs_modifies.append("type_facture")
if "est_prospect" in client_data:
if try_set_attribute(client, "CT_Prospect", 1 if client_data["est_prospect"] else 0):
champs_modifies.append("est_prospect")
factu_map = {
"CT_BLFact": "bl_en_facture",
"CT_Saut": "saut_page",
"CT_ValidEch": "validation_echeance",
"CT_ControlEnc": "controle_encours",
"CT_NotRappel": "exclure_relance",
"CT_NotPenal": "exclure_penalites",
"CT_BonAPayer": "bon_a_payer",
}
for attr, key in factu_map.items():
if key in client_data:
if try_set_attribute(client, attr, safe_int(client_data[key])):
champs_modifies.append(key)
logistique_keys = ["priorite_livraison", "livraison_partielle", "delai_transport", "delai_appro"]
if any(k in client_data for k in logistique_keys):
logger.info("[ETAPE 11] LOGISTIQUE")
logistique_map = {
"CT_PrioriteLivr": "priorite_livraison",
"CT_LivrPartielle": "livraison_partielle",
"CT_DelaiTransport": "delai_transport",
"CT_DelaiAppro": "delai_appro",
}
for attr, key in logistique_map.items():
if key in client_data:
if try_set_attribute(client, attr, safe_int(client_data[key])):
champs_modifies.append(key)
if "commentaire" in client_data:
logger.info("[ETAPE 12] COMMENTAIRE")
if try_set_attribute(client, "CT_Commentaire", clean_str(client_data["commentaire"], 35)):
champs_modifies.append("commentaire")
if "section_analytique" in client_data:
logger.info("[ETAPE 13] ANALYTIQUE")
if try_set_attribute(client, "CA_Num", clean_str(client_data["section_analytique"], 13)):
champs_modifies.append("section_analytique")
organisation_keys = [
"mode_reglement_code", "surveillance_active", "coface",
"forme_juridique", "effectif", "sv_regularite", "sv_cotation",
"sv_objet_maj", "ca_annuel", "sv_chiffre_affaires", "sv_resultat"
]
if any(k in client_data for k in organisation_keys):
logger.info("[ETAPE 14] ORGANISATION & SURVEILLANCE")
if "mode_reglement_code" in client_data:
mr_no = safe_int(client_data["mode_reglement_code"])
if not try_set_attribute(client, "MR_No", mr_no):
try:
factory_mr = self.cial.CptaApplication.FactoryModeRegl
persist_mr = factory_mr.ReadIntitule(str(mr_no))
if persist_mr:
mr_obj = win32com.client.CastTo(persist_mr, "IBOModeRegl3")
mr_obj.Read()
client.ModeRegl = mr_obj
champs_modifies.append("mode_reglement_code")
logger.info(f" ModeRegl = {mr_no} [OK]")
except Exception as e:
logger.warning(f" ModeRegl erreur: {e}")
if "surveillance_active" in client_data:
surveillance = 1 if client_data["surveillance_active"] else 0
try:
client.CT_Surveillance = surveillance
champs_modifies.append("surveillance_active")
logger.info(f" CT_Surveillance = {surveillance} [OK]")
except Exception as e:
logger.warning(f" CT_Surveillance [ECHEC: {e}]")
if "coface" in client_data:
coface = clean_str(client_data["coface"], 25)
try:
client.CT_Coface = coface
champs_modifies.append("coface")
logger.info(f" CT_Coface = {coface} [OK]")
except Exception as e:
logger.warning(f" CT_Coface [ECHEC: {e}]")
if "forme_juridique" in client_data:
if try_set_attribute(client, "CT_SvFormeJuri", clean_str(client_data["forme_juridique"], 33)):
champs_modifies.append("forme_juridique")
if "effectif" in client_data:
if try_set_attribute(client, "CT_SvEffectif", clean_str(client_data["effectif"], 11)):
champs_modifies.append("effectif")
if "sv_regularite" in client_data:
if try_set_attribute(client, "CT_SvRegul", clean_str(client_data["sv_regularite"], 3)):
champs_modifies.append("sv_regularite")
if "sv_cotation" in client_data:
if try_set_attribute(client, "CT_SvCotation", clean_str(client_data["sv_cotation"], 5)):
champs_modifies.append("sv_cotation")
if "sv_objet_maj" in client_data:
if try_set_attribute(client, "CT_SvObjetMaj", clean_str(client_data["sv_objet_maj"], 61)):
champs_modifies.append("sv_objet_maj")
ca = client_data.get("ca_annuel") or client_data.get("sv_chiffre_affaires")
if ca:
if try_set_attribute(client, "CT_SvCA", safe_float(ca)):
champs_modifies.append("ca_annuel/sv_chiffre_affaires")
if "sv_resultat" in client_data:
if try_set_attribute(client, "CT_SvResultat", safe_float(client_data["sv_resultat"])):
champs_modifies.append("sv_resultat")
if not champs_modifies:
logger.warning("Aucun champ à modifier")
return self._extraire_client(client)
logger.info("=" * 80)
logger.info(f"[WRITE] {len(champs_modifies)} champs modifiés:")
for i, champ in enumerate(champs_modifies, 1):
logger.info(f" {i}. {champ}")
logger.info("=" * 80)
try:
client.Write()
client.Read()
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:
pass
logger.error(f"[ERREUR] {error_detail}")
raise RuntimeError(f"Echec Write(): {error_detail}")
logger.info("=" * 80)
logger.info(f"[SUCCES] CLIENT MODIFIÉ: {code} ({len(champs_modifies)} champs)")
logger.info("=" * 80)
return self._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 dans Sage avec support des dates.
Args:
commande_data: dict contenant:
- client: {code: str}
- date_commande: str ou date
- date_livraison: str ou date (optionnel)
- reference: str (optionnel)
- lignes: list[dict]
"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(
f" Début création commande pour client {commande_data['client']['code']}"
)
try:
with self._com_context(), self._lock_com:
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug(" Transaction Sage démarrée")
except:
pass
try:
process = self.cial.CreateProcess_Document(
settings.SAGE_TYPE_BON_COMMANDE
)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
logger.info(" Document commande créé")
doc.DO_Date = pywintypes.Time(
self.normaliser_date(commande_data.get("date_commande"))
)
if ("date_livraison" in commande_data and commande_data["date_livraison"]):
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(commande_data["date_livraison"])
)
logger.info(
f" Date livraison: {commande_data['date_livraison']}"
)
factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(
commande_data["client"]["code"]
)
if not persist_client:
raise ValueError(
f"Client {commande_data['client']['code']} introuvable"
)
client_obj = self._cast_client(persist_client)
if not client_obj:
raise ValueError(f"Impossible de charger le client")
doc.SetDefaultClient(client_obj)
doc.Write()
logger.info(f" Client {commande_data['client']['code']} associé")
if commande_data.get("reference"):
try:
doc.DO_Ref = commande_data["reference"]
logger.info(f" Référence: {commande_data['reference']}")
except:
pass
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
logger.info(f" Ajout de {len(commande_data['lignes'])} lignes...")
for idx, ligne_data in enumerate(commande_data["lignes"], 1):
logger.info(
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f" Article {ligne_data['article_code']} introuvable dans Sage"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
designation_sage = getattr(article_obj, "AR_Design", "")
logger.info(f" Prix Sage: {prix_sage}")
if prix_sage == 0:
logger.warning(
f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
f" Article associé via SetDefaultArticleReference"
)
except Exception as e:
logger.warning(
f"SetDefaultArticleReference échoué: {e}, tentative avec objet"
)
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(f" Article associé via SetDefaultArticle")
except Exception as e2:
logger.error(f" Toutes les méthodes ont échoué")
ligne_obj.DL_Design = (
designation_sage
or ligne_data.get("designation", "")
)
ligne_obj.DL_Qte = quantite
logger.warning("Configuration manuelle appliquée")
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
logger.info(f" Prix auto chargé: {prix_auto}")
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
if prix_a_utiliser is not None and prix_a_utiliser > 0:
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
logger.info(f" Prix personnalisé: {prix_a_utiliser}")
elif prix_auto == 0 and prix_sage > 0:
ligne_obj.DL_PrixUnitaire = float(prix_sage)
logger.info(f" Prix Sage forcé: {prix_sage}")
elif prix_auto > 0:
logger.info(f" Prix auto conservé: {prix_auto}")
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
montant_ligne = quantite * prix_final
logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}")
remise = ligne_data.get("remise_pourcentage", 0)
if remise > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(remise)
ligne_obj.DL_Remise01REM_Type = 0
montant_apres_remise = montant_ligne * (
1 - remise / 100
)
logger.info(
f" Remise {remise}% → {montant_apres_remise}"
)
except Exception as e:
logger.warning(f"Remise non appliquée: {e}")
ligne_obj.Write()
logger.info(f" Ligne {idx} écrite")
try:
ligne_obj.Read()
prix_enregistre = float(
getattr(ligne_obj, "DL_PrixUnitaire", 0.0)
)
montant_enregistre = float(
getattr(ligne_obj, "DL_MontantHT", 0.0)
)
logger.info(
f" Vérif: Prix={prix_enregistre}€, Montant HT={montant_enregistre}"
)
except Exception as e:
logger.warning(f"Impossible de vérifier: {e}")
doc.Write()
process.Process()
if transaction_active:
self.cial.CptaApplication.CommitTrans()
time.sleep(2)
numero_commande = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_commande = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_commande:
numero_commande = getattr(doc, "DO_Piece", "")
factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(
settings.SAGE_TYPE_BON_COMMANDE, numero_commande
)
if persist_reread:
doc_final = win32com.client.CastTo(
persist_reread, "IBODocumentVente3"
)
doc_final.Read()
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc_final, "DO_Ref", "")
date_livraison_final = None
try:
date_livr = getattr(doc_final, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except:
pass
else:
total_ht = 0.0
total_ttc = 0.0
reference_finale = commande_data.get("reference", "")
date_livraison_final = commande_data.get("date_livraison")
logger.info(
f" COMMANDE CRÉÉE: {numero_commande} - {total_ttc}€ TTC "
)
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
return {
"numero_commande": numero_commande,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(commande_data["lignes"]),
"client_code": commande_data["client"]["code"],
"date_commande": str(
self.normaliser_date(commande_data.get("date_commande"))
),
"date_livraison": date_livraison_final,
"reference": reference_finale,
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except:
pass
raise
except Exception as e:
logger.error(f" Erreur création commande: {e}", exc_info=True)
raise RuntimeError(f"Échec création commande: {str(e)}")
def modifier_commande(self, numero: str, commande_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f" === MODIFICATION COMMANDE {numero} ===")
logger.info(" Chargement document...")
factory = self.cial.FactoryDocumentVente
persist = None
for type_test in [10, 3, settings.SAGE_TYPE_BON_COMMANDE]:
try:
persist_test = factory.ReadPiece(type_test, numero)
if persist_test:
persist = persist_test
logger.info(f" Document trouvé (type={type_test})")
break
except:
continue
if not persist:
raise ValueError(f" Commande {numero} INTROUVABLE")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
type_reel = getattr(doc, "DO_Type", -1)
logger.info(f" Type={type_reel}, Statut={statut_actuel}")
client_code_initial = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code_initial = getattr(client_obj, "CT_Num", "").strip()
logger.info(f" Client initial: {client_code_initial}")
else:
logger.error(" Objet Client NULL à l'état initial !")
except Exception as e:
logger.error(f" Erreur lecture client initial: {e}")
if not client_code_initial:
raise ValueError(" Client introuvable dans le document")
nb_lignes_initial = 0
try:
factory_lignes = getattr(
doc, "FactoryDocumentLigne", None
) or getattr(doc, "FactoryDocumentVenteLigne", None)
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
nb_lignes_initial += 1
index += 1
except:
break
logger.info(f" Lignes initiales: {nb_lignes_initial}")
except Exception as e:
logger.warning(f" Erreur comptage lignes: {e}")
champs_modifies = []
modif_date = "date_commande" in commande_data
modif_date_livraison = "date_livraison" in commande_data
modif_statut = "statut" in commande_data
modif_ref = "reference" in commande_data
modif_lignes = (
"lignes" in commande_data and commande_data["lignes"] is not None
)
logger.info(f"Modifications demandées:")
logger.info(f" Date commande: {modif_date}")
logger.info(f" Date livraison: {modif_date_livraison}")
logger.info(f" Statut: {modif_statut}")
logger.info(f" Référence: {modif_ref}")
logger.info(f" Lignes: {modif_lignes}")
commande_data_temp = commande_data.copy()
reference_a_modifier = None
statut_a_modifier = None
if modif_lignes:
if modif_ref:
reference_a_modifier = commande_data_temp.pop("reference")
logger.info(
" Modification de la référence reportée après les lignes"
)
modif_ref = False
if modif_statut:
statut_a_modifier = commande_data_temp.pop("statut")
logger.info(
" Modification du statut reportée après les lignes"
)
modif_statut = False
logger.info(" Test Write() basique (sans modification)...")
try:
doc.Write()
logger.info(" Write() basique OK")
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
if client_apres == client_code_initial:
logger.info(f" Client préservé: {client_apres}")
else:
logger.error(
f" Client a changé: {client_code_initial}{client_apres}"
)
else:
logger.error(" Client devenu NULL après Write() basique")
except Exception as e:
logger.error(f" Write() basique ÉCHOUE: {e}")
logger.error(f" ABANDON: Le document est VERROUILLÉ")
raise ValueError(
f"Document verrouillé, impossible de modifier: {e}"
)
if not modif_lignes and (
modif_date
or modif_date_livraison
or modif_statut
or modif_ref
):
logger.info(" Modifications simples (sans lignes)...")
if modif_date:
logger.info(" Modification date commande...")
doc.DO_Date = pywintypes.Time(
self.normaliser_date(
commande_data_temp.get("date_commande")
)
)
champs_modifies.append("date")
if modif_date_livraison:
logger.info(" Modification date livraison...")
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(commande_data_temp["date_livraison"])
)
logger.info(
f" Date livraison: {commande_data_temp['date_livraison']}"
)
champs_modifies.append("date_livraison")
if modif_statut:
logger.info(" Modification statut...")
nouveau_statut = commande_data_temp["statut"]
doc.DO_Statut = nouveau_statut
champs_modifies.append("statut")
logger.info(f" Statut défini: {nouveau_statut}")
if modif_ref:
logger.info(" Modification référence...")
try:
doc.DO_Ref = commande_data_temp["reference"]
champs_modifies.append("reference")
logger.info(
f" Référence définie: {commande_data_temp['reference']}"
)
except Exception as e:
logger.warning(f" Référence non définie: {e}")
logger.info(" Write() sans réassociation client...")
try:
doc.Write()
logger.info(" Write() réussi")
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
if client_apres == client_code_initial:
logger.info(f" Client préservé: {client_apres}")
else:
logger.error(
f" Client perdu: {client_code_initial}{client_apres}"
)
except Exception as e:
error_msg = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_msg = f"{sage_error.Description} (Code: {sage_error.Number})"
except:
pass
logger.error(f" Write() échoue: {error_msg}")
raise ValueError(f"Sage refuse: {error_msg}")
elif modif_lignes:
logger.info(" REMPLACEMENT COMPLET DES LIGNES...")
if modif_date:
doc.DO_Date = pywintypes.Time(
self.normaliser_date(
commande_data_temp.get("date_commande")
)
)
champs_modifies.append("date")
logger.info(" Date commande modifiée")
if modif_date_livraison:
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(commande_data_temp["date_livraison"])
)
logger.info(" Date livraison modifiée")
champs_modifies.append("date_livraison")
nouvelles_lignes = commande_data["lignes"]
nb_nouvelles = len(nouvelles_lignes)
logger.info(
f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes"
)
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
if nb_lignes_initial > 0:
logger.info(
f" Suppression de {nb_lignes_initial} lignes existantes..."
)
for idx in range(nb_lignes_initial, 0, -1):
try:
ligne_p = factory_lignes.List(idx)
if ligne_p:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
ligne.Read()
ligne.Remove()
logger.debug(f" Ligne {idx} supprimée")
except Exception as e:
logger.warning(
f" Impossible de supprimer ligne {idx}: {e}"
)
logger.info(" Toutes les lignes existantes supprimées")
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
logger.info(
f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f"Article {ligne_data['article_code']} introuvable"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Design = ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(
ligne_data["prix_unitaire_ht"]
)
if ligne_data.get("remise_pourcentage", 0) > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(
ligne_data["remise_pourcentage"]
)
ligne_obj.DL_Remise01REM_Type = 0
except:
pass
ligne_obj.Write()
logger.info(f" Ligne {idx} ajoutée")
logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées")
logger.info(" Write() document après remplacement lignes...")
doc.Write()
logger.info(" Document écrit")
import time
time.sleep(0.5)
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
logger.info(f" Client après remplacement: {client_apres}")
else:
logger.error(" Client NULL après remplacement")
champs_modifies.append("lignes")
if reference_a_modifier is not None:
try:
ancienne_reference = getattr(doc, "DO_Ref", "")
nouvelle_reference = (
str(reference_a_modifier) if reference_a_modifier else ""
)
doc.DO_Ref = nouvelle_reference
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("reference")
logger.info(
f" Référence modifiée: '{ancienne_reference}''{nouvelle_reference}'"
)
except Exception as e:
logger.warning(f"Impossible de modifier la référence: {e}")
if statut_a_modifier is not None:
try:
statut_actuel = getattr(doc, "DO_Statut", 0)
nouveau_statut = int(statut_a_modifier)
if nouveau_statut != statut_actuel:
doc.DO_Statut = nouveau_statut
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("statut")
logger.info(
f" Statut modifié: {statut_actuel}{nouveau_statut}"
)
except Exception as e:
logger.warning(f"Impossible de modifier le statut: {e}")
logger.info(" Relecture finale...")
import time
time.sleep(0.5)
doc.Read()
client_obj_final = getattr(doc, "Client", None)
if client_obj_final:
client_obj_final.Read()
client_final = getattr(client_obj_final, "CT_Num", "")
else:
client_final = ""
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc, "DO_Ref", "")
date_livraison_final = None
try:
date_livr = getattr(doc, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except:
pass
logger.info(f" SUCCÈS: {numero} modifiée ")
logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
logger.info(f" Client final: {client_final}")
logger.info(f" Référence: {reference_finale}")
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
logger.info(f" Champs modifiés: {champs_modifies}")
return {
"numero": numero,
"total_ht": total_ht,
"total_ttc": total_ttc,
"reference": reference_finale,
"date_livraison": date_livraison_final,
"champs_modifies": champs_modifies,
"statut": getattr(doc, "DO_Statut", 0),
"client_code": client_final,
}
except ValueError as e:
logger.error(f" ERREUR MÉTIER: {e}")
raise
except Exception as e:
logger.error(f" 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} (Code: {err.Number})"
)
except:
pass
raise RuntimeError(f"Erreur Sage: {error_message}")
def creer_livraison_enrichi(self, livraison_data: dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(
f" Début création livraison pour client {livraison_data['client']['code']}"
)
try:
with self._com_context(), self._lock_com:
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug(" Transaction Sage démarrée")
except:
pass
try:
process = self.cial.CreateProcess_Document(
settings.SAGE_TYPE_BON_LIVRAISON
)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
logger.info(" Document livraison créé")
doc.DO_Date = pywintypes.Time(
self.normaliser_date(livraison_data.get("date_livraison"))
)
if (
"date_livraison_prevue" in livraison_data
and livraison_data["date_livraison_prevue"]
):
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(
livraison_data["date_livraison_prevue"]
)
)
logger.info(
f" Date livraison prévue: {livraison_data['date_livraison_prevue']}"
)
factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(
livraison_data["client"]["code"]
)
if not persist_client:
raise ValueError(
f"Client {livraison_data['client']['code']} introuvable"
)
client_obj = self._cast_client(persist_client)
if not client_obj:
raise ValueError(f"Impossible de charger le client")
doc.SetDefaultClient(client_obj)
doc.Write()
logger.info(f" Client {livraison_data['client']['code']} associé")
if livraison_data.get("reference"):
try:
doc.DO_Ref = livraison_data["reference"]
logger.info(f" Référence: {livraison_data['reference']}")
except:
pass
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
logger.info(
f" Ajout de {len(livraison_data['lignes'])} lignes..."
)
for idx, ligne_data in enumerate(livraison_data["lignes"], 1):
logger.info(
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f" Article {ligne_data['article_code']} introuvable dans Sage"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
designation_sage = getattr(article_obj, "AR_Design", "")
logger.info(f" Prix Sage: {prix_sage}")
if prix_sage == 0:
logger.warning(
f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
f" Article associé via SetDefaultArticleReference"
)
except Exception as e:
logger.warning(
f"SetDefaultArticleReference échoué: {e}, tentative avec objet"
)
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(f" Article associé via SetDefaultArticle")
except Exception as e2:
logger.error(f" Toutes les méthodes ont échoué")
ligne_obj.DL_Design = (
designation_sage
or ligne_data.get("designation", "")
)
ligne_obj.DL_Qte = quantite
logger.warning("Configuration manuelle appliquée")
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
logger.info(f" Prix auto chargé: {prix_auto}")
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
if prix_a_utiliser is not None and prix_a_utiliser > 0:
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
logger.info(f" Prix personnalisé: {prix_a_utiliser}")
elif prix_auto == 0 and prix_sage > 0:
ligne_obj.DL_PrixUnitaire = float(prix_sage)
logger.info(f" Prix Sage forcé: {prix_sage}")
elif prix_auto > 0:
logger.info(f" Prix auto conservé: {prix_auto}")
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
montant_ligne = quantite * prix_final
logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}")
remise = ligne_data.get("remise_pourcentage", 0)
if remise > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(remise)
ligne_obj.DL_Remise01REM_Type = 0
montant_apres_remise = montant_ligne * (
1 - remise / 100
)
logger.info(
f" Remise {remise}% → {montant_apres_remise}"
)
except Exception as e:
logger.warning(f"Remise non appliquée: {e}")
ligne_obj.Write()
logger.info(f" Ligne {idx} écrite")
doc.Write()
process.Process()
if transaction_active:
self.cial.CptaApplication.CommitTrans()
time.sleep(2)
numero_livraison = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_livraison = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_livraison:
numero_livraison = getattr(doc, "DO_Piece", "")
factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(
settings.SAGE_TYPE_BON_LIVRAISON, numero_livraison
)
if persist_reread:
doc_final = win32com.client.CastTo(
persist_reread, "IBODocumentVente3"
)
doc_final.Read()
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc_final, "DO_Ref", "")
date_livraison_prevue_final = None
try:
date_livr = getattr(doc_final, "DO_DateLivr", None)
if date_livr:
date_livraison_prevue_final = date_livr.strftime(
"%Y-%m-%d"
)
except:
pass
else:
total_ht = 0.0
total_ttc = 0.0
reference_finale = livraison_data.get("reference", "")
date_livraison_prevue_final = livraison_data.get(
"date_livraison_prevue"
)
logger.info(
f" LIVRAISON CRÉÉE: {numero_livraison} - {total_ttc}€ TTC "
)
if date_livraison_prevue_final:
logger.info(
f" Date livraison prévue: {date_livraison_prevue_final}"
)
return {
"numero_livraison": numero_livraison,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(livraison_data["lignes"]),
"client_code": livraison_data["client"]["code"],
"date_livraison": str(
self.normaliser_date(livraison_data.get("date_livraison"))
),
"date_livraison_prevue": date_livraison_prevue_final,
"reference": reference_finale,
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except:
pass
raise
except Exception as e:
logger.error(f" Erreur création livraison: {e}", exc_info=True)
raise RuntimeError(f"Échec création livraison: {str(e)}")
def modifier_livraison(self, numero: str, livraison_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f" === MODIFICATION LIVRAISON {numero} ===")
logger.info(" Chargement document...")
factory = self.cial.FactoryDocumentVente
persist = None
for type_test in [30, settings.SAGE_TYPE_BON_LIVRAISON]:
try:
persist_test = factory.ReadPiece(type_test, numero)
if persist_test:
persist = persist_test
logger.info(f" Document trouvé (type={type_test})")
break
except:
continue
if not persist:
raise ValueError(f" Livraison {numero} INTROUVABLE")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
logger.info(f" Statut={statut_actuel}")
if statut_actuel == 5:
raise ValueError(f"La livraison {numero} a déjà été transformée")
if statut_actuel == 6:
raise ValueError(f"La livraison {numero} est annulée")
nb_lignes_initial = 0
try:
factory_lignes = getattr(
doc, "FactoryDocumentLigne", None
) or getattr(doc, "FactoryDocumentVenteLigne", None)
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
nb_lignes_initial += 1
index += 1
except:
break
logger.info(f" Lignes initiales: {nb_lignes_initial}")
except Exception as e:
logger.warning(f" Erreur comptage lignes: {e}")
champs_modifies = []
modif_date = "date_livraison" in livraison_data
modif_date_livraison_prevue = "date_livraison_prevue" in livraison_data
modif_statut = "statut" in livraison_data
modif_ref = "reference" in livraison_data
modif_lignes = (
"lignes" in livraison_data and livraison_data["lignes"] is not None
)
logger.info(f"Modifications demandées:")
logger.info(f" Date livraison: {modif_date}")
logger.info(f" Date livraison prévue: {modif_date_livraison_prevue}")
logger.info(f" Statut: {modif_statut}")
logger.info(f" Référence: {modif_ref}")
logger.info(f" Lignes: {modif_lignes}")
livraison_data_temp = livraison_data.copy()
reference_a_modifier = None
statut_a_modifier = None
if modif_lignes:
if modif_ref:
reference_a_modifier = livraison_data_temp.pop("reference")
logger.info(
" Modification de la référence reportée après les lignes"
)
modif_ref = False
if modif_statut:
statut_a_modifier = livraison_data_temp.pop("statut")
logger.info(
" Modification du statut reportée après les lignes"
)
modif_statut = False
if not modif_lignes and (
modif_date
or modif_date_livraison_prevue
or modif_statut
or modif_ref
):
logger.info(" Modifications simples (sans lignes)...")
if modif_date:
logger.info(" Modification date livraison...")
doc.DO_Date = pywintypes.Time(
self.normaliser_date(
livraison_data_temp.get("date_livraison")
)
)
champs_modifies.append("date")
if modif_date_livraison_prevue:
logger.info(" Modification date livraison prévue...")
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(
livraison_data_temp["date_livraison_prevue"]
)
)
logger.info(
f" Date livraison prévue: {livraison_data_temp['date_livraison_prevue']}"
)
champs_modifies.append("date_livraison_prevue")
if modif_statut:
logger.info(" Modification statut...")
nouveau_statut = livraison_data_temp["statut"]
doc.DO_Statut = nouveau_statut
champs_modifies.append("statut")
logger.info(f" Statut défini: {nouveau_statut}")
if modif_ref:
logger.info(" Modification référence...")
try:
doc.DO_Ref = livraison_data_temp["reference"]
champs_modifies.append("reference")
logger.info(
f" Référence définie: {livraison_data_temp['reference']}"
)
except Exception as e:
logger.warning(f" Référence non définie: {e}")
logger.info(" Write()...")
doc.Write()
logger.info(" Write() réussi")
elif modif_lignes:
logger.info(" REMPLACEMENT COMPLET DES LIGNES...")
if modif_date:
doc.DO_Date = pywintypes.Time(
self.normaliser_date(
livraison_data_temp.get("date_livraison")
)
)
champs_modifies.append("date")
logger.info(" Date livraison modifiée")
if modif_date_livraison_prevue:
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(
livraison_data_temp["date_livraison_prevue"]
)
)
logger.info(" Date livraison prévue modifiée")
champs_modifies.append("date_livraison_prevue")
nouvelles_lignes = livraison_data["lignes"]
nb_nouvelles = len(nouvelles_lignes)
logger.info(
f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles"
)
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
if nb_lignes_initial > 0:
logger.info(f" Suppression de {nb_lignes_initial} lignes...")
for idx in range(nb_lignes_initial, 0, -1):
try:
ligne_p = factory_lignes.List(idx)
if ligne_p:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
ligne.Read()
ligne.Remove()
logger.debug(f" Ligne {idx} supprimée")
except Exception as e:
logger.warning(
f" Erreur suppression ligne {idx}: {e}"
)
logger.info(" Toutes les lignes supprimées")
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
logger.info(
f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f"Article {ligne_data['article_code']} introuvable"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Design = ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(
ligne_data["prix_unitaire_ht"]
)
if ligne_data.get("remise_pourcentage", 0) > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(
ligne_data["remise_pourcentage"]
)
ligne_obj.DL_Remise01REM_Type = 0
except:
pass
ligne_obj.Write()
logger.info(f" Ligne {idx} ajoutée")
logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées")
logger.info(" Write() document après remplacement lignes...")
doc.Write()
logger.info(" Document écrit")
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("lignes")
if reference_a_modifier is not None:
try:
ancienne_reference = getattr(doc, "DO_Ref", "")
nouvelle_reference = (
str(reference_a_modifier) if reference_a_modifier else ""
)
logger.info(
f" Modification référence: '{ancienne_reference}''{nouvelle_reference}'"
)
doc.DO_Ref = nouvelle_reference
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("reference")
logger.info(f" Référence modifiée avec succès")
except Exception as e:
logger.warning(f"Impossible de modifier la référence: {e}")
if statut_a_modifier is not None:
try:
statut_actuel = getattr(doc, "DO_Statut", 0)
nouveau_statut = int(statut_a_modifier)
if nouveau_statut != statut_actuel:
logger.info(
f" Modification statut: {statut_actuel}{nouveau_statut}"
)
doc.DO_Statut = nouveau_statut
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("statut")
logger.info(f" Statut modifié avec succès")
except Exception as e:
logger.warning(f"Impossible de modifier le statut: {e}")
logger.info(" Relecture finale...")
import time
time.sleep(0.5)
doc.Read()
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc, "DO_Ref", "")
statut_final = getattr(doc, "DO_Statut", 0)
date_livraison_prevue_final = None
try:
date_livr = getattr(doc, "DO_DateLivr", None)
if date_livr:
date_livraison_prevue_final = date_livr.strftime("%Y-%m-%d")
except:
pass
logger.info(f" SUCCÈS: {numero} modifiée ")
logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
logger.info(f" Référence: {reference_finale}")
logger.info(f" Statut: {statut_final}")
if date_livraison_prevue_final:
logger.info(
f" Date livraison prévue: {date_livraison_prevue_final}"
)
logger.info(f" Champs modifiés: {champs_modifies}")
return {
"numero": numero,
"total_ht": total_ht,
"total_ttc": total_ttc,
"reference": reference_finale,
"date_livraison_prevue": date_livraison_prevue_final,
"champs_modifies": champs_modifies,
"statut": statut_final,
}
except ValueError as e:
logger.error(f" ERREUR MÉTIER: {e}")
raise
except Exception as e:
logger.error(f" 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} (Code: {err.Number})"
)
except:
pass
raise RuntimeError(f"Erreur Sage: {error_message}")
def creer_avoir_enrichi(self, avoir_data: dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(
f" Début création avoir pour client {avoir_data['client']['code']}"
)
try:
with self._com_context(), self._lock_com:
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug(" Transaction Sage démarrée")
except:
pass
try:
process = self.cial.CreateProcess_Document(
settings.SAGE_TYPE_BON_AVOIR
)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
logger.info(" Document avoir créé")
doc.DO_Date = pywintypes.Time(
self.normaliser_date(avoir_data.get("date_avoir"))
)
if "date_livraison" in avoir_data and avoir_data["date_livraison"]:
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(avoir_data["date_livraison"])
)
logger.info(
f" Date livraison: {avoir_data['date_livraison']}"
)
factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(
avoir_data["client"]["code"]
)
if not persist_client:
raise ValueError(
f"Client {avoir_data['client']['code']} introuvable"
)
client_obj = self._cast_client(persist_client)
if not client_obj:
raise ValueError(f"Impossible de charger le client")
doc.SetDefaultClient(client_obj)
doc.Write()
logger.info(f" Client {avoir_data['client']['code']} associé")
if avoir_data.get("reference"):
try:
doc.DO_Ref = avoir_data["reference"]
logger.info(f" Référence: {avoir_data['reference']}")
except:
pass
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
logger.info(f" Ajout de {len(avoir_data['lignes'])} lignes...")
for idx, ligne_data in enumerate(avoir_data["lignes"], 1):
logger.info(
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f" Article {ligne_data['article_code']} introuvable dans Sage"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
designation_sage = getattr(article_obj, "AR_Design", "")
logger.info(f" Prix Sage: {prix_sage}")
if prix_sage == 0:
logger.warning(
f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
f" Article associé via SetDefaultArticleReference"
)
except Exception as e:
logger.warning(
f"SetDefaultArticleReference échoué: {e}, tentative avec objet"
)
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(f" Article associé via SetDefaultArticle")
except Exception as e2:
logger.error(f" Toutes les méthodes ont échoué")
ligne_obj.DL_Design = (
designation_sage
or ligne_data.get("designation", "")
)
ligne_obj.DL_Qte = quantite
logger.warning("Configuration manuelle appliquée")
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
logger.info(f" Prix auto chargé: {prix_auto}")
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
if prix_a_utiliser is not None and prix_a_utiliser > 0:
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
logger.info(f" Prix personnalisé: {prix_a_utiliser}")
elif prix_auto == 0 and prix_sage > 0:
ligne_obj.DL_PrixUnitaire = float(prix_sage)
logger.info(f" Prix Sage forcé: {prix_sage}")
elif prix_auto > 0:
logger.info(f" Prix auto conservé: {prix_auto}")
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
montant_ligne = quantite * prix_final
logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}")
remise = ligne_data.get("remise_pourcentage", 0)
if remise > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(remise)
ligne_obj.DL_Remise01REM_Type = 0
montant_apres_remise = montant_ligne * (
1 - remise / 100
)
logger.info(
f" Remise {remise}% → {montant_apres_remise}"
)
except Exception as e:
logger.warning(f"Remise non appliquée: {e}")
ligne_obj.Write()
logger.info(f" Ligne {idx} écrite")
doc.Write()
process.Process()
if transaction_active:
self.cial.CptaApplication.CommitTrans()
time.sleep(2)
numero_avoir = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_avoir = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_avoir:
numero_avoir = getattr(doc, "DO_Piece", "")
factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(
settings.SAGE_TYPE_BON_AVOIR, numero_avoir
)
if persist_reread:
doc_final = win32com.client.CastTo(
persist_reread, "IBODocumentVente3"
)
doc_final.Read()
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc_final, "DO_Ref", "")
date_livraison_final = None
try:
date_livr = getattr(doc_final, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except:
pass
else:
total_ht = 0.0
total_ttc = 0.0
reference_finale = avoir_data.get("reference", "")
date_livraison_final = avoir_data.get("date_livraison")
logger.info(
f" AVOIR CRÉÉ: {numero_avoir} - {total_ttc}€ TTC "
)
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
return {
"numero_avoir": numero_avoir,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(avoir_data["lignes"]),
"client_code": avoir_data["client"]["code"],
"date_avoir": str(
self.normaliser_date(avoir_data.get("date_avoir"))
),
"date_livraison": date_livraison_final,
"reference": reference_finale,
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except:
pass
raise
except Exception as e:
logger.error(f" Erreur création avoir: {e}", exc_info=True)
raise RuntimeError(f"Échec création avoir: {str(e)}")
def modifier_avoir(self, numero: str, avoir_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f" === MODIFICATION AVOIR {numero} ===")
logger.info(" Chargement document...")
factory = self.cial.FactoryDocumentVente
persist = None
for type_test in [50, settings.SAGE_TYPE_BON_AVOIR]:
try:
persist_test = factory.ReadPiece(type_test, numero)
if persist_test:
persist = persist_test
logger.info(f" Document trouvé (type={type_test})")
break
except:
continue
if not persist:
raise ValueError(f" Avoir {numero} INTROUVABLE")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
type_reel = getattr(doc, "DO_Type", -1)
logger.info(f" Type={type_reel}, Statut={statut_actuel}")
if statut_actuel == 5:
raise ValueError(f"L'avoir {numero} a déjà été transformé")
if statut_actuel == 6:
raise ValueError(f"L'avoir {numero} est annulé")
client_code_initial = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code_initial = getattr(client_obj, "CT_Num", "").strip()
logger.info(f" Client initial: {client_code_initial}")
else:
logger.error(" Objet Client NULL à l'état initial !")
except Exception as e:
logger.error(f" Erreur lecture client initial: {e}")
if not client_code_initial:
raise ValueError(" Client introuvable dans le document")
nb_lignes_initial = 0
try:
factory_lignes = getattr(
doc, "FactoryDocumentLigne", None
) or getattr(doc, "FactoryDocumentVenteLigne", None)
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
nb_lignes_initial += 1
index += 1
except:
break
logger.info(f" Lignes initiales: {nb_lignes_initial}")
except Exception as e:
logger.warning(f" Erreur comptage lignes: {e}")
champs_modifies = []
modif_date = "date_avoir" in avoir_data
modif_date_livraison = "date_livraison" in avoir_data
modif_statut = "statut" in avoir_data
modif_ref = "reference" in avoir_data
modif_lignes = (
"lignes" in avoir_data and avoir_data["lignes"] is not None
)
logger.info(f"Modifications demandées:")
logger.info(f" Date avoir: {modif_date}")
logger.info(f" Date livraison: {modif_date_livraison}")
logger.info(f" Statut: {modif_statut}")
logger.info(f" Référence: {modif_ref}")
logger.info(f" Lignes: {modif_lignes}")
avoir_data_temp = avoir_data.copy()
reference_a_modifier = None
statut_a_modifier = None
if modif_lignes:
if modif_ref:
reference_a_modifier = avoir_data_temp.pop("reference")
logger.info(
" Modification de la référence reportée après les lignes"
)
modif_ref = False
if modif_statut:
statut_a_modifier = avoir_data_temp.pop("statut")
logger.info(
" Modification du statut reportée après les lignes"
)
modif_statut = False
logger.info(" Test Write() basique (sans modification)...")
try:
doc.Write()
logger.info(" Write() basique OK")
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
if client_apres == client_code_initial:
logger.info(f" Client préservé: {client_apres}")
else:
logger.error(
f" Client a changé: {client_code_initial}{client_apres}"
)
else:
logger.error(" Client devenu NULL après Write() basique")
except Exception as e:
logger.error(f" Write() basique ÉCHOUE: {e}")
logger.error(f" ABANDON: Le document est VERROUILLÉ")
raise ValueError(
f"Document verrouillé, impossible de modifier: {e}"
)
if not modif_lignes and (
modif_date
or modif_date_livraison
or modif_statut
or modif_ref
):
logger.info(" Modifications simples (sans lignes)...")
if modif_date:
logger.info(" Modification date avoir...")
doc.DO_Date = pywintypes.Time(
self.normaliser_date(avoir_data_temp.get("date_avoir"))
)
champs_modifies.append("date")
if modif_date_livraison:
logger.info(" Modification date livraison...")
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(avoir_data_temp["date_livraison"])
)
logger.info(
f" Date livraison: {avoir_data_temp['date_livraison']}"
)
champs_modifies.append("date_livraison")
if modif_statut:
logger.info(" Modification statut...")
nouveau_statut = avoir_data_temp["statut"]
doc.DO_Statut = nouveau_statut
champs_modifies.append("statut")
logger.info(f" Statut défini: {nouveau_statut}")
if modif_ref:
logger.info(" Modification référence...")
try:
doc.DO_Ref = avoir_data_temp["reference"]
champs_modifies.append("reference")
logger.info(
f" Référence définie: {avoir_data_temp['reference']}"
)
except Exception as e:
logger.warning(f" Référence non définie: {e}")
logger.info(" Write() sans réassociation client...")
try:
doc.Write()
logger.info(" Write() réussi")
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
if client_apres == client_code_initial:
logger.info(f" Client préservé: {client_apres}")
else:
logger.error(
f" Client perdu: {client_code_initial}{client_apres}"
)
except Exception as e:
error_msg = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_msg = f"{sage_error.Description} (Code: {sage_error.Number})"
except:
pass
logger.error(f" Write() échoue: {error_msg}")
raise ValueError(f"Sage refuse: {error_msg}")
elif modif_lignes:
logger.info(" REMPLACEMENT COMPLET DES LIGNES...")
if modif_date:
doc.DO_Date = pywintypes.Time(
self.normaliser_date(avoir_data_temp.get("date_avoir"))
)
champs_modifies.append("date")
logger.info(" Date avoir modifiée")
if modif_date_livraison:
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(avoir_data_temp["date_livraison"])
)
logger.info(" Date livraison modifiée")
champs_modifies.append("date_livraison")
nouvelles_lignes = avoir_data["lignes"]
nb_nouvelles = len(nouvelles_lignes)
logger.info(
f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes"
)
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
if nb_lignes_initial > 0:
logger.info(
f" Suppression de {nb_lignes_initial} lignes existantes..."
)
for idx in range(nb_lignes_initial, 0, -1):
try:
ligne_p = factory_lignes.List(idx)
if ligne_p:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
ligne.Read()
ligne.Remove()
logger.debug(f" Ligne {idx} supprimée")
except Exception as e:
logger.warning(
f" Impossible de supprimer ligne {idx}: {e}"
)
logger.info(" Toutes les lignes existantes supprimées")
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
logger.info(
f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f"Article {ligne_data['article_code']} introuvable"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Design = ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(
ligne_data["prix_unitaire_ht"]
)
if ligne_data.get("remise_pourcentage", 0) > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(
ligne_data["remise_pourcentage"]
)
ligne_obj.DL_Remise01REM_Type = 0
except:
pass
ligne_obj.Write()
logger.info(f" Ligne {idx} ajoutée")
logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées")
logger.info(" Write() document après remplacement lignes...")
doc.Write()
logger.info(" Document écrit")
import time
time.sleep(0.5)
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
logger.info(f" Client après remplacement: {client_apres}")
else:
logger.error(" Client NULL après remplacement")
champs_modifies.append("lignes")
if reference_a_modifier is not None:
try:
ancienne_reference = getattr(doc, "DO_Ref", "")
nouvelle_reference = (
str(reference_a_modifier) if reference_a_modifier else ""
)
logger.info(
f" Modification référence: '{ancienne_reference}''{nouvelle_reference}'"
)
doc.DO_Ref = nouvelle_reference
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("reference")
logger.info(f" Référence modifiée avec succès")
except Exception as e:
logger.warning(f"Impossible de modifier la référence: {e}")
if statut_a_modifier is not None:
try:
statut_actuel = getattr(doc, "DO_Statut", 0)
nouveau_statut = int(statut_a_modifier)
if nouveau_statut != statut_actuel:
logger.info(
f" Modification statut: {statut_actuel}{nouveau_statut}"
)
doc.DO_Statut = nouveau_statut
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("statut")
logger.info(f" Statut modifié avec succès")
except Exception as e:
logger.warning(f"Impossible de modifier le statut: {e}")
logger.info(" Relecture finale...")
import time
time.sleep(0.5)
doc.Read()
client_obj_final = getattr(doc, "Client", None)
if client_obj_final:
client_obj_final.Read()
client_final = getattr(client_obj_final, "CT_Num", "")
else:
client_final = ""
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc, "DO_Ref", "")
statut_final = getattr(doc, "DO_Statut", 0)
date_livraison_final = None
try:
date_livr = getattr(doc, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except:
pass
logger.info(f" SUCCÈS: {numero} modifié ")
logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
logger.info(f" Client final: {client_final}")
logger.info(f" Référence: {reference_finale}")
logger.info(f" Statut: {statut_final}")
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
logger.info(f" Champs modifiés: {champs_modifies}")
return {
"numero": numero,
"total_ht": total_ht,
"total_ttc": total_ttc,
"reference": reference_finale,
"date_livraison": date_livraison_final,
"champs_modifies": champs_modifies,
"statut": statut_final,
"client_code": client_final,
}
except ValueError as e:
logger.error(f" ERREUR MÉTIER: {e}")
raise
except Exception as e:
logger.error(f" 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} (Code: {err.Number})"
)
except:
pass
raise RuntimeError(f"Erreur Sage: {error_message}")
def creer_facture_enrichi(self, facture_data: dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(
f" Début création facture pour client {facture_data['client']['code']}"
)
try:
with self._com_context(), self._lock_com:
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug(" Transaction Sage démarrée")
except:
pass
try:
process = self.cial.CreateProcess_Document(
settings.SAGE_TYPE_FACTURE
)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
logger.info(" Document facture créé")
doc.DO_Date = pywintypes.Time(
self.normaliser_date(facture_data.get("date_facture"))
)
if (
"date_livraison" in facture_data
and facture_data["date_livraison"]
):
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(facture_data["date_livraison"])
)
logger.info(
f" Date livraison: {facture_data['date_livraison']}"
)
factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(
facture_data["client"]["code"]
)
if not persist_client:
raise ValueError(
f"Client {facture_data['client']['code']} introuvable"
)
client_obj = self._cast_client(persist_client)
if not client_obj:
raise ValueError(f"Impossible de charger le client")
doc.SetDefaultClient(client_obj)
doc.Write()
logger.info(f" Client {facture_data['client']['code']} associé")
if facture_data.get("reference"):
try:
doc.DO_Ref = facture_data["reference"]
logger.info(f" Référence: {facture_data['reference']}")
except:
pass
logger.info(" Configuration champs spécifiques factures...")
try:
if hasattr(doc, "DO_CodeJournal"):
try:
param_societe = (
self.cial.CptaApplication.ParametreSociete
)
journal_defaut = getattr(
param_societe, "P_CodeJournalVte", "VTE"
)
doc.DO_CodeJournal = journal_defaut
logger.info(f" Code journal: {journal_defaut}")
except:
doc.DO_CodeJournal = "VTE"
logger.info(" Code journal: VTE (défaut)")
except Exception as e:
logger.debug(f" Code journal: {e}")
try:
if hasattr(doc, "DO_Souche"):
doc.DO_Souche = 0
logger.debug(" Souche: 0 (défaut)")
except:
pass
try:
if hasattr(doc, "DO_Regime"):
doc.DO_Regime = 0
logger.debug(" Régime: 0 (défaut)")
except:
pass
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
logger.info(f" Ajout de {len(facture_data['lignes'])} lignes...")
for idx, ligne_data in enumerate(facture_data["lignes"], 1):
logger.info(
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f" Article {ligne_data['article_code']} introuvable dans Sage"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
designation_sage = getattr(article_obj, "AR_Design", "")
logger.info(f" Prix Sage: {prix_sage}")
if prix_sage == 0:
logger.warning(
f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
f" Article associé via SetDefaultArticleReference"
)
except Exception as e:
logger.warning(
f"SetDefaultArticleReference échoué: {e}, tentative avec objet"
)
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(f" Article associé via SetDefaultArticle")
except Exception as e2:
logger.error(f" Toutes les méthodes ont échoué")
ligne_obj.DL_Design = (
designation_sage
or ligne_data.get("designation", "")
)
ligne_obj.DL_Qte = quantite
logger.warning("Configuration manuelle appliquée")
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
logger.info(f" Prix auto chargé: {prix_auto}")
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
if prix_a_utiliser is not None and prix_a_utiliser > 0:
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
logger.info(f" Prix personnalisé: {prix_a_utiliser}")
elif prix_auto == 0 and prix_sage > 0:
ligne_obj.DL_PrixUnitaire = float(prix_sage)
logger.info(f" Prix Sage forcé: {prix_sage}")
elif prix_auto > 0:
logger.info(f" Prix auto conservé: {prix_auto}")
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
montant_ligne = quantite * prix_final
logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}")
remise = ligne_data.get("remise_pourcentage", 0)
if remise > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(remise)
ligne_obj.DL_Remise01REM_Type = 0
montant_apres_remise = montant_ligne * (
1 - remise / 100
)
logger.info(
f" Remise {remise}% → {montant_apres_remise}"
)
except Exception as e:
logger.warning(f"Remise non appliquée: {e}")
ligne_obj.Write()
logger.info(f" Ligne {idx} écrite")
logger.info(" Validation facture...")
try:
doc.SetClient(client_obj)
logger.debug(" Client réassocié avant validation")
except:
try:
doc.SetDefaultClient(client_obj)
except:
pass
doc.Write()
logger.info(" Process()...")
process.Process()
if transaction_active:
self.cial.CptaApplication.CommitTrans()
logger.info(" Transaction committée")
time.sleep(2)
numero_facture = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_facture = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_facture:
numero_facture = getattr(doc, "DO_Piece", "")
if not numero_facture:
raise RuntimeError("Numéro facture vide après création")
logger.info(f" Numéro facture: {numero_facture}")
factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(
settings.SAGE_TYPE_FACTURE, numero_facture
)
if persist_reread:
doc_final = win32com.client.CastTo(
persist_reread, "IBODocumentVente3"
)
doc_final.Read()
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc_final, "DO_Ref", "")
date_livraison_final = None
try:
date_livr = getattr(doc_final, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except:
pass
else:
total_ht = 0.0
total_ttc = 0.0
reference_finale = facture_data.get("reference", "")
date_livraison_final = facture_data.get("date_livraison")
logger.info(
f" FACTURE CRÉÉE: {numero_facture} - {total_ttc}€ TTC "
)
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
return {
"numero_facture": numero_facture,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(facture_data["lignes"]),
"client_code": facture_data["client"]["code"],
"date_facture": str(
self.normaliser_date(facture_data.get("date_facture"))
),
"date_livraison": date_livraison_final,
"reference": reference_finale,
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.error(" Transaction annulée (rollback)")
except:
pass
raise
except Exception as e:
logger.error(f" Erreur création facture: {e}", exc_info=True)
raise RuntimeError(f"Échec création facture: {str(e)}")
def modifier_facture(self, numero: str, facture_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f" === MODIFICATION FACTURE {numero} ===")
logger.info(" Chargement document...")
factory = self.cial.FactoryDocumentVente
persist = None
for type_test in [60, 5, settings.SAGE_TYPE_FACTURE]:
try:
persist_test = factory.ReadPiece(type_test, numero)
if persist_test:
persist = persist_test
logger.info(f" Document trouvé (type={type_test})")
break
except:
continue
if not persist:
raise ValueError(f" Facture {numero} INTROUVABLE")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
type_reel = getattr(doc, "DO_Type", -1)
logger.info(f" Type={type_reel}, Statut={statut_actuel}")
if statut_actuel == 5:
raise ValueError(f"La facture {numero} a déjà été transformée")
if statut_actuel == 6:
raise ValueError(f"La facture {numero} est annulée")
client_code_initial = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code_initial = getattr(client_obj, "CT_Num", "").strip()
logger.info(f" Client initial: {client_code_initial}")
else:
logger.error(" Objet Client NULL à l'état initial !")
except Exception as e:
logger.error(f" Erreur lecture client initial: {e}")
if not client_code_initial:
raise ValueError(" Client introuvable dans le document")
nb_lignes_initial = 0
try:
factory_lignes = getattr(
doc, "FactoryDocumentLigne", None
) or getattr(doc, "FactoryDocumentVenteLigne", None)
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
nb_lignes_initial += 1
index += 1
except:
break
logger.info(f" Lignes initiales: {nb_lignes_initial}")
except Exception as e:
logger.warning(f" Erreur comptage lignes: {e}")
champs_modifies = []
modif_date = "date_facture" in facture_data
modif_date_livraison = "date_livraison" in facture_data
modif_statut = "statut" in facture_data
modif_ref = "reference" in facture_data
modif_lignes = (
"lignes" in facture_data and facture_data["lignes"] is not None
)
logger.info(f"Modifications demandées:")
logger.info(f" Date facture: {modif_date}")
logger.info(f" Date livraison: {modif_date_livraison}")
logger.info(f" Statut: {modif_statut}")
logger.info(f" Référence: {modif_ref}")
logger.info(f" Lignes: {modif_lignes}")
facture_data_temp = facture_data.copy()
reference_a_modifier = None
statut_a_modifier = None
if modif_lignes:
if modif_ref:
reference_a_modifier = facture_data_temp.pop("reference")
logger.info(
" Modification de la référence reportée après les lignes"
)
modif_ref = False
if modif_statut:
statut_a_modifier = facture_data_temp.pop("statut")
logger.info(
" Modification du statut reportée après les lignes"
)
modif_statut = False
logger.info(" Test Write() basique (sans modification)...")
try:
doc.Write()
logger.info(" Write() basique OK")
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
if client_apres == client_code_initial:
logger.info(f" Client préservé: {client_apres}")
else:
logger.error(
f" Client a changé: {client_code_initial}{client_apres}"
)
else:
logger.error(" Client devenu NULL après Write() basique")
except Exception as e:
logger.error(f" Write() basique ÉCHOUE: {e}")
logger.error(f" ABANDON: Le document est VERROUILLÉ")
raise ValueError(
f"Document verrouillé, impossible de modifier: {e}"
)
if not modif_lignes and (
modif_date
or modif_date_livraison
or modif_statut
or modif_ref
):
logger.info(" Modifications simples (sans lignes)...")
if modif_date:
logger.info(" Modification date facture...")
doc.DO_Date = pywintypes.Time(
self.normaliser_date(facture_data_temp.get("date_facture"))
)
champs_modifies.append("date")
if modif_date_livraison:
logger.info(" Modification date livraison...")
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(facture_data_temp["date_livraison"])
)
logger.info(
f" Date livraison: {facture_data_temp['date_livraison']}"
)
champs_modifies.append("date_livraison")
if modif_statut:
logger.info(" Modification statut...")
nouveau_statut = facture_data_temp["statut"]
doc.DO_Statut = nouveau_statut
champs_modifies.append("statut")
logger.info(f" Statut défini: {nouveau_statut}")
if modif_ref:
logger.info(" Modification référence...")
try:
doc.DO_Ref = facture_data_temp["reference"]
champs_modifies.append("reference")
logger.info(
f" Référence définie: {facture_data_temp['reference']}"
)
except Exception as e:
logger.warning(f" Référence non définie: {e}")
logger.info(" Write() sans réassociation client...")
try:
doc.Write()
logger.info(" Write() réussi")
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
if client_apres == client_code_initial:
logger.info(f" Client préservé: {client_apres}")
else:
logger.error(
f" Client perdu: {client_code_initial}{client_apres}"
)
except Exception as e:
error_msg = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_msg = f"{sage_error.Description} (Code: {sage_error.Number})"
except:
pass
logger.error(f" Write() échoue: {error_msg}")
raise ValueError(f"Sage refuse: {error_msg}")
elif modif_lignes:
logger.info(" REMPLACEMENT COMPLET DES LIGNES...")
if modif_date:
doc.DO_Date = pywintypes.Time(
self.normaliser_date(facture_data_temp.get("date_facture"))
)
champs_modifies.append("date")
logger.info(" Date facture modifiée")
if modif_date_livraison:
doc.DO_DateLivr = pywintypes.Time(
self.normaliser_date(facture_data_temp["date_livraison"])
)
logger.info(" Date livraison modifiée")
champs_modifies.append("date_livraison")
nouvelles_lignes = facture_data["lignes"]
nb_nouvelles = len(nouvelles_lignes)
logger.info(
f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes"
)
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
if nb_lignes_initial > 0:
logger.info(
f" Suppression de {nb_lignes_initial} lignes existantes..."
)
for idx in range(nb_lignes_initial, 0, -1):
try:
ligne_p = factory_lignes.List(idx)
if ligne_p:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
ligne.Read()
ligne.Remove()
logger.debug(f" Ligne {idx} supprimée")
except Exception as e:
logger.warning(
f" Impossible de supprimer ligne {idx}: {e}"
)
logger.info(" Toutes les lignes existantes supprimées")
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
logger.info(
f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f"Article {ligne_data['article_code']} introuvable"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Design = ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(
ligne_data["prix_unitaire_ht"]
)
if ligne_data.get("remise_pourcentage", 0) > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(
ligne_data["remise_pourcentage"]
)
ligne_obj.DL_Remise01REM_Type = 0
except:
pass
ligne_obj.Write()
logger.info(f" Ligne {idx} ajoutée")
logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées")
logger.info(" Write() document après remplacement lignes...")
doc.Write()
logger.info(" Document écrit")
import time
time.sleep(0.5)
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
logger.info(f" Client après remplacement: {client_apres}")
else:
logger.error(" Client NULL après remplacement")
champs_modifies.append("lignes")
if reference_a_modifier is not None:
try:
ancienne_reference = getattr(doc, "DO_Ref", "")
nouvelle_reference = (
str(reference_a_modifier) if reference_a_modifier else ""
)
logger.info(
f" Modification référence: '{ancienne_reference}''{nouvelle_reference}'"
)
doc.DO_Ref = nouvelle_reference
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("reference")
logger.info(f" Référence modifiée avec succès")
except Exception as e:
logger.warning(f"Impossible de modifier la référence: {e}")
if statut_a_modifier is not None:
try:
statut_actuel = getattr(doc, "DO_Statut", 0)
nouveau_statut = int(statut_a_modifier)
if nouveau_statut != statut_actuel:
logger.info(
f" Modification statut: {statut_actuel}{nouveau_statut}"
)
doc.DO_Statut = nouveau_statut
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("statut")
logger.info(f" Statut modifié avec succès")
except Exception as e:
logger.warning(f"Impossible de modifier le statut: {e}")
logger.info(" Relecture finale...")
import time
time.sleep(0.5)
doc.Read()
client_obj_final = getattr(doc, "Client", None)
if client_obj_final:
client_obj_final.Read()
client_final = getattr(client_obj_final, "CT_Num", "")
else:
client_final = ""
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc, "DO_Ref", "")
statut_final = getattr(doc, "DO_Statut", 0)
date_livraison_final = None
try:
date_livr = getattr(doc, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except:
pass
logger.info(f" SUCCÈS: {numero} modifiée ")
logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
logger.info(f" Client final: {client_final}")
logger.info(f" Référence: {reference_finale}")
logger.info(f" Statut: {statut_final}")
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
logger.info(f" Champs modifiés: {champs_modifies}")
return {
"numero": numero,
"total_ht": total_ht,
"total_ttc": total_ttc,
"reference": reference_finale,
"date_livraison": date_livraison_final,
"champs_modifies": champs_modifies,
"statut": statut_final,
"client_code": client_final,
}
except ValueError as e:
logger.error(f" ERREUR MÉTIER: {e}")
raise
except Exception as e:
logger.error(f" 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} (Code: {err.Number})"
)
except:
pass
raise RuntimeError(f"Erreur Sage: {error_message}")
def creer_article(self, article_data: dict) -> dict:
with self._com_context(), self._lock_com:
try:
logger.info("[ARTICLE] === CREATION ARTICLE ===")
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug("Transaction Sage démarrée")
except Exception as e:
logger.debug(f"BeginTrans non disponible : {e}")
try:
depots_disponibles = []
depot_a_utiliser = None
depot_code_demande = article_data.get("depot_code")
try:
factory_depot = self.cial.FactoryDepot
index = 1
while index <= 100:
try:
persist = factory_depot.List(index)
if persist is None:
break
depot_obj = win32com.client.CastTo(persist, "IBODepot3")
depot_obj.Read()
code = getattr(depot_obj, "DE_Code", "").strip()
if not code:
index += 1
continue
numero = int(getattr(depot_obj, "Compteur", 0))
intitule = getattr(
depot_obj, "DE_Intitule", f"Depot {code}"
)
depot_info = {
"code": code,
"numero": numero,
"intitule": intitule,
"objet": depot_obj,
}
depots_disponibles.append(depot_info)
if depot_code_demande and code == depot_code_demande:
depot_a_utiliser = depot_info
elif not depot_code_demande and not depot_a_utiliser:
depot_a_utiliser = depot_info
index += 1
except Exception as e:
if "Acces refuse" in str(e):
break
index += 1
except Exception as e:
logger.warning(f"[DEPOT] Erreur découverte dépôts : {e}")
if not depots_disponibles:
raise ValueError(
"Aucun dépôt trouvé dans Sage. Créez d'abord un dépôt."
)
if not depot_a_utiliser:
depot_a_utiliser = depots_disponibles[0]
logger.info(
f"[DEPOT] Utilisation : '{depot_a_utiliser['code']}' ({depot_a_utiliser['intitule']})"
)
reference = article_data.get("reference", "").upper().strip()
if not reference:
raise ValueError("La référence est obligatoire")
if len(reference) > 18:
raise ValueError(
"La référence ne peut pas dépasser 18 caractères"
)
designation = article_data.get("designation", "").strip()
if not designation:
raise ValueError("La désignation est obligatoire")
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}")
logger.info(f"[ARTICLE] Stock réel demandé : {stock_reel}")
logger.info(f"[ARTICLE] Stock mini demandé : {stock_mini}")
logger.info(f"[ARTICLE] Stock maxi demandé : {stock_maxi}")
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" in error_msg
or "non trouve" in error_msg
or "-2607" in error_msg
):
logger.debug(
f"[ARTICLE] {reference} n'existe pas encore, création possible"
)
else:
logger.error(f"[ARTICLE] Erreur vérification : {e}")
raise
persist = factory.Create()
article = win32com.client.CastTo(persist, "IBOArticle3")
article.SetDefault()
article.AR_Ref = reference
article.AR_Design = designation
logger.info("[MODELE] Recherche article modèle via SQL...")
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 = self._safe_strip(row.AR_Ref)
logger.info(
f" [SQL] Article modèle trouvé : {article_modele_ref}"
)
except Exception as e:
logger.warning(f" [SQL] Erreur recherche article : {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] Article modèle chargé : {article_modele_ref}"
)
except Exception as e:
logger.warning(f" [WARN] Erreur chargement modèle : {e}")
article_modele = None
if not article_modele:
raise ValueError(
"Aucun article modèle trouvé dans Sage.\n"
"Créez au moins un article manuellement dans Sage pour servir de modèle."
)
logger.info("[OBJETS] Copie Unite + Famille depuis modèle...")
unite_trouvee = False
try:
unite_obj = getattr(article_modele, "Unite", None)
if unite_obj:
article.Unite = unite_obj
logger.info(
f" [OK] Objet Unite copié depuis {article_modele_ref}"
)
unite_trouvee = True
except Exception as e:
logger.debug(f" Unite non copiable : {str(e)[:80]}")
if not unite_trouvee:
raise ValueError(
"Impossible de copier l'unité de vente depuis le modèle"
)
famille_trouvee = False
famille_code_personnalise = article_data.get("famille")
famille_obj = None
if famille_code_personnalise:
logger.info(
f" [FAMILLE] Code personnalisé demandé : {famille_code_personnalise}"
)
try:
famille_existe_sql = False
famille_code_exact = None
try:
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 = self._safe_strip(
row.FA_CodeFamille
)
famille_existe_sql = True
logger.info(
f" [SQL] Famille trouvée : {famille_code_exact}"
)
else:
raise ValueError(
f"Famille '{famille_code_personnalise}' introuvable"
)
except ValueError:
raise
except Exception as e_sql:
logger.warning(f" [SQL] Erreur : {e_sql}")
if famille_existe_sql and famille_code_exact:
factory_famille = self.cial.FactoryFamille
try:
index = 1
max_scan = 1000
while index <= max_scan:
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 à l'index {index}"
)
break
index += 1
except Exception as e:
if "Accès refusé" in str(e):
break
index += 1
if famille_obj:
famille_obj.Read()
article.Famille = famille_obj
logger.info(
f" [OK] Famille '{famille_code_personnalise}' assignée"
)
else:
raise ValueError(
f"Famille '{famille_code_personnalise}' inaccessible via COM"
)
except Exception as e:
logger.warning(f" [COM] Erreur scanner : {e}")
raise
except ValueError:
raise
except Exception as e:
logger.warning(
f" [WARN] Famille personnalisée non chargée : {str(e)[:100]}"
)
if not famille_trouvee:
try:
famille_obj = getattr(article_modele, "Famille", None)
if famille_obj:
article.Famille = famille_obj
logger.info(
f" [OK] Objet Famille copié depuis {article_modele_ref}"
)
famille_trouvee = True
except Exception as e:
logger.debug(f" Famille non copiable : {str(e)[:80]}")
logger.info("[CHAMPS] Copie champs obligatoires depuis modèle...")
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(f" [OK] AR_SuiviStock=2 (FIFO/LIFO)")
prix_vente = article_data.get("prix_vente")
if prix_vente is not None:
try:
article.AR_PrixVen = float(prix_vente)
logger.info(f" Prix vente : {prix_vente} EUR")
except Exception as e:
logger.warning(f" Prix vente erreur : {str(e)[:100]}")
prix_achat = article_data.get("prix_achat")
if prix_achat is not None:
try:
try:
article.AR_PrixAch = float(prix_achat)
logger.info(
f" Prix achat (AR_PrixAch) : {prix_achat} EUR"
)
except:
article.AR_PrixAchat = float(prix_achat)
logger.info(
f" Prix achat (AR_PrixAchat) : {prix_achat} EUR"
)
except Exception as e:
logger.warning(f" Prix achat erreur : {str(e)[:100]}")
code_ean = article_data.get("code_ean")
if code_ean:
article.AR_CodeBarre = str(code_ean)
logger.info(f" Code EAN/Barre : {code_ean}")
description = article_data.get("description")
if description:
try:
article.AR_Commentaire = description
logger.info(f" Description définie")
except:
pass
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:
pass
logger.error(f" [ERREUR] Write() échoué : {error_detail}")
raise RuntimeError(f"Échec création article : {error_detail}")
stock_defini = False
stock_erreur = None
has_stock_values = stock_reel or stock_mini or stock_maxi
if has_stock_values:
logger.info(
f"[STOCK] Définition stock dans F_ARTSTOCK (dépôt '{depot_a_utiliser['code']}')..."
)
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 trouvée : {factory_name}"
)
break
except:
continue
if not factory_stock:
raise RuntimeError(
"Factory de stock introuvable sur le dépôt"
)
stock_persist = factory_stock.Create()
stock_obj = win32com.client.CastTo(
stock_persist, "IBODepotStock3"
)
stock_obj.SetDefault()
stock_obj.AR_Ref = reference
if stock_reel:
stock_obj.AS_QteSto = float(stock_reel)
logger.info(f" AS_QteSto = {stock_reel}")
if stock_mini:
try:
stock_obj.AS_QteMini = float(stock_mini)
logger.info(f" AS_QteMini = {stock_mini}")
except Exception as e:
logger.warning(f" AS_QteMini non défini : {e}")
if stock_maxi:
try:
stock_obj.AS_QteMaxi = float(stock_maxi)
logger.info(f" AS_QteMaxi = {stock_maxi}")
except Exception as e:
logger.warning(f" AS_QteMaxi non défini : {e}")
stock_obj.Write()
stock_defini = True
logger.info(
f" [OK] Stock défini dans F_ARTSTOCK : réel={stock_reel}, min={stock_mini}, max={stock_maxi}"
)
except Exception as e:
stock_erreur = str(e)
logger.error(
f" [ERREUR] Stock non défini dans F_ARTSTOCK : {e}",
exc_info=True,
)
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.info(
"[COMMIT] Transaction committée - Article persiste dans Sage"
)
except Exception as e:
logger.warning(f"[COMMIT] Erreur commit : {e}")
logger.info("[VERIF] Relecture article créé...")
article_cree_persist = factory.ReadReference(reference)
if not article_cree_persist:
raise RuntimeError(
"Article créé mais introuvable à la relecture"
)
article_cree = win32com.client.CastTo(
article_cree_persist, "IBOArticle3"
)
article_cree.Read()
stocks_par_depot = []
stock_total = 0.0
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
d.DE_Code,
s.AS_QteSto,
s.AS_QteMini,
s.AS_QteMaxi
FROM F_ARTSTOCK s
LEFT JOIN F_DEPOT d ON s.DE_No = d.DE_No
WHERE s.AR_Ref = ?
""",
(reference.upper(),),
)
depot_rows = cursor.fetchall()
for depot_row in depot_rows:
if len(depot_row) >= 4:
qte = float(depot_row[1]) if depot_row[1] else 0.0
stock_total += qte
stocks_par_depot.append(
{
"depot_code": self._safe_strip(
depot_row[0]
),
"quantite": qte,
"qte_mini": (
float(depot_row[2])
if depot_row[2]
else 0.0
),
"qte_maxi": (
float(depot_row[3])
if depot_row[3]
else 0.0
),
}
)
logger.info(
f"[VERIF] Stock dans F_ARTSTOCK : {stock_total} unités dans {len(stocks_par_depot)} dépôt(s)"
)
except Exception as e:
logger.warning(
f"[VERIF] Impossible de vérifier le stock dans F_ARTSTOCK : {e}"
)
logger.info(
f"[OK] ARTICLE CREE : {reference} - Stock total : {stock_total}"
)
logger.info("[EXTRACTION] Extraction complète de l'article créé...")
resultat = self._extraire_article(article_cree)
if not resultat:
resultat = {
"reference": reference,
"designation": designation,
}
resultat["stock_reel"] = stock_total
if stock_mini:
resultat["stock_mini"] = float(stock_mini)
if stock_maxi:
resultat["stock_maxi"] = float(stock_maxi)
resultat["stock_disponible"] = stock_total
resultat["stock_reserve"] = 0.0
resultat["stock_commande"] = 0.0
if prix_vente is not None:
resultat["prix_vente"] = float(prix_vente)
if prix_achat is not None:
resultat["prix_achat"] = float(prix_achat)
if description:
resultat["description"] = description
if code_ean:
resultat["code_ean"] = str(code_ean)
resultat["code_barre"] = str(code_ean)
if famille_code_personnalise and famille_trouvee:
resultat["famille_code"] = famille_code_personnalise
try:
if famille_obj:
famille_obj.Read()
resultat["famille_libelle"] = getattr(
famille_obj, "FA_Intitule", ""
)
except:
pass
if stocks_par_depot:
resultat["stocks_par_depot"] = stocks_par_depot
resultat["depot_principal"] = {
"code": depot_a_utiliser["code"],
"intitule": depot_a_utiliser["intitule"],
}
resultat["suivi_stock_active"] = stock_defini
if has_stock_values and not stock_defini and stock_erreur:
resultat["avertissement"] = (
f"Stock demandé mais non défini dans F_ARTSTOCK : {stock_erreur}"
)
logger.info(
f"[EXTRACTION] Article extrait et enrichi avec {len(resultat)} champs"
)
return resultat
except ValueError:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except:
pass
raise
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except:
pass
logger.error(f"Erreur creation article : {e}", exc_info=True)
raise RuntimeError(f"Erreur creation article : {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:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
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()
designation_actuelle = getattr(article, "AR_Design", "")
logger.info(f"[ARTICLE] Trouvé : {reference} - {designation_actuelle}")
logger.info("[ARTICLE] Mise à jour des champs...")
champs_modifies = []
if "famille" in article_data and article_data["famille"]:
famille_code_demande = article_data["famille"].upper().strip()
logger.info(
f"[FAMILLE] Changement demandé : {famille_code_demande}"
)
try:
famille_existe_sql = False
famille_code_exact = None
famille_type = None
try:
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 = self._safe_strip(
row.FA_CodeFamille
)
famille_type = row.FA_Type if len(row) > 1 else 0
famille_existe_sql = True
if famille_type == 1:
raise ValueError(
f"La famille '{famille_code_demande}' est de type 'Total' "
f"et ne peut pas contenir d'articles. "
f"Utilisez une famille de type Détail."
)
logger.info(
f" [SQL] Famille trouvée : {famille_code_exact} (type={famille_type})"
)
else:
raise ValueError(
f"Famille '{famille_code_demande}' introuvable dans Sage"
)
except ValueError:
raise
except Exception as e:
logger.warning(f" [SQL] Erreur : {e}")
raise ValueError(f"Impossible de vérifier la famille : {e}")
if famille_existe_sql and famille_code_exact:
logger.info(f" [COM] Recherche via scanner...")
factory_famille = self.cial.FactoryFamille
famille_obj = None
try:
index = 1
max_scan = 1000
while index <= max_scan:
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 trouvée à l'index {index}"
)
break
index += 1
except Exception as e:
if "Accès refusé" in str(e) or "Access" in str(
e
):
break
index += 1
except Exception as e:
logger.warning(
f" [COM] Scanner échoué : {str(e)[:200]}"
)
if famille_obj:
famille_obj.Read()
article.Famille = famille_obj
champs_modifies.append(f"famille={famille_code_exact}")
logger.info(
f" [OK] Famille changée : {famille_code_exact}"
)
else:
raise ValueError(
f"Famille '{famille_code_demande}' trouvée en SQL mais inaccessible via COM. "
f"Essayez avec une autre famille."
)
except ValueError:
raise
except Exception as e:
logger.error(f" [ERREUR] Changement famille : {e}")
raise ValueError(f"Impossible de changer la famille : {str(e)}")
if "designation" in article_data:
designation = str(article_data["designation"])[:69].strip()
article.AR_Design = designation
champs_modifies.append(f"designation")
logger.info(f" [OK] Désignation : {designation}")
if "prix_vente" in article_data:
try:
prix_vente = float(article_data["prix_vente"])
article.AR_PrixVen = prix_vente
champs_modifies.append("prix_vente")
logger.info(f" [OK] Prix vente : {prix_vente} EUR")
except Exception as e:
logger.warning(f" [WARN] Prix vente : {e}")
if "prix_achat" in article_data:
try:
prix_achat = float(article_data["prix_achat"])
try:
article.AR_PrixAch = prix_achat
champs_modifies.append("prix_achat")
logger.info(
f" [OK] Prix achat (AR_PrixAch) : {prix_achat} EUR"
)
except:
article.AR_PrixAchat = prix_achat
champs_modifies.append("prix_achat")
logger.info(
f" [OK] Prix achat (AR_PrixAchat) : {prix_achat} EUR"
)
except Exception as e:
logger.warning(f" [WARN] Prix achat : {e}")
if "stock_reel" in article_data:
try:
stock_reel = float(article_data["stock_reel"])
ancien_stock = float(getattr(article, "AR_Stock", 0.0))
article.AR_Stock = stock_reel
champs_modifies.append("stock_reel")
logger.info(f" [OK] Stock : {ancien_stock} -> {stock_reel}")
if stock_reel > ancien_stock:
logger.info(
f" [+] Stock augmenté de {stock_reel - ancien_stock}"
)
except Exception as e:
logger.error(f" [ERREUR] Stock : {e}")
raise ValueError(f"Impossible de modifier le stock: {e}")
if "stock_mini" in article_data:
try:
stock_mini = float(article_data["stock_mini"])
article.AR_StockMini = stock_mini
champs_modifies.append("stock_mini")
logger.info(f" [OK] Stock mini : {stock_mini}")
except Exception as e:
logger.warning(f" [WARN] Stock mini : {e}")
if "stock_maxi" in article_data:
try:
stock_maxi = float(article_data["stock_maxi"])
article.AR_StockMaxi = stock_maxi
champs_modifies.append("stock_maxi")
logger.info(f" [OK] Stock maxi : {stock_maxi}")
except Exception as e:
logger.warning(f" [WARN] Stock maxi : {e}")
if "code_ean" in article_data:
try:
code_ean = str(article_data["code_ean"])[:13].strip()
article.AR_CodeBarre = code_ean
champs_modifies.append("code_ean")
logger.info(f" [OK] Code EAN : {code_ean}")
except Exception as e:
logger.warning(f" [WARN] Code EAN : {e}")
if "description" in article_data:
try:
description = str(article_data["description"])[:255].strip()
article.AR_Commentaire = description
champs_modifies.append("description")
logger.info(f" [OK] Description définie")
except Exception as e:
logger.warning(f" [WARN] Description : {e}")
if not champs_modifies:
logger.warning("[ARTICLE] Aucun champ à modifier")
return self._extraire_article(article)
logger.info(
f"[ARTICLE] Champs à modifier : {', '.join(champs_modifies)}"
)
logger.info("[ARTICLE] Écriture des modifications...")
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:
pass
logger.error(f"[ARTICLE] Erreur Write() : {error_detail}")
raise RuntimeError(f"Échec modification : {error_detail}")
article.Read()
logger.info(
f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)"
)
resultat = self._extraire_article(article)
if not resultat:
resultat = {
"reference": reference,
"designation": getattr(article, "AR_Design", ""),
}
if "prix_vente" in article_data:
resultat["prix_vente"] = float(article_data["prix_vente"])
if "prix_achat" in article_data:
resultat["prix_achat"] = float(article_data["prix_achat"])
if "stock_reel" in article_data:
resultat["stock_reel"] = float(article_data["stock_reel"])
if "stock_mini" in article_data:
resultat["stock_mini"] = float(article_data["stock_mini"])
if "stock_maxi" in article_data:
resultat["stock_maxi"] = float(article_data["stock_maxi"])
if "code_ean" in article_data:
resultat["code_ean"] = str(article_data["code_ean"])
resultat["code_barre"] = str(article_data["code_ean"])
if "description" in article_data:
resultat["description"] = str(article_data["description"])
if "famille" in article_data:
resultat["famille_code"] = (
famille_code_exact if "famille_code_exact" in locals() else ""
)
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:
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 # Re-raise si c'est notre erreur
except:
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 # Toujours Détail
logger.info(f"[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:
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, # Toujours Détail
"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:
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], # datetime - garder tel quel
"cb_creation": row[idx+3], # datetime - garder tel quel
"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], # datetime
"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:
"""
Lit une seule famille - même structure que lister_toutes_familles
Args:
code: Code de la famille à lire
Returns:
Dict avec la structure identique à lister_toutes_familles
"""
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(f"[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:
pass
try:
factory_doc = self.cial.FactoryDocumentStock
persist_doc = factory_doc.CreateType(180) # 180 = Entrée
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(f"[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:
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:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentStockLigne3"
)
ligne_obj.SetDefault()
try:
ligne_obj.SetDefaultArticleReference(
article_ref, float(quantite)
)
except:
try:
ligne_obj.SetDefaultArticle(
article_obj, float(quantite)
)
except:
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:
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(
f" [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:
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:
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:
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(
f" 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(
f" [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:
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:
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(f"[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:
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:
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:
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(f"[STOCK] Transaction committée")
except:
logger.info(f"[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(f"[STOCK] Transaction annulée")
except:
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 lire_stock_article(self, reference: str, calcul_complet: bool = False) -> Dict:
try:
with self._com_context(), self._lock_com:
logger.info(f"[STOCK] Lecture stock : {reference}")
factory_article = self.cial.FactoryArticle
persist_article = factory_article.ReadReference(reference.upper())
if not persist_article:
raise ValueError(f"Article {reference} introuvable")
article = win32com.client.CastTo(persist_article, "IBOArticle3")
article.Read()
ar_suivi = getattr(article, "AR_SuiviStock", 0)
ar_design = getattr(article, "AR_Design", reference)
stock_info = {
"article": reference.upper(),
"designation": ar_design,
"stock_total": 0.0,
"suivi_stock": ar_suivi,
"suivi_libelle": {
0: "Aucun suivi",
1: "CMUP (sans lot)",
2: "FIFO/LIFO (avec lot)",
}.get(ar_suivi, f"Code {ar_suivi}"),
"depots": [],
"methode_lecture": None,
}
logger.info("[STOCK] Tentative Méthode 1 : Depot.FactoryDepotStock...")
try:
factory_depot = self.cial.FactoryDepot
index_depot = 1
stocks_trouves = []
while index_depot <= 20:
try:
persist_depot = factory_depot.List(index_depot)
if persist_depot is None:
break
depot = win32com.client.CastTo(persist_depot, "IBODepot3")
depot.Read()
depot_code = ""
depot_intitule = ""
try:
depot_code = getattr(depot, "DE_Code", "").strip()
depot_intitule = getattr(
depot, "DE_Intitule", f"Dépôt {depot_code}"
)
except:
pass
if not depot_code:
index_depot += 1
continue
factory_depot_stock = None
for factory_name in [
"FactoryDepotStock",
"FactoryArticleStock",
]:
try:
factory_depot_stock = getattr(
depot, factory_name, None
)
if factory_depot_stock:
break
except:
pass
if factory_depot_stock:
index_stock = 1
while index_stock <= 1000:
try:
stock_persist = factory_depot_stock.List(
index_stock
)
if stock_persist is None:
break
stock = win32com.client.CastTo(
stock_persist, "IBODepotStock3"
)
stock.Read()
article_ref_stock = ""
for attr_ref in [
"AR_Ref",
"AS_Article",
"Article_Ref",
]:
try:
val = getattr(stock, attr_ref, None)
if val:
article_ref_stock = (
str(val).strip().upper()
)
break
except:
pass
if not article_ref_stock:
try:
article_obj = getattr(
stock, "Article", None
)
if article_obj:
article_obj.Read()
article_ref_stock = (
getattr(
article_obj, "AR_Ref", ""
)
.strip()
.upper()
)
except:
pass
if article_ref_stock == reference.upper():
quantite = 0.0
qte_mini = 0.0
qte_maxi = 0.0
for attr_qte in [
"AS_QteSto",
"AS_Qte",
"QteSto",
"Quantite",
]:
try:
val = getattr(stock, attr_qte, None)
if val is not None:
quantite = float(val)
break
except:
pass
try:
qte_mini = float(
getattr(stock, "AS_QteMini", 0.0)
)
except:
pass
try:
qte_maxi = float(
getattr(stock, "AS_QteMaxi", 0.0)
)
except:
pass
stocks_trouves.append(
{
"code": depot_code,
"intitule": depot_intitule,
"quantite": quantite,
"qte_mini": qte_mini,
"qte_maxi": qte_maxi,
}
)
logger.info(
f"[STOCK] Trouvé dépôt {depot_code} : {quantite} unités"
)
break
index_stock += 1
except Exception as e:
if "Accès refusé" in str(e):
break
index_stock += 1
index_depot += 1
except Exception as e:
if "Accès refusé" in str(e):
break
index_depot += 1
if stocks_trouves:
stock_info["depots"] = stocks_trouves
stock_info["stock_total"] = sum(
d["quantite"] for d in stocks_trouves
)
stock_info["methode_lecture"] = (
"Depot.FactoryDepotStock (RAPIDE)"
)
logger.info(
f"[STOCK] Méthode 1 réussie : {stock_info['stock_total']} unités"
)
return stock_info
except Exception as e:
logger.warning(f"[STOCK] Méthode 1 échouée : {e}")
logger.info("[STOCK] Tentative Méthode 2 : Attributs Article...")
try:
stock_trouve = False
for attr_stock in ["AR_Stock", "AR_QteSto", "AR_QteStock"]:
try:
val = getattr(article, attr_stock, None)
if val is not None:
stock_info["stock_total"] = float(val)
stock_info["methode_lecture"] = (
f"Article.{attr_stock} (RAPIDE)"
)
stock_trouve = True
logger.info(
f"[STOCK] Méthode 2 réussie via {attr_stock}"
)
break
except:
pass
if stock_trouve:
return stock_info
except Exception as e:
logger.warning(f"[STOCK] Méthode 2 échouée : {e}")
if not calcul_complet:
logger.warning(
f"[STOCK] Méthodes rapides échouées pour {reference}"
)
stock_info["methode_lecture"] = "ÉCHEC - Méthodes rapides échouées"
stock_info["stock_total"] = 0.0
stock_info["note"] = (
"Les méthodes rapides de lecture de stock ont échoué. "
"Options disponibles :\n"
"1. Utiliser GET /sage/diagnostic/stock-rapide-test/{reference} pour un diagnostic détaillé\n"
"2. Appeler lire_stock_article(reference, calcul_complet=True) pour un calcul depuis mouvements (LENT - 20+ min)\n"
"3. Vérifier que l'article a bien un suivi de stock activé (AR_SuiviStock)"
)
return stock_info
logger.warning(
"[STOCK] CALCUL DEPUIS MOUVEMENTS ACTIVÉ - PEUT PRENDRE 20+ MINUTES"
)
except ValueError:
raise
except Exception as e:
logger.error(f"[STOCK] Erreur lecture : {e}", exc_info=True)
raise ValueError(f"Erreur lecture stock : {str(e)}")
def creer_sortie_stock(self, sortie_data: Dict) -> Dict:
try:
with self._com_context(), self._lock_com:
logger.info(f"[STOCK] === CRÉATION SORTIE STOCK ===")
logger.info(f"[STOCK] {len(sortie_data.get('lignes', []))} ligne(s)")
try:
self.cial.CptaApplication.BeginTrans()
except:
pass
try:
factory = self.cial.FactoryDocumentStock
persist = factory.CreateType(181) # 181 = Sortie
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:
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 = self.verifier_stock_suffisant(
article_ref, quantite, 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: # CMUP
if numero_lot:
logger.warning(f"[STOCK] CMUP : Suppression du lot")
numero_lot = None
elif ar_suivi == 2: # FIFO/LIFO
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:
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(f"[STOCK] SetDefaultArticleReference()")
except:
try:
ligne_obj.SetDefaultArticle(
article_obj, float(quantite)
)
article_lie = True
logger.info(f"[STOCK] SetDefaultArticle()")
except:
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(f"[STOCK] Lot défini")
except:
try:
ligne_obj.LS_NoSerie = numero_lot
logger.info(f"[STOCK] Lot via LS_NoSerie")
except:
pass
prix = ligne_data.get("prix_unitaire")
if prix:
try:
ligne_obj.DL_PrixUnitaire = float(prix)
except:
pass
ligne_obj.Write()
logger.info(f"[STOCK] Write() réussi")
ligne_obj.Read()
ref_verifiee = article_ref # Supposer OK si Write() réussi
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:
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(f"[STOCK] Transaction committée")
except:
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:
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:
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:
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:
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:
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 verifier_stock_suffisant(self, article_ref, quantite, depot=None):
"""Version thread-safe avec lock SQL"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
cursor.execute("BEGIN TRANSACTION")
try:
cursor.execute(
"""
SELECT SUM(AS_QteSto)
FROM F_ARTSTOCK WITH (UPDLOCK, ROWLOCK)
WHERE AR_Ref = ?
""",
(article_ref.upper(),),
)
row = cursor.fetchone()
stock_dispo = float(row[0]) if row and row[0] else 0.0
suffisant = stock_dispo >= quantite
cursor.execute("COMMIT")
return {
"suffisant": suffisant,
"stock_disponible": stock_dispo,
"quantite_demandee": quantite,
}
except:
cursor.execute("ROLLBACK")
raise
except Exception as e:
logger.error(f"Erreur vérification stock: {e}")
raise