Sage100-ws/sage_connector.py

11246 lines
489 KiB
Python

import win32com.client
import pythoncom
from datetime import datetime, date
from typing import Dict, List, Optional
import threading
import time
import logging
from config import settings
import pyodbc
from contextlib import contextmanager
import pywintypes
from utils.articles.articles_data_sql import (
_enrichir_stock_emplacements,
_enrichir_gammes_articles,
_enrichir_tarifs_clients,
_enrichir_nomenclature,
_enrichir_compta_articles,
_enrichir_fournisseurs_multiples,
_enrichir_depots_details,
_enrichir_emplacements_details,
_enrichir_gammes_enumeres,
_enrichir_references_enumerees,
_enrichir_medias_articles,
_enrichir_prix_gammes,
_enrichir_conditionnements,
_mapper_article_depuis_row,
_enrichir_stocks_articles,
_enrichir_fournisseurs_articles,
_enrichir_familles_articles,
_enrichir_tva_articles,
)
from utils.tiers.clients.clients_data import (
_extraire_client,
_cast_client,
)
from utils.articles.stock_check import verifier_stock_suffisant
from utils.articles.articles_data_com import _extraire_article
from utils.tiers.tiers_data_sql import _build_tiers_select_query
from utils.functions.functions import (
_safe_strip,
_safe_int,
_clean_str,
_try_set_attribute,
normaliser_date,
_get_type_libelle,
)
from utils.functions.items_to_dict import (
_contact_to_dict,
_row_to_contact_dict,
_row_to_tiers_dict,
)
from utils.functions.sage_utilities import (
_verifier_devis_non_transforme,
peut_etre_transforme,
lire_erreurs_sage,
)
from utils.documents.documents_data_sql import (
_afficher_etat_document,
_compter_lignes_document,
_rechercher_devis_par_numero,
_lire_document_sql,
_lister_documents_avec_lignes_sql,
)
from utils.documents.devis.devis_extraction import _extraire_infos_devis
from utils.documents.devis.devis_check import (
_recuperer_numero_devis,
_rechercher_devis_dans_liste,
)
from utils.tiers.contacts.contacts import (
_get_contacts_client,
_chercher_contact_en_base,
_lire_contact_depuis_base,
)
from utils import (
valider_donnees_creation,
mapper_champ_api_vers_sage,
CHAMPS_STOCK_INITIAL,
CHAMPS_ASSIGNABLES_CREATION,
CHAMPS_ASSIGNABLES_MODIFICATION,
CHAMPS_OBJETS_SPECIAUX,
valider_champ,
valider_donnees_modification,
obtenir_champs_assignables,
)
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 Exception:
pass
def connecter(self):
"""Connexion initiale à Sage - VERSION HYBRIDE"""
try:
with self._com_context():
self.cial = win32com.client.gencache.EnsureDispatch(
"Objets100c.Cial.Stream"
)
self.cial.Name = self.chemin_base
self.cial.Loggable.UserName = self.utilisateur
self.cial.Loggable.UserPwd = self.mot_de_passe
self.cial.Open()
logger.info(f" Connexion COM Sage réussie: {self.chemin_base}")
try:
with self._get_sql_connection() as conn:
conn.cursor()
except Exception as e:
logger.warning(f"SQL non disponible: {e}")
logger.warning(" Les lectures utiliseront COM (plus lent)")
return True
except Exception as e:
logger.error(f" Erreur connexion Sage: {e}", exc_info=True)
return False
def deconnecter(self):
"""Déconnexion propre"""
if self.cial:
try:
with self._com_context():
self.cial.Close()
logger.info("Connexion Sage fermée")
except Exception:
pass
def lister_tous_fournisseurs(self, filtre=""):
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": _safe_strip(row.CT_Num),
"intitule": _safe_strip(row.CT_Intitule),
"type_tiers": row.CT_Type,
"qualite": _safe_strip(row.CT_Qualite),
"classement": _safe_strip(row.CT_Classement),
"raccourci": _safe_strip(row.CT_Raccourci),
"siret": _safe_strip(row.CT_Siret),
"tva_intra": _safe_strip(row.CT_Identifiant),
"code_naf": _safe_strip(row.CT_Ape),
"contact": _safe_strip(row.CT_Contact),
"adresse": _safe_strip(row.CT_Adresse),
"complement": _safe_strip(row.CT_Complement),
"code_postal": _safe_strip(row.CT_CodePostal),
"ville": _safe_strip(row.CT_Ville),
"region": _safe_strip(row.CT_CodeRegion),
"pays": _safe_strip(row.CT_Pays),
"telephone": _safe_strip(row.CT_Telephone),
"telecopie": _safe_strip(row.CT_Telecopie),
"email": _safe_strip(row.CT_EMail),
"site_web": _safe_strip(row.CT_Site),
"facebook": _safe_strip(row.CT_Facebook),
"linkedin": _safe_strip(row.CT_LinkedIn),
"taux01": row.CT_Taux01,
"taux02": row.CT_Taux02,
"taux03": row.CT_Taux03,
"taux04": row.CT_Taux04,
"statistique01": _safe_strip(row.CT_Statistique01),
"statistique02": _safe_strip(row.CT_Statistique02),
"statistique03": _safe_strip(row.CT_Statistique03),
"statistique04": _safe_strip(row.CT_Statistique04),
"statistique05": _safe_strip(row.CT_Statistique05),
"statistique06": _safe_strip(row.CT_Statistique06),
"statistique07": _safe_strip(row.CT_Statistique07),
"statistique08": _safe_strip(row.CT_Statistique08),
"statistique09": _safe_strip(row.CT_Statistique09),
"statistique10": _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": _safe_strip(row.CT_Commentaire),
"section_analytique": _safe_strip(row.CA_Num),
"mode_reglement_code": row.MR_No,
"surveillance_active": (row.CT_Surveillance == 1),
"coface": _safe_strip(row.CT_Coface),
"forme_juridique": _safe_strip(row.CT_SvFormeJuri),
"effectif": _safe_strip(row.CT_SvEffectif),
"sv_regularite": _safe_strip(row.CT_SvRegul),
"sv_cotation": _safe_strip(row.CT_SvCotation),
"sv_objet_maj": _safe_strip(row.CT_SvObjetMaj),
"sv_chiffre_affaires": row.CT_SvCA,
"sv_resultat": row.CT_SvResultat,
"compte_general": _safe_strip(row.CG_NumPrinc),
"categorie_tarif": row.N_CatTarif,
"categorie_compta": row.N_CatCompta,
}
fournisseur["contacts"] = _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):
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": _safe_strip(row.CT_Num),
"intitule": _safe_strip(row.CT_Intitule),
"type_tiers": row.CT_Type,
"qualite": _safe_strip(row.CT_Qualite),
"classement": _safe_strip(row.CT_Classement),
"raccourci": _safe_strip(row.CT_Raccourci),
"siret": _safe_strip(row.CT_Siret),
"tva_intra": _safe_strip(row.CT_Identifiant),
"code_naf": _safe_strip(row.CT_Ape),
"contact": _safe_strip(row.CT_Contact),
"adresse": _safe_strip(row.CT_Adresse),
"complement": _safe_strip(row.CT_Complement),
"code_postal": _safe_strip(row.CT_CodePostal),
"ville": _safe_strip(row.CT_Ville),
"region": _safe_strip(row.CT_CodeRegion),
"pays": _safe_strip(row.CT_Pays),
"telephone": _safe_strip(row.CT_Telephone),
"telecopie": _safe_strip(row.CT_Telecopie),
"email": _safe_strip(row.CT_EMail),
"site_web": _safe_strip(row.CT_Site),
"facebook": _safe_strip(row.CT_Facebook),
"linkedin": _safe_strip(row.CT_LinkedIn),
"taux01": row.CT_Taux01,
"taux02": row.CT_Taux02,
"taux03": row.CT_Taux03,
"taux04": row.CT_Taux04,
"statistique01": _safe_strip(row.CT_Statistique01),
"statistique02": _safe_strip(row.CT_Statistique02),
"statistique03": _safe_strip(row.CT_Statistique03),
"statistique04": _safe_strip(row.CT_Statistique04),
"statistique05": _safe_strip(row.CT_Statistique05),
"statistique06": _safe_strip(row.CT_Statistique06),
"statistique07": _safe_strip(row.CT_Statistique07),
"statistique08": _safe_strip(row.CT_Statistique08),
"statistique09": _safe_strip(row.CT_Statistique09),
"statistique10": _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": _safe_strip(row.CT_Commentaire),
"section_analytique": _safe_strip(row.CA_Num),
"mode_reglement_code": row.MR_No,
"surveillance_active": (row.CT_Surveillance == 1),
"coface": _safe_strip(row.CT_Coface),
"forme_juridique": _safe_strip(row.CT_SvFormeJuri),
"effectif": _safe_strip(row.CT_SvEffectif),
"sv_regularite": _safe_strip(row.CT_SvRegul),
"sv_cotation": _safe_strip(row.CT_SvCotation),
"sv_objet_maj": _safe_strip(row.CT_SvObjetMaj),
"sv_chiffre_affaires": row.CT_SvCA,
"sv_resultat": row.CT_SvResultat,
"compte_general": _safe_strip(row.CG_NumPrinc),
"categorie_tarif": row.N_CatTarif,
"categorie_compta": row.N_CatCompta,
}
fournisseur["contacts"] = _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
logger.debug(" CT_Type: 1 (Fournisseur)")
except Exception:
logger.debug(" CT_Type non défini (géré par FactoryFournisseur)")
try:
fournisseur.CT_Qualite = "FOU"
logger.debug(" CT_Qualite: 'FOU'")
except Exception:
logger.debug(" CT_Qualite non défini (pas critique)")
try:
factory_compte = self.cial.CptaApplication.FactoryCompteG
persist_compte = factory_compte.ReadNumero(compte)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
fournisseur.CompteGPrinc = compte_obj
logger.debug(f" CompteGPrinc: objet '{compte}' assigné")
else:
logger.warning(
f" Compte {compte} introuvable - utilisation défaut"
)
except Exception as e:
logger.warning(f" Erreur CompteGPrinc: {e}")
if num_prop:
fournisseur.CT_Num = num_prop
logger.debug(f" CT_Num fourni: '{num_prop}'")
else:
try:
if hasattr(fournisseur, "SetDefaultNumPiece"):
fournisseur.SetDefaultNumPiece()
num_genere = getattr(fournisseur, "CT_Num", "")
logger.debug(f" CT_Num auto-généré: '{num_genere}'")
else:
num_genere = factory_fournisseur.GetNextNumero()
if num_genere:
fournisseur.CT_Num = num_genere
logger.debug(
f" CT_Num auto (GetNextNumero): '{num_genere}'"
)
else:
import time
num_genere = f"FOUR{int(time.time()) % 1000000}"
fournisseur.CT_Num = num_genere
logger.warning(f" CT_Num fallback: '{num_genere}'")
except Exception as e:
logger.error(f" Impossible de générer CT_Num: {e}")
raise ValueError(
"Impossible de générer le numéro fournisseur automatiquement"
)
try:
if hasattr(fournisseur, "N_CatTarif"):
fournisseur.N_CatTarif = 1
if hasattr(fournisseur, "N_CatCompta"):
fournisseur.N_CatCompta = 1
if hasattr(fournisseur, "N_Period"):
fournisseur.N_Period = 1
logger.debug(" Catégories (N_*) initialisées")
except Exception as e:
logger.warning(f" Catégories: {e}")
logger.info(" Définition champs optionnels...")
if any([adresse, code_postal, ville, pays]):
try:
adresse_obj = fournisseur.Adresse
if adresse:
adresse_obj.Adresse = adresse
if code_postal:
adresse_obj.CodePostal = code_postal
if ville:
adresse_obj.Ville = ville
if pays:
adresse_obj.Pays = pays
logger.debug(" Adresse définie")
except Exception as e:
logger.warning(f" Adresse: {e}")
if telephone or email:
try:
telecom_obj = fournisseur.Telecom
if telephone:
telecom_obj.Telephone = telephone
if email:
telecom_obj.EMail = email
logger.debug(" Télécom défini")
except Exception as e:
logger.warning(f" Télécom: {e}")
if siret:
try:
fournisseur.CT_Siret = siret
logger.debug(f" SIRET: '{siret}'")
except Exception as e:
logger.warning(f" SIRET: {e}")
if tva_intra:
try:
fournisseur.CT_Identifiant = tva_intra
logger.debug(f" TVA intra: '{tva_intra}'")
except Exception as e:
logger.warning(f" TVA: {e}")
try:
if hasattr(fournisseur, "CT_Lettrage"):
fournisseur.CT_Lettrage = True
if hasattr(fournisseur, "CT_Sommeil"):
fournisseur.CT_Sommeil = False
logger.debug(" Options par défaut définies")
except Exception as e:
logger.debug(f" Options: {e}")
logger.info(" === DIAGNOSTIC PRÉ-WRITE ===")
num_avant_write = getattr(fournisseur, "CT_Num", "")
if not num_avant_write:
logger.error(" CRITIQUE: CT_Num toujours vide !")
raise ValueError("Le numéro fournisseur (CT_Num) est obligatoire")
logger.info(f" CT_Num confirmé: '{num_avant_write}'")
logger.info(" Écriture du fournisseur dans Sage...")
try:
fournisseur.Write()
logger.info(" Write() réussi !")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
logger.error(f" Erreur Sage: {error_detail}")
except Exception:
pass
if (
"doublon" in error_detail.lower()
or "existe" in error_detail.lower()
):
raise ValueError(f"Ce fournisseur existe déjà : {error_detail}")
raise RuntimeError(f"Échec Write(): {error_detail}")
try:
fournisseur.Read()
except Exception as e:
logger.warning(f"Impossible de relire: {e}")
num_final = getattr(fournisseur, "CT_Num", "")
if not num_final:
raise RuntimeError("CT_Num vide après Write()")
logger.info(f" FOURNISSEUR CRÉÉ: {num_final} - {intitule} ")
resultat = {
"numero": num_final,
"intitule": intitule,
"compte_collectif": compte,
"type": 1,
"est_fournisseur": True,
"adresse": adresse or None,
"code_postal": code_postal or None,
"ville": ville or None,
"pays": pays or None,
"email": email or None,
"telephone": telephone or None,
"siret": siret or None,
"tva_intra": tva_intra or None,
}
return resultat
except ValueError as e:
logger.error(f" Erreur métier: {e}")
raise
except Exception as e:
logger.error(f" Erreur création fournisseur: {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = f"Erreur Sage: {err.Description}"
except Exception:
pass
raise RuntimeError(f"Erreur technique Sage: {error_message}")
def modifier_fournisseur(self, code: str, fournisseur_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f" Recherche fournisseur {code}...")
factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur
persist = factory_fournisseur.ReadNumero(code)
if not persist:
raise ValueError(f"Fournisseur {code} introuvable")
fournisseur = _cast_client(persist)
if not fournisseur:
raise ValueError(f"Impossible de charger le fournisseur {code}")
logger.info(
f" Fournisseur {code} trouvé: {getattr(fournisseur, 'CT_Intitule', '')}"
)
logger.info(" Mise à jour des champs...")
champs_modifies = []
if "intitule" in fournisseur_data:
intitule = str(fournisseur_data["intitule"])[:69].strip()
fournisseur.CT_Intitule = intitule
champs_modifies.append(f"intitule='{intitule}'")
if any(
k in fournisseur_data
for k in ["adresse", "code_postal", "ville", "pays"]
):
try:
adresse_obj = fournisseur.Adresse
if "adresse" in fournisseur_data:
adresse = str(fournisseur_data["adresse"])[:35].strip()
adresse_obj.Adresse = adresse
champs_modifies.append("adresse")
if "code_postal" in fournisseur_data:
cp = str(fournisseur_data["code_postal"])[:9].strip()
adresse_obj.CodePostal = cp
champs_modifies.append("code_postal")
if "ville" in fournisseur_data:
ville = str(fournisseur_data["ville"])[:35].strip()
adresse_obj.Ville = ville
champs_modifies.append("ville")
if "pays" in fournisseur_data:
pays = str(fournisseur_data["pays"])[:35].strip()
adresse_obj.Pays = pays
champs_modifies.append("pays")
except Exception as e:
logger.warning(f"Erreur mise à jour adresse: {e}")
if "email" in fournisseur_data or "telephone" in fournisseur_data:
try:
telecom_obj = fournisseur.Telecom
if "email" in fournisseur_data:
email = str(fournisseur_data["email"])[:69].strip()
telecom_obj.EMail = email
champs_modifies.append("email")
if "telephone" in fournisseur_data:
tel = str(fournisseur_data["telephone"])[:21].strip()
telecom_obj.Telephone = tel
champs_modifies.append("telephone")
except Exception as e:
logger.warning(f"Erreur mise à jour télécom: {e}")
if "siret" in fournisseur_data:
try:
siret = str(fournisseur_data["siret"])[:14].strip()
fournisseur.CT_Siret = siret
champs_modifies.append("siret")
except Exception as e:
logger.warning(f"Erreur mise à jour SIRET: {e}")
if "tva_intra" in fournisseur_data:
try:
tva = str(fournisseur_data["tva_intra"])[:25].strip()
fournisseur.CT_Identifiant = tva
champs_modifies.append("tva_intra")
except Exception as e:
logger.warning(f"Erreur mise à jour TVA: {e}")
if not champs_modifies:
logger.warning("Aucun champ à modifier")
return {
"numero": getattr(fournisseur, "CT_Num", "").strip(),
"intitule": getattr(fournisseur, "CT_Intitule", "").strip(),
"type": 1,
"est_fournisseur": True,
}
logger.info(f" Champs à modifier: {', '.join(champs_modifies)}")
logger.info(" Écriture des modifications...")
try:
fournisseur.Write()
logger.info(" Write() réussi !")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f" Erreur Write(): {error_detail}")
raise RuntimeError(f"Échec modification: {error_detail}")
fournisseur.Read()
logger.info(
f" FOURNISSEUR MODIFIÉ: {code} ({len(champs_modifies)} champs) "
)
numero = getattr(fournisseur, "CT_Num", "").strip()
intitule = getattr(fournisseur, "CT_Intitule", "").strip()
data = {
"numero": numero,
"intitule": intitule,
"type": 1,
"est_fournisseur": True,
}
try:
adresse_obj = getattr(fournisseur, "Adresse", None)
if adresse_obj:
data["adresse"] = getattr(adresse_obj, "Adresse", "").strip()
data["code_postal"] = getattr(
adresse_obj, "CodePostal", ""
).strip()
data["ville"] = getattr(adresse_obj, "Ville", "").strip()
except Exception:
data["adresse"] = ""
data["code_postal"] = ""
data["ville"] = ""
try:
telecom_obj = getattr(fournisseur, "Telecom", None)
if telecom_obj:
data["telephone"] = getattr(
telecom_obj, "Telephone", ""
).strip()
data["email"] = getattr(telecom_obj, "EMail", "").strip()
except Exception:
data["telephone"] = ""
data["email"] = ""
return data
except ValueError as e:
logger.error(f" Erreur métier: {e}")
raise
except Exception as e:
logger.error(f" Erreur modification fournisseur: {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = f"Erreur Sage: {err.Description}"
except Exception:
pass
raise RuntimeError(f"Erreur technique Sage: {error_message}")
def lister_tous_clients(self, filtre=""):
"""
Liste tous les clients avec 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": _safe_strip(row.CT_Num),
"intitule": _safe_strip(row.CT_Intitule),
"type_tiers": row.CT_Type,
"qualite": _safe_strip(row.CT_Qualite),
"classement": _safe_strip(row.CT_Classement),
"raccourci": _safe_strip(row.CT_Raccourci),
"siret": _safe_strip(row.CT_Siret),
"tva_intra": _safe_strip(row.CT_Identifiant),
"code_naf": _safe_strip(row.CT_Ape),
"contact": _safe_strip(row.CT_Contact),
"adresse": _safe_strip(row.CT_Adresse),
"complement": _safe_strip(row.CT_Complement),
"code_postal": _safe_strip(row.CT_CodePostal),
"ville": _safe_strip(row.CT_Ville),
"region": _safe_strip(row.CT_CodeRegion),
"pays": _safe_strip(row.CT_Pays),
"telephone": _safe_strip(row.CT_Telephone),
"telecopie": _safe_strip(row.CT_Telecopie),
"email": _safe_strip(row.CT_EMail),
"site_web": _safe_strip(row.CT_Site),
"facebook": _safe_strip(row.CT_Facebook),
"linkedin": _safe_strip(row.CT_LinkedIn),
"taux01": row.CT_Taux01,
"taux02": row.CT_Taux02,
"taux03": row.CT_Taux03,
"taux04": row.CT_Taux04,
"statistique01": _safe_strip(row.CT_Statistique01),
"statistique02": _safe_strip(row.CT_Statistique02),
"statistique03": _safe_strip(row.CT_Statistique03),
"statistique04": _safe_strip(row.CT_Statistique04),
"statistique05": _safe_strip(row.CT_Statistique05),
"statistique06": _safe_strip(row.CT_Statistique06),
"statistique07": _safe_strip(row.CT_Statistique07),
"statistique08": _safe_strip(row.CT_Statistique08),
"statistique09": _safe_strip(row.CT_Statistique09),
"statistique10": _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": _safe_strip(row.CT_Commentaire),
"section_analytique": _safe_strip(row.CA_Num),
"mode_reglement_code": row.MR_No,
"surveillance_active": (row.CT_Surveillance == 1),
"coface": _safe_strip(row.CT_Coface),
"forme_juridique": _safe_strip(row.CT_SvFormeJuri),
"effectif": _safe_strip(row.CT_SvEffectif),
"sv_regularite": _safe_strip(row.CT_SvRegul),
"sv_cotation": _safe_strip(row.CT_SvCotation),
"sv_objet_maj": _safe_strip(row.CT_SvObjetMaj),
"sv_chiffre_affaires": row.CT_SvCA,
"sv_resultat": row.CT_SvResultat,
"compte_general": _safe_strip(row.CG_NumPrinc),
"categorie_tarif": row.N_CatTarif,
"categorie_compta": row.N_CatCompta,
}
client["contacts"] = _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": _safe_strip(row.CT_Num),
"intitule": _safe_strip(row.CT_Intitule),
"type_tiers": row.CT_Type,
"qualite": _safe_strip(row.CT_Qualite),
"classement": _safe_strip(row.CT_Classement),
"raccourci": _safe_strip(row.CT_Raccourci),
"siret": _safe_strip(row.CT_Siret),
"tva_intra": _safe_strip(row.CT_Identifiant),
"code_naf": _safe_strip(row.CT_Ape),
"contact": _safe_strip(row.CT_Contact),
"adresse": _safe_strip(row.CT_Adresse),
"complement": _safe_strip(row.CT_Complement),
"code_postal": _safe_strip(row.CT_CodePostal),
"ville": _safe_strip(row.CT_Ville),
"region": _safe_strip(row.CT_CodeRegion),
"pays": _safe_strip(row.CT_Pays),
"telephone": _safe_strip(row.CT_Telephone),
"telecopie": _safe_strip(row.CT_Telecopie),
"email": _safe_strip(row.CT_EMail),
"site_web": _safe_strip(row.CT_Site),
"facebook": _safe_strip(row.CT_Facebook),
"linkedin": _safe_strip(row.CT_LinkedIn),
"taux01": row.CT_Taux01,
"taux02": row.CT_Taux02,
"taux03": row.CT_Taux03,
"taux04": row.CT_Taux04,
"statistique01": _safe_strip(row.CT_Statistique01),
"statistique02": _safe_strip(row.CT_Statistique02),
"statistique03": _safe_strip(row.CT_Statistique03),
"statistique04": _safe_strip(row.CT_Statistique04),
"statistique05": _safe_strip(row.CT_Statistique05),
"statistique06": _safe_strip(row.CT_Statistique06),
"statistique07": _safe_strip(row.CT_Statistique07),
"statistique08": _safe_strip(row.CT_Statistique08),
"statistique09": _safe_strip(row.CT_Statistique09),
"statistique10": _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": _safe_strip(row.CT_Commentaire),
"section_analytique": _safe_strip(row.CA_Num),
"mode_reglement_code": row.MR_No,
"surveillance_active": (row.CT_Surveillance == 1),
"coface": _safe_strip(row.CT_Coface),
"forme_juridique": _safe_strip(row.CT_SvFormeJuri),
"effectif": _safe_strip(row.CT_SvEffectif),
"sv_regularite": _safe_strip(row.CT_SvRegul),
"sv_cotation": _safe_strip(row.CT_SvCotation),
"sv_objet_maj": _safe_strip(row.CT_SvObjetMaj),
"sv_chiffre_affaires": row.CT_SvCA,
"sv_resultat": row.CT_SvResultat,
"compte_general": _safe_strip(row.CG_NumPrinc),
"categorie_tarif": row.N_CatTarif,
"categorie_compta": row.N_CatCompta,
}
client["contacts"] = _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 = _mapper_article_depuis_row(row_data, colonnes_config)
articles.append(article_data)
articles = _enrichir_stocks_articles(articles, cursor)
articles = _enrichir_familles_articles(articles, cursor)
articles = _enrichir_fournisseurs_articles(articles, cursor)
articles = _enrichir_tva_articles(articles, cursor)
articles = _enrichir_stock_emplacements(articles, cursor)
articles = _enrichir_gammes_articles(articles, cursor)
articles = _enrichir_tarifs_clients(articles, cursor)
articles = _enrichir_nomenclature(articles, cursor)
articles = _enrichir_compta_articles(articles, cursor)
articles = _enrichir_fournisseurs_multiples(articles, cursor)
articles = _enrichir_depots_details(articles, cursor)
articles = _enrichir_emplacements_details(articles, cursor)
articles = _enrichir_gammes_enumeres(articles, cursor)
articles = _enrichir_references_enumerees(articles, cursor)
articles = _enrichir_medias_articles(articles, cursor)
articles = _enrichir_prix_gammes(articles, cursor)
articles = _enrichir_conditionnements(articles, cursor)
return articles
except Exception as e:
logger.error(f" Erreur SQL articles: {e}", exc_info=True)
raise RuntimeError(f"Erreur lecture articles: {str(e)}")
def lire_article(self, reference):
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
AR_Ref, AR_Design, AR_PrixVen, AR_PrixAch,
AR_UniteVen, FA_CodeFamille, AR_Sommeil,
AR_CodeBarre, AR_Type
FROM F_ARTICLE
WHERE AR_Ref = ?
""",
(reference.upper(),),
)
row = cursor.fetchone()
if not row:
return None
article = {
"reference": _safe_strip(row[0]),
"designation": _safe_strip(row[1]),
"prix_vente": float(row[2]) if row[2] is not None else 0.0,
"prix_achat": float(row[3]) if row[3] is not None else 0.0,
"unite_vente": str(row[4]).strip() if row[4] is not None else "",
"famille_code": _safe_strip(row[5]),
"est_actif": (row[6] == 0),
"code_ean": _safe_strip(row[7]),
"code_barre": _safe_strip(row[7]),
"type_article": row[8] if row[8] is not None else 0,
"type_article_libelle": {
0: "Article",
1: "Prestation",
2: "Divers",
}.get(row[8] if row[8] is not None else 0, "Article"),
"description": "",
"designation_complementaire": "",
"poids": 0.0,
"volume": 0.0,
"tva_code": "",
"date_creation": "",
"date_modification": "",
"stock_reel": 0.0,
"stock_mini": 0.0,
"stock_maxi": 0.0,
"stock_reserve": 0.0,
"stock_commande": 0.0,
"stock_disponible": 0.0,
}
article["tva_taux"] = 20.0
logger.info(f" Lecture stock depuis F_ARTSTOCK pour {reference}...")
try:
cursor.execute(
"""
SELECT
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 = ?
GROUP BY AR_Ref
""",
(reference.upper(),),
)
stock_row = cursor.fetchone()
if stock_row:
article["stock_reel"] = (
float(stock_row[0]) if stock_row[0] else 0.0
)
article["stock_mini"] = (
float(stock_row[1]) if stock_row[1] else 0.0
)
article["stock_maxi"] = (
float(stock_row[2]) if stock_row[2] else 0.0
)
stock_reserve_artstock = (
float(stock_row[3]) if stock_row[3] else 0.0
)
stock_commande_artstock = (
float(stock_row[4]) if stock_row[4] else 0.0
)
if stock_reserve_artstock > 0:
article["stock_reserve"] = stock_reserve_artstock
if stock_commande_artstock > 0:
article["stock_commande"] = stock_commande_artstock
article["stock_disponible"] = (
article["stock_reel"] - article["stock_reserve"]
)
logger.info(
f" Stock trouvé dans F_ARTSTOCK: {article['stock_reel']} unités"
)
else:
logger.info(
f"Aucun stock trouvé dans F_ARTSTOCK pour {reference}"
)
except Exception as e:
logger.error(f" Erreur lecture F_ARTSTOCK pour {reference}: {e}")
if article["famille_code"]:
try:
cursor.execute(
"SELECT FA_Intitule FROM F_FAMILLE WHERE FA_CodeFamille = ?",
(article["famille_code"],),
)
famille_row = cursor.fetchone()
if famille_row:
article["famille_libelle"] = _safe_strip(famille_row[0])
else:
article["famille_libelle"] = ""
except Exception:
article["famille_libelle"] = ""
else:
article["famille_libelle"] = ""
return article
except Exception as e:
logger.error(f" Erreur SQL article {reference}: {e}")
return None
def obtenir_contact(self, numero: str, contact_numero: int) -> Optional[Dict]:
"""
Récupère un contact spécifique par son CT_No
"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT
CT_Num, CT_No, N_Contact,
CT_Civilite, CT_Nom, CT_Prenom, CT_Fonction,
N_Service,
CT_Telephone, CT_TelPortable, CT_Telecopie, CT_EMail,
CT_Facebook, CT_LinkedIn, CT_Skype
FROM F_CONTACTT
WHERE CT_Num = ? AND CT_No = ?
"""
cursor.execute(query, [numero, contact_numero])
row = cursor.fetchone()
if not row:
return None
return _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 lister_tous_devis_cache(self, filtre=""):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lister_documents_avec_lignes_sql(cursor, type_doc=0, filtre=filtre)
def lire_devis_cache(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=0)
def lister_toutes_commandes_cache(self, filtre=""):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lister_documents_avec_lignes_sql(cursor, type_doc=1, filtre=filtre)
def lire_commande_cache(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=1)
def lister_toutes_factures_cache(self, filtre=""):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lister_documents_avec_lignes_sql(cursor, type_doc=6, filtre=filtre)
def lire_facture_cache(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=6)
def lister_tous_fournisseurs_cache(self, filtre=""):
return self.lister_tous_fournisseurs()
def lire_fournisseur_cache(self, code):
return self.lire_fournisseur()
def lister_toutes_livraisons_cache(self, filtre=""):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lister_documents_avec_lignes_sql(cursor, type_doc=3, filtre=filtre)
def lire_livraison_cache(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=3)
def lister_tous_avoirs_cache(self, filtre=""):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lister_documents_avec_lignes_sql(cursor, type_doc=5, filtre=filtre)
def lire_avoir_cache(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=5)
def creer_devis_enrichi(self, devis_data: dict):
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(
f" Début création devis pour client {devis_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 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 Exception:
pass
logger.info(" Document devis créé")
doc.DO_Date = pywintypes.Time(
normaliser_date(devis_data.get("date_devis"))
)
if "date_livraison" in devis_data and devis_data["date_livraison"]:
doc.DO_DateLivr = pywintypes.Time(
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 = _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é")
doc.DO_Statut = 0
doc.Write()
try:
factory_lignes = doc.FactoryDocumentLigne
except Exception:
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 Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except Exception:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except Exception:
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()
try:
process.Process()
logger.info(" Process() appelé (brouillon)")
except Exception:
logger.debug("Process() ignoré pour brouillon")
numero_devis = _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 Exception:
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)
logger.info(
f" DEVIS CRÉÉ: {numero_devis} - {doc_final_data['total_ttc']}€ TTC "
)
return doc_final_data
except Exception:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.error(" Transaction annulée (rollback)")
except Exception:
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 _relire_devis(self, numero_devis, devis_data):
"""Relit le devis créé et extrait les informations finales."""
factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(0, numero_devis)
if not persist_reread:
logger.debug("ReadPiece échoué, recherche dans List()...")
persist_reread = _rechercher_devis_dans_liste(numero_devis, factory_doc)
if persist_reread:
doc_final = win32com.client.CastTo(persist_reread, "IBODocumentVente3")
doc_final.Read()
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
statut_final = getattr(doc_final, "DO_Statut", 0)
reference_final = getattr(doc_final, "DO_Ref", "")
date_livraison_final = None
try:
date_livr = getattr(doc_final, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except Exception:
pass
else:
total_calcule = sum(
ligne.get("montant_ligne_ht", 0) for ligne in devis_data["lignes"]
)
total_ht = total_calcule
total_ttc = round(total_calcule * 1.20, 2)
statut_final = 0
reference_final = devis_data.get("reference", "")
date_livraison_final = devis_data.get("date_livraison")
logger.info(f" Total HT: {total_ht}")
logger.info(f" Total TTC: {total_ttc}")
logger.info(f" Statut final: {statut_final}")
if reference_final:
logger.info(f" Référence: {reference_final}")
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
return {
"numero_devis": numero_devis,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(devis_data["lignes"]),
"client_code": devis_data["client"]["code"],
"date_devis": str(devis_data.get("date_devis", "")),
"date_livraison": date_livraison_final,
"reference": reference_final,
"statut": statut_final,
}
def modifier_devis(self, numero: str, devis_data: Dict) -> Dict:
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,
self._get_sql_connection() as conn,
):
cursor = conn.cursor()
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("")
_afficher_etat_document(doc, "📸 ÉTAT INITIAL")
logger.info(" Vérification statut transformation...")
_verifier_devis_non_transforme(numero, doc, cursor)
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 = _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 = 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 = 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 = 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 = 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 Exception:
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 Exception:
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(" Article chargé")
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
try:
ligne_obj.SetDefaultArticleReference(
article_code, quantite
)
logger.info(
" Article associé via SetDefaultArticleReference"
)
except Exception:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(
" Article associé via SetDefaultArticle"
)
except Exception:
ligne_obj.DL_Design = ligne_data.get(
"designation", ""
)
ligne_obj.DL_Qte = quantite
logger.info(" Article associé manuellement")
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(
ligne_data["prix_unitaire_ht"]
)
logger.info(" 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(" Remise définie")
except Exception:
logger.debug(" 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(
" 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("")
_afficher_etat_document(doc, "📸 ÉTAT FINAL")
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 9] EXTRACTION RÉSULTAT")
logger.info("=" * 80)
resultat = _extraire_infos_devis(doc, numero, champs_modifies)
logger.info(" 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 _charger_devis(self, numero: str):
"""Charge un devis depuis Sage."""
logger.info(f" Chargement devis {numero}...")
factory = self.cial.FactoryDocumentVente
logger.info(" Tentative ReadPiece(0, numero)...")
persist = factory.ReadPiece(0, numero)
if not persist:
logger.warning(" ReadPiece a échoué, recherche dans la liste...")
persist = _rechercher_devis_par_numero(numero, factory)
if not persist:
raise ValueError(f" Devis {numero} INTROUVABLE")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
logger.info(f" Devis {numero} chargé")
return doc
def lire_devis(self, numero_devis):
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
devis = _lire_document_sql(cursor, numero_devis, type_doc=0)
if not devis:
return None
return devis
except Exception as e:
logger.error(f" Erreur SQL lecture devis {numero_devis}: {e}")
return None
def lire_document(self, numero, type_doc):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc)
def transformer_document(
self,
numero_source,
type_source,
type_cible,
conserver_document_source=True,
verifier_doublons=True,
):
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
type_source = int(type_source)
type_cible = int(type_cible)
logger.info(
f"[TRANSFORM] Transformation: {numero_source} ({type_source}) → type {type_cible}"
)
transformations_valides = {
(0, 10): ("Vente", "CreateProcess_Commander"),
(0, 60): ("Vente", "CreateProcess_Facturer"),
(10, 30): ("Vente", "CreateProcess_Livrer"),
(10, 60): ("Vente", "CreateProcess_Facturer"),
(30, 60): ("Vente", "CreateProcess_Facturer"),
}
if (type_source, type_cible) not in transformations_valides:
raise ValueError(
f"Transformation non autorisée: "
f"{_get_type_libelle(type_source)}{_get_type_libelle(type_cible)}"
)
module, methode = transformations_valides[(type_source, type_cible)]
logger.info(f"[TRANSFORM] Méthode: Transformation.{module}.{methode}()")
try:
with (
self._com_context(),
self._lock_com,
self._get_sql_connection() as conn,
):
cursor = conn.cursor()
if verifier_doublons:
logger.info("[TRANSFORM] Vérification des doublons...")
verif = peut_etre_transforme(
cursor, numero_source, type_source, type_cible
)
if not verif["possible"]:
docs = [
d["numero"] for d in verif.get("documents_existants", [])
]
raise ValueError(
f"{verif['raison']}. Document(s) existant(s): {', '.join(docs)}"
)
logger.info("[TRANSFORM] Aucun doublon détecté")
factory = self.cial.FactoryDocumentVente
logger.info(f"[TRANSFORM] Lecture de {numero_source}...")
if not factory.ExistPiece(type_source, numero_source):
raise ValueError(f"Document {numero_source} introuvable")
persist_source = factory.ReadPiece(type_source, numero_source)
if not persist_source:
raise ValueError(f"Impossible de lire {numero_source}")
doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3")
doc_source.Read()
statut_source = getattr(doc_source, "DO_Statut", 0)
nb_lignes_source = 0
try:
factory_lignes = getattr(doc_source, "FactoryDocumentLigne", None)
if factory_lignes:
lignes_list = factory_lignes.List
nb_lignes_source = lignes_list.Count if lignes_list else 0
except Exception:
pass
logger.info(
f"[TRANSFORM] Source: statut={statut_source}, {nb_lignes_source} ligne(s)"
)
if nb_lignes_source == 0:
raise ValueError(f"Document {numero_source} vide (0 lignes)")
logger.info("[TRANSFORM] 🔧 Création du transformer...")
transformation = getattr(self.cial, "Transformation", None)
if not transformation:
raise RuntimeError("API Transformation non disponible")
module_obj = getattr(transformation, module, None)
if not module_obj:
raise RuntimeError(f"Module {module} non disponible")
methode_func = getattr(module_obj, methode, None)
if not methode_func:
raise RuntimeError(f"Méthode {methode} non disponible")
transformer = methode_func()
if not transformer:
raise RuntimeError("Échec création transformer")
logger.info("[TRANSFORM] Transformer créé")
logger.info("[TRANSFORM] Configuration...")
if hasattr(transformer, "ConserveDocuments"):
try:
transformer.ConserveDocuments = conserver_document_source
logger.info(
f"[TRANSFORM] ConserveDocuments = {conserver_document_source}"
)
except Exception as e:
logger.warning(
f"[TRANSFORM] ConserveDocuments non modifiable: {e}"
)
logger.info("[TRANSFORM] Ajout du document...")
try:
transformer.AddDocument(doc_source)
logger.info("[TRANSFORM] Document ajouté")
except Exception as e:
raise RuntimeError(f"Impossible d'ajouter le document: {e}")
try:
can_process = getattr(transformer, "CanProcess", False)
logger.info(f"[TRANSFORM] CanProcess: {can_process}")
except Exception:
can_process = True
if not can_process:
erreurs = lire_erreurs_sage(transformer, "Transformer")
if erreurs:
msgs = [f"{e['field']}: {e['description']}" for e in erreurs]
raise RuntimeError(
f"Transformation impossible: {' | '.join(msgs)}"
)
raise RuntimeError("Transformation impossible (CanProcess=False)")
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug("[TRANSFORM] Transaction démarrée")
except Exception:
pass
try:
logger.info("[TRANSFORM] Process()...")
try:
transformer.Process()
logger.info("[TRANSFORM] Process() réussi")
except Exception as e:
logger.error(f"[TRANSFORM] Erreur Process(): {e}")
erreurs = lire_erreurs_sage(transformer, "Transformer")
if erreurs:
msgs = [
f"{e['field']}: {e['description']}" for e in erreurs
]
raise RuntimeError(f"Échec: {' | '.join(msgs)}")
raise RuntimeError(f"Échec transformation: {e}")
logger.info("[TRANSFORM] Récupération des résultats...")
list_results = getattr(transformer, "ListDocumentsResult", None)
if not list_results:
raise RuntimeError("ListDocumentsResult non disponible")
documents_crees = []
index = 1
while index <= 100:
try:
doc_result = list_results.Item(index)
if doc_result is None:
break
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_cible = getattr(doc_result, "DO_Piece", "").strip()
total_ht = float(getattr(doc_result, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_result, "DO_TotalTTC", 0.0))
nb_lignes = 0
try:
factory_lignes_result = getattr(
doc_result, "FactoryDocumentLigne", None
)
if factory_lignes_result:
lignes_list = factory_lignes_result.List
nb_lignes = lignes_list.Count if lignes_list else 0
except Exception:
pass
documents_crees.append(
{
"numero": numero_cible,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": nb_lignes,
}
)
logger.info(
f"[TRANSFORM] Document créé: {numero_cible} "
f"({nb_lignes} lignes, {total_ht}€ HT)"
)
index += 1
except Exception:
logger.debug(f"Fin de liste à index {index}")
break
if not documents_crees:
raise RuntimeError("Aucun document créé après Process()")
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.debug("[TRANSFORM] Transaction committée")
except Exception:
pass
time.sleep(1.5)
doc_principal = documents_crees[0]
logger.info(
f"[TRANSFORM] SUCCÈS: {numero_source}{doc_principal['numero']}"
)
logger.info(
f"[TRANSFORM] • {doc_principal['nb_lignes']} ligne(s)"
)
logger.info(
f"[TRANSFORM] • {doc_principal['total_ht']}€ HT / "
f"{doc_principal['total_ttc']}€ TTC"
)
return {
"success": True,
"document_source": numero_source,
"document_cible": doc_principal["numero"],
"type_source": type_source,
"type_cible": type_cible,
"nb_documents_crees": len(documents_crees),
"documents": documents_crees,
"nb_lignes": doc_principal["nb_lignes"],
"total_ht": doc_principal["total_ht"],
"total_ttc": doc_principal["total_ttc"],
"methode_transformation": f"{module}.{methode}",
}
except Exception:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.error("[TRANSFORM] Transaction annulée (rollback)")
except Exception:
pass
raise
except ValueError as e:
logger.error(f"[TRANSFORM] Erreur métier: {e}")
raise
except RuntimeError as e:
logger.error(f"[TRANSFORM] Erreur technique: {e}")
raise
except Exception as e:
logger.error(f"[TRANSFORM] Erreur inattendue: {e}", exc_info=True)
raise RuntimeError(f"Échec transformation: {str(e)}")
def mettre_a_jour_champ_libre(self, doc_id, type_doc, nom_champ, valeur):
"""Mise à jour champ libre pour Universign ID"""
try:
with self._com_context(), self._lock_com:
factory = self.cial.FactoryDocumentVente
persist = factory.ReadPiece(type_doc, doc_id)
if persist:
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
try:
setattr(doc, f"DO_{nom_champ}", valeur)
doc.Write()
logger.debug(f"Champ libre {nom_champ} = {valeur} sur {doc_id}")
return True
except Exception as e:
logger.warning(f"Impossible de mettre à jour {nom_champ}: {e}")
except Exception as e:
logger.error(f"Erreur MAJ champ libre: {e}")
return False
def _lire_client_obj(self, code_client):
"""Retourne l'objet client Sage brut (pour remises)"""
if not self.cial:
return None
try:
with self._com_context(), self._lock_com:
factory = self.cial.CptaApplication.FactoryClient
persist = factory.ReadNumero(code_client)
if persist:
return _cast_client(persist)
except Exception:
pass
return None
def lire_contact_principal_client(self, code_client):
if not self.cial:
return None
try:
with self._com_context(), self._lock_com:
factory_client = self.cial.CptaApplication.FactoryClient
persist_client = factory_client.ReadNumero(code_client)
if not persist_client:
return None
client = _cast_client(persist_client)
if not client:
return None
contact_info = {
"client_code": code_client,
"client_intitule": getattr(client, "CT_Intitule", ""),
"email": None,
"nom": None,
"telephone": None,
}
try:
telecom = getattr(client, "Telecom", None)
if telecom:
contact_info["email"] = getattr(telecom, "EMail", "")
contact_info["telephone"] = getattr(telecom, "Telephone", "")
except Exception:
pass
try:
contact_info["nom"] = (
getattr(client, "CT_Contact", "")
or contact_info["client_intitule"]
)
except Exception:
contact_info["nom"] = contact_info["client_intitule"]
return contact_info
except Exception as e:
logger.error(f"Erreur lecture contact client {code_client}: {e}")
return None
def mettre_a_jour_derniere_relance(self, doc_id, type_doc):
date_relance = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return self.mettre_a_jour_champ_libre(
doc_id, type_doc, "DerniereRelance", date_relance
)
def lister_tous_prospects(self, filtre=""):
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT
CT_Num, CT_Intitule, CT_Adresse, CT_Ville,
CT_CodePostal, CT_Telephone, CT_EMail
FROM F_COMPTET
WHERE CT_Type = 0 AND CT_Prospect = 1
"""
params = []
if filtre:
query += " AND (CT_Num LIKE ? OR CT_Intitule LIKE ?)"
params.extend([f"%{filtre}%", f"%{filtre}%"])
query += " ORDER BY CT_Intitule"
cursor.execute(query, params)
rows = cursor.fetchall()
prospects = []
for row in rows:
prospects.append(
{
"numero": _safe_strip(row.CT_Num),
"intitule": _safe_strip(row.CT_Intitule),
"adresse": _safe_strip(row.CT_Adresse),
"ville": _safe_strip(row.CT_Ville),
"code_postal": _safe_strip(row.CT_CodePostal),
"telephone": _safe_strip(row.CT_Telephone),
"email": _safe_strip(row.CT_EMail),
"type": 0,
"est_prospect": True,
}
)
logger.info(f" SQL: {len(prospects)} prospects")
return prospects
except Exception as e:
logger.error(f" Erreur SQL prospects: {e}")
return []
def lire_prospect(self, code_prospect):
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
CT_Num, CT_Intitule, CT_Type, CT_Qualite,
CT_Adresse, CT_Complement, CT_Ville, CT_CodePostal, CT_Pays,
CT_Telephone, CT_Portable, CT_EMail, CT_Telecopie,
CT_Siret, CT_Identifiant, CT_Sommeil, CT_Prospect,
CT_Contact, CT_FormeJuridique, CT_Secteur
FROM F_COMPTET
WHERE CT_Num = ? AND CT_Type = 0 AND CT_Prospect = 1
""",
(code_prospect.upper(),),
)
row = cursor.fetchone()
if not row:
return None
return {
"numero": _safe_strip(row.CT_Num),
"intitule": _safe_strip(row.CT_Intitule),
"type": 0,
"qualite": _safe_strip(row.CT_Qualite),
"est_prospect": True,
"adresse": _safe_strip(row.CT_Adresse),
"complement": _safe_strip(row.CT_Complement),
"ville": _safe_strip(row.CT_Ville),
"code_postal": _safe_strip(row.CT_CodePostal),
"pays": _safe_strip(row.CT_Pays),
"telephone": _safe_strip(row.CT_Telephone),
"portable": _safe_strip(row.CT_Portable),
"email": _safe_strip(row.CT_EMail),
"telecopie": _safe_strip(row.CT_Telecopie),
"siret": _safe_strip(row.CT_Siret),
"tva_intra": _safe_strip(row.CT_Identifiant),
"est_actif": (row.CT_Sommeil == 0),
"contact": _safe_strip(row.CT_Contact),
"forme_juridique": _safe_strip(row.CT_FormeJuridique),
"secteur": _safe_strip(row.CT_Secteur),
}
except Exception as e:
logger.error(f" Erreur SQL prospect {code_prospect}: {e}")
return None
def lire_avoir(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=50)
def lire_livraison(self, numero):
with self._get_sql_connection() as conn:
cursor = conn.cursor()
return _lire_document_sql(cursor, numero, type_doc=30)
def creer_contact(self, contact_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with (
self._com_context(),
self._lock_com,
self._get_sql_connection() as conn,
):
logger.info("=" * 80)
logger.info("[CREATION CONTACT F_CONTACTT]")
logger.info("=" * 80)
if not contact_data.get("numero"):
raise ValueError("numero (code client) obligatoire")
if not contact_data.get("nom"):
raise ValueError("nom obligatoire")
numero_client = _clean_str(contact_data["numero"], 17).upper()
nom = _clean_str(contact_data["nom"], 35)
prenom = _clean_str(contact_data.get("prenom", ""), 35)
logger.info(f" CLIENT: {numero_client}")
logger.info(f" CONTACT: {prenom} {nom}")
logger.info(f"[1] Chargement du 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(" OK Client charge")
except Exception as e:
raise ValueError(f"Client {numero_client} introuvable: {e}")
logger.info("[2] Creation via FactoryTiersContact")
if not hasattr(client_obj, "FactoryTiersContact"):
raise RuntimeError("FactoryTiersContact non trouvee sur le client")
factory_contact = client_obj.FactoryTiersContact
logger.info(
f" OK FactoryTiersContact: {type(factory_contact).__name__}"
)
persist = factory_contact.Create()
logger.info(f" Objet cree: {type(persist).__name__}")
contact = None
interfaces_a_tester = [
"IBOTiersContact3",
"IBOTiersContact",
"IBOContactT3",
"IBOContactT",
]
for interface_name in interfaces_a_tester:
try:
temp = win32com.client.CastTo(persist, interface_name)
if hasattr(temp, "_prop_map_put_"):
props = list(temp._prop_map_put_.keys())
logger.info(f" Test {interface_name}: props={props[:15]}")
if "Nom" in props or "CT_Nom" in props:
contact = temp
logger.info(f" OK Cast reussi vers {interface_name}")
break
except Exception as e:
logger.debug(f" {interface_name}: {str(e)[:50]}")
if not contact:
logger.error(" ERROR Aucun cast ne fonctionne")
raise RuntimeError(
"Impossible de caster vers une interface contact valide"
)
logger.info("[3] Configuration du contact")
if hasattr(contact, "_prop_map_put_"):
props = list(contact._prop_map_put_.keys())
logger.info(f" Proprietes disponibles: {props}")
try:
contact.Nom = nom
logger.info(f" OK Nom = {nom}")
except Exception as e:
logger.error(f" ERROR Impossible de definir Nom: {e}")
raise RuntimeError(f"Echec definition Nom: {e}")
if prenom:
try:
contact.Prenom = prenom
logger.info(f" OK Prenom = {prenom}")
except Exception as e:
logger.warning(f" WARN Prenom: {e}")
if contact_data.get("civilite"):
civilite_map = {"M.": 0, "Mme": 1, "Mlle": 2, "Societe": 3}
civilite_code = civilite_map.get(contact_data["civilite"])
if civilite_code is not None:
try:
contact.Civilite = civilite_code
logger.info(f" OK Civilite = {civilite_code}")
except Exception as e:
logger.warning(f" WARN Civilite: {e}")
if contact_data.get("fonction"):
fonction = _clean_str(contact_data["fonction"], 35)
try:
contact.Fonction = fonction
logger.info(f" OK Fonction = {fonction}")
except Exception as e:
logger.warning(f" WARN Fonction: {e}")
if contact_data.get("service_code") is not None:
try:
service = _safe_int(contact_data["service_code"])
if service is not None and hasattr(contact, "ServiceContact"):
contact.ServiceContact = service
logger.info(f" OK ServiceContact = {service}")
except Exception as e:
logger.warning(f" WARN ServiceContact: {e}")
logger.info("[4] Coordonnees (Telecom)")
if hasattr(contact, "Telecom"):
try:
telecom = contact.Telecom
logger.info(f" Type Telecom: {type(telecom).__name__}")
if contact_data.get("telephone"):
telephone = _clean_str(contact_data["telephone"], 21)
if _try_set_attribute(telecom, "Telephone", telephone):
logger.info(f" Telephone = {telephone}")
if contact_data.get("portable"):
portable = _clean_str(contact_data["portable"], 21)
if _try_set_attribute(telecom, "Portable", portable):
logger.info(f" Portable = {portable}")
if contact_data.get("email"):
email = _clean_str(contact_data["email"], 69)
if _try_set_attribute(telecom, "EMail", email):
logger.info(f" EMail = {email}")
if contact_data.get("telecopie"):
fax = _clean_str(contact_data["telecopie"], 21)
if _try_set_attribute(telecom, "Telecopie", fax):
logger.info(f" Telecopie = {fax}")
except Exception as e:
logger.warning(f" WARN Erreur Telecom: {e}")
logger.info("[5] Reseaux sociaux")
if contact_data.get("facebook"):
facebook = _clean_str(contact_data["facebook"], 69)
try:
contact.Facebook = facebook
logger.info(f" Facebook = {facebook}")
except Exception:
pass
if contact_data.get("linkedin"):
linkedin = _clean_str(contact_data["linkedin"], 69)
try:
contact.LinkedIn = linkedin
logger.info(f" LinkedIn = {linkedin}")
except Exception:
pass
if contact_data.get("skype"):
skype = _clean_str(contact_data["skype"], 69)
try:
contact.Skype = skype
logger.info(f" Skype = {skype}")
except Exception:
pass
try:
contact.SetDefault()
logger.info(" OK SetDefault() applique")
except Exception as e:
logger.warning(f" WARN SetDefault(): {e}")
logger.info("[6] Enregistrement du contact")
contact_cree_malgre_erreur = False
contact_no = None
n_contact = None
erreur_com = None
try:
contact.Write()
logger.info(" Write() reussi")
try:
contact.Read()
logger.info(" Read() reussi")
except Exception as read_err:
logger.warning(f" WARN Read() échoué: {read_err}")
try:
contact_no = getattr(contact, "CT_No", None)
n_contact = getattr(contact, "N_Contact", None)
logger.info(
f" IDs COM: CT_No={contact_no}, N_Contact={n_contact}"
)
except Exception:
pass
if not contact_no:
logger.info(
" 🔍 CT_No non disponible via COM - Recherche en base..."
)
import time
time.sleep(0.3)
contact_sql = _chercher_contact_en_base(
conn,
numero_client=numero_client,
nom=nom,
prenom=prenom if prenom else None,
)
if contact_sql:
logger.info(
f" Contact trouvé en base: CT_No={contact_sql['contact_numero']}"
)
contact_no = contact_sql["contact_numero"]
n_contact = contact_sql["n_contact"]
else:
logger.warning(" Contact non trouvé en base immédiatement")
except Exception as e:
erreur_com = str(e)
logger.warning(f" Write() a levé une exception: {erreur_com}")
if (
"existe déjà" in erreur_com.lower()
or "already exists" in erreur_com.lower()
):
logger.info(
" 🔍 Erreur 'existe déjà' détectée - Vérification en base..."
)
import time
time.sleep(0.5)
contact_sql = _chercher_contact_en_base(
conn,
numero_client=numero_client,
nom=nom,
prenom=prenom if prenom else None,
)
if contact_sql:
logger.info(" Contact CRÉÉ malgré l'erreur COM !")
logger.info(
f" CT_No={contact_sql['contact_numero']}, N_Contact={contact_sql['n_contact']}"
)
contact_cree_malgre_erreur = True
contact_no = contact_sql["contact_numero"]
n_contact = contact_sql["n_contact"]
else:
logger.error(" Contact NON trouvé en base - Erreur réelle")
raise RuntimeError(f"Echec enregistrement: {erreur_com}")
else:
logger.error(f" Erreur Write non gérée: {erreur_com}")
raise RuntimeError(f"Echec enregistrement: {erreur_com}")
est_defaut = contact_data.get("est_defaut", False)
if est_defaut and (contact_no or n_contact):
logger.info("[7] Definition comme contact par defaut")
try:
nom_complet = f"{prenom} {nom}".strip() if prenom else nom
persist_client = factory_client.ReadNumero(numero_client)
client_obj = win32com.client.CastTo(
persist_client, "IBOClient3"
)
client_obj.Read()
client_obj.CT_Contact = nom_complet
logger.info(f" CT_Contact = '{nom_complet}'")
if contact_no and hasattr(client_obj, "CT_NoContact"):
try:
client_obj.CT_NoContact = contact_no
logger.info(f" CT_NoContact = {contact_no}")
except Exception:
pass
client_obj.Write()
client_obj.Read()
logger.info(" OK Contact par defaut defini")
except Exception as e:
logger.warning(f" WARN Echec: {e}")
est_defaut = False
logger.info("=" * 80)
if contact_cree_malgre_erreur:
logger.info(
f"[SUCCES] Contact créé MALGRÉ erreur COM: {prenom} {nom}"
)
logger.info(
" (Bug connu de Sage 100c - Le contact est bien en base)"
)
else:
logger.info(f"[SUCCES] Contact créé: {prenom} {nom}")
logger.info(f" Lié au client {numero_client}")
if contact_no:
logger.info(f" CT_No={contact_no}")
logger.info("=" * 80)
logger.info("[7] Construction du retour")
contact_dict = None
if contact_no:
logger.info(f" Stratégie 1: Lecture base (CT_No={contact_no})")
try:
contact_dict = _lire_contact_depuis_base(
conn, numero_client=numero_client, contact_no=contact_no
)
if contact_dict:
logger.info(
f" Lecture base réussie: {len(contact_dict)} champs"
)
logger.info(f" Type: {type(contact_dict)}")
logger.info(f" Keys: {list(contact_dict.keys())}")
logger.info(
f" Sample: numero={contact_dict.get('numero')}, nom={contact_dict.get('nom')}"
)
else:
logger.warning(
" _lire_contact_depuis_base() retourne None"
)
except Exception as e:
logger.error(f" Erreur lecture base: {e}", exc_info=True)
contact_dict = None
if not contact_dict:
logger.info(" Stratégie 2: Lecture objet COM (_contact_to_dict)")
try:
contact_dict = _contact_to_dict(
contact,
numero_client=numero_client,
contact_numero=contact_no,
n_contact=n_contact,
)
if contact_dict:
logger.info(
f" _contact_to_dict réussi: {len(contact_dict)} champs"
)
else:
logger.warning(" _contact_to_dict() retourne None/vide")
except Exception as e:
logger.error(f" Erreur _contact_to_dict: {e}", exc_info=True)
contact_dict = None
if not contact_dict:
logger.info(" Stratégie 3: Construction manuelle (fallback)")
contact_dict = self._construire_contact_minimal(
numero_client=numero_client,
contact_no=contact_no,
n_contact=n_contact,
nom=nom,
prenom=prenom,
contact_data=contact_data,
)
logger.info(
f" Contact minimal construit: {len(contact_dict)} champs"
)
if not contact_dict or not isinstance(contact_dict, dict):
logger.error(
f" ERREUR: contact_dict invalide: type={type(contact_dict)}, value={contact_dict}"
)
raise RuntimeError(
"Impossible de construire le dictionnaire de retour"
)
contact_dict["est_defaut"] = est_defaut
logger.info(" DICT FINAL AVANT RETURN:")
logger.info(f" Type: {type(contact_dict)}")
logger.info(f" Len: {len(contact_dict)}")
logger.info(f" Keys: {list(contact_dict.keys())}")
for key, value in contact_dict.items():
logger.info(f" {key}: {value} (type: {type(value).__name__})")
logger.info(" RETURN contact_dict")
return contact_dict
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def _construire_contact_minimal(
self,
numero_client: str,
contact_no: Optional[int],
n_contact: Optional[int],
nom: str,
prenom: Optional[str],
contact_data: Dict,
) -> Dict:
logger.info(
f" _construire_contact_minimal(client={numero_client}, CT_No={contact_no})"
)
civilite_map_reverse = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"}
civilite_input = contact_data.get("civilite")
civilite_code = None
if civilite_input:
if isinstance(civilite_input, int):
civilite_code = civilite_map_reverse.get(civilite_input)
else:
civilite_code = civilite_input
result = {
"numero": numero_client,
"contact_numero": contact_no,
"n_contact": n_contact,
"civilite": civilite_code,
"nom": nom,
"prenom": prenom,
"fonction": contact_data.get("fonction"),
"service_code": contact_data.get("service_code"),
"telephone": contact_data.get("telephone"),
"portable": contact_data.get("portable"),
"telecopie": contact_data.get("telecopie"),
"email": contact_data.get("email"),
"facebook": contact_data.get("facebook"),
"linkedin": contact_data.get("linkedin"),
"skype": contact_data.get("skype"),
"est_defaut": False,
}
logger.info(
f" Contact minimal: numero={result['numero']}, nom={result['nom']}, email={result['email']}"
)
return result
def modifier_contact(self, numero: str, contact_numero: int, updates: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info(f"[MODIFICATION CONTACT] CT_No={contact_numero}")
logger.info("=" * 80)
logger.info("[1] Chargement du 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: {client_obj.CT_Intitule}")
except Exception as e:
raise ValueError(f"Client {numero} introuvable: {e}")
logger.info("[2] Chargement du contact")
contact = None
nom_recherche = None
prenom_recherche = None
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""SELECT CT_No, CT_Nom, CT_Prenom, cbMarq
FROM F_CONTACTT
WHERE CT_Num = ? AND CT_No = ?""",
[numero, contact_numero],
)
row = cursor.fetchone()
if not row:
raise ValueError(
f"Contact CT_No={contact_numero} non trouve en base"
)
ct_no_sql = row.CT_No
nom_recherche = row.CT_Nom.strip() if row.CT_Nom else ""
prenom_recherche = (
row.CT_Prenom.strip() if row.CT_Prenom else ""
)
cbmarq_sql = row.cbMarq
logger.info(
f" Contact SQL: CT_No={ct_no_sql}, cbMarq={cbmarq_sql}"
)
logger.info(
f" Nom='{nom_recherche}', Prenom='{prenom_recherche}'"
)
except ValueError:
raise
except Exception as e:
raise ValueError(f"Erreur lecture contact en base: {e}")
logger.info(" Strategie 1: Parcours FactoryTiersContact du client...")
try:
factory_contact = client_obj.FactoryTiersContact
if hasattr(factory_contact, "List"):
liste_contacts = factory_contact.List
if liste_contacts and hasattr(liste_contacts, "Count"):
count = liste_contacts.Count
logger.info(f" {count} contact(s) pour ce client")
for i in range(count):
try:
item = liste_contacts.Item(i + 1)
temp_contact = None
for iface in [
"IBOTiersContact3",
"IBOTiersContact",
"IBOContactT3",
]:
try:
temp_contact = win32com.client.CastTo(
item, iface
)
break
except Exception:
continue
if not temp_contact:
continue
temp_contact.Read()
nom_com = getattr(temp_contact, "Nom", "") or ""
prenom_com = (
getattr(temp_contact, "Prenom", "") or ""
)
if (
nom_com.strip().lower() == nom_recherche.lower()
and prenom_com.strip().lower()
== prenom_recherche.lower()
):
contact = temp_contact
logger.info(
f" OK Contact trouve a l'index {i + 1}: '{prenom_com}' '{nom_com}'"
)
break
except Exception as e:
logger.debug(f" Item {i + 1} erreur: {e}")
continue
except Exception as e:
logger.warning(f" Strategie 1 echouee: {e}")
if not contact:
logger.info(" Strategie 2: FactoryDossierContact.ReadNomPrenom...")
try:
factory_dossier = (
self.cial.CptaApplication.FactoryDossierContact
)
persist = factory_dossier.ReadNomPrenom(
nom_recherche, prenom_recherche
)
if persist:
contact = win32com.client.CastTo(
persist, "IBOTiersContact3"
)
contact.Read()
logger.info(" OK Contact charge via ReadNomPrenom")
except Exception as e:
logger.warning(f" Strategie 2 echouee: {e}")
if not contact:
logger.info(" Strategie 3: Variations nom/prenom...")
try:
factory_dossier = (
self.cial.CptaApplication.FactoryDossierContact
)
variations = [
(nom_recherche.upper(), prenom_recherche.upper()),
(nom_recherche.lower(), prenom_recherche.lower()),
(nom_recherche.capitalize(), prenom_recherche.capitalize()),
(nom_recherche, ""),
]
for nom_var, prenom_var in variations:
try:
persist = factory_dossier.ReadNomPrenom(
nom_var, prenom_var
)
if persist:
contact = win32com.client.CastTo(
persist, "IBOTiersContact3"
)
contact.Read()
logger.info(
f" OK Contact trouve avec: '{nom_var}'/'{prenom_var}'"
)
break
except Exception:
continue
except Exception as e:
logger.warning(f" Strategie 3 echouee: {e}")
if not contact:
logger.info(
" Strategie 4: Parcours global FactoryDossierContact..."
)
try:
factory_dossier = (
self.cial.CptaApplication.FactoryDossierContact
)
if hasattr(factory_dossier, "List"):
liste = factory_dossier.List
if liste and hasattr(liste, "Count"):
count = min(liste.Count, 500)
logger.info(f" Parcours de {count} contacts...")
for i in range(count):
try:
item = liste.Item(i + 1)
temp = win32com.client.CastTo(
item, "IBOTiersContact3"
)
temp.Read()
nom = getattr(temp, "Nom", "") or ""
prenom = getattr(temp, "Prenom", "") or ""
if (
nom.strip().lower() == nom_recherche.lower()
and prenom.strip().lower()
== prenom_recherche.lower()
):
contact = temp
logger.info(
f" OK Contact trouve a l'index global {i + 1}"
)
break
except Exception:
continue
except Exception as e:
logger.warning(f" Strategie 4 echouee: {e}")
if not contact:
logger.error(
f" ECHEC: Impossible de charger le contact CT_No={contact_numero}"
)
raise ValueError(
f"Contact CT_No={contact_numero} introuvable via COM. "
f"Nom='{nom_recherche}', Prenom='{prenom_recherche}'"
)
logger.info(f" OK Contact charge: {contact.Nom}")
logger.info("[3] Application des modifications")
modifications_appliquees = []
if "civilite" in updates:
civilite_map = {"M.": 0, "Mme": 1, "Mlle": 2, "Societe": 3}
civilite_code = civilite_map.get(updates["civilite"])
if civilite_code is not None:
try:
contact.Civilite = civilite_code
logger.info(f" Civilite = {civilite_code}")
modifications_appliquees.append("civilite")
except Exception as e:
logger.warning(f" WARN Civilite: {e}")
if "nom" in updates:
nom = _clean_str(updates["nom"], 35)
if nom:
try:
contact.Nom = nom
logger.info(f" Nom = {nom}")
modifications_appliquees.append("nom")
except Exception as e:
logger.warning(f" WARN Nom: {e}")
if "prenom" in updates:
prenom = _clean_str(updates["prenom"], 35)
try:
contact.Prenom = prenom
logger.info(f" Prenom = {prenom}")
modifications_appliquees.append("prenom")
except Exception as e:
logger.warning(f" WARN Prenom: {e}")
if "fonction" in updates:
fonction = _clean_str(updates["fonction"], 35)
try:
contact.Fonction = fonction
logger.info(f" Fonction = {fonction}")
modifications_appliquees.append("fonction")
except Exception as e:
logger.warning(f" WARN Fonction: {e}")
if "service_code" in updates:
service = _safe_int(updates["service_code"])
if service is not None and hasattr(contact, "ServiceContact"):
try:
contact.ServiceContact = service
logger.info(f" ServiceContact = {service}")
modifications_appliquees.append("service_code")
except Exception as e:
logger.warning(f" WARN ServiceContact: {e}")
if hasattr(contact, "Telecom"):
try:
telecom = contact.Telecom
if "telephone" in updates:
telephone = _clean_str(updates["telephone"], 21)
if _try_set_attribute(telecom, "Telephone", telephone):
logger.info(f" Telephone = {telephone}")
modifications_appliquees.append("telephone")
if "portable" in updates:
portable = _clean_str(updates["portable"], 21)
if _try_set_attribute(telecom, "Portable", portable):
logger.info(f" Portable = {portable}")
modifications_appliquees.append("portable")
if "email" in updates:
email = _clean_str(updates["email"], 69)
if _try_set_attribute(telecom, "EMail", email):
logger.info(f" EMail = {email}")
modifications_appliquees.append("email")
if "telecopie" in updates:
fax = _clean_str(updates["telecopie"], 21)
if _try_set_attribute(telecom, "Telecopie", fax):
logger.info(f" Telecopie = {fax}")
modifications_appliquees.append("telecopie")
except Exception as e:
logger.warning(f" WARN Telecom: {e}")
if "facebook" in updates:
facebook = _clean_str(updates["facebook"], 69)
try:
contact.Facebook = facebook
logger.info(f" Facebook = {facebook}")
modifications_appliquees.append("facebook")
except Exception as e:
logger.warning(f" WARN Facebook: {e}")
if "linkedin" in updates:
linkedin = _clean_str(updates["linkedin"], 69)
try:
contact.LinkedIn = linkedin
logger.info(f" LinkedIn = {linkedin}")
modifications_appliquees.append("linkedin")
except Exception as e:
logger.warning(f" WARN LinkedIn: {e}")
if "skype" in updates:
skype = _clean_str(updates["skype"], 69)
try:
contact.Skype = skype
logger.info(f" Skype = {skype}")
modifications_appliquees.append("skype")
except Exception as e:
logger.warning(f" WARN Skype: {e}")
logger.info(
f" Modifications preparees: {', '.join(modifications_appliquees) if modifications_appliquees else 'aucune'}"
)
logger.info("[4] Enregistrement")
try:
contact.Write()
logger.info(" Write() reussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f" ERROR Write: {error_detail}")
raise RuntimeError(f"Echec modification contact: {error_detail}")
try:
contact.Read()
logger.info(" Read() reussi")
except Exception as e:
logger.warning(f" WARN Read() apres Write: {e}")
est_defaut_demande = updates.get("est_defaut")
est_actuellement_defaut = False
if est_defaut_demande:
logger.info("[5] Definition comme contact par defaut")
try:
nom_complet = (
f"{contact.Prenom} {contact.Nom}".strip()
if contact.Prenom
else contact.Nom
)
persist_client = factory_client.ReadNumero(numero)
client_obj = win32com.client.CastTo(
persist_client, "IBOClient3"
)
client_obj.Read()
client_obj.CT_Contact = nom_complet
logger.info(f" CT_Contact = '{nom_complet}'")
if hasattr(client_obj, "CT_NoContact"):
try:
client_obj.CT_NoContact = contact_numero
logger.info(f" CT_NoContact = {contact_numero}")
except Exception as e:
logger.warning(f" WARN CT_NoContact: {e}")
client_obj.Write()
client_obj.Read()
logger.info(" OK Contact par defaut defini")
est_actuellement_defaut = True
except Exception as e:
logger.warning(f" WARN Echec definition contact defaut: {e}")
logger.info("[6] Construction du retour")
contact_dict = None
try:
with self._get_sql_connection() as conn:
contact_dict = _lire_contact_depuis_base(
conn, numero_client=numero, contact_no=contact_numero
)
if contact_dict:
logger.info(" Lecture base reussie")
except Exception as e:
logger.warning(f" Lecture base echouee: {e}")
if not contact_dict:
try:
contact_dict = _contact_to_dict(
contact,
numero_client=numero,
contact_numero=contact_numero,
n_contact=None,
)
if contact_dict:
logger.info(" _contact_to_dict reussi")
except Exception as e:
logger.warning(f" _contact_to_dict echoue: {e}")
if not contact_dict:
logger.info(" Construction manuelle du retour")
contact_dict = {
"numero": numero,
"contact_numero": contact_numero,
"n_contact": None,
"civilite": None,
"nom": getattr(contact, "Nom", nom_recherche),
"prenom": getattr(contact, "Prenom", prenom_recherche),
"fonction": getattr(contact, "Fonction", None),
"service_code": None,
"telephone": None,
"portable": None,
"telecopie": None,
"email": None,
"facebook": None,
"linkedin": None,
"skype": None,
}
if hasattr(contact, "Telecom"):
try:
telecom = contact.Telecom
contact_dict["telephone"] = getattr(
telecom, "Telephone", None
)
contact_dict["portable"] = getattr(
telecom, "Portable", None
)
contact_dict["email"] = getattr(telecom, "EMail", None)
contact_dict["telecopie"] = getattr(
telecom, "Telecopie", None
)
except Exception:
pass
contact_dict["est_defaut"] = est_actuellement_defaut
logger.info("=" * 80)
logger.info(f"[SUCCES] Contact modifie: CT_No={contact_numero}")
logger.info(
f" Modifications: {', '.join(modifications_appliquees) if modifications_appliquees else 'aucune'}"
)
logger.info("=" * 80)
return contact_dict
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def definir_contact_defaut(self, numero: str, contact_numero: int) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info(
f"[DEFINIR CONTACT PAR DEFAUT] Client={numero}, Contact={contact_numero}"
)
logger.info("=" * 80)
logger.info("[1] Recuperation infos contact")
nom_contact = None
prenom_contact = None
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT CT_Nom, CT_Prenom FROM F_CONTACTT WHERE CT_Num = ? AND CT_No = ?",
[numero, contact_numero],
)
row = cursor.fetchone()
if not row:
raise ValueError(
f"Contact CT_No={contact_numero} non trouve"
)
nom_contact = row.CT_Nom.strip() if row.CT_Nom else ""
prenom_contact = row.CT_Prenom.strip() if row.CT_Prenom else ""
nom_complet = (
f"{prenom_contact} {nom_contact}".strip()
if prenom_contact
else nom_contact
)
logger.info(f" OK Contact trouve: {nom_complet}")
except Exception as e:
raise ValueError(f"Contact introuvable: {e}")
logger.info("[2] Chargement du client")
factory_client = self.cial.CptaApplication.FactoryClient
try:
persist_client = factory_client.ReadNumero(numero)
if not persist_client:
raise ValueError(f"Client {numero} non trouve")
client = win32com.client.CastTo(persist_client, "IBOClient3")
client.Read()
logger.info(f" OK Client charge: {client.CT_Intitule}")
except Exception as e:
raise ValueError(f"Client introuvable: {e}")
logger.info("[3] Definition du contact par defaut")
ancien_contact = getattr(client, "CT_Contact", "")
client.CT_Contact = nom_complet
logger.info(f" CT_Contact: '{ancien_contact}' -> '{nom_complet}'")
if hasattr(client, "CT_NoContact"):
try:
client.CT_NoContact = contact_numero
logger.info(f" CT_NoContact = {contact_numero}")
except Exception:
pass
logger.info("[4] Enregistrement")
try:
client.Write()
client.Read()
logger.info(" OK Client mis a jour")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
raise RuntimeError(f"Echec mise a jour: {error_detail}")
logger.info("=" * 80)
logger.info(f"[SUCCES] Contact par defaut: {nom_complet}")
logger.info("=" * 80)
return {
"numero": numero,
"contact_numero": contact_numero,
"contact_nom": nom_complet,
"client_intitule": client.CT_Intitule,
"est_defaut": True,
"date_modification": datetime.now().isoformat(),
}
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def lister_contacts(self, numero: str) -> List[Dict]:
try:
with self._get_sql_connection() as conn:
return _get_contacts_client(numero, conn)
except Exception as e:
logger.error(f"Erreur liste contacts: {e}")
raise RuntimeError(f"Erreur lecture contacts: {str(e)}")
def supprimer_contact(self, numero: str, contact_numero: int) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info(f"[SUPPRESSION CONTACT] CT_No={contact_numero}")
logger.info("=" * 80)
logger.info("[1] Recuperation infos contact")
nom_contact = None
prenom_contact = None
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT CT_Nom, CT_Prenom FROM F_CONTACTT WHERE CT_Num = ? AND CT_No = ?",
[numero, contact_numero],
)
row = cursor.fetchone()
if not row:
raise ValueError(
f"Contact CT_No={contact_numero} non trouve"
)
nom_contact = row.CT_Nom.strip() if row.CT_Nom else ""
prenom_contact = row.CT_Prenom.strip() if row.CT_Prenom else ""
logger.info(
f" OK Contact trouve: {prenom_contact} {nom_contact}"
)
except Exception as e:
raise ValueError(f"Contact introuvable: {e}")
logger.info("[2] Chargement du contact")
factory_dossier = self.cial.CptaApplication.FactoryDossierContact
try:
persist = factory_dossier.ReadNomPrenom(nom_contact, prenom_contact)
if not persist:
raise ValueError("Contact non trouvable via ReadNomPrenom")
contact = win32com.client.CastTo(persist, "IBOTiersContact3")
contact.Read()
logger.info(f" OK Contact charge: {contact.Nom}")
except Exception as e:
raise ValueError(f"Contact introuvable: {e}")
logger.info("[3] Suppression")
try:
contact.Remove()
logger.info(" OK Remove() reussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f" ERROR Remove: {error_detail}")
raise RuntimeError(f"Echec suppression contact: {error_detail}")
logger.info("=" * 80)
logger.info(
f"[SUCCES] Contact supprime: {prenom_contact} {nom_contact}"
)
logger.info("=" * 80)
return {
"numero": numero,
"contact_numero": contact_numero,
"nom": nom_contact,
"prenom": prenom_contact,
"supprime": True,
"date_suppression": datetime.now().isoformat(),
}
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def creer_client(self, client_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non etablie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info("[CREATION CLIENT SAGE - DIAGNOSTIC COMPLET]")
logger.info("=" * 80)
def clean_str(value, max_len: int) -> str:
if value is None or str(value).lower() in ("none", "null", ""):
return ""
return str(value)[:max_len].strip()
def safe_int(value, default=None):
if value is None:
return default
try:
return int(value)
except (ValueError, TypeError):
return default
def safe_float(value, default=None):
if value is None:
return default
try:
return float(value)
except (ValueError, TypeError):
return default
def try_set_attribute(obj, attr_name, value, variants=None):
"""Essaie de definir un attribut avec plusieurs variantes de noms"""
if variants is None:
variants = [attr_name]
else:
variants = [attr_name] + variants
for variant in variants:
try:
if hasattr(obj, variant):
setattr(obj, variant, value)
logger.debug(f" {variant} = {value} [OK]")
return True
except Exception as e:
logger.debug(f" {variant} echec: {str(e)[:50]}")
return False
if not client_data.get("intitule"):
raise ValueError("intitule obligatoire")
if not client_data.get("numero"):
raise ValueError("numero obligatoire")
intitule = clean_str(client_data["intitule"], 69)
numero = clean_str(client_data["numero"], 17).upper()
type_tiers = safe_int(client_data.get("type_tiers"), 0)
logger.info("[ETAPE 1] CREATION OBJET")
factory_map = {
0: ("FactoryClient", "IBOClient3"),
1: ("FactoryFourniss", "IBOFournisseur3"),
2: ("FactorySalarie", "IBOSalarie3"),
3: ("FactoryAutre", "IBOAutre3"),
}
factory_name, interface_name = factory_map[type_tiers]
factory = getattr(self.cial.CptaApplication, factory_name)
persist = factory.Create()
client = win32com.client.CastTo(persist, interface_name)
logger.info(f" Objet cree: {interface_name}")
logger.info("[ETAPE 2] CONFIGURATION OBLIGATOIRE")
client.CT_Intitule = intitule
client.CT_Num = numero
logger.info(f" CT_Num = {numero}")
logger.info(f" CT_Intitule = {intitule}")
qualite = clean_str(client_data.get("qualite", "CLI"), 17)
if qualite:
client.CT_Qualite = qualite
logger.info(f" CT_Qualite = {qualite}")
client.SetDefault()
logger.info(" SetDefault() applique")
if client_data.get("raccourci"):
raccourci = clean_str(client_data["raccourci"], 7).upper().strip()
try:
factory_client = self.cial.CptaApplication.FactoryClient
exist_client = factory_client.ReadRaccourci(raccourci)
if exist_client:
logger.warning(
f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]"
)
else:
client.CT_Raccourci = raccourci
logger.info(f" CT_Raccourci = {raccourci} [OK]")
except Exception:
try:
client.CT_Raccourci = raccourci
logger.info(f" CT_Raccourci = {raccourci} [OK]")
except Exception as e2:
logger.warning(
f" CT_Raccourci = {raccourci} [ECHEC: {e2}]"
)
try:
if not hasattr(client, "CT_Type") or client.CT_Type is None:
client.CT_Type = type_tiers
logger.info(f" CT_Type force a {type_tiers}")
except Exception:
pass
COMPTES_DEFAUT = {0: "4110000", 1: "4010000", 2: "421", 3: "471"}
compte = clean_str(
client_data.get("compte_general")
or COMPTES_DEFAUT.get(type_tiers, "4110000"),
13,
)
factory_compte = self.cial.CptaApplication.FactoryCompteG
compte_trouve = False
comptes_a_tester = [
compte,
COMPTES_DEFAUT.get(type_tiers, "4110000"),
"4110000",
"411000",
"411",
"4010000",
"401000",
"401",
]
for test_compte in comptes_a_tester:
try:
persist_compte = factory_compte.ReadNumero(test_compte)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
type_compte = getattr(compte_obj, "CG_Type", None)
if type_compte == 0:
client.CompteGPrinc = compte_obj
compte = test_compte
compte_trouve = True
logger.info(
f" CompteGPrinc = {test_compte} (Type: {type_compte}) [OK]"
)
break
else:
logger.debug(
f" Compte {test_compte} - Type {type_compte} incompatible"
)
except Exception as e:
logger.debug(f" Compte {test_compte} - erreur: {e}")
if not compte_trouve:
raise RuntimeError("Aucun compte general valide trouve")
logger.info(" Configuration categories:")
try:
factory_cat_tarif = self.cial.CptaApplication.FactoryCategorieTarif
for cat_id in ["0", "1"]:
try:
persist_cat = factory_cat_tarif.ReadIntitule(cat_id)
if persist_cat:
cat_tarif_obj = win32com.client.CastTo(
persist_cat, "IBOCategorieTarif3"
)
cat_tarif_obj.Read()
client.CatTarif = cat_tarif_obj
logger.info(f" CatTarif = {cat_id} [OK]")
break
except Exception:
continue
except Exception as e:
logger.warning(f" CatTarif erreur: {e}")
try:
factory_cat_compta = (
self.cial.CptaApplication.FactoryCategorieCompta
)
for cat_id in ["0", "1"]:
try:
persist_cat = factory_cat_compta.ReadIntitule(cat_id)
if persist_cat:
cat_compta_obj = win32com.client.CastTo(
persist_cat, "IBOCategorieCompta3"
)
cat_compta_obj.Read()
client.CatCompta = cat_compta_obj
logger.info(f" CatCompta = {cat_id} [OK]")
break
except Exception:
continue
except Exception as e:
logger.warning(f" CatCompta erreur: {e}")
logger.info("[ETAPE 3] IDENTIFICATION")
if client_data.get("classement"):
try_set_attribute(
client,
"CT_Classement",
clean_str(client_data["classement"], 17),
)
if client_data.get("raccourci"):
raccourci = clean_str(client_data["raccourci"], 7).upper()
try_set_attribute(client, "CT_Raccourci", raccourci)
if client_data.get("siret"):
try_set_attribute(
client, "CT_Siret", clean_str(client_data["siret"], 15)
)
if client_data.get("tva_intra"):
try_set_attribute(
client,
"CT_Identifiant",
clean_str(client_data["tva_intra"], 25),
)
if client_data.get("code_naf"):
try_set_attribute(
client, "CT_Ape", clean_str(client_data["code_naf"], 7)
)
logger.info("[ETAPE 4] ADRESSE")
if client_data.get("contact"):
contact_nom = clean_str(client_data["contact"], 35)
try:
client.CT_Contact = contact_nom
logger.info(f" CT_Contact (client) = {contact_nom} [OK]")
except Exception as e:
logger.warning(f" CT_Contact (client) [ECHEC: {e}]")
try:
adresse_obj = client.Adresse
adresse_obj.Contact = contact_nom
logger.info(f" Contact (adresse) = {contact_nom} [OK]")
except Exception as e:
logger.warning(f" Contact (adresse) [ECHEC: {e}]")
try:
adresse_obj = client.Adresse
logger.info(" Objet Adresse OK")
if client_data.get("adresse"):
adresse_obj.Adresse = clean_str(client_data["adresse"], 35)
if client_data.get("complement"):
adresse_obj.Complement = clean_str(
client_data["complement"], 35
)
if client_data.get("code_postal"):
adresse_obj.CodePostal = clean_str(
client_data["code_postal"], 9
)
if client_data.get("ville"):
adresse_obj.Ville = clean_str(client_data["ville"], 35)
if client_data.get("region"):
adresse_obj.CodeRegion = clean_str(client_data["region"], 25)
if client_data.get("pays"):
adresse_obj.Pays = clean_str(client_data["pays"], 35)
except Exception as e:
logger.error(f" Adresse erreur: {e}")
logger.info("[ETAPE 5] TELECOM")
try:
telecom_obj = client.Telecom
logger.info(" Objet Telecom OK")
if client_data.get("telephone"):
telecom_obj.Telephone = clean_str(client_data["telephone"], 21)
if client_data.get("telecopie"):
telecom_obj.Telecopie = clean_str(client_data["telecopie"], 21)
if client_data.get("email"):
telecom_obj.EMail = clean_str(client_data["email"], 69)
if client_data.get("site_web"):
telecom_obj.Site = clean_str(client_data["site_web"], 69)
if client_data.get("portable"):
portable = clean_str(client_data["portable"], 21)
try_set_attribute(telecom_obj, "Portable", portable)
logger.info(f" Portable = {portable}")
if client_data.get("facebook"):
facebook = clean_str(client_data["facebook"], 69)
if not try_set_attribute(telecom_obj, "Facebook", facebook):
try_set_attribute(client, "CT_Facebook", facebook)
logger.info(f" Facebook = {facebook}")
if client_data.get("linkedin"):
linkedin = clean_str(client_data["linkedin"], 69)
if not try_set_attribute(telecom_obj, "LinkedIn", linkedin):
try_set_attribute(client, "CT_LinkedIn", linkedin)
logger.info(f" LinkedIn = {linkedin}")
except Exception as e:
logger.error(f" Telecom erreur: {e}")
logger.info("[ETAPE 6] TAUX")
for i in range(1, 5):
val = client_data.get(f"taux{i:02d}")
if val is not None:
try_set_attribute(client, f"CT_Taux{i:02d}", safe_float(val))
logger.info("[ETAPE 7] STATISTIQUES")
stat01 = client_data.get("statistique01") or client_data.get("secteur")
if stat01:
try_set_attribute(client, "CT_Statistique01", clean_str(stat01, 21))
for i in range(2, 11):
val = client_data.get(f"statistique{i:02d}")
if val:
try_set_attribute(
client, f"CT_Statistique{i:02d}", clean_str(val, 21)
)
logger.info("[ETAPE 8] COMMERCIAL")
if client_data.get("encours_autorise"):
try_set_attribute(
client,
"CT_Encours",
safe_float(client_data["encours_autorise"]),
)
if client_data.get("assurance_credit"):
try_set_attribute(
client,
"CT_Assurance",
safe_float(client_data["assurance_credit"]),
)
if client_data.get("langue") is not None:
try_set_attribute(
client, "CT_Langue", safe_int(client_data["langue"])
)
if client_data.get("commercial_code") is not None:
co_no = safe_int(client_data["commercial_code"])
if not try_set_attribute(client, "CO_No", co_no):
try:
factory_collab = (
self.cial.CptaApplication.FactoryCollaborateur
)
persist_collab = factory_collab.ReadIntitule(str(co_no))
if persist_collab:
collab_obj = win32com.client.CastTo(
persist_collab, "IBOCollaborateur3"
)
collab_obj.Read()
client.Collaborateur = collab_obj
logger.debug(
f" Collaborateur (objet) = {co_no} [OK]"
)
except Exception as e:
logger.debug(f" Collaborateur echec: {e}")
logger.info("[ETAPE 9] FACTURATION")
try_set_attribute(
client,
"CT_Lettrage",
1 if client_data.get("lettrage_auto", True) else 0,
)
try_set_attribute(
client, "CT_Sommeil", 0 if client_data.get("est_actif", True) else 1
)
try_set_attribute(
client, "CT_Facture", safe_int(client_data.get("type_facture", 1))
)
if client_data.get("est_prospect") is not None:
try_set_attribute(
client, "CT_Prospect", 1 if client_data["est_prospect"] else 0
)
factu_map = {
"CT_BLFact": "bl_en_facture",
"CT_Saut": "saut_page",
"CT_ValidEch": "validation_echeance",
"CT_ControlEnc": "controle_encours",
"CT_NotRappel": "exclure_relance",
"CT_NotPenal": "exclure_penalites",
"CT_BonAPayer": "bon_a_payer",
}
for attr, key in factu_map.items():
if client_data.get(key) is not None:
try_set_attribute(client, attr, safe_int(client_data[key]))
logger.info("[ETAPE 10] LOGISTIQUE")
logistique_map = {
"CT_PrioriteLivr": "priorite_livraison",
"CT_LivrPartielle": "livraison_partielle",
"CT_DelaiTransport": "delai_transport",
"CT_DelaiAppro": "delai_appro",
}
for attr, key in logistique_map.items():
if client_data.get(key) is not None:
try_set_attribute(client, attr, safe_int(client_data[key]))
if client_data.get("commentaire"):
try_set_attribute(
client,
"CT_Commentaire",
clean_str(client_data["commentaire"], 35),
)
logger.info("[ETAPE 12] ANALYTIQUE")
if client_data.get("section_analytique"):
try_set_attribute(
client,
"CA_Num",
clean_str(client_data["section_analytique"], 13),
)
logger.info("[ETAPE 13] ORGANISATION")
if client_data.get("mode_reglement_code") is not None:
mr_no = safe_int(client_data["mode_reglement_code"])
if not try_set_attribute(client, "MR_No", mr_no):
try:
factory_mr = self.cial.CptaApplication.FactoryModeRegl
persist_mr = factory_mr.ReadIntitule(str(mr_no))
if persist_mr:
mr_obj = win32com.client.CastTo(
persist_mr, "IBOModeRegl3"
)
mr_obj.Read()
client.ModeRegl = mr_obj
logger.debug(f" ModeRegl (objet) = {mr_no} [OK]")
except Exception as e:
logger.debug(f" ModeRegl echec: {e}")
if client_data.get("surveillance_active") is not None:
surveillance = 1 if client_data["surveillance_active"] else 0
try:
client.CT_Surveillance = surveillance
logger.info(f" CT_Surveillance = {surveillance} [OK]")
except Exception as e:
logger.warning(f" CT_Surveillance [ECHEC: {e}]")
if client_data.get("coface"):
coface = clean_str(client_data["coface"], 25)
try:
client.CT_Coface = coface
logger.info(f" CT_Coface = {coface} [OK]")
except Exception as e:
logger.warning(f" CT_Coface [ECHEC: {e}]")
if client_data.get("forme_juridique"):
try_set_attribute(
client,
"CT_SvFormeJuri",
clean_str(client_data["forme_juridique"], 33),
)
if client_data.get("effectif"):
try_set_attribute(
client, "CT_SvEffectif", clean_str(client_data["effectif"], 11)
)
if client_data.get("sv_regularite"):
try_set_attribute(
client, "CT_SvRegul", clean_str(client_data["sv_regularite"], 3)
)
if client_data.get("sv_cotation"):
try_set_attribute(
client,
"CT_SvCotation",
clean_str(client_data["sv_cotation"], 5),
)
if client_data.get("sv_objet_maj"):
try_set_attribute(
client,
"CT_SvObjetMaj",
clean_str(client_data["sv_objet_maj"], 61),
)
ca = client_data.get("ca_annuel") or client_data.get(
"sv_chiffre_affaires"
)
if ca:
try_set_attribute(client, "CT_SvCA", safe_float(ca))
if client_data.get("sv_resultat"):
try_set_attribute(
client, "CT_SvResultat", safe_float(client_data["sv_resultat"])
)
logger.info("=" * 80)
logger.info("[DIAGNOSTIC PRE-WRITE]")
champs_diagnostic = [
"CT_Num",
"CT_Intitule",
"CT_Type",
"CT_Qualite",
"CT_Facture",
"CT_Lettrage",
"CT_Sommeil",
]
for champ in champs_diagnostic:
try:
valeur = getattr(client, champ, "ATTRIBUT_INEXISTANT")
logger.info(f" {champ}: {valeur}")
except Exception as e:
logger.info(f" {champ}: ERREUR ({str(e)[:50]})")
try:
compte_obj = client.CompteGPrinc
if compte_obj:
logger.info(f" CompteGPrinc.CG_Num: {compte_obj.CG_Num}")
logger.info(f" CompteGPrinc.CG_Type: {compte_obj.CG_Type}")
else:
logger.error(" CompteGPrinc: NULL !!!")
except Exception as e:
logger.error(f" CompteGPrinc: ERREUR - {e}")
logger.info("=" * 80)
logger.info("[WRITE]")
try:
client.Write()
client.Read()
logger.info("[OK] Write reussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f"[ERREUR] {error_detail}")
raise RuntimeError(f"Echec Write(): {error_detail}")
num_final = getattr(client, "CT_Num", numero)
logger.info("=" * 80)
logger.info(f"[SUCCES] CLIENT CREE: {num_final}")
logger.info("=" * 80)
return {
"numero": num_final,
"intitule": intitule,
"type_tiers": type_tiers,
"qualite": qualite,
"compte_general": compte,
"date_creation": datetime.now().isoformat(),
}
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def modifier_client(self, code: str, client_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info(f"[MODIFICATION CLIENT SAGE - {code}]")
logger.info("=" * 80)
def clean_str(value, max_len: int) -> str:
if value is None or str(value).lower() in ("none", "null", ""):
return ""
return str(value)[:max_len].strip()
def safe_int(value, default=None):
if value is None:
return default
try:
return int(value)
except (ValueError, TypeError):
return default
def safe_float(value, default=None):
if value is None:
return default
try:
return float(value)
except (ValueError, TypeError):
return default
def try_set_attribute(obj, attr_name, value, variants=None):
"""Essaie de définir un attribut avec plusieurs variantes de noms"""
if variants is None:
variants = [attr_name]
else:
variants = [attr_name] + variants
for variant in variants:
try:
if hasattr(obj, variant):
setattr(obj, variant, value)
logger.debug(f" {variant} = {value} [OK]")
return True
except Exception as e:
logger.debug(f" {variant} echec: {str(e)[:50]}")
return False
champs_modifies = []
logger.info("[ETAPE 1] CHARGEMENT CLIENT")
factory_client = self.cial.CptaApplication.FactoryClient
persist = factory_client.ReadNumero(code)
if not persist:
raise ValueError(f"Client {code} introuvable")
client = win32com.client.CastTo(persist, "IBOClient3")
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 Exception:
if try_set_attribute(client, "CT_Raccourci", raccourci):
champs_modifies.append("raccourci")
if "siret" in client_data:
if try_set_attribute(
client, "CT_Siret", clean_str(client_data["siret"], 15)
):
champs_modifies.append("siret")
if "tva_intra" in client_data:
if try_set_attribute(
client,
"CT_Identifiant",
clean_str(client_data["tva_intra"], 25),
):
champs_modifies.append("tva_intra")
if "code_naf" in client_data:
if try_set_attribute(
client, "CT_Ape", clean_str(client_data["code_naf"], 7)
):
champs_modifies.append("code_naf")
adresse_keys = [
"contact",
"adresse",
"complement",
"code_postal",
"ville",
"region",
"pays",
]
if any(k in client_data for k in adresse_keys):
logger.info("[ETAPE 3] ADRESSE")
try:
if "contact" in client_data:
contact_nom = clean_str(client_data["contact"], 35)
try:
client.CT_Contact = contact_nom
champs_modifies.append("contact (client)")
logger.info(
f" CT_Contact (client) = {contact_nom} [OK]"
)
except Exception as e:
logger.warning(f" CT_Contact (client) [ECHEC: {e}]")
try:
adresse_obj = client.Adresse
adresse_obj.Contact = contact_nom
champs_modifies.append("contact (adresse)")
logger.info(f" Contact (adresse) = {contact_nom} [OK]")
except Exception as e:
logger.warning(f" Contact (adresse) [ECHEC: {e}]")
adresse_obj = client.Adresse
if "adresse" in client_data:
adresse_obj.Adresse = clean_str(client_data["adresse"], 35)
champs_modifies.append("adresse")
if "complement" in client_data:
adresse_obj.Complement = clean_str(
client_data["complement"], 35
)
champs_modifies.append("complement")
if "code_postal" in client_data:
adresse_obj.CodePostal = clean_str(
client_data["code_postal"], 9
)
champs_modifies.append("code_postal")
if "ville" in client_data:
adresse_obj.Ville = clean_str(client_data["ville"], 35)
champs_modifies.append("ville")
if "region" in client_data:
adresse_obj.CodeRegion = clean_str(
client_data["region"], 25
)
champs_modifies.append("region")
if "pays" in client_data:
adresse_obj.Pays = clean_str(client_data["pays"], 35)
champs_modifies.append("pays")
logger.info(
f" Adresse mise à jour ({len([k for k in adresse_keys if k in client_data])} champs)"
)
except Exception as e:
logger.error(f" Adresse erreur: {e}")
telecom_keys = [
"telephone",
"telecopie",
"email",
"site_web",
"portable",
"facebook",
"linkedin",
]
if any(k in client_data for k in telecom_keys):
logger.info("[ETAPE 4] TELECOM")
try:
telecom_obj = client.Telecom
if "telephone" in client_data:
telecom_obj.Telephone = clean_str(
client_data["telephone"], 21
)
champs_modifies.append("telephone")
if "telecopie" in client_data:
telecom_obj.Telecopie = clean_str(
client_data["telecopie"], 21
)
champs_modifies.append("telecopie")
if "email" in client_data:
telecom_obj.EMail = clean_str(client_data["email"], 69)
champs_modifies.append("email")
if "site_web" in client_data:
telecom_obj.Site = clean_str(client_data["site_web"], 69)
champs_modifies.append("site_web")
if "portable" in client_data:
portable = clean_str(client_data["portable"], 21)
if try_set_attribute(telecom_obj, "Portable", portable):
champs_modifies.append("portable")
if "facebook" in client_data:
facebook = clean_str(client_data["facebook"], 69)
if not try_set_attribute(telecom_obj, "Facebook", facebook):
try_set_attribute(client, "CT_Facebook", facebook)
champs_modifies.append("facebook")
if "linkedin" in client_data:
linkedin = clean_str(client_data["linkedin"], 69)
if not try_set_attribute(telecom_obj, "LinkedIn", linkedin):
try_set_attribute(client, "CT_LinkedIn", linkedin)
champs_modifies.append("linkedin")
logger.info(
f" Telecom mis à jour ({len([k for k in telecom_keys if k in client_data])} champs)"
)
except Exception as e:
logger.error(f" Telecom erreur: {e}")
if "compte_general" in client_data:
logger.info("[ETAPE 5] COMPTE GENERAL")
compte = clean_str(client_data["compte_general"], 13)
factory_compte = self.cial.CptaApplication.FactoryCompteG
try:
persist_compte = factory_compte.ReadNumero(compte)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
type_compte = getattr(compte_obj, "CG_Type", None)
if type_compte == 0:
client.CompteGPrinc = compte_obj
champs_modifies.append("compte_general")
logger.info(f" CompteGPrinc = {compte} [OK]")
else:
logger.warning(
f" Compte {compte} - Type {type_compte} incompatible"
)
except Exception as e:
logger.warning(f" CompteGPrinc erreur: {e}")
if (
"categorie_tarifaire" in client_data
or "categorie_comptable" in client_data
):
logger.info("[ETAPE 6] CATEGORIES")
if "categorie_tarifaire" in client_data:
try:
cat_id = str(client_data["categorie_tarifaire"])
factory_cat_tarif = (
self.cial.CptaApplication.FactoryCategorieTarif
)
persist_cat = factory_cat_tarif.ReadIntitule(cat_id)
if persist_cat:
cat_tarif_obj = win32com.client.CastTo(
persist_cat, "IBOCategorieTarif3"
)
cat_tarif_obj.Read()
client.CatTarif = cat_tarif_obj
champs_modifies.append("categorie_tarifaire")
logger.info(f" CatTarif = {cat_id} [OK]")
except Exception as e:
logger.warning(f" CatTarif erreur: {e}")
if "categorie_comptable" in client_data:
try:
cat_id = str(client_data["categorie_comptable"])
factory_cat_compta = (
self.cial.CptaApplication.FactoryCategorieCompta
)
persist_cat = factory_cat_compta.ReadIntitule(cat_id)
if persist_cat:
cat_compta_obj = win32com.client.CastTo(
persist_cat, "IBOCategorieCompta3"
)
cat_compta_obj.Read()
client.CatCompta = cat_compta_obj
champs_modifies.append("categorie_comptable")
logger.info(f" CatCompta = {cat_id} [OK]")
except Exception as e:
logger.warning(f" CatCompta erreur: {e}")
taux_modifies = False
for i in range(1, 5):
key = f"taux{i:02d}"
if key in client_data:
if not taux_modifies:
logger.info("[ETAPE 7] TAUX")
taux_modifies = True
val = safe_float(client_data[key])
if try_set_attribute(client, f"CT_Taux{i:02d}", val):
champs_modifies.append(key)
stat_modifies = False
stat01 = client_data.get("statistique01") or client_data.get("secteur")
if stat01:
if not stat_modifies:
logger.info("[ETAPE 8] STATISTIQUES")
stat_modifies = True
if try_set_attribute(
client, "CT_Statistique01", clean_str(stat01, 21)
):
champs_modifies.append("statistique01")
for i in range(2, 11):
key = f"statistique{i:02d}"
if key in client_data:
if not stat_modifies:
logger.info("[ETAPE 8] STATISTIQUES")
stat_modifies = True
if try_set_attribute(
client,
f"CT_Statistique{i:02d}",
clean_str(client_data[key], 21),
):
champs_modifies.append(key)
commercial_keys = [
"encours_autorise",
"assurance_credit",
"langue",
"commercial_code",
]
if any(k in client_data for k in commercial_keys):
logger.info("[ETAPE 9] COMMERCIAL")
if "encours_autorise" in client_data:
if try_set_attribute(
client,
"CT_Encours",
safe_float(client_data["encours_autorise"]),
):
champs_modifies.append("encours_autorise")
if "assurance_credit" in client_data:
if try_set_attribute(
client,
"CT_Assurance",
safe_float(client_data["assurance_credit"]),
):
champs_modifies.append("assurance_credit")
if "langue" in client_data:
if try_set_attribute(
client, "CT_Langue", safe_int(client_data["langue"])
):
champs_modifies.append("langue")
if "commercial_code" in client_data:
co_no = safe_int(client_data["commercial_code"])
if not try_set_attribute(client, "CO_No", co_no):
try:
factory_collab = (
self.cial.CptaApplication.FactoryCollaborateur
)
persist_collab = factory_collab.ReadIntitule(str(co_no))
if persist_collab:
collab_obj = win32com.client.CastTo(
persist_collab, "IBOCollaborateur3"
)
collab_obj.Read()
client.Collaborateur = collab_obj
champs_modifies.append("commercial_code")
logger.info(f" Collaborateur = {co_no} [OK]")
except Exception as e:
logger.warning(f" Collaborateur erreur: {e}")
facturation_keys = [
"lettrage_auto",
"est_actif",
"type_facture",
"est_prospect",
"bl_en_facture",
"saut_page",
"validation_echeance",
"controle_encours",
"exclure_relance",
"exclure_penalites",
"bon_a_payer",
]
if any(k in client_data for k in facturation_keys):
logger.info("[ETAPE 10] FACTURATION")
if "lettrage_auto" in client_data:
if try_set_attribute(
client,
"CT_Lettrage",
1 if client_data["lettrage_auto"] else 0,
):
champs_modifies.append("lettrage_auto")
if "est_actif" in client_data:
if try_set_attribute(
client, "CT_Sommeil", 0 if client_data["est_actif"] else 1
):
champs_modifies.append("est_actif")
if "type_facture" in client_data:
if try_set_attribute(
client, "CT_Facture", safe_int(client_data["type_facture"])
):
champs_modifies.append("type_facture")
if "est_prospect" in client_data:
if try_set_attribute(
client,
"CT_Prospect",
1 if client_data["est_prospect"] else 0,
):
champs_modifies.append("est_prospect")
factu_map = {
"CT_BLFact": "bl_en_facture",
"CT_Saut": "saut_page",
"CT_ValidEch": "validation_echeance",
"CT_ControlEnc": "controle_encours",
"CT_NotRappel": "exclure_relance",
"CT_NotPenal": "exclure_penalites",
"CT_BonAPayer": "bon_a_payer",
}
for attr, key in factu_map.items():
if key in client_data:
if try_set_attribute(
client, attr, safe_int(client_data[key])
):
champs_modifies.append(key)
logistique_keys = [
"priorite_livraison",
"livraison_partielle",
"delai_transport",
"delai_appro",
]
if any(k in client_data for k in logistique_keys):
logger.info("[ETAPE 11] LOGISTIQUE")
logistique_map = {
"CT_PrioriteLivr": "priorite_livraison",
"CT_LivrPartielle": "livraison_partielle",
"CT_DelaiTransport": "delai_transport",
"CT_DelaiAppro": "delai_appro",
}
for attr, key in logistique_map.items():
if key in client_data:
if try_set_attribute(
client, attr, safe_int(client_data[key])
):
champs_modifies.append(key)
if "commentaire" in client_data:
logger.info("[ETAPE 12] COMMENTAIRE")
if try_set_attribute(
client,
"CT_Commentaire",
clean_str(client_data["commentaire"], 35),
):
champs_modifies.append("commentaire")
if "section_analytique" in client_data:
logger.info("[ETAPE 13] ANALYTIQUE")
if try_set_attribute(
client,
"CA_Num",
clean_str(client_data["section_analytique"], 13),
):
champs_modifies.append("section_analytique")
organisation_keys = [
"mode_reglement_code",
"surveillance_active",
"coface",
"forme_juridique",
"effectif",
"sv_regularite",
"sv_cotation",
"sv_objet_maj",
"ca_annuel",
"sv_chiffre_affaires",
"sv_resultat",
]
if any(k in client_data for k in organisation_keys):
logger.info("[ETAPE 14] ORGANISATION & SURVEILLANCE")
if "mode_reglement_code" in client_data:
mr_no = safe_int(client_data["mode_reglement_code"])
if not try_set_attribute(client, "MR_No", mr_no):
try:
factory_mr = self.cial.CptaApplication.FactoryModeRegl
persist_mr = factory_mr.ReadIntitule(str(mr_no))
if persist_mr:
mr_obj = win32com.client.CastTo(
persist_mr, "IBOModeRegl3"
)
mr_obj.Read()
client.ModeRegl = mr_obj
champs_modifies.append("mode_reglement_code")
logger.info(f" ModeRegl = {mr_no} [OK]")
except Exception as e:
logger.warning(f" ModeRegl erreur: {e}")
if "surveillance_active" in client_data:
surveillance = 1 if client_data["surveillance_active"] else 0
try:
client.CT_Surveillance = surveillance
champs_modifies.append("surveillance_active")
logger.info(f" CT_Surveillance = {surveillance} [OK]")
except Exception as e:
logger.warning(f" CT_Surveillance [ECHEC: {e}]")
if "coface" in client_data:
coface = clean_str(client_data["coface"], 25)
try:
client.CT_Coface = coface
champs_modifies.append("coface")
logger.info(f" CT_Coface = {coface} [OK]")
except Exception as e:
logger.warning(f" CT_Coface [ECHEC: {e}]")
if "forme_juridique" in client_data:
if try_set_attribute(
client,
"CT_SvFormeJuri",
clean_str(client_data["forme_juridique"], 33),
):
champs_modifies.append("forme_juridique")
if "effectif" in client_data:
if try_set_attribute(
client,
"CT_SvEffectif",
clean_str(client_data["effectif"], 11),
):
champs_modifies.append("effectif")
if "sv_regularite" in client_data:
if try_set_attribute(
client,
"CT_SvRegul",
clean_str(client_data["sv_regularite"], 3),
):
champs_modifies.append("sv_regularite")
if "sv_cotation" in client_data:
if try_set_attribute(
client,
"CT_SvCotation",
clean_str(client_data["sv_cotation"], 5),
):
champs_modifies.append("sv_cotation")
if "sv_objet_maj" in client_data:
if try_set_attribute(
client,
"CT_SvObjetMaj",
clean_str(client_data["sv_objet_maj"], 61),
):
champs_modifies.append("sv_objet_maj")
ca = client_data.get("ca_annuel") or client_data.get(
"sv_chiffre_affaires"
)
if ca:
if try_set_attribute(client, "CT_SvCA", safe_float(ca)):
champs_modifies.append("ca_annuel/sv_chiffre_affaires")
if "sv_resultat" in client_data:
if try_set_attribute(
client,
"CT_SvResultat",
safe_float(client_data["sv_resultat"]),
):
champs_modifies.append("sv_resultat")
if not champs_modifies:
logger.warning("Aucun champ à modifier")
return _extraire_client(client)
logger.info("=" * 80)
logger.info(f"[WRITE] {len(champs_modifies)} champs modifiés:")
for i, champ in enumerate(champs_modifies, 1):
logger.info(f" {i}. {champ}")
logger.info("=" * 80)
try:
client.Write()
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 Exception:
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 _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:
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 Exception:
pass
try:
process = self.cial.CreateProcess_Document(
settings.SAGE_TYPE_BON_COMMANDE
)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except Exception:
pass
logger.info(" Document commande créé")
doc.DO_Date = pywintypes.Time(
normaliser_date(commande_data.get("date_commande"))
)
if (
"date_livraison" in commande_data
and commande_data["date_livraison"]
):
doc.DO_DateLivr = pywintypes.Time(
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 = _cast_client(persist_client)
if not client_obj:
raise ValueError("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 Exception:
pass
try:
factory_lignes = doc.FactoryDocumentLigne
except Exception:
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 Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
" 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(" Article associé via SetDefaultArticle")
except Exception:
logger.error(" 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 Exception:
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 Exception:
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(
normaliser_date(commande_data.get("date_commande"))
),
"date_livraison": date_livraison_final,
"reference": reference_finale,
}
except Exception:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except Exception:
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 Exception:
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 Exception:
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("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(" 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(
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(
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 Exception:
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(
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(
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 Exception:
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 Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except Exception:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except Exception:
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 Exception:
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 Exception:
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 Exception:
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 Exception:
pass
try:
process = self.cial.CreateProcess_Document(
settings.SAGE_TYPE_BON_LIVRAISON
)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except Exception:
pass
logger.info(" Document livraison créé")
doc.DO_Date = pywintypes.Time(
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(
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 = _cast_client(persist_client)
if not client_obj:
raise ValueError("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 Exception:
pass
try:
factory_lignes = doc.FactoryDocumentLigne
except Exception:
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 Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
" 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(" Article associé via SetDefaultArticle")
except Exception:
logger.error(" 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 Exception:
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 Exception:
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(
normaliser_date(livraison_data.get("date_livraison"))
),
"date_livraison_prevue": date_livraison_prevue_final,
"reference": reference_finale,
}
except Exception:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except Exception:
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 Exception:
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 Exception:
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("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(
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(
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(
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(
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 Exception:
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 Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except Exception:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except Exception:
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 Exception:
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(" 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(" 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 Exception:
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 Exception:
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 Exception:
pass
try:
process = self.cial.CreateProcess_Document(
settings.SAGE_TYPE_BON_AVOIR
)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except Exception:
pass
logger.info(" Document avoir créé")
doc.DO_Date = pywintypes.Time(
normaliser_date(avoir_data.get("date_avoir"))
)
if "date_livraison" in avoir_data and avoir_data["date_livraison"]:
doc.DO_DateLivr = pywintypes.Time(
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 = _cast_client(persist_client)
if not client_obj:
raise ValueError("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 Exception:
pass
try:
factory_lignes = doc.FactoryDocumentLigne
except Exception:
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 Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
" 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(" Article associé via SetDefaultArticle")
except Exception:
logger.error(" 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 Exception:
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 Exception:
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(
normaliser_date(avoir_data.get("date_avoir"))
),
"date_livraison": date_livraison_final,
"reference": reference_finale,
}
except Exception:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except Exception:
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 Exception:
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 Exception:
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("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(" 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(
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(
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 Exception:
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(
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(
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 Exception:
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 Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except Exception:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except Exception:
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 Exception:
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(" 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(" 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 Exception:
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 Exception:
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 Exception:
pass
try:
process = self.cial.CreateProcess_Document(
settings.SAGE_TYPE_FACTURE
)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except Exception:
pass
logger.info(" Document facture créé")
doc.DO_Date = pywintypes.Time(
normaliser_date(facture_data.get("date_facture"))
)
if (
"date_livraison" in facture_data
and facture_data["date_livraison"]
):
doc.DO_DateLivr = pywintypes.Time(
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 = _cast_client(persist_client)
if not client_obj:
raise ValueError("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 Exception:
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 Exception:
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 Exception:
pass
try:
if hasattr(doc, "DO_Regime"):
doc.DO_Regime = 0
logger.debug(" Régime: 0 (défaut)")
except Exception:
pass
try:
factory_lignes = doc.FactoryDocumentLigne
except Exception:
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 Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
" 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(" Article associé via SetDefaultArticle")
except Exception:
logger.error(" 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 Exception:
try:
doc.SetDefaultClient(client_obj)
except Exception:
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 Exception:
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 Exception:
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(
normaliser_date(facture_data.get("date_facture"))
),
"date_livraison": date_livraison_final,
"reference": reference_finale,
}
except Exception:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.error(" Transaction annulée (rollback)")
except Exception:
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 Exception:
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 Exception:
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("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(" 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(
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(
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 Exception:
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(
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(
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 Exception:
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 Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except Exception:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except Exception:
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 Exception:
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(" 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(" 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 Exception:
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 Exception:
pass
raise RuntimeError(f"Erreur Sage: {error_message}")
def creer_article(self, article_data: dict) -> dict:
"""Crée un article dans Sage 100 avec gestion complète des champs"""
with self._com_context(), self._lock_com:
try:
logger.info("[ARTICLE] === CREATION ARTICLE ===")
valide, erreur = valider_donnees_creation(article_data)
if not valide:
raise ValueError(erreur)
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug("Transaction Sage démarrée")
except Exception as e:
logger.debug(f"BeginTrans non disponible : {e}")
try:
reference = article_data.get("reference", "").upper().strip()
designation = article_data.get("designation", "").strip()
if len(designation) > 69:
designation = designation[:69]
stock_reel = article_data.get("stock_reel", 0.0)
stock_mini = article_data.get("stock_mini", 0.0)
stock_maxi = article_data.get("stock_maxi", 0.0)
logger.info(f"[ARTICLE] Référence : {reference}")
logger.info(f"[ARTICLE] Désignation : {designation}")
# Vérifier si article existe
factory = self.cial.FactoryArticle
try:
article_existant = factory.ReadReference(reference)
if article_existant:
raise ValueError(f"L'article {reference} existe déjà")
except Exception as e:
error_msg = str(e)
if (
"Enregistrement non trouve" not in error_msg
and "-2607" not in error_msg
):
raise
# Créer l'article
persist = factory.Create()
article = win32com.client.CastTo(persist, "IBOArticle3")
article.SetDefault()
article.AR_Ref = reference
article.AR_Design = designation
# === Recherche article modèle ===
logger.info("[MODELE] Recherche article modèle...")
article_modele_ref = None
article_modele = None
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT TOP 1 AR_Ref
FROM F_ARTICLE
WHERE AR_Sommeil = 0
ORDER BY AR_Ref
""")
row = cursor.fetchone()
if row:
article_modele_ref = _safe_strip(row.AR_Ref)
logger.info(
f" [SQL] Article modèle : {article_modele_ref}"
)
except Exception as e:
logger.warning(f" [SQL] Erreur recherche : {e}")
if article_modele_ref:
try:
persist_modele = factory.ReadReference(article_modele_ref)
if persist_modele:
article_modele = win32com.client.CastTo(
persist_modele, "IBOArticle3"
)
article_modele.Read()
logger.info(
f" [OK] Modèle chargé : {article_modele_ref}"
)
except Exception as e:
logger.warning(f" [WARN] Erreur chargement : {e}")
article_modele = None
if not article_modele:
raise ValueError(
"Aucun article modèle trouvé. Créez au moins un article dans Sage."
)
# === Copie Unite depuis modèle ===
logger.info("[UNITE] Copie Unite depuis modèle...")
unite_trouvee = False
try:
unite_obj = getattr(article_modele, "Unite", None)
if unite_obj:
article.Unite = unite_obj
logger.info(" [OK] Unite copiée")
unite_trouvee = True
except Exception as e:
logger.warning(f" Unite non copiable : {e}")
if not unite_trouvee:
raise ValueError(
"Impossible de copier l'unité depuis le modèle"
)
# === Gestion famille ===
famille_trouvee = False
famille_code_personnalise = article_data.get("famille")
if famille_code_personnalise:
logger.info(
f" [FAMILLE] Code demandé : {famille_code_personnalise}"
)
try:
famille_code_exact = None
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT FA_CodeFamille, FA_Type
FROM F_FAMILLE
WHERE UPPER(FA_CodeFamille) = ?
""",
(famille_code_personnalise.upper(),),
)
row = cursor.fetchone()
if row:
famille_code_exact = _safe_strip(row.FA_CodeFamille)
if row.FA_Type == 1:
raise ValueError(
f"Famille '{famille_code_personnalise}' est de type Total"
)
logger.info(
f" [SQL] Famille trouvée : {famille_code_exact}"
)
else:
raise ValueError(
f"Famille '{famille_code_personnalise}' introuvable"
)
if famille_code_exact:
factory_famille = self.cial.FactoryFamille
famille_obj = None
index = 1
while index <= 1000:
try:
persist_test = factory_famille.List(index)
if persist_test is None:
break
fam_test = win32com.client.CastTo(
persist_test, "IBOFamille3"
)
fam_test.Read()
code_test = (
getattr(fam_test, "FA_CodeFamille", "")
.strip()
.upper()
)
if code_test == famille_code_exact.upper():
famille_obj = fam_test
famille_trouvee = True
logger.info(
f" [OK] Famille trouvée à index {index}"
)
break
index += 1
except Exception:
break
if famille_obj:
famille_obj.Read()
article.Famille = famille_obj
else:
raise ValueError(
f"Famille '{famille_code_personnalise}' inaccessible via COM"
)
except Exception as e:
logger.error(f" [ERREUR] Famille : {e}")
raise
if not famille_trouvee:
try:
famille_obj = getattr(article_modele, "Famille", None)
if famille_obj:
article.Famille = famille_obj
logger.info(" [OK] Famille copiée depuis modèle")
famille_trouvee = True
except Exception as e:
logger.debug(f" Famille non copiable : {e}")
# === Champs obligatoires depuis modèle ===
logger.info("[CHAMPS] Copie champs obligatoires...")
article.AR_Type = int(getattr(article_modele, "AR_Type", 0))
article.AR_Nature = int(getattr(article_modele, "AR_Nature", 0))
article.AR_Nomencl = int(getattr(article_modele, "AR_Nomencl", 0))
article.AR_SuiviStock = 2
logger.info(" [OK] Champs de base copiés")
# === Application des champs fournis ===
logger.info("[CHAMPS] Application champs fournis...")
champs_appliques = []
champs_echoues = []
# Prix de vente
if "prix_vente" in article_data:
try:
article.AR_PrixVen = float(article_data["prix_vente"])
champs_appliques.append("prix_vente")
logger.info(
f" ✓ prix_vente = {article_data['prix_vente']}"
)
except Exception as e:
champs_echoues.append(f"prix_vente: {e}")
# Prix d'achat
if "prix_achat" in article_data:
try:
article.AR_PrixAch = float(article_data["prix_achat"])
champs_appliques.append("prix_achat")
logger.info(
f" ✓ prix_achat = {article_data['prix_achat']}"
)
except Exception as e:
champs_echoues.append(f"prix_achat: {e}")
# Coefficient
if "coef" in article_data:
try:
article.AR_Coef = float(article_data["coef"])
champs_appliques.append("coef")
logger.info(f" ✓ coef = {article_data['coef']}")
except Exception as e:
champs_echoues.append(f"coef: {e}")
# Code EAN
if "code_ean" in article_data:
try:
article.AR_CodeBarre = str(article_data["code_ean"])
champs_appliques.append("code_ean")
logger.info(f" ✓ code_ean = {article_data['code_ean']}")
except Exception as e:
champs_echoues.append(f"code_ean: {e}")
# Description -> Utiliser AR_Langue1 ou AR_Langue2 (pas de AR_Commentaire)
if "description" in article_data:
try:
article.AR_Langue1 = str(article_data["description"])[:255]
champs_appliques.append("description")
logger.info(" ✓ description définie (AR_Langue1)")
except Exception as e:
champs_echoues.append(f"description: {e}")
# Pays
if "pays" in article_data:
try:
article.AR_Pays = str(article_data["pays"])[:3].upper()
champs_appliques.append("pays")
logger.info(f" ✓ pays = {article_data['pays']}")
except Exception as e:
champs_echoues.append(f"pays: {e}")
# Garantie
if "garantie" in article_data:
try:
article.AR_Garantie = int(article_data["garantie"])
champs_appliques.append("garantie")
logger.info(f" ✓ garantie = {article_data['garantie']}")
except Exception as e:
champs_echoues.append(f"garantie: {e}")
# Délai
if "delai" in article_data:
try:
article.AR_Delai = int(article_data["delai"])
champs_appliques.append("delai")
logger.info(f" ✓ delai = {article_data['delai']}")
except Exception as e:
champs_echoues.append(f"delai: {e}")
# Poids net
if "poids_net" in article_data:
try:
article.AR_PoidsNet = float(article_data["poids_net"])
champs_appliques.append("poids_net")
logger.info(f" ✓ poids_net = {article_data['poids_net']}")
except Exception as e:
champs_echoues.append(f"poids_net: {e}")
# Poids brut
if "poids_brut" in article_data:
try:
article.AR_PoidsBrut = float(article_data["poids_brut"])
champs_appliques.append("poids_brut")
logger.info(
f" ✓ poids_brut = {article_data['poids_brut']}"
)
except Exception as e:
champs_echoues.append(f"poids_brut: {e}")
# Code fiscal
if "code_fiscal" in article_data:
try:
article.AR_CodeFiscal = str(article_data["code_fiscal"])[
:10
]
champs_appliques.append("code_fiscal")
logger.info(
f" ✓ code_fiscal = {article_data['code_fiscal']}"
)
except Exception as e:
champs_echoues.append(f"code_fiscal: {e}")
# Statistiques (AR_Stat01 à AR_Stat05 existent bien !)
for i in range(1, 6):
stat_key = f"stat_0{i}"
if stat_key in article_data:
try:
setattr(
article,
f"AR_Stat0{i}",
str(article_data[stat_key])[:20],
)
champs_appliques.append(stat_key)
logger.info(
f"{stat_key} = {article_data[stat_key]}"
)
except Exception as e:
champs_echoues.append(f"{stat_key}: {e}")
# Soumis escompte
if "soumis_escompte" in article_data:
try:
article.AR_Escompte = (
1 if article_data["soumis_escompte"] else 0
)
champs_appliques.append("soumis_escompte")
logger.info(
f" ✓ soumis_escompte = {article_data['soumis_escompte']}"
)
except Exception as e:
champs_echoues.append(f"soumis_escompte: {e}")
# Publié
if "publie" in article_data:
try:
article.AR_Publie = 1 if article_data["publie"] else 0
champs_appliques.append("publie")
logger.info(f" ✓ publie = {article_data['publie']}")
except Exception as e:
champs_echoues.append(f"publie: {e}")
# En sommeil
if "en_sommeil" in article_data:
try:
article.AR_Sommeil = 1 if article_data["en_sommeil"] else 0
champs_appliques.append("en_sommeil")
logger.info(
f" ✓ en_sommeil = {article_data['en_sommeil']}"
)
except Exception as e:
champs_echoues.append(f"en_sommeil: {e}")
if champs_echoues:
logger.warning(
f"[WARN] Champs échoués : {', '.join(champs_echoues)}"
)
logger.info(
f"[CHAMPS] Appliqués : {len(champs_appliques)} / Échoués : {len(champs_echoues)}"
)
# === Écriture dans Sage ===
logger.info("[ARTICLE] Écriture dans Sage...")
try:
article.Write()
logger.info(" [OK] Write() réussi")
# Vérification immédiate SQL
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT AR_PrixAch, AR_Coef, AR_Stat01, AR_Stat02
FROM F_ARTICLE
WHERE AR_Ref = ?
""",
(reference.upper(),),
)
verif_row = cursor.fetchone()
if verif_row:
logger.info(
f" [VERIF SQL] prix_achat={verif_row[0]}, coef={verif_row[1]}, stat01={verif_row[2]}, stat02={verif_row[3]}"
)
except Exception as e_verif:
logger.warning(f" [VERIF SQL] Impossible : {e_verif}")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
except Exception:
pass
logger.error(f" [ERREUR] Write() : {error_detail}")
raise RuntimeError(f"Échec création : {error_detail}")
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.info("[COMMIT] Transaction committée")
except Exception as e:
logger.warning(f"[COMMIT] Erreur : {e}")
# === Gestion des stocks ===
has_stock_values = stock_reel or stock_mini or stock_maxi
if has_stock_values:
logger.info("[STOCK] Initialisation stocks...")
# Créer l'entrée de stock pour le stock_reel
if stock_reel:
try:
lignes = [
{
"article_ref": reference,
"quantite": stock_reel,
"stock_mini": stock_mini if stock_mini else 0.0,
"stock_maxi": stock_maxi if stock_maxi else 0.0,
}
]
entree_stock_data = {
"date_mouvement": datetime.now().date(),
"reference": f"INIT-{reference}",
"lignes": lignes,
}
resultat_stock = self.creer_entree_stock(
entree_stock_data
)
logger.info(
f"[STOCK] Entrée créée : {resultat_stock.get('numero')}"
)
except Exception as e:
logger.error(f"[STOCK] Erreur entrée : {e}")
# Mise à jour stocks mini/maxi via SQL
if stock_mini or stock_maxi:
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT TOP 1 DE_No FROM F_DEPOT ORDER BY DE_No"
)
row = cursor.fetchone()
if row:
depot_no = row.DE_No
cursor.execute(
"""
SELECT COUNT(*)
FROM F_ARTSTOCK
WHERE AR_Ref = ? AND DE_No = ?
""",
(reference.upper(), depot_no),
)
count = cursor.fetchone()[0]
if count > 0:
update_parts = []
params = []
if stock_mini:
update_parts.append("AS_QteMini = ?")
params.append(float(stock_mini))
if stock_maxi:
update_parts.append("AS_QteMaxi = ?")
params.append(float(stock_maxi))
if update_parts:
params.extend(
[reference.upper(), depot_no]
)
cursor.execute(
f"""
UPDATE F_ARTSTOCK
SET {", ".join(update_parts)}
WHERE AR_Ref = ? AND DE_No = ?
""",
params,
)
conn.commit()
logger.info(
f" [SQL] Stocks mini/maxi mis à jour (mini={stock_mini}, maxi={stock_maxi})"
)
else:
cursor.execute(
"""
INSERT INTO F_ARTSTOCK (AR_Ref, DE_No, AS_QteSto, AS_QteMini, AS_QteMaxi)
VALUES (?, ?, ?, ?, ?)
""",
(
reference.upper(),
depot_no,
0.0,
float(stock_mini)
if stock_mini
else 0.0,
float(stock_maxi)
if stock_maxi
else 0.0,
),
)
conn.commit()
logger.info(
f" [SQL] Ligne stock créée (mini={stock_mini}, maxi={stock_maxi})"
)
except Exception as e:
logger.error(f"[STOCK] Erreur SQL mini/maxi : {e}")
# === Construction réponse depuis SQL ===
logger.info("[RESPONSE] Construction réponse depuis SQL...")
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
# Lecture complète article
cursor.execute(
"""
SELECT
a.AR_Ref, a.AR_Design, a.AR_PrixVen, a.AR_PrixAch, a.AR_Coef,
a.AR_CodeBarre, a.AR_CodeFiscal, a.AR_Pays, a.AR_Garantie, a.AR_Delai,
a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_Escompte, a.AR_Publie, a.AR_Sommeil,
a.FA_CodeFamille, f.FA_Intitule,
a.AR_Type, a.AR_UniteVen, a.AR_Langue1,
a.AR_Stat01, a.AR_Stat02, a.AR_Stat03, a.AR_Stat04, a.AR_Stat05
FROM F_ARTICLE a
LEFT JOIN F_FAMILLE f ON a.FA_CodeFamille = f.FA_CodeFamille
WHERE a.AR_Ref = ?
""",
(reference.upper(),),
)
row = cursor.fetchone()
if row:
resultat = {
"reference": _safe_strip(row[0]),
"designation": _safe_strip(row[1]),
"prix_vente": float(row[2]) if row[2] else 0.0,
"prix_achat": float(row[3]) if row[3] else 0.0,
"coef": float(row[4]) if row[4] else None,
"code_ean": _safe_strip(row[5]),
"code_barre": _safe_strip(row[5]),
"code_fiscal": _safe_strip(row[6]),
"pays": _safe_strip(row[7]),
"garantie": int(row[8]) if row[8] else None,
"delai": int(row[9]) if row[9] else None,
"poids_net": float(row[10]) if row[10] else None,
"poids_brut": float(row[11]) if row[11] else None,
"soumis_escompte": bool(row[12])
if row[12] is not None
else None,
"publie": bool(row[13])
if row[13] is not None
else None,
"en_sommeil": bool(row[14]) if row[14] else False,
"est_actif": not bool(row[14])
if row[14] is not None
else True,
"famille_code": _safe_strip(row[15]),
"famille_libelle": _safe_strip(row[16])
if row[16]
else "",
"type_article": int(row[17])
if row[17] is not None
else 0,
"type_article_libelle": "Article"
if not row[17]
else None,
"unite_vente": _safe_strip(row[18])
if row[18]
else None,
"description": _safe_strip(row[19])
if row[19]
else None,
"stat_01": _safe_strip(row[20])
if row[20]
else None,
"stat_02": _safe_strip(row[21])
if row[21]
else None,
"stat_03": _safe_strip(row[22])
if row[22]
else None,
"stat_04": _safe_strip(row[23])
if row[23]
else None,
"stat_05": _safe_strip(row[24])
if row[24]
else None,
}
# Lecture stocks
cursor.execute(
"""
SELECT s.AS_QteSto, s.AS_QteMini, s.AS_QteMaxi, s.AS_QteRes, s.AS_QteCom
FROM F_ARTSTOCK s
WHERE s.AR_Ref = ?
""",
(reference.upper(),),
)
stock_total = 0.0
stock_mini_val = 0.0
stock_maxi_val = 0.0
stock_reserve_val = 0.0
stock_commande_val = 0.0
stock_row = cursor.fetchone()
if stock_row:
stock_total = (
float(stock_row[0]) if stock_row[0] else 0.0
)
stock_mini_val = (
float(stock_row[1]) if stock_row[1] else 0.0
)
stock_maxi_val = (
float(stock_row[2]) if stock_row[2] else 0.0
)
stock_reserve_val = (
float(stock_row[3]) if stock_row[3] else 0.0
)
stock_commande_val = (
float(stock_row[4]) if stock_row[4] else 0.0
)
resultat["stock_reel"] = stock_total
resultat["stock_mini"] = stock_mini_val
resultat["stock_maxi"] = stock_maxi_val
resultat["stock_disponible"] = (
stock_total - stock_reserve_val
)
resultat["stock_reserve"] = stock_reserve_val
resultat["stock_commande"] = stock_commande_val
logger.info(
f"[RESPONSE] Réponse SQL construite : prix_achat={resultat['prix_achat']}, coef={resultat['coef']}"
)
logger.info(
f"[OK] ARTICLE CREE : {reference} - Stock : {stock_total}"
)
return resultat
except Exception as e:
logger.warning(f"[RESPONSE] Erreur lecture SQL : {e}")
# Fallback sur extraction COM si SQL échoue
logger.info("[FALLBACK] Extraction COM...")
article_cree_persist = factory.ReadReference(reference)
if not article_cree_persist:
raise RuntimeError("Article créé mais introuvable")
article_cree = win32com.client.CastTo(
article_cree_persist, "IBOArticle3"
)
article_cree.Read()
resultat = _extraire_article(article_cree)
if not resultat:
resultat = {"reference": reference, "designation": designation}
# Forcer les valeurs connues
for key in [
"prix_vente",
"prix_achat",
"coef",
"stock_mini",
"stock_maxi",
"code_ean",
"code_fiscal",
"pays",
"garantie",
"delai",
"poids_net",
"poids_brut",
"soumis_escompte",
"publie",
]:
if key in article_data and article_data[key] is not None:
resultat[key] = article_data[key]
return resultat
except ValueError:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except Exception:
pass
raise
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except Exception:
pass
logger.error(f"Erreur création : {e}", exc_info=True)
raise RuntimeError(f"Erreur création : {str(e)}")
except Exception as e:
logger.error(f"Erreur globale : {e}", exc_info=True)
raise
def modifier_article(self, reference: str, article_data: Dict) -> Dict:
"""Modifie un article existant dans Sage 100"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
valide, erreur = valider_donnees_modification(article_data)
if not valide:
raise ValueError(erreur)
with self._com_context(), self._lock_com:
logger.info(f"[ARTICLE] === MODIFICATION {reference} ===")
factory_article = self.cial.FactoryArticle
persist = factory_article.ReadReference(reference.upper())
if not persist:
raise ValueError(f"Article {reference} introuvable")
article = win32com.client.CastTo(persist, "IBOArticle3")
article.Read()
logger.info(f"[ARTICLE] Trouvé : {reference}")
champs_modifies = []
champs_echoues = []
# === Gestion famille ===
if "famille" in article_data and article_data["famille"]:
famille_code_demande = article_data["famille"].upper().strip()
logger.info(f"[FAMILLE] Changement : {famille_code_demande}")
try:
famille_code_exact = None
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT FA_CodeFamille, FA_Type
FROM F_FAMILLE
WHERE UPPER(FA_CodeFamille) = ?
""",
(famille_code_demande,),
)
row = cursor.fetchone()
if row:
famille_code_exact = _safe_strip(row.FA_CodeFamille)
if row.FA_Type == 1:
raise ValueError(
f"Famille '{famille_code_demande}' est de type Total"
)
logger.info(
f" [SQL] Famille trouvée : {famille_code_exact}"
)
else:
raise ValueError(
f"Famille '{famille_code_demande}' introuvable"
)
if famille_code_exact:
factory_famille = self.cial.FactoryFamille
famille_obj = None
index = 1
while index <= 1000:
try:
persist_test = factory_famille.List(index)
if persist_test is None:
break
fam_test = win32com.client.CastTo(
persist_test, "IBOFamille3"
)
fam_test.Read()
code_test = (
getattr(fam_test, "FA_CodeFamille", "")
.strip()
.upper()
)
if code_test == famille_code_exact.upper():
famille_obj = fam_test
logger.info(f" [OK] Famille à index {index}")
break
index += 1
except Exception:
break
if famille_obj:
famille_obj.Read()
article.Famille = famille_obj
champs_modifies.append("famille")
logger.info(
f" [OK] Famille changée : {famille_code_exact}"
)
else:
raise ValueError(
f"Famille '{famille_code_demande}' inaccessible via COM"
)
except Exception as e:
logger.error(f" [ERREUR] Famille : {e}")
champs_echoues.append(f"famille: {e}")
# === Traitement explicite des champs ===
if "designation" in article_data:
try:
designation = str(article_data["designation"])[:69].strip()
article.AR_Design = designation
champs_modifies.append("designation")
logger.info(f" ✓ designation = {designation}")
except Exception as e:
champs_echoues.append(f"designation: {e}")
if "prix_vente" in article_data:
try:
article.AR_PrixVen = float(article_data["prix_vente"])
champs_modifies.append("prix_vente")
logger.info(f" ✓ prix_vente = {article_data['prix_vente']}")
except Exception as e:
champs_echoues.append(f"prix_vente: {e}")
if "prix_achat" in article_data:
try:
article.AR_PrixAch = float(article_data["prix_achat"])
champs_modifies.append("prix_achat")
logger.info(f" ✓ prix_achat = {article_data['prix_achat']}")
except Exception as e:
champs_echoues.append(f"prix_achat: {e}")
if "coef" in article_data:
try:
article.AR_Coef = float(article_data["coef"])
champs_modifies.append("coef")
logger.info(f" ✓ coef = {article_data['coef']}")
except Exception as e:
champs_echoues.append(f"coef: {e}")
if "code_ean" in article_data:
try:
article.AR_CodeBarre = str(article_data["code_ean"])[
:13
].strip()
champs_modifies.append("code_ean")
logger.info(f" ✓ code_ean = {article_data['code_ean']}")
except Exception as e:
champs_echoues.append(f"code_ean: {e}")
if "description" in article_data:
try:
article.AR_Langue1 = str(article_data["description"])[
:255
].strip()
champs_modifies.append("description")
logger.info(" ✓ description définie (AR_Langue1)")
except Exception as e:
champs_echoues.append(f"description: {e}")
if "pays" in article_data:
try:
article.AR_Pays = str(article_data["pays"])[:3].upper()
champs_modifies.append("pays")
logger.info(f" ✓ pays = {article_data['pays']}")
except Exception as e:
champs_echoues.append(f"pays: {e}")
if "garantie" in article_data:
try:
article.AR_Garantie = int(article_data["garantie"])
champs_modifies.append("garantie")
logger.info(f" ✓ garantie = {article_data['garantie']}")
except Exception as e:
champs_echoues.append(f"garantie: {e}")
if "delai" in article_data:
try:
article.AR_Delai = int(article_data["delai"])
champs_modifies.append("delai")
logger.info(f" ✓ delai = {article_data['delai']}")
except Exception as e:
champs_echoues.append(f"delai: {e}")
if "poids_net" in article_data:
try:
article.AR_PoidsNet = float(article_data["poids_net"])
champs_modifies.append("poids_net")
logger.info(f" ✓ poids_net = {article_data['poids_net']}")
except Exception as e:
champs_echoues.append(f"poids_net: {e}")
if "poids_brut" in article_data:
try:
article.AR_PoidsBrut = float(article_data["poids_brut"])
champs_modifies.append("poids_brut")
logger.info(f" ✓ poids_brut = {article_data['poids_brut']}")
except Exception as e:
champs_echoues.append(f"poids_brut: {e}")
if "code_fiscal" in article_data:
try:
article.AR_CodeFiscal = str(article_data["code_fiscal"])[:10]
champs_modifies.append("code_fiscal")
logger.info(f" ✓ code_fiscal = {article_data['code_fiscal']}")
except Exception as e:
champs_echoues.append(f"code_fiscal: {e}")
# Statistiques
for i in range(1, 6):
stat_key = f"stat_0{i}"
if stat_key in article_data:
try:
setattr(
article,
f"AR_Stat0{i}",
str(article_data[stat_key])[:20],
)
champs_modifies.append(stat_key)
logger.info(f"{stat_key} = {article_data[stat_key]}")
except Exception as e:
champs_echoues.append(f"{stat_key}: {e}")
if "soumis_escompte" in article_data:
try:
article.AR_Escompte = (
1 if article_data["soumis_escompte"] else 0
)
champs_modifies.append("soumis_escompte")
logger.info(
f" ✓ soumis_escompte = {article_data['soumis_escompte']}"
)
except Exception as e:
champs_echoues.append(f"soumis_escompte: {e}")
if "publie" in article_data:
try:
article.AR_Publie = 1 if article_data["publie"] else 0
champs_modifies.append("publie")
logger.info(f" ✓ publie = {article_data['publie']}")
except Exception as e:
champs_echoues.append(f"publie: {e}")
if "en_sommeil" in article_data:
try:
article.AR_Sommeil = 1 if article_data["en_sommeil"] else 0
champs_modifies.append("en_sommeil")
logger.info(f" ✓ en_sommeil = {article_data['en_sommeil']}")
except Exception as e:
champs_echoues.append(f"en_sommeil: {e}")
if champs_echoues:
logger.warning(
f"[WARN] Champs échoués : {', '.join(champs_echoues)}"
)
if not champs_modifies:
logger.warning("[ARTICLE] Aucun champ modifié")
return _extraire_article(article)
logger.info(f"[ARTICLE] Champs modifiés : {', '.join(champs_modifies)}")
logger.info("[ARTICLE] Écriture...")
# === Écriture COM ===
try:
article.Write()
logger.info("[ARTICLE] Write() réussi")
# Vérification immédiate SQL
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT AR_PrixAch, AR_Coef, AR_Stat01, AR_Stat02
FROM F_ARTICLE
WHERE AR_Ref = ?
""",
(reference.upper(),),
)
verif_row = cursor.fetchone()
if verif_row:
logger.info(
f" [VERIF SQL] prix_achat={verif_row[0]}, coef={verif_row[1]}, stat01={verif_row[2]}, stat02={verif_row[3]}"
)
except Exception as e_verif:
logger.warning(f" [VERIF SQL] Impossible : {e_verif}")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f"[ARTICLE] Erreur Write() : {error_detail}")
raise RuntimeError(f"Échec modification : {error_detail}")
# === Gestion stocks mini/maxi via SQL ===
if "stock_mini" in article_data or "stock_maxi" in article_data:
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"SELECT TOP 1 DE_No FROM F_DEPOT ORDER BY DE_No"
)
row = cursor.fetchone()
if row:
depot_no = row.DE_No
update_parts = []
params = []
if "stock_mini" in article_data:
update_parts.append("AS_QteMini = ?")
params.append(float(article_data["stock_mini"]))
champs_modifies.append("stock_mini")
if "stock_maxi" in article_data:
update_parts.append("AS_QteMaxi = ?")
params.append(float(article_data["stock_maxi"]))
champs_modifies.append("stock_maxi")
if update_parts:
params.extend([reference.upper(), depot_no])
cursor.execute(
f"""
UPDATE F_ARTSTOCK
SET {", ".join(update_parts)}
WHERE AR_Ref = ? AND DE_No = ?
""",
params,
)
conn.commit()
logger.info(" [SQL] Stocks mini/maxi mis à jour")
except Exception as e:
logger.error(f"[STOCK] Erreur SQL : {e}")
champs_echoues.append(f"stocks: {e}")
# === Construction réponse depuis SQL ===
logger.info("[RESPONSE] Construction réponse depuis SQL...")
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
# Lecture complète article
cursor.execute(
"""
SELECT
a.AR_Ref, a.AR_Design, a.AR_PrixVen, a.AR_PrixAch, a.AR_Coef,
a.AR_CodeBarre, a.AR_CodeFiscal, a.AR_Pays, a.AR_Garantie, a.AR_Delai,
a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_Escompte, a.AR_Publie, a.AR_Sommeil,
a.FA_CodeFamille, f.FA_Intitule,
a.AR_Type, a.AR_UniteVen, a.AR_Langue1,
a.AR_Stat01, a.AR_Stat02, a.AR_Stat03, a.AR_Stat04, a.AR_Stat05
FROM F_ARTICLE a
LEFT JOIN F_FAMILLE f ON a.FA_CodeFamille = f.FA_CodeFamille
WHERE a.AR_Ref = ?
""",
(reference.upper(),),
)
row = cursor.fetchone()
if row:
resultat = {
"reference": _safe_strip(row[0]),
"designation": _safe_strip(row[1]),
"prix_vente": float(row[2]) if row[2] else 0.0,
"prix_achat": float(row[3]) if row[3] else 0.0,
"coef": float(row[4]) if row[4] else None,
"code_ean": _safe_strip(row[5]),
"code_barre": _safe_strip(row[5]),
"code_fiscal": _safe_strip(row[6]),
"pays": _safe_strip(row[7]),
"garantie": int(row[8]) if row[8] else None,
"delai": int(row[9]) if row[9] else None,
"poids_net": float(row[10]) if row[10] else None,
"poids_brut": float(row[11]) if row[11] else None,
"soumis_escompte": bool(row[12])
if row[12] is not None
else None,
"publie": bool(row[13])
if row[13] is not None
else None,
"en_sommeil": bool(row[14]) if row[14] else False,
"est_actif": not bool(row[14])
if row[14] is not None
else True,
"famille_code": _safe_strip(row[15]),
"famille_libelle": _safe_strip(row[16])
if row[16]
else "",
"type_article": int(row[17])
if row[17] is not None
else 0,
"type_article_libelle": "Article"
if not row[17]
else None,
"unite_vente": _safe_strip(row[18])
if row[18]
else None,
"description": _safe_strip(row[19])
if row[19]
else None,
"stat_01": _safe_strip(row[20]) if row[20] else None,
"stat_02": _safe_strip(row[21]) if row[21] else None,
"stat_03": _safe_strip(row[22]) if row[22] else None,
"stat_04": _safe_strip(row[23]) if row[23] else None,
"stat_05": _safe_strip(row[24]) if row[24] else None,
}
# Lecture stocks
cursor.execute(
"""
SELECT s.AS_QteSto, s.AS_QteMini, s.AS_QteMaxi, s.AS_QteRes, s.AS_QteCom
FROM F_ARTSTOCK s
WHERE s.AR_Ref = ?
""",
(reference.upper(),),
)
stock_total = 0.0
stock_mini_val = 0.0
stock_maxi_val = 0.0
stock_reserve_val = 0.0
stock_commande_val = 0.0
stock_row = cursor.fetchone()
if stock_row:
stock_total = (
float(stock_row[0]) if stock_row[0] else 0.0
)
stock_mini_val = (
float(stock_row[1]) if stock_row[1] else 0.0
)
stock_maxi_val = (
float(stock_row[2]) if stock_row[2] else 0.0
)
stock_reserve_val = (
float(stock_row[3]) if stock_row[3] else 0.0
)
stock_commande_val = (
float(stock_row[4]) if stock_row[4] else 0.0
)
resultat["stock_reel"] = stock_total
resultat["stock_mini"] = stock_mini_val
resultat["stock_maxi"] = stock_maxi_val
resultat["stock_disponible"] = (
stock_total - stock_reserve_val
)
resultat["stock_reserve"] = stock_reserve_val
resultat["stock_commande"] = stock_commande_val
logger.info(
f"[RESPONSE] Réponse SQL construite : prix_achat={resultat['prix_achat']}, coef={resultat['coef']}"
)
logger.info(
f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)"
)
return resultat
except Exception as e:
logger.warning(f"[RESPONSE] Erreur lecture SQL : {e}")
# Fallback sur extraction COM
article.Read()
logger.info(
f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)"
)
resultat = _extraire_article(article)
if not resultat:
resultat = {"reference": reference}
return resultat
except ValueError as e:
logger.error(f"[ARTICLE] Erreur métier : {e}")
raise
except Exception as e:
logger.error(f"[ARTICLE] Erreur technique : {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = f"Erreur Sage: {err.Description}"
except Exception:
pass
raise RuntimeError(f"Erreur technique Sage : {error_message}")
def creer_famille(self, famille_data: dict) -> dict:
with self._com_context(), self._lock_com:
try:
logger.info("[FAMILLE] === CRÉATION FAMILLE (TYPE DÉTAIL) ===")
code = famille_data.get("code", "").upper().strip()
if not code:
raise ValueError("Le code famille est obligatoire")
if len(code) > 18:
raise ValueError(
"Le code famille ne peut pas dépasser 18 caractères"
)
intitule = famille_data.get("intitule", "").strip()
if not intitule:
raise ValueError("L'intitulé est obligatoire")
if len(intitule) > 69:
intitule = intitule[:69]
logger.info(f"[FAMILLE] Code : {code}")
logger.info(f"[FAMILLE] Intitulé : {intitule}")
type_demande = famille_data.get("type", 0)
if type_demande == 1:
logger.warning(
"[FAMILLE] Type 'Total' demandé mais IGNORÉ - Création en type Détail uniquement"
)
factory_famille = self.cial.FactoryFamille
try:
index = 1
while index <= 1000:
try:
persist_test = factory_famille.List(index)
if persist_test is None:
break
fam_test = win32com.client.CastTo(
persist_test, "IBOFamille3"
)
fam_test.Read()
code_existant = (
getattr(fam_test, "FA_CodeFamille", "").strip().upper()
)
if code_existant == code:
raise ValueError(f"La famille {code} existe déjà")
index += 1
except ValueError:
raise
except Exception:
index += 1
except ValueError:
raise
persist = factory_famille.Create()
famille = win32com.client.CastTo(persist, "IBOFamille3")
famille.SetDefault()
famille.FA_CodeFamille = code
famille.FA_Intitule = intitule
try:
famille.FA_Type = 0
logger.info("[FAMILLE] Type : 0 (Détail)")
except Exception as e:
logger.warning(f"[FAMILLE] FA_Type non défini : {e}")
compte_achat = famille_data.get("compte_achat")
if compte_achat:
try:
factory_compte = self.cial.CptaApplication.FactoryCompteG
persist_compte = factory_compte.ReadNumero(compte_achat)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
famille.CompteGAchat = compte_obj
logger.info(f"[FAMILLE] Compte achat : {compte_achat}")
except Exception as e:
logger.warning(f"[FAMILLE] Compte achat non défini : {e}")
compte_vente = famille_data.get("compte_vente")
if compte_vente:
try:
factory_compte = self.cial.CptaApplication.FactoryCompteG
persist_compte = factory_compte.ReadNumero(compte_vente)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
famille.CompteGVente = compte_obj
logger.info(f"[FAMILLE] Compte vente : {compte_vente}")
except Exception as e:
logger.warning(f"[FAMILLE] Compte vente non défini : {e}")
logger.info("[FAMILLE] Écriture dans Sage...")
try:
famille.Write()
logger.info("[FAMILLE] Write() réussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except Exception:
pass
logger.error(f"[FAMILLE] Erreur Write() : {error_detail}")
raise RuntimeError(f"Échec création famille : {error_detail}")
famille.Read()
resultat = {
"code": getattr(famille, "FA_CodeFamille", "").strip(),
"intitule": getattr(famille, "FA_Intitule", "").strip(),
"type": 0,
"type_libelle": "Détail",
}
logger.info(f"[FAMILLE] FAMILLE CRÉÉE : {code} - {intitule} (Détail)")
return resultat
except ValueError as e:
logger.error(f"[FAMILLE] Erreur métier : {e}")
raise
except Exception as e:
logger.error(f"[FAMILLE] Erreur technique : {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = f"Erreur Sage: {err.Description}"
except Exception:
pass
raise RuntimeError(f"Erreur technique Sage : {error_message}")
def lister_toutes_familles(
self, filtre: str = "", inclure_totaux: bool = True
) -> List[Dict]:
"""Liste toutes les familles avec leurs comptes comptables et fournisseur principal"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
logger.info(
"[SQL] Chargement des familles avec F_FAMCOMPTA et F_FAMFOURNISS..."
)
query = """
SELECT
-- F_FAMILLE - Identification
f.FA_CodeFamille,
f.FA_Type,
f.FA_Intitule,
f.FA_UniteVen,
f.FA_Coef,
f.FA_SuiviStock,
f.FA_Garantie,
f.FA_Central,
-- F_FAMILLE - Statistiques
f.FA_Stat01,
f.FA_Stat02,
f.FA_Stat03,
f.FA_Stat04,
f.FA_Stat05,
-- F_FAMILLE - Fiscal et gestion
f.FA_CodeFiscal,
f.FA_Pays,
f.FA_UnitePoids,
f.FA_Escompte,
f.FA_Delai,
f.FA_HorsStat,
f.FA_VteDebit,
f.FA_NotImp,
-- F_FAMILLE - Frais annexes (3 frais avec 3 remises chacun)
f.FA_Frais01FR_Denomination,
f.FA_Frais01FR_Rem01REM_Valeur,
f.FA_Frais01FR_Rem01REM_Type,
f.FA_Frais01FR_Rem02REM_Valeur,
f.FA_Frais01FR_Rem02REM_Type,
f.FA_Frais01FR_Rem03REM_Valeur,
f.FA_Frais01FR_Rem03REM_Type,
f.FA_Frais02FR_Denomination,
f.FA_Frais02FR_Rem01REM_Valeur,
f.FA_Frais02FR_Rem01REM_Type,
f.FA_Frais02FR_Rem02REM_Valeur,
f.FA_Frais02FR_Rem02REM_Type,
f.FA_Frais02FR_Rem03REM_Valeur,
f.FA_Frais02FR_Rem03REM_Type,
f.FA_Frais03FR_Denomination,
f.FA_Frais03FR_Rem01REM_Valeur,
f.FA_Frais03FR_Rem01REM_Type,
f.FA_Frais03FR_Rem02REM_Valeur,
f.FA_Frais03FR_Rem02REM_Type,
f.FA_Frais03FR_Rem03REM_Valeur,
f.FA_Frais03FR_Rem03REM_Type,
-- F_FAMILLE - Options diverses
f.FA_Contremarque,
f.FA_FactPoids,
f.FA_FactForfait,
f.FA_Publie,
f.FA_RacineRef,
f.FA_RacineCB,
-- F_FAMILLE - Catégories
f.CL_No1,
f.CL_No2,
f.CL_No3,
f.CL_No4,
-- F_FAMILLE - Gestion avancée
f.FA_Nature,
f.FA_NbColis,
f.FA_SousTraitance,
f.FA_Fictif,
f.FA_Criticite,
-- F_FAMILLE - Métadonnées système
f.cbMarq,
f.cbCreateur,
f.cbModification,
f.cbCreation,
f.cbCreationUser,
-- F_FAMCOMPTA Vente (FCP_Type = 0)
vte.FCP_ComptaCPT_CompteG,
vte.FCP_ComptaCPT_CompteA,
vte.FCP_ComptaCPT_Taxe1,
vte.FCP_ComptaCPT_Taxe2,
vte.FCP_ComptaCPT_Taxe3,
vte.FCP_ComptaCPT_Date1,
vte.FCP_ComptaCPT_Date2,
vte.FCP_ComptaCPT_Date3,
vte.FCP_TypeFacture,
-- F_FAMCOMPTA Achat (FCP_Type = 1)
ach.FCP_ComptaCPT_CompteG,
ach.FCP_ComptaCPT_CompteA,
ach.FCP_ComptaCPT_Taxe1,
ach.FCP_ComptaCPT_Taxe2,
ach.FCP_ComptaCPT_Taxe3,
ach.FCP_ComptaCPT_Date1,
ach.FCP_ComptaCPT_Date2,
ach.FCP_ComptaCPT_Date3,
ach.FCP_TypeFacture,
-- F_FAMCOMPTA Stock (FCP_Type = 2)
sto.FCP_ComptaCPT_CompteG,
sto.FCP_ComptaCPT_CompteA,
-- F_FAMFOURNISS (fournisseur principal FF_Principal=1)
ff.CT_Num,
ff.FF_Unite,
ff.FF_Conversion,
ff.FF_DelaiAppro,
ff.FF_Garantie,
ff.FF_Colisage,
ff.FF_QteMini,
ff.FF_QteMont,
ff.EG_Champ,
ff.FF_Devise,
ff.FF_Remise,
ff.FF_ConvDiv,
ff.FF_TypeRem,
-- Nombre d'articles
ISNULL(COUNT(DISTINCT a.AR_Ref), 0) as nb_articles
FROM F_FAMILLE f
-- Jointures comptables
LEFT JOIN F_FAMCOMPTA vte ON f.FA_CodeFamille = vte.FA_CodeFamille
AND vte.FCP_Type = 0 -- Vente
AND vte.FCP_Champ = 1 -- Compte principal
LEFT JOIN F_FAMCOMPTA ach ON f.FA_CodeFamille = ach.FA_CodeFamille
AND ach.FCP_Type = 1 -- Achat
AND ach.FCP_Champ = 1
LEFT JOIN F_FAMCOMPTA sto ON f.FA_CodeFamille = sto.FA_CodeFamille
AND sto.FCP_Type = 2 -- Stock
AND sto.FCP_Champ = 1
-- Fournisseur principal
LEFT JOIN F_FAMFOURNISS ff ON f.FA_CodeFamille = ff.FA_CodeFamille
AND ff.FF_Principal = 1
-- Nombre d'articles
LEFT JOIN F_ARTICLE a ON f.FA_CodeFamille = a.FA_CodeFamille
WHERE 1=1
"""
params = []
if not inclure_totaux:
query += " AND f.FA_Type = 0"
logger.info("[SQL] Filtre : FA_Type = 0 (Détail uniquement)")
if filtre:
query += """
AND (
f.FA_CodeFamille LIKE ?
OR f.FA_Intitule LIKE ?
)
"""
params.extend([f"%{filtre}%", f"%{filtre}%"])
query += """
GROUP BY
f.FA_CodeFamille, f.FA_Type, f.FA_Intitule, f.FA_UniteVen, f.FA_Coef,
f.FA_SuiviStock, f.FA_Garantie, f.FA_Central,
f.FA_Stat01, f.FA_Stat02, f.FA_Stat03, f.FA_Stat04, f.FA_Stat05,
f.FA_CodeFiscal, f.FA_Pays, f.FA_UnitePoids, f.FA_Escompte, f.FA_Delai,
f.FA_HorsStat, f.FA_VteDebit, f.FA_NotImp,
f.FA_Frais01FR_Denomination, f.FA_Frais01FR_Rem01REM_Valeur, f.FA_Frais01FR_Rem01REM_Type,
f.FA_Frais01FR_Rem02REM_Valeur, f.FA_Frais01FR_Rem02REM_Type,
f.FA_Frais01FR_Rem03REM_Valeur, f.FA_Frais01FR_Rem03REM_Type,
f.FA_Frais02FR_Denomination, f.FA_Frais02FR_Rem01REM_Valeur, f.FA_Frais02FR_Rem01REM_Type,
f.FA_Frais02FR_Rem02REM_Valeur, f.FA_Frais02FR_Rem02REM_Type,
f.FA_Frais02FR_Rem03REM_Valeur, f.FA_Frais02FR_Rem03REM_Type,
f.FA_Frais03FR_Denomination, f.FA_Frais03FR_Rem01REM_Valeur, f.FA_Frais03FR_Rem01REM_Type,
f.FA_Frais03FR_Rem02REM_Valeur, f.FA_Frais03FR_Rem02REM_Type,
f.FA_Frais03FR_Rem03REM_Valeur, f.FA_Frais03FR_Rem03REM_Type,
f.FA_Contremarque, f.FA_FactPoids, f.FA_FactForfait, f.FA_Publie,
f.FA_RacineRef, f.FA_RacineCB,
f.CL_No1, f.CL_No2, f.CL_No3, f.CL_No4,
f.FA_Nature, f.FA_NbColis, f.FA_SousTraitance, f.FA_Fictif, f.FA_Criticite,
f.cbMarq, f.cbCreateur, f.cbModification, f.cbCreation, f.cbCreationUser,
vte.FCP_ComptaCPT_CompteG, vte.FCP_ComptaCPT_CompteA,
vte.FCP_ComptaCPT_Taxe1, vte.FCP_ComptaCPT_Taxe2, vte.FCP_ComptaCPT_Taxe3,
vte.FCP_ComptaCPT_Date1, vte.FCP_ComptaCPT_Date2, vte.FCP_ComptaCPT_Date3,
vte.FCP_TypeFacture,
ach.FCP_ComptaCPT_CompteG, ach.FCP_ComptaCPT_CompteA,
ach.FCP_ComptaCPT_Taxe1, ach.FCP_ComptaCPT_Taxe2, ach.FCP_ComptaCPT_Taxe3,
ach.FCP_ComptaCPT_Date1, ach.FCP_ComptaCPT_Date2, ach.FCP_ComptaCPT_Date3,
ach.FCP_TypeFacture,
sto.FCP_ComptaCPT_CompteG, sto.FCP_ComptaCPT_CompteA,
ff.CT_Num, ff.FF_Unite, ff.FF_Conversion, ff.FF_DelaiAppro,
ff.FF_Garantie, ff.FF_Colisage, ff.FF_QteMini, ff.FF_QteMont,
ff.EG_Champ, ff.FF_Devise, ff.FF_Remise, ff.FF_ConvDiv, ff.FF_TypeRem
ORDER BY f.FA_Intitule
"""
cursor.execute(query, params)
rows = cursor.fetchall()
def to_str(val):
"""Convertit en string, gère None et int"""
if val is None:
return ""
return str(val).strip() if isinstance(val, str) else str(val)
def to_float(val):
"""Convertit en float, gère None"""
if val is None or val == "":
return 0.0
try:
return float(val)
except (ValueError, TypeError):
return 0.0
def to_int(val):
"""Convertit en int, gère None"""
if val is None or val == "":
return 0
try:
return int(val)
except (ValueError, TypeError):
return 0
def to_bool(val):
"""Convertit en bool"""
if val is None:
return False
if isinstance(val, bool):
return val
if isinstance(val, int):
return val != 0
return bool(val)
familles = []
for row in rows:
idx = 0
famille = {
"code": to_str(row[idx]),
"type": to_int(row[idx + 1]),
"intitule": to_str(row[idx + 2]),
"unite_vente": to_str(row[idx + 3]),
"coef": to_float(row[idx + 4]),
"suivi_stock": to_bool(row[idx + 5]),
"garantie": to_int(row[idx + 6]),
"est_centrale": to_bool(row[idx + 7]),
}
idx += 8
famille.update(
{
"stat_01": to_str(row[idx]),
"stat_02": to_str(row[idx + 1]),
"stat_03": to_str(row[idx + 2]),
"stat_04": to_str(row[idx + 3]),
"stat_05": to_str(row[idx + 4]),
}
)
idx += 5
famille.update(
{
"code_fiscal": to_str(row[idx]),
"pays": to_str(row[idx + 1]),
"unite_poids": to_str(row[idx + 2]),
"escompte": to_bool(row[idx + 3]),
"delai": to_int(row[idx + 4]),
"hors_statistique": to_bool(row[idx + 5]),
"vente_debit": to_bool(row[idx + 6]),
"non_imprimable": to_bool(row[idx + 7]),
}
)
idx += 8
famille.update(
{
"frais_01_libelle": to_str(row[idx]),
"frais_01_remise_1_valeur": to_float(row[idx + 1]),
"frais_01_remise_1_type": to_int(row[idx + 2]),
"frais_01_remise_2_valeur": to_float(row[idx + 3]),
"frais_01_remise_2_type": to_int(row[idx + 4]),
"frais_01_remise_3_valeur": to_float(row[idx + 5]),
"frais_01_remise_3_type": to_int(row[idx + 6]),
"frais_02_libelle": to_str(row[idx + 7]),
"frais_02_remise_1_valeur": to_float(row[idx + 8]),
"frais_02_remise_1_type": to_int(row[idx + 9]),
"frais_02_remise_2_valeur": to_float(row[idx + 10]),
"frais_02_remise_2_type": to_int(row[idx + 11]),
"frais_02_remise_3_valeur": to_float(row[idx + 12]),
"frais_02_remise_3_type": to_int(row[idx + 13]),
"frais_03_libelle": to_str(row[idx + 14]),
"frais_03_remise_1_valeur": to_float(row[idx + 15]),
"frais_03_remise_1_type": to_int(row[idx + 16]),
"frais_03_remise_2_valeur": to_float(row[idx + 17]),
"frais_03_remise_2_type": to_int(row[idx + 18]),
"frais_03_remise_3_valeur": to_float(row[idx + 19]),
"frais_03_remise_3_type": to_int(row[idx + 20]),
}
)
idx += 21
famille.update(
{
"contremarque": to_bool(row[idx]),
"fact_poids": to_bool(row[idx + 1]),
"fact_forfait": to_bool(row[idx + 2]),
"publie": to_bool(row[idx + 3]),
"racine_reference": to_str(row[idx + 4]),
"racine_code_barre": to_str(row[idx + 5]),
}
)
idx += 6
famille.update(
{
"categorie_1": to_int(row[idx]),
"categorie_2": to_int(row[idx + 1]),
"categorie_3": to_int(row[idx + 2]),
"categorie_4": to_int(row[idx + 3]),
}
)
idx += 4
famille.update(
{
"nature": to_int(row[idx]),
"nb_colis": to_int(row[idx + 1]),
"sous_traitance": to_bool(row[idx + 2]),
"fictif": to_bool(row[idx + 3]),
"criticite": to_int(row[idx + 4]),
}
)
idx += 5
famille.update(
{
"cb_marq": to_int(row[idx]),
"cb_createur": to_str(row[idx + 1]),
"cb_modification": row[idx + 2],
"cb_creation": row[idx + 3],
"cb_creation_user": to_str(row[idx + 4]),
}
)
idx += 5
famille.update(
{
"compte_vente": to_str(row[idx]),
"compte_auxiliaire_vente": to_str(row[idx + 1]),
"tva_vente_1": to_str(row[idx + 2]),
"tva_vente_2": to_str(row[idx + 3]),
"tva_vente_3": to_str(row[idx + 4]),
"tva_vente_date_1": row[idx + 5],
"tva_vente_date_2": row[idx + 6],
"tva_vente_date_3": row[idx + 7],
"type_facture_vente": to_int(row[idx + 8]),
}
)
idx += 9
famille.update(
{
"compte_achat": to_str(row[idx]),
"compte_auxiliaire_achat": to_str(row[idx + 1]),
"tva_achat_1": to_str(row[idx + 2]),
"tva_achat_2": to_str(row[idx + 3]),
"tva_achat_3": to_str(row[idx + 4]),
"tva_achat_date_1": row[idx + 5],
"tva_achat_date_2": row[idx + 6],
"tva_achat_date_3": row[idx + 7],
"type_facture_achat": to_int(row[idx + 8]),
}
)
idx += 9
famille.update(
{
"compte_stock": to_str(row[idx]),
"compte_auxiliaire_stock": to_str(row[idx + 1]),
}
)
idx += 2
famille.update(
{
"fournisseur_principal": to_str(row[idx]),
"fournisseur_unite": to_str(row[idx + 1]),
"fournisseur_conversion": to_float(row[idx + 2]),
"fournisseur_delai_appro": to_int(row[idx + 3]),
"fournisseur_garantie": to_int(row[idx + 4]),
"fournisseur_colisage": to_int(row[idx + 5]),
"fournisseur_qte_mini": to_float(row[idx + 6]),
"fournisseur_qte_mont": to_float(row[idx + 7]),
"fournisseur_enumere_gamme": to_int(row[idx + 8]),
"fournisseur_devise": to_int(row[idx + 9]),
"fournisseur_remise": to_float(row[idx + 10]),
"fournisseur_conv_div": to_float(row[idx + 11]),
"fournisseur_type_remise": to_int(row[idx + 12]),
}
)
idx += 13
famille["nb_articles"] = to_int(row[idx])
famille["type_libelle"] = (
"Total" if famille["type"] == 1 else "Détail"
)
famille["est_total"] = famille["type"] == 1
famille["est_detail"] = famille["type"] == 0
famille["FA_CodeFamille"] = famille["code"]
famille["FA_Intitule"] = famille["intitule"]
famille["FA_Type"] = famille["type"]
famille["CG_NumVte"] = famille["compte_vente"]
famille["CG_NumAch"] = famille["compte_achat"]
familles.append(famille)
type_msg = "DÉTAIL uniquement" if not inclure_totaux else "TOUS types"
logger.info(f" {len(familles)} familles chargées ({type_msg})")
return familles
except Exception as e:
logger.error(f"Erreur SQL familles: {e}", exc_info=True)
raise RuntimeError(f"Erreur lecture familles: {str(e)}")
def lire_famille(self, code: str) -> Dict:
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
logger.info(f"[SQL] Lecture famille : {code}")
cursor.execute("SELECT TOP 1 * FROM F_FAMILLE")
colonnes_disponibles = [column[0] for column in cursor.description]
logger.info(f"[SQL] Colonnes trouvées : {len(colonnes_disponibles)}")
colonnes_souhaitees = [
"FA_CodeFamille",
"FA_Intitule",
"FA_Type",
"FA_UniteVen",
"FA_Coef",
"FA_SuiviStock",
"FA_Garantie",
"FA_UnitePoids",
"FA_Delai",
"FA_NbColis",
"CG_NumAch",
"CG_NumVte",
"FA_CodeFiscal",
"FA_Escompte",
"FA_Central",
"FA_Nature",
"CL_No1",
"CL_No2",
"CL_No3",
"CL_No4",
"FA_Stat01",
"FA_Stat02",
"FA_Stat03",
"FA_Stat04",
"FA_Stat05",
"FA_HorsStat",
"FA_Pays",
"FA_VteDebit",
"FA_NotImp",
"FA_Contremarque",
"FA_FactPoids",
"FA_FactForfait",
"FA_Publie",
"FA_RacineRef",
"FA_RacineCB",
"FA_Raccourci",
"FA_SousTraitance",
"FA_Fictif",
"FA_Criticite",
]
colonnes_a_lire = [
col for col in colonnes_souhaitees if col in colonnes_disponibles
]
if not colonnes_a_lire:
colonnes_a_lire = colonnes_disponibles
logger.info(f"[SQL] Colonnes sélectionnées : {len(colonnes_a_lire)}")
colonnes_str = ", ".join([f"f.{col}" for col in colonnes_a_lire])
query = f"""
SELECT {colonnes_str},
ISNULL(COUNT(a.AR_Ref), 0) as nb_articles
FROM F_FAMILLE f
LEFT JOIN F_ARTICLE a ON f.FA_CodeFamille = a.FA_CodeFamille
WHERE UPPER(f.FA_CodeFamille) = ?
GROUP BY {colonnes_str}
"""
cursor.execute(query, (code.upper().strip(),))
row = cursor.fetchone()
if not row:
raise ValueError(f"Famille '{code}' introuvable dans Sage")
famille = {}
for idx, colonne in enumerate(colonnes_a_lire):
valeur = row[idx]
if isinstance(valeur, str):
valeur = valeur.strip()
famille[colonne] = valeur
famille["nb_articles"] = row[-1]
if "FA_CodeFamille" in famille:
famille["code"] = famille["FA_CodeFamille"]
if "FA_Intitule" in famille:
famille["intitule"] = famille["FA_Intitule"]
if "FA_Type" in famille:
type_val = famille["FA_Type"]
famille["type"] = type_val
famille["type_libelle"] = "Total" if type_val == 1 else "Détail"
famille["est_total"] = type_val == 1
else:
famille["type"] = 0
famille["type_libelle"] = "Détail"
famille["est_total"] = False
famille["unite_vente"] = str(famille.get("FA_UniteVen", ""))
famille["unite_poids"] = str(famille.get("FA_UnitePoids", ""))
famille["coef"] = (
float(famille.get("FA_Coef", 0.0))
if famille.get("FA_Coef") is not None
else 0.0
)
famille["suivi_stock"] = bool(famille.get("FA_SuiviStock", 0))
famille["garantie"] = int(famille.get("FA_Garantie", 0))
famille["delai"] = int(famille.get("FA_Delai", 0))
famille["nb_colis"] = int(famille.get("FA_NbColis", 0))
famille["compte_achat"] = famille.get("CG_NumAch", "")
famille["compte_vente"] = famille.get("CG_NumVte", "")
famille["code_fiscal"] = famille.get("FA_CodeFiscal", "")
famille["escompte"] = bool(famille.get("FA_Escompte", 0))
famille["est_centrale"] = bool(famille.get("FA_Central", 0))
famille["nature"] = famille.get("FA_Nature", 0)
famille["pays"] = famille.get("FA_Pays", "")
famille["categorie_1"] = famille.get("CL_No1", 0)
famille["categorie_2"] = famille.get("CL_No2", 0)
famille["categorie_3"] = famille.get("CL_No3", 0)
famille["categorie_4"] = famille.get("CL_No4", 0)
famille["stat_01"] = famille.get("FA_Stat01", "")
famille["stat_02"] = famille.get("FA_Stat02", "")
famille["stat_03"] = famille.get("FA_Stat03", "")
famille["stat_04"] = famille.get("FA_Stat04", "")
famille["stat_05"] = famille.get("FA_Stat05", "")
famille["hors_statistique"] = bool(famille.get("FA_HorsStat", 0))
famille["vente_debit"] = bool(famille.get("FA_VteDebit", 0))
famille["non_imprimable"] = bool(famille.get("FA_NotImp", 0))
famille["contremarque"] = bool(famille.get("FA_Contremarque", 0))
famille["fact_poids"] = bool(famille.get("FA_FactPoids", 0))
famille["fact_forfait"] = bool(famille.get("FA_FactForfait", 0))
famille["publie"] = bool(famille.get("FA_Publie", 0))
famille["racine_reference"] = famille.get("FA_RacineRef", "")
famille["racine_code_barre"] = famille.get("FA_RacineCB", "")
famille["raccourci"] = famille.get("FA_Raccourci", "")
famille["sous_traitance"] = bool(famille.get("FA_SousTraitance", 0))
famille["fictif"] = bool(famille.get("FA_Fictif", 0))
famille["criticite"] = int(famille.get("FA_Criticite", 0))
logger.info(
f"SQL: Famille '{famille['code']}' chargée ({famille['nb_articles']} articles)"
)
return famille
except ValueError as e:
logger.error(f"Erreur famille: {e}")
raise
except Exception as e:
logger.error(f"Erreur SQL famille: {e}", exc_info=True)
raise RuntimeError(f"Erreur lecture famille: {str(e)}")
def creer_entree_stock(self, entree_data: Dict) -> Dict:
try:
with self._com_context(), self._lock_com:
logger.info("[STOCK] === CRÉATION ENTRÉE STOCK (COM pur) ===")
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug("Transaction démarrée")
except Exception:
pass
try:
factory_doc = self.cial.FactoryDocumentStock
persist_doc = factory_doc.CreateType(180)
doc = win32com.client.CastTo(persist_doc, "IBODocumentStock3")
doc.SetDefault()
date_mouv = entree_data.get("date_mouvement")
if isinstance(date_mouv, date):
doc.DO_Date = pywintypes.Time(
datetime.combine(date_mouv, datetime.min.time())
)
else:
doc.DO_Date = pywintypes.Time(datetime.now())
if entree_data.get("reference"):
doc.DO_Ref = entree_data["reference"]
doc.Write()
logger.info("[STOCK] Document créé")
factory_article = self.cial.FactoryArticle
factory_depot = self.cial.FactoryDepot
stocks_mis_a_jour = []
depot_principal = None
try:
persist_depot = factory_depot.List(1)
if persist_depot:
depot_principal = win32com.client.CastTo(
persist_depot, "IBODepot3"
)
depot_principal.Read()
logger.info(
f"Dépôt principal: {getattr(depot_principal, 'DE_Code', '?')}"
)
except Exception as e:
logger.warning(f"Erreur chargement dépôt: {e}")
try:
factory_lignes = doc.FactoryDocumentLigne
except Exception:
factory_lignes = doc.FactoryDocumentStockLigne
for idx, ligne_data in enumerate(entree_data["lignes"], 1):
article_ref = ligne_data["article_ref"].upper()
quantite = ligne_data["quantite"]
stock_mini = ligne_data.get("stock_mini")
stock_maxi = ligne_data.get("stock_maxi")
logger.info(f"[STOCK] Ligne {idx}: {article_ref} x {quantite}")
persist_article = factory_article.ReadReference(article_ref)
if not persist_article:
raise ValueError(f"Article {article_ref} introuvable")
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentStockLigne3"
)
ligne_obj.SetDefault()
try:
ligne_obj.SetDefaultArticleReference(
article_ref, float(quantite)
)
except Exception:
try:
ligne_obj.SetDefaultArticle(
article_obj, float(quantite)
)
except Exception:
raise ValueError(
f"Impossible de lier l'article {article_ref}"
)
prix = ligne_data.get("prix_unitaire")
if prix:
try:
ligne_obj.DL_PrixUnitaire = float(prix)
except Exception:
pass
ligne_obj.Write()
if stock_mini is not None or stock_maxi is not None:
logger.info(
f"[STOCK] Ajustement stock pour {article_ref}..."
)
try:
logger.info(
" [COM] Méthode A : Article.FactoryArticleStock"
)
factory_article = self.cial.FactoryArticle
persist_article_full = factory_article.ReadReference(
article_ref
)
article_full = win32com.client.CastTo(
persist_article_full, "IBOArticle3"
)
article_full.Read()
factory_article_stock = None
try:
factory_article_stock = (
article_full.FactoryArticleStock
)
logger.info(" FactoryArticleStock trouvée")
except AttributeError:
logger.warning(
" FactoryArticleStock non disponible"
)
if factory_article_stock:
stock_trouve = None
index_stock = 1
while index_stock <= 100:
try:
stock_persist = factory_article_stock.List(
index_stock
)
if stock_persist is None:
break
stock_obj = win32com.client.CastTo(
stock_persist, "IBOArticleStock3"
)
stock_obj.Read()
depot_stock = None
try:
depot_stock = getattr(
stock_obj, "Depot", None
)
if depot_stock:
depot_stock.Read()
depot_code = getattr(
depot_stock, "DE_Code", ""
).strip()
logger.debug(
f" Dépôt {index_stock}: {depot_code}"
)
if (
not stock_trouve
or depot_code
== getattr(
depot_principal,
"DE_Code",
"",
)
):
stock_trouve = stock_obj
logger.info(
f" Stock trouvé pour dépôt {depot_code}"
)
except Exception:
pass
index_stock += 1
except Exception as e:
logger.debug(
f" Erreur stock {index_stock}: {e}"
)
index_stock += 1
if not stock_trouve:
try:
stock_persist = (
factory_article_stock.Create()
)
stock_trouve = win32com.client.CastTo(
stock_persist, "IBOArticleStock3"
)
stock_trouve.SetDefault()
if depot_principal:
try:
stock_trouve.Depot = depot_principal
logger.info(
" Dépôt principal lié"
)
except Exception:
pass
logger.info(" Nouvel ArticleStock créé")
except Exception as e:
logger.error(
f" Impossible de créer ArticleStock: {e}"
)
raise
if stock_trouve:
try:
stock_trouve.Read()
except Exception:
pass
if stock_mini is not None:
try:
for prop_name in [
"AS_QteMini",
"AS_Mini",
"AR_StockMini",
"StockMini",
]:
try:
setattr(
stock_trouve,
prop_name,
float(stock_mini),
)
logger.info(
f" Stock mini défini via {prop_name}: {stock_mini}"
)
break
except AttributeError:
continue
except Exception as e:
logger.debug(
f" {prop_name} échoué: {e}"
)
continue
except Exception as e:
logger.warning(
f" Stock mini non défini: {e}"
)
if stock_maxi is not None:
try:
for prop_name in [
"AS_QteMaxi",
"AS_Maxi",
"AR_StockMaxi",
"StockMaxi",
]:
try:
setattr(
stock_trouve,
prop_name,
float(stock_maxi),
)
logger.info(
f" Stock maxi défini via {prop_name}: {stock_maxi}"
)
break
except AttributeError:
continue
except Exception as e:
logger.debug(
f" {prop_name} échoué: {e}"
)
continue
except Exception as e:
logger.warning(
f" Stock maxi non défini: {e}"
)
try:
stock_trouve.Write()
logger.info(" ArticleStock sauvegardé")
except Exception as e:
logger.error(
f" Erreur Write() ArticleStock: {e}"
)
raise
if depot_principal and (
stock_mini is not None or stock_maxi is not None
):
logger.info(
" [COM] Méthode B : Depot.FactoryDepotStock (alternative)"
)
try:
factory_depot_stock = None
for factory_name in [
"FactoryDepotStock",
"FactoryArticleStock",
]:
try:
factory_depot_stock = getattr(
depot_principal, factory_name, None
)
if factory_depot_stock:
logger.info(
f" Factory trouvée: {factory_name}"
)
break
except Exception:
continue
if factory_depot_stock:
stock_depot_trouve = None
index_ds = 1
while index_ds <= 100:
try:
stock_ds_persist = (
factory_depot_stock.List(
index_ds
)
)
if stock_ds_persist is None:
break
stock_ds = win32com.client.CastTo(
stock_ds_persist,
"IBODepotStock3",
)
stock_ds.Read()
ar_ref_ds = (
getattr(stock_ds, "AR_Ref", "")
.strip()
.upper()
)
if ar_ref_ds == article_ref:
stock_depot_trouve = stock_ds
break
index_ds += 1
except Exception:
index_ds += 1
if not stock_depot_trouve:
try:
stock_ds_persist = (
factory_depot_stock.Create()
)
stock_depot_trouve = (
win32com.client.CastTo(
stock_ds_persist,
"IBODepotStock3",
)
)
stock_depot_trouve.SetDefault()
stock_depot_trouve.AR_Ref = (
article_ref
)
logger.info(
" Nouveau DepotStock créé"
)
except Exception as e:
logger.error(
f" Impossible de créer DepotStock: {e}"
)
if stock_depot_trouve:
if stock_mini is not None:
try:
stock_depot_trouve.AS_QteMini = float(
stock_mini
)
logger.info(
f" DepotStock.AS_QteMini = {stock_mini}"
)
except Exception as e:
logger.warning(
f" DepotStock mini échoué: {e}"
)
if stock_maxi is not None:
try:
stock_depot_trouve.AS_QteMaxi = float(
stock_maxi
)
logger.info(
f" DepotStock.AS_QteMaxi = {stock_maxi}"
)
except Exception as e:
logger.warning(
f" DepotStock maxi échoué: {e}"
)
try:
stock_depot_trouve.Write()
logger.info(
" DepotStock sauvegardé"
)
except Exception as e:
logger.error(
f" DepotStock Write() échoué: {e}"
)
except Exception as e:
logger.warning(f" Méthode B échouée: {e}")
except Exception as e:
logger.error(
f"[STOCK] Erreur ajustement stock: {e}",
exc_info=True,
)
stocks_mis_a_jour.append(
{
"article_ref": article_ref,
"quantite_ajoutee": quantite,
"stock_mini_defini": stock_mini,
"stock_maxi_defini": stock_maxi,
}
)
doc.Write()
doc.Read()
numero = getattr(doc, "DO_Piece", "")
logger.info(f"[STOCK] Document finalisé: {numero}")
logger.info("[STOCK] Vérification finale via COM...")
for stock_info in stocks_mis_a_jour:
article_ref = stock_info["article_ref"]
try:
persist_article = factory_article.ReadReference(article_ref)
article_verif = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_verif.Read()
stock_total = 0.0
stock_mini_lu = 0.0
stock_maxi_lu = 0.0
for attr in ["AR_Stock", "AS_QteSto", "Stock"]:
try:
val = getattr(article_verif, attr, None)
if val is not None:
stock_total = float(val)
break
except Exception:
pass
for attr in ["AR_StockMini", "AS_QteMini", "StockMini"]:
try:
val = getattr(article_verif, attr, None)
if val is not None:
stock_mini_lu = float(val)
break
except Exception:
pass
for attr in ["AR_StockMaxi", "AS_QteMaxi", "StockMaxi"]:
try:
val = getattr(article_verif, attr, None)
if val is not None:
stock_maxi_lu = float(val)
break
except Exception:
pass
logger.info(
f"[VERIF] {article_ref}: "
f"Total={stock_total}, "
f"Mini={stock_mini_lu}, "
f"Maxi={stock_maxi_lu}"
)
stock_info["stock_total_verifie"] = stock_total
stock_info["stock_mini_verifie"] = stock_mini_lu
stock_info["stock_maxi_verifie"] = stock_maxi_lu
except Exception as e:
logger.warning(
f"[VERIF] Erreur vérification {article_ref}: {e}"
)
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.info("[STOCK] Transaction committée")
except Exception:
logger.info("[STOCK] Changements sauvegardés")
return {
"article_ref": article_ref,
"numero": numero,
"type": 180,
"type_libelle": "Entrée en stock",
"date": str(getattr(doc, "DO_Date", "")),
"nb_lignes": len(stocks_mis_a_jour),
"stocks_mis_a_jour": stocks_mis_a_jour,
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.info("[STOCK] Transaction annulée")
except Exception:
pass
logger.error(f"[STOCK] ERREUR : {e}", exc_info=True)
raise ValueError(f"Erreur création entrée stock : {str(e)}")
except Exception as e:
logger.error(f"[STOCK] ERREUR GLOBALE : {e}", exc_info=True)
raise ValueError(f"Erreur création entrée stock : {str(e)}")
def creer_sortie_stock(self, sortie_data: Dict) -> Dict:
try:
with (
self._com_context(),
self._lock_com,
self._get_sql_connection() as conn,
):
cursor = conn.cursor()
logger.info("[STOCK] === CRÉATION SORTIE STOCK ===")
logger.info(f"[STOCK] {len(sortie_data.get('lignes', []))} ligne(s)")
try:
self.cial.CptaApplication.BeginTrans()
except Exception:
pass
try:
factory = self.cial.FactoryDocumentStock
persist = factory.CreateType(181)
doc = win32com.client.CastTo(persist, "IBODocumentStock3")
doc.SetDefault()
date_mouv = sortie_data.get("date_mouvement")
if isinstance(date_mouv, date):
doc.DO_Date = pywintypes.Time(
datetime.combine(date_mouv, datetime.min.time())
)
else:
doc.DO_Date = pywintypes.Time(datetime.now())
if sortie_data.get("reference"):
doc.DO_Ref = sortie_data["reference"]
doc.Write()
logger.info(
f"[STOCK] Document créé : {getattr(doc, 'DO_Piece', '?')}"
)
try:
factory_lignes = doc.FactoryDocumentLigne
except Exception:
factory_lignes = doc.FactoryDocumentStockLigne
factory_article = self.cial.FactoryArticle
stocks_mis_a_jour = []
for idx, ligne_data in enumerate(sortie_data["lignes"], 1):
article_ref = ligne_data["article_ref"].upper()
quantite = ligne_data["quantite"]
logger.info(
f"[STOCK] ======== LIGNE {idx} : {article_ref} x {quantite} ========"
)
persist_article = factory_article.ReadReference(article_ref)
if not persist_article:
raise ValueError(f"Article {article_ref} introuvable")
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ar_suivi = getattr(article_obj, "AR_SuiviStock", 0)
ar_design = getattr(article_obj, "AR_Design", article_ref)
logger.info(f"[STOCK] Article : {ar_design}")
logger.info(f"[STOCK] AR_SuiviStock : {ar_suivi}")
stock_dispo = verifier_stock_suffisant(
article_ref, quantite, cursor, None
)
if not stock_dispo["suffisant"]:
raise ValueError(
f"Stock insuffisant pour {article_ref} : "
f"disponible={stock_dispo['stock_disponible']}, "
f"demandé={quantite}"
)
logger.info(
f"[STOCK] Stock disponible : {stock_dispo['stock_disponible']}"
)
numero_lot = ligne_data.get("numero_lot")
if ar_suivi == 1:
if numero_lot:
logger.warning("[STOCK] CMUP : Suppression du lot")
numero_lot = None
elif ar_suivi == 2:
if not numero_lot:
import uuid
numero_lot = f"AUTO-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6].upper()}"
logger.info(
f"[STOCK] FIFO/LIFO : Lot auto-généré '{numero_lot}'"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except Exception:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentStockLigne3"
)
ligne_obj.SetDefault()
article_lie = False
try:
ligne_obj.SetDefaultArticleReference(
article_ref, float(quantite)
)
article_lie = True
logger.info("[STOCK] SetDefaultArticleReference()")
except Exception:
try:
ligne_obj.SetDefaultArticle(
article_obj, float(quantite)
)
article_lie = True
logger.info("[STOCK] SetDefaultArticle()")
except Exception:
pass
if not article_lie:
raise ValueError(
f"Impossible de lier l'article {article_ref}"
)
if numero_lot and ar_suivi == 2:
try:
ligne_obj.SetDefaultLot(numero_lot)
logger.info("[STOCK] Lot défini")
except Exception:
try:
ligne_obj.LS_NoSerie = numero_lot
logger.info("[STOCK] Lot via LS_NoSerie")
except Exception:
pass
prix = ligne_data.get("prix_unitaire")
if prix:
try:
ligne_obj.DL_PrixUnitaire = float(prix)
except Exception:
pass
ligne_obj.Write()
logger.info("[STOCK] Write() réussi")
ligne_obj.Read()
ref_verifiee = article_ref
try:
article_lie_obj = getattr(ligne_obj, "Article", None)
if article_lie_obj:
article_lie_obj.Read()
ref_verifiee = (
getattr(article_lie_obj, "AR_Ref", "").strip()
or article_ref
)
except Exception:
pass
logger.info(f"[STOCK] LIGNE {idx} COMPLÈTE")
stocks_mis_a_jour.append(
{
"article_ref": article_ref,
"quantite_retiree": quantite,
"reference_verifiee": ref_verifiee,
"stock_avant": stock_dispo["stock_disponible"],
"stock_apres": stock_dispo["stock_apres"],
"numero_lot": numero_lot if ar_suivi == 2 else None,
}
)
doc.Write()
doc.Read()
numero = getattr(doc, "DO_Piece", "")
logger.info(f"[STOCK] Document finalisé : {numero}")
try:
self.cial.CptaApplication.CommitTrans()
logger.info("[STOCK] Transaction committée")
except Exception:
pass
return {
"numero": numero,
"type": 1,
"date": str(getattr(doc, "DO_Date", "")),
"nb_lignes": len(stocks_mis_a_jour),
"reference": sortie_data.get("reference"),
"stocks_mis_a_jour": stocks_mis_a_jour,
}
except Exception as e:
logger.error(f"[STOCK] ERREUR : {e}", exc_info=True)
try:
self.cial.CptaApplication.RollbackTrans()
except Exception:
pass
raise ValueError(f"Erreur création sortie stock : {str(e)}")
except Exception as e:
logger.error(f"[STOCK] ERREUR GLOBALE : {e}", exc_info=True)
raise ValueError(f"Erreur création sortie stock : {str(e)}")
def lire_mouvement_stock(self, numero: str) -> Dict:
try:
with self._com_context(), self._lock_com:
factory = self.cial.FactoryDocumentStock
persist = None
index = 1
logger.info(f"[MOUVEMENT] Recherche de {numero}...")
while index < 10000:
try:
persist_test = factory.List(index)
if persist_test is None:
break
doc_test = win32com.client.CastTo(
persist_test, "IBODocumentStock3"
)
doc_test.Read()
if getattr(doc_test, "DO_Piece", "") == numero:
persist = persist_test
logger.info(f"[MOUVEMENT] Trouvé à l'index {index}")
break
index += 1
except Exception:
index += 1
if not persist:
raise ValueError(f"Mouvement {numero} introuvable")
doc = win32com.client.CastTo(persist, "IBODocumentStock3")
doc.Read()
do_type = getattr(doc, "DO_Type", -1)
types_mouvements = {
180: "Entrée",
181: "Sortie",
182: "Transfert",
183: "Inventaire",
}
mouvement = {
"numero": numero,
"type": do_type,
"type_libelle": types_mouvements.get(do_type, f"Type {do_type}"),
"date": str(getattr(doc, "DO_Date", "")),
"reference": getattr(doc, "DO_Ref", ""),
"lignes": [],
}
try:
factory_lignes = getattr(
doc, "FactoryDocumentLigne", None
) or getattr(doc, "FactoryDocumentStockLigne", None)
if factory_lignes:
idx = 1
while idx <= 100:
try:
ligne_p = factory_lignes.List(idx)
if ligne_p is None:
break
try:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
except Exception:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentStockLigne3"
)
ligne.Read()
article_ref = ""
try:
article_obj = getattr(ligne, "Article", None)
if article_obj:
article_obj.Read()
article_ref = getattr(
article_obj, "AR_Ref", ""
).strip()
except Exception:
pass
ligne_info = {
"article_ref": article_ref,
"designation": getattr(ligne, "DL_Design", ""),
"quantite": float(getattr(ligne, "DL_Qte", 0.0)),
"prix_unitaire": float(
getattr(ligne, "DL_PrixUnitaire", 0.0)
),
"montant_ht": float(
getattr(ligne, "DL_MontantHT", 0.0)
),
"numero_lot": getattr(ligne, "LS_NoSerie", ""),
}
mouvement["lignes"].append(ligne_info)
idx += 1
except Exception:
break
except Exception as e:
logger.warning(f"[MOUVEMENT] Erreur lecture lignes : {e}")
mouvement["nb_lignes"] = len(mouvement["lignes"])
logger.info(
f"[MOUVEMENT] Lu : {numero} - {mouvement['nb_lignes']} ligne(s)"
)
return mouvement
except ValueError:
raise
except Exception as e:
logger.error(f"[MOUVEMENT] Erreur : {e}", exc_info=True)
raise ValueError(f"Erreur lecture mouvement : {str(e)}")
def lister_tous_tiers(
self, type_tiers: Optional[str] = None, filtre: str = ""
) -> List[Dict]:
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = _build_tiers_select_query()
query += " FROM F_COMPTET WHERE 1=1"
params = []
if type_tiers and type_tiers != "all":
if type_tiers == "prospect":
query += " AND CT_Type = 0 AND CT_Prospect = 1"
elif type_tiers == "client":
query += " AND CT_Type = 0 AND CT_Prospect = 0"
elif type_tiers == "fournisseur":
query += " AND CT_Type = 1"
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()
tiers_list = []
for row in rows:
tiers = _row_to_tiers_dict(row)
tiers["contacts"] = _get_contacts_client(row.CT_Num, conn)
tiers_list.append(tiers)
logger.info(
f" SQL: {len(tiers_list)} tiers retournés (type={type_tiers}, filtre={filtre})"
)
return tiers_list
except Exception as e:
logger.error(f" Erreur SQL tiers: {e}")
raise RuntimeError(f"Erreur lecture tiers: {str(e)}")
def lire_tiers(self, code: str) -> Optional[Dict]:
"""Lit un tiers (client/fournisseur/prospect) par code"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = _build_tiers_select_query()
query += " FROM F_COMPTET WHERE CT_Num = ?"
cursor.execute(query, (code.upper(),))
row = cursor.fetchone()
if not row:
return None
tiers = _row_to_tiers_dict(row)
tiers["contacts"] = _get_contacts_client(row.CT_Num, conn)
logger.info(f" SQL: Tiers {code} lu avec succès")
return tiers
except Exception as e:
logger.error(f" Erreur SQL tiers {code}: {e}")
return None