Sage100-ws/sage_connector.py

10312 lines
455 KiB
Python

import win32com.client
import pythoncom # AJOUT CRITIQUE
from datetime import datetime, timedelta, date
from typing import Dict, List, Optional, Any
import threading
import time
import logging
from config import settings, validate_settings
import pyodbc
from contextlib import contextmanager
import pywintypes
import os
import glob
import tempfile
from dataclasses import dataclass, field
import zlib
import struct
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
)
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 (
_relire_devis,
_recuperer_numero_devis
)
from utils.tiers.contacts.contacts import _get_contacts_client
logger = logging.getLogger(__name__)
class SageConnector:
def __init__(self, chemin_base, utilisateur="<Administrateur>", mot_de_passe=""):
self.chemin_base = chemin_base
self.utilisateur = utilisateur
self.mot_de_passe = mot_de_passe
self.cial = None
self.sql_server = "OV-FDDDC6\\SAGE100"
self.sql_database = "BIJOU"
self.sql_conn_string = (
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
f"SERVER={self.sql_server};"
f"DATABASE={self.sql_database};"
f"Trusted_Connection=yes;"
f"Encrypt=no;"
)
self._lock_com = threading.RLock()
self._thread_local = threading.local()
@contextmanager
def _com_context(self):
if not hasattr(self._thread_local, "com_initialized"):
try:
pythoncom.CoInitialize()
self._thread_local.com_initialized = True
logger.debug(
f"COM initialisé pour thread {threading.current_thread().name}"
)
except Exception as e:
logger.error(f"Erreur initialisation COM: {e}")
raise
try:
yield
finally:
pass
@contextmanager
def _get_sql_connection(self):
"""Context manager pour connexions SQL"""
conn = None
try:
conn = pyodbc.connect(self.sql_conn_string, timeout=10)
yield conn
except pyodbc.Error as e:
logger.error(f" Erreur SQL: {e}")
raise RuntimeError(f"Erreur SQL: {str(e)}")
finally:
if conn:
conn.close()
def _cleanup_com_thread(self):
"""Nettoie COM pour le thread actuel (à appeler à la fin)"""
if hasattr(self._thread_local, "com_initialized"):
try:
pythoncom.CoUninitialize()
delattr(self._thread_local, "com_initialized")
logger.debug(
f"COM nettoyé pour thread {threading.current_thread().name}"
)
except:
pass
def connecter(self):
"""Connexion initiale à Sage - VERSION HYBRIDE"""
try:
with self._com_context():
self.cial = win32com.client.gencache.EnsureDispatch(
"Objets100c.Cial.Stream"
)
self.cial.Name = self.chemin_base
self.cial.Loggable.UserName = self.utilisateur
self.cial.Loggable.UserPwd = self.mot_de_passe
self.cial.Open()
logger.info(f" Connexion COM Sage réussie: {self.chemin_base}")
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
except Exception as e:
logger.warning(f"SQL non disponible: {e}")
logger.warning(" Les lectures utiliseront COM (plus lent)")
return True
except Exception as e:
logger.error(f" Erreur connexion Sage: {e}", exc_info=True)
return False
def deconnecter(self):
"""Déconnexion propre"""
if self.cial:
try:
with self._com_context():
self.cial.Close()
logger.info("Connexion Sage fermée")
except:
pass
def lister_tous_fournisseurs(self, filtre=""):
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 # 1 = Fournisseur
logger.debug(" CT_Type: 1 (Fournisseur)")
except:
logger.debug(" CT_Type non défini (géré par FactoryFournisseur)")
try:
fournisseur.CT_Qualite = "FOU"
logger.debug(" CT_Qualite: 'FOU'")
except:
logger.debug(" CT_Qualite non défini (pas critique)")
try:
factory_compte = self.cial.CptaApplication.FactoryCompteG
persist_compte = factory_compte.ReadNumero(compte)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
fournisseur.CompteGPrinc = compte_obj
logger.debug(f" CompteGPrinc: objet '{compte}' assigné")
else:
logger.warning(
f" Compte {compte} introuvable - utilisation défaut"
)
except Exception as e:
logger.warning(f" Erreur CompteGPrinc: {e}")
if num_prop:
fournisseur.CT_Num = num_prop
logger.debug(f" CT_Num fourni: '{num_prop}'")
else:
try:
if hasattr(fournisseur, "SetDefaultNumPiece"):
fournisseur.SetDefaultNumPiece()
num_genere = getattr(fournisseur, "CT_Num", "")
logger.debug(f" CT_Num auto-généré: '{num_genere}'")
else:
num_genere = factory_fournisseur.GetNextNumero()
if num_genere:
fournisseur.CT_Num = num_genere
logger.debug(
f" CT_Num auto (GetNextNumero): '{num_genere}'"
)
else:
import time
num_genere = f"FOUR{int(time.time()) % 1000000}"
fournisseur.CT_Num = num_genere
logger.warning(f" CT_Num fallback: '{num_genere}'")
except Exception as e:
logger.error(f" Impossible de générer CT_Num: {e}")
raise ValueError(
"Impossible de générer le numéro fournisseur automatiquement"
)
try:
if hasattr(fournisseur, "N_CatTarif"):
fournisseur.N_CatTarif = 1
if hasattr(fournisseur, "N_CatCompta"):
fournisseur.N_CatCompta = 1
if hasattr(fournisseur, "N_Period"):
fournisseur.N_Period = 1
logger.debug(" Catégories (N_*) initialisées")
except Exception as e:
logger.warning(f" Catégories: {e}")
logger.info(" Définition champs optionnels...")
if any([adresse, code_postal, ville, pays]):
try:
adresse_obj = fournisseur.Adresse
if adresse:
adresse_obj.Adresse = adresse
if code_postal:
adresse_obj.CodePostal = code_postal
if ville:
adresse_obj.Ville = ville
if pays:
adresse_obj.Pays = pays
logger.debug(" Adresse définie")
except Exception as e:
logger.warning(f" Adresse: {e}")
if telephone or email:
try:
telecom_obj = fournisseur.Telecom
if telephone:
telecom_obj.Telephone = telephone
if email:
telecom_obj.EMail = email
logger.debug(" Télécom défini")
except Exception as e:
logger.warning(f" Télécom: {e}")
if siret:
try:
fournisseur.CT_Siret = siret
logger.debug(f" SIRET: '{siret}'")
except Exception as e:
logger.warning(f" SIRET: {e}")
if tva_intra:
try:
fournisseur.CT_Identifiant = tva_intra
logger.debug(f" TVA intra: '{tva_intra}'")
except Exception as e:
logger.warning(f" TVA: {e}")
try:
if hasattr(fournisseur, "CT_Lettrage"):
fournisseur.CT_Lettrage = True
if hasattr(fournisseur, "CT_Sommeil"):
fournisseur.CT_Sommeil = False
logger.debug(" Options par défaut définies")
except Exception as e:
logger.debug(f" Options: {e}")
logger.info(" === DIAGNOSTIC PRÉ-WRITE ===")
num_avant_write = getattr(fournisseur, "CT_Num", "")
if not num_avant_write:
logger.error(" CRITIQUE: CT_Num toujours vide !")
raise ValueError("Le numéro fournisseur (CT_Num) est obligatoire")
logger.info(f" CT_Num confirmé: '{num_avant_write}'")
logger.info(" Écriture du fournisseur dans Sage...")
try:
fournisseur.Write()
logger.info(" Write() réussi !")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
logger.error(f" Erreur Sage: {error_detail}")
except:
pass
if (
"doublon" in error_detail.lower()
or "existe" in error_detail.lower()
):
raise ValueError(f"Ce fournisseur existe déjà : {error_detail}")
raise RuntimeError(f"Échec Write(): {error_detail}")
try:
fournisseur.Read()
except Exception as e:
logger.warning(f"Impossible de relire: {e}")
num_final = getattr(fournisseur, "CT_Num", "")
if not num_final:
raise RuntimeError("CT_Num vide après Write()")
logger.info(f" FOURNISSEUR CRÉÉ: {num_final} - {intitule} ")
resultat = {
"numero": num_final,
"intitule": intitule,
"compte_collectif": compte,
"type": 1, # Fournisseur
"est_fournisseur": True,
"adresse": adresse or None,
"code_postal": code_postal or None,
"ville": ville or None,
"pays": pays or None,
"email": email or None,
"telephone": telephone or None,
"siret": siret or None,
"tva_intra": tva_intra or None,
}
return resultat
except ValueError as e:
logger.error(f" Erreur métier: {e}")
raise
except Exception as e:
logger.error(f" Erreur création fournisseur: {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = f"Erreur Sage: {err.Description}"
except:
pass
raise RuntimeError(f"Erreur technique Sage: {error_message}")
def modifier_fournisseur(self, code: str, fournisseur_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f" Recherche fournisseur {code}...")
factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur
persist = factory_fournisseur.ReadNumero(code)
if not persist:
raise ValueError(f"Fournisseur {code} introuvable")
fournisseur = _cast_client(persist) # Réutiliser _cast_client
if not fournisseur:
raise ValueError(f"Impossible de charger le fournisseur {code}")
logger.info(
f" Fournisseur {code} trouvé: {getattr(fournisseur, 'CT_Intitule', '')}"
)
logger.info(" Mise à jour des champs...")
champs_modifies = []
if "intitule" in fournisseur_data:
intitule = str(fournisseur_data["intitule"])[:69].strip()
fournisseur.CT_Intitule = intitule
champs_modifies.append(f"intitule='{intitule}'")
if any(
k in fournisseur_data
for k in ["adresse", "code_postal", "ville", "pays"]
):
try:
adresse_obj = fournisseur.Adresse
if "adresse" in fournisseur_data:
adresse = str(fournisseur_data["adresse"])[:35].strip()
adresse_obj.Adresse = adresse
champs_modifies.append("adresse")
if "code_postal" in fournisseur_data:
cp = str(fournisseur_data["code_postal"])[:9].strip()
adresse_obj.CodePostal = cp
champs_modifies.append("code_postal")
if "ville" in fournisseur_data:
ville = str(fournisseur_data["ville"])[:35].strip()
adresse_obj.Ville = ville
champs_modifies.append("ville")
if "pays" in fournisseur_data:
pays = str(fournisseur_data["pays"])[:35].strip()
adresse_obj.Pays = pays
champs_modifies.append("pays")
except Exception as e:
logger.warning(f"Erreur mise à jour adresse: {e}")
if "email" in fournisseur_data or "telephone" in fournisseur_data:
try:
telecom_obj = fournisseur.Telecom
if "email" in fournisseur_data:
email = str(fournisseur_data["email"])[:69].strip()
telecom_obj.EMail = email
champs_modifies.append("email")
if "telephone" in fournisseur_data:
tel = str(fournisseur_data["telephone"])[:21].strip()
telecom_obj.Telephone = tel
champs_modifies.append("telephone")
except Exception as e:
logger.warning(f"Erreur mise à jour télécom: {e}")
if "siret" in fournisseur_data:
try:
siret = str(fournisseur_data["siret"])[:14].strip()
fournisseur.CT_Siret = siret
champs_modifies.append("siret")
except Exception as e:
logger.warning(f"Erreur mise à jour SIRET: {e}")
if "tva_intra" in fournisseur_data:
try:
tva = str(fournisseur_data["tva_intra"])[:25].strip()
fournisseur.CT_Identifiant = tva
champs_modifies.append("tva_intra")
except Exception as e:
logger.warning(f"Erreur mise à jour TVA: {e}")
if not champs_modifies:
logger.warning("Aucun champ à modifier")
return {
"numero": getattr(fournisseur, "CT_Num", "").strip(),
"intitule": getattr(fournisseur, "CT_Intitule", "").strip(),
"type": 1,
"est_fournisseur": True,
}
logger.info(f" Champs à modifier: {', '.join(champs_modifies)}")
logger.info(" Écriture des modifications...")
try:
fournisseur.Write()
logger.info(" Write() réussi !")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except:
pass
logger.error(f" Erreur Write(): {error_detail}")
raise RuntimeError(f"Échec modification: {error_detail}")
fournisseur.Read()
logger.info(
f" FOURNISSEUR MODIFIÉ: {code} ({len(champs_modifies)} champs) "
)
numero = getattr(fournisseur, "CT_Num", "").strip()
intitule = getattr(fournisseur, "CT_Intitule", "").strip()
data = {
"numero": numero,
"intitule": intitule,
"type": 1,
"est_fournisseur": True,
}
try:
adresse_obj = getattr(fournisseur, "Adresse", None)
if adresse_obj:
data["adresse"] = getattr(adresse_obj, "Adresse", "").strip()
data["code_postal"] = getattr(
adresse_obj, "CodePostal", ""
).strip()
data["ville"] = getattr(adresse_obj, "Ville", "").strip()
except:
data["adresse"] = ""
data["code_postal"] = ""
data["ville"] = ""
try:
telecom_obj = getattr(fournisseur, "Telecom", None)
if telecom_obj:
data["telephone"] = getattr(
telecom_obj, "Telephone", ""
).strip()
data["email"] = getattr(telecom_obj, "EMail", "").strip()
except:
data["telephone"] = ""
data["email"] = ""
return data
except ValueError as e:
logger.error(f" Erreur métier: {e}")
raise
except Exception as e:
logger.error(f" Erreur modification fournisseur: {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = f"Erreur Sage: {err.Description}"
except:
pass
raise RuntimeError(f"Erreur technique Sage: {error_message}")
def 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 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, forcer_brouillon: bool = False):
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(
f" Début création devis pour client {devis_data['client']['code']} "
f"(brouillon={forcer_brouillon})"
)
try:
with self._com_context(), self._lock_com:
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug(" Transaction Sage démarrée")
except Exception as e:
logger.warning(f"BeginTrans échoué: {e}")
try:
process = self.cial.CreateProcess_Document(0)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
logger.info(" Document devis créé")
doc.DO_Date = pywintypes.Time(
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é")
if forcer_brouillon:
doc.DO_Statut = 0
logger.info(" Statut défini: 0 (Brouillon)")
else:
doc.DO_Statut = 2
logger.info(" Statut défini: 2 (Accepté)")
doc.Write()
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
logger.info(f" Ajout de {len(devis_data['lignes'])} lignes...")
for idx, ligne_data in enumerate(devis_data["lignes"], 1):
logger.debug(
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f" Article {ligne_data['article_code']} introuvable"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
designation_sage = getattr(article_obj, "AR_Design", "")
if prix_sage == 0:
logger.warning(
f"Article {ligne_data['article_code']} a un prix = 0€"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Design = (
designation_sage
or ligne_data.get("designation", "")
)
ligne_obj.DL_Qte = quantite
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
if prix_a_utiliser is not None and prix_a_utiliser > 0:
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
elif prix_auto == 0:
if prix_sage == 0:
raise ValueError(
f"Prix nul pour article {ligne_data['article_code']}"
)
ligne_obj.DL_PrixUnitaire = float(prix_sage)
remise = ligne_data.get("remise_pourcentage", 0)
if remise > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(remise)
ligne_obj.DL_Remise01REM_Type = 0
except Exception as e:
logger.warning(f"Remise non appliquée: {e}")
ligne_obj.Write()
logger.info(f" {len(devis_data['lignes'])} lignes écrites")
doc.Write()
if not forcer_brouillon:
logger.info(" Lancement Process()...")
process.Process()
else:
try:
process.Process()
logger.info(" Process() appelé (brouillon)")
except:
logger.debug("Process() ignoré pour brouillon")
numero_devis = _recuperer_numero_devis(process, doc)
if not numero_devis:
raise RuntimeError(" Numéro devis vide après création")
logger.info(f" Numéro: {numero_devis}")
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.info(" Transaction committée")
except:
pass
import time
time.sleep(0.5)
if "reference" in devis_data and devis_data["reference"]:
try:
logger.info(
f" Application de la référence: {devis_data['reference']}"
)
doc_reload = self._charger_devis(numero_devis)
nouvelle_reference = devis_data["reference"]
doc_reload.DO_Ref = (
str(nouvelle_reference) if nouvelle_reference else ""
)
doc_reload.Write()
time.sleep(0.5)
doc_reload.Read()
logger.info(f" Référence définie: {nouvelle_reference}")
except Exception as e:
logger.warning(
f"Impossible de définir la référence: {e}",
exc_info=True,
)
time.sleep(0.5)
doc_final_data = _relire_devis(
numero_devis, devis_data, forcer_brouillon
)
logger.info(
f" DEVIS CRÉÉ: {numero_devis} - {doc_final_data['total_ttc']}€ TTC "
)
return doc_final_data
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.error(" Transaction annulée (rollback)")
except:
pass
raise
except Exception as e:
logger.error(f" ERREUR CRÉATION DEVIS: {e}", exc_info=True)
raise RuntimeError(f"Échec création devis: {str(e)}")
def 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:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
if nb_lignes_initial > 0:
logger.info("")
logger.info(f" Suppression de {nb_lignes_initial} lignes existantes...")
for idx in range(nb_lignes_initial, 0, -1):
try:
ligne_p = factory_lignes.List(idx)
if ligne_p:
try:
ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3")
except:
ligne = win32com.client.CastTo(ligne_p, "IBODocumentVenteLigne3")
ligne.Read()
ligne.Remove()
logger.debug(f" Ligne {idx} supprimée")
except Exception as e:
logger.warning(f" Ligne {idx} non supprimée: {e}")
logger.info(f" {nb_lignes_initial} lignes supprimées")
logger.info("")
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
article_code = ligne_data["article_code"]
quantite = float(ligne_data["quantite"])
logger.info("")
logger.info(f" Ligne {idx}/{nb_nouvelles}: {article_code}")
logger.info(f" Quantité: {quantite}")
if ligne_data.get("prix_unitaire_ht"):
logger.info(f" Prix HT: {ligne_data['prix_unitaire_ht']}")
if ligne_data.get("remise_pourcentage"):
logger.info(f" Remise: {ligne_data['remise_pourcentage']}%")
try:
persist_article = factory_article.ReadReference(article_code)
if not persist_article:
raise ValueError(f"Article {article_code} INTROUVABLE")
article_obj = win32com.client.CastTo(persist_article, "IBOArticle3")
article_obj.Read()
logger.info(f" Article chargé")
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3")
except:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3")
try:
ligne_obj.SetDefaultArticleReference(article_code, quantite)
logger.info(f" Article associé via SetDefaultArticleReference")
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(f" Article associé via SetDefaultArticle")
except:
ligne_obj.DL_Design = ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
logger.info(f" Article associé manuellement")
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"])
logger.info(f" Prix unitaire défini")
if ligne_data.get("remise_pourcentage", 0) > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(ligne_data["remise_pourcentage"])
ligne_obj.DL_Remise01REM_Type = 0
logger.info(f" Remise définie")
except:
logger.debug(f" Remise non supportée")
ligne_obj.Write()
logger.info(f" Ligne {idx} créée avec succès")
except Exception as e:
logger.error(f" ERREUR ligne {idx}: {e}", exc_info=True)
raise
logger.info("")
logger.info(f" {nb_nouvelles} lignes créées")
logger.info("")
logger.info(" Write() après remplacement lignes...")
try:
doc.Write()
logger.info(" Write() réussi")
time.sleep(0.5)
doc.Read()
logger.info(" Read() après Write() OK")
except Exception as e:
logger.error(f" Write() a échoué: {e}", exc_info=True)
raise
champs_modifies.append("lignes")
if reference_a_modifier is not None:
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 6] MODIFICATION RÉFÉRENCE (après lignes)")
logger.info("=" * 80)
try:
ancienne_ref = getattr(doc, "DO_Ref", "")
nouvelle_ref = str(reference_a_modifier) if reference_a_modifier else ""
logger.info(f" Actuelle: '{ancienne_ref}'")
logger.info(f" Cible: '{nouvelle_ref}'")
doc.DO_Ref = nouvelle_ref
logger.info(" doc.DO_Ref affecté")
doc.Write()
logger.info(" Write()")
time.sleep(0.5)
doc.Read()
logger.info(" Read()")
champs_modifies.append("reference")
logger.info(f" Référence modifiée: '{ancienne_ref}''{nouvelle_ref}'")
except Exception as e:
logger.error(f" Erreur référence: {e}", exc_info=True)
if statut_a_modifier is not None:
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 7] MODIFICATION STATUT (en dernier)")
logger.info("=" * 80)
try:
statut_actuel = getattr(doc, "DO_Statut", 0)
nouveau_statut = int(statut_a_modifier)
logger.info(f" Actuel: {statut_actuel}")
logger.info(f" Cible: {nouveau_statut}")
if nouveau_statut != statut_actuel and nouveau_statut in [0, 1, 2, 3]:
doc.DO_Statut = nouveau_statut
logger.info(" doc.DO_Statut affecté")
doc.Write()
logger.info(" Write()")
time.sleep(0.5)
doc.Read()
logger.info(" Read()")
champs_modifies.append("statut")
logger.info(f" Statut modifié: {statut_actuel}{nouveau_statut}")
else:
logger.info(f" Pas de modification (identique ou invalide)")
except Exception as e:
logger.error(f" Erreur statut: {e}", exc_info=True)
logger.info("")
logger.info("=" * 80)
logger.info(" [ÉTAPE 8] VALIDATION FINALE")
logger.info("=" * 80)
try:
doc.Write()
logger.info(" Write() final")
except Exception as e:
logger.warning(f" Write() final: {e}")
time.sleep(0.5)
doc.Read()
logger.info(" Read() final")
logger.info("")
_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(f" Résultat extrait:")
logger.info(f" Numéro: {resultat['numero']}")
logger.info(f" Référence: '{resultat['reference']}'")
logger.info(f" Date devis: {resultat['date_devis']}")
logger.info(f" Date livraison: {resultat['date_livraison']}")
logger.info(f" Statut: {resultat['statut']}")
logger.info(f" Total HT: {resultat['total_ht']}")
logger.info(f" Total TTC: {resultat['total_ttc']}")
logger.info(f" Champs modifiés: {resultat['champs_modifies']}")
logger.info("")
logger.info("=" * 100)
logger.info(f" MODIFICATION DEVIS {numero} TERMINÉE AVEC SUCCÈS ")
logger.info("=" * 100)
return resultat
except ValueError as e:
logger.error(f" ERREUR MÉTIER: {e}")
raise
except Exception as e:
logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True)
raise RuntimeError(f"Erreur technique Sage: {str(e)}")
def _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,
ignorer_controle_stock=False,
conserver_document_source=True,
verifier_doublons=True,
):
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
type_source = int(type_source)
type_cible = int(type_cible)
logger.info(
f"[TRANSFORM] Transformation: {numero_source} ({type_source}) → type {type_cible}"
)
transformations_valides = {
(0, 10): ("Vente", "CreateProcess_Commander"),
(0, 60): ("Vente", "CreateProcess_Facturer"),
(10, 30): ("Vente", "CreateProcess_Livrer"),
(10, 60): ("Vente", "CreateProcess_Facturer"),
(30, 60): ("Vente", "CreateProcess_Facturer"),
}
if (type_source, type_cible) not in transformations_valides:
raise ValueError(
f"Transformation non autorisée: "
f"{self._get_type_libelle(type_source)}{self._get_type_libelle(type_cible)}"
)
module, methode = transformations_valides[(type_source, type_cible)]
logger.info(f"[TRANSFORM] Méthode: Transformation.{module}.{methode}()")
if verifier_doublons:
logger.info("[TRANSFORM] Vérification des doublons...")
verif = peut_etre_transforme(numero_source, type_source, type_cible)
if not verif["possible"]:
docs = [d["numero"] for d in verif.get("documents_existants", [])]
raise ValueError(
f"{verif['raison']}. Document(s) existant(s): {', '.join(docs)}"
)
logger.info("[TRANSFORM] Aucun doublon détecté")
try:
with self._com_context(), self._lock_com:
factory = self.cial.FactoryDocumentVente
logger.info(f"[TRANSFORM] Lecture de {numero_source}...")
if not factory.ExistPiece(type_source, numero_source):
raise ValueError(f"Document {numero_source} introuvable")
persist_source = factory.ReadPiece(type_source, numero_source)
if not persist_source:
raise ValueError(f"Impossible de lire {numero_source}")
doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3")
doc_source.Read()
statut_source = getattr(doc_source, "DO_Statut", 0)
nb_lignes_source = 0
try:
factory_lignes = getattr(doc_source, "FactoryDocumentLigne", None)
if factory_lignes:
lignes_list = factory_lignes.List
nb_lignes_source = lignes_list.Count if lignes_list else 0
except:
pass
logger.info(
f"[TRANSFORM] Source: statut={statut_source}, {nb_lignes_source} ligne(s)"
)
if nb_lignes_source == 0:
raise ValueError(f"Document {numero_source} vide (0 lignes)")
logger.info("[TRANSFORM] 🔧 Création du transformer...")
transformation = getattr(self.cial, "Transformation", None)
if not transformation:
raise RuntimeError("API Transformation non disponible")
module_obj = getattr(transformation, module, None)
if not module_obj:
raise RuntimeError(f"Module {module} non disponible")
methode_func = getattr(module_obj, methode, None)
if not methode_func:
raise RuntimeError(f"Méthode {methode} non disponible")
transformer = methode_func()
if not transformer:
raise RuntimeError("Échec création transformer")
logger.info("[TRANSFORM] Transformer créé")
logger.info("[TRANSFORM] Configuration...")
if hasattr(transformer, "ConserveDocuments"):
try:
transformer.ConserveDocuments = conserver_document_source
logger.info(
f"[TRANSFORM] ConserveDocuments = {conserver_document_source}"
)
except Exception as e:
logger.warning(
f"[TRANSFORM] ConserveDocuments non modifiable: {e}"
)
logger.info("[TRANSFORM] Ajout du document...")
try:
transformer.AddDocument(doc_source)
logger.info("[TRANSFORM] Document ajouté")
except Exception as e:
raise RuntimeError(f"Impossible d'ajouter le document: {e}")
try:
can_process = getattr(transformer, "CanProcess", False)
logger.info(f"[TRANSFORM] CanProcess: {can_process}")
except:
can_process = True
if not can_process:
erreurs = lire_erreurs_sage(transformer, "Transformer")
if erreurs:
msgs = [f"{e['field']}: {e['description']}" for e in erreurs]
raise RuntimeError(
f"Transformation impossible: {' | '.join(msgs)}"
)
raise RuntimeError("Transformation impossible (CanProcess=False)")
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug("[TRANSFORM] Transaction démarrée")
except:
pass
try:
logger.info("[TRANSFORM] Process()...")
try:
transformer.Process()
logger.info("[TRANSFORM] Process() réussi")
except Exception as e:
logger.error(f"[TRANSFORM] Erreur Process(): {e}")
erreurs = lire_erreurs_sage(transformer, "Transformer")
if erreurs:
msgs = [
f"{e['field']}: {e['description']}" for e in erreurs
]
raise RuntimeError(f"Échec: {' | '.join(msgs)}")
raise RuntimeError(f"Échec transformation: {e}")
logger.info("[TRANSFORM] Récupération des résultats...")
list_results = getattr(transformer, "ListDocumentsResult", None)
if not list_results:
raise RuntimeError("ListDocumentsResult non disponible")
documents_crees = []
index = 1
while index <= 100:
try:
doc_result = list_results.Item(index)
if doc_result is None:
break
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_cible = getattr(doc_result, "DO_Piece", "").strip()
total_ht = float(getattr(doc_result, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_result, "DO_TotalTTC", 0.0))
nb_lignes = 0
try:
factory_lignes_result = getattr(
doc_result, "FactoryDocumentLigne", None
)
if factory_lignes_result:
lignes_list = factory_lignes_result.List
nb_lignes = lignes_list.Count if lignes_list else 0
except:
pass
documents_crees.append(
{
"numero": numero_cible,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": nb_lignes,
}
)
logger.info(
f"[TRANSFORM] Document créé: {numero_cible} "
f"({nb_lignes} lignes, {total_ht}€ HT)"
)
index += 1
except Exception as e:
logger.debug(f"Fin de liste à index {index}")
break
if not documents_crees:
raise RuntimeError("Aucun document créé après Process()")
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.debug("[TRANSFORM] Transaction committée")
except:
pass
time.sleep(1.5)
doc_principal = documents_crees[0]
logger.info(
f"[TRANSFORM] SUCCÈS: {numero_source}{doc_principal['numero']}"
)
logger.info(
f"[TRANSFORM] • {doc_principal['nb_lignes']} ligne(s)"
)
logger.info(
f"[TRANSFORM] • {doc_principal['total_ht']}€ HT / "
f"{doc_principal['total_ttc']}€ TTC"
)
return {
"success": True,
"document_source": numero_source,
"document_cible": doc_principal["numero"],
"type_source": type_source,
"type_cible": type_cible,
"nb_documents_crees": len(documents_crees),
"documents": documents_crees,
"nb_lignes": doc_principal["nb_lignes"],
"total_ht": doc_principal["total_ht"],
"total_ttc": doc_principal["total_ttc"],
"methode_transformation": f"{module}.{methode}",
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.error("[TRANSFORM] Transaction annulée (rollback)")
except:
pass
raise
except ValueError as e:
logger.error(f"[TRANSFORM] Erreur métier: {e}")
raise
except RuntimeError as e:
logger.error(f"[TRANSFORM] Erreur technique: {e}")
raise
except Exception as e:
logger.error(f"[TRANSFORM] Erreur inattendue: {e}", exc_info=True)
raise RuntimeError(f"Échec transformation: {str(e)}")
def 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:
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:
pass
try:
contact_info["nom"] = (
getattr(client, "CT_Contact", "")
or contact_info["client_intitule"]
)
except:
contact_info["nom"] = contact_info["client_intitule"]
return contact_info
except Exception as e:
logger.error(f"Erreur lecture contact client {code_client}: {e}")
return None
def mettre_a_jour_derniere_relance(self, doc_id, type_doc):
date_relance = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return self.mettre_a_jour_champ_libre(
doc_id, type_doc, "DerniereRelance", date_relance
)
def lister_tous_prospects(self, filtre=""):
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
query = """
SELECT
CT_Num, CT_Intitule, CT_Adresse, CT_Ville,
CT_CodePostal, CT_Telephone, CT_EMail
FROM F_COMPTET
WHERE CT_Type = 0 AND CT_Prospect = 1
"""
params = []
if filtre:
query += " AND (CT_Num LIKE ? OR CT_Intitule LIKE ?)"
params.extend([f"%{filtre}%", f"%{filtre}%"])
query += " ORDER BY CT_Intitule"
cursor.execute(query, params)
rows = cursor.fetchall()
prospects = []
for row in rows:
prospects.append(
{
"numero": _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):
""" Lit UNE livraison via SQL (avec lignes)"""
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:
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(f" 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:
pass
if contact_data.get("linkedin"):
linkedin = _clean_str(contact_data["linkedin"], 69)
try:
contact.LinkedIn = linkedin
logger.info(f" LinkedIn = {linkedin}")
except:
pass
if contact_data.get("skype"):
skype = _clean_str(contact_data["skype"], 69)
try:
contact.Skype = skype
logger.info(f" Skype = {skype}")
except:
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")
try:
contact.Write()
logger.info(" OK Write() reussi")
contact.Read()
logger.info(" OK Read() reussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
except:
pass
logger.error(f" ERROR Write: {error_detail}")
raise RuntimeError(f"Echec enregistrement: {error_detail}")
contact_no = None
n_contact = None
try:
contact_no = getattr(contact, 'CT_No', None)
n_contact = getattr(contact, 'N_Contact', None)
logger.info(f" Contact CT_No={contact_no}, N_Contact={n_contact}")
except:
pass
est_defaut = contact_data.get("est_defaut", False)
if est_defaut and (contact_no or n_contact):
logger.info("[7] Definition comme contact par defaut")
try:
nom_complet = f"{prenom} {nom}".strip() if prenom else nom
persist_client = factory_client.ReadNumero(numero_client)
client_obj = win32com.client.CastTo(persist_client, "IBOClient3")
client_obj.Read()
client_obj.CT_Contact = nom_complet
logger.info(f" CT_Contact = '{nom_complet}'")
if contact_no and hasattr(client_obj, 'CT_NoContact'):
try:
client_obj.CT_NoContact = contact_no
logger.info(f" CT_NoContact = {contact_no}")
except:
pass
client_obj.Write()
client_obj.Read()
logger.info(" OK Contact par defaut defini")
except Exception as e:
logger.warning(f" WARN Echec: {e}")
est_defaut = False
logger.info("=" * 80)
logger.info(f"[SUCCES] Contact cree: {prenom} {nom}")
logger.info(f" Lie au client {numero_client}")
if contact_no:
logger.info(f" CT_No={contact_no}")
logger.info("=" * 80)
contact_dict = _contact_to_dict(
contact,
numero_client=numero_client,
contact_numero=contact_no,
n_contact=n_contact
)
contact_dict["est_defaut"] = est_defaut
logger.info("=" * 80)
logger.info("[DEBUG RETOUR]")
logger.info(f" numero_client = {numero_client}")
logger.info(f" contact_no = {contact_no}")
logger.info(f" n_contact = {n_contact}")
logger.info(f" contact_dict COMPLET = {contact_dict}")
logger.info("=" * 80)
return contact_dict
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def modifier_contact(self, numero: str, contact_numero: int, updates: Dict) -> Dict:
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")
except Exception as e:
raise ValueError(f"Client {numero} introuvable: {e}")
logger.info("[2] Chargement du contact")
nom_recherche = None
prenom_recherche = 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_recherche = row.CT_Nom.strip() if row.CT_Nom else ""
prenom_recherche = row.CT_Prenom.strip() if row.CT_Prenom else ""
logger.info(f" Contact trouve en SQL: {prenom_recherche} {nom_recherche}")
except Exception as e:
raise ValueError(f"Contact introuvable: {e}")
factory_dossier = self.cial.CptaApplication.FactoryDossierContact
persist = factory_dossier.ReadNomPrenom(nom_recherche, prenom_recherche)
if not persist:
raise ValueError(f"Contact non trouvable via ReadNomPrenom")
contact = win32com.client.CastTo(persist, "IBOTiersContact3")
contact.Read()
logger.info(f" OK Contact charge: {contact.Nom}")
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:
pass
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:
pass
if "prenom" in updates:
prenom = _clean_str(updates["prenom"], 35)
try:
contact.Prenom = prenom
logger.info(f" Prenom = {prenom}")
modifications_appliquees.append("prenom")
except:
pass
if "fonction" in updates:
fonction = _clean_str(updates["fonction"], 35)
try:
contact.Fonction = fonction
logger.info(f" Fonction = {fonction}")
modifications_appliquees.append("fonction")
except:
pass
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:
pass
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:
pass
if "facebook" in updates:
facebook = _clean_str(updates["facebook"], 69)
try:
contact.Facebook = facebook
logger.info(f" Facebook = {facebook}")
modifications_appliquees.append("facebook")
except:
pass
if "linkedin" in updates:
linkedin = _clean_str(updates["linkedin"], 69)
try:
contact.LinkedIn = linkedin
logger.info(f" LinkedIn = {linkedin}")
modifications_appliquees.append("linkedin")
except:
pass
if "skype" in updates:
skype = _clean_str(updates["skype"], 69)
try:
contact.Skype = skype
logger.info(f" Skype = {skype}")
modifications_appliquees.append("skype")
except:
pass
logger.info("[4] Enregistrement")
try:
contact.Write()
contact.Read()
logger.info(" OK Write() reussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
except:
pass
logger.error(f" ERROR Write: {error_detail}")
raise RuntimeError(f"Echec modification contact: {error_detail}")
logger.info(f" Modifications appliquees: {', '.join(modifications_appliquees)}")
est_defaut_demande = updates.get("est_defaut")
est_actuellement_defaut = False
if est_defaut_demande is not None and est_defaut_demande:
logger.info("[5] Gestion contact par defaut")
try:
nom_complet = f"{contact.Prenom} {contact.Nom}".strip() if contact.Prenom else contact.Nom
persist_client = factory_client.ReadNumero(numero)
client_obj = win32com.client.CastTo(persist_client, "IBOClient3")
client_obj.Read()
client_obj.CT_Contact = nom_complet
logger.info(f" CT_Contact = '{nom_complet}'")
if hasattr(client_obj, 'CT_NoContact'):
try:
client_obj.CT_NoContact = contact_numero
logger.info(f" CT_NoContact = {contact_numero}")
except:
pass
client_obj.Write()
client_obj.Read()
logger.info(" OK Contact par defaut defini")
est_actuellement_defaut = True
except Exception as e:
logger.warning(f" WARN Echec: {e}")
logger.info("=" * 80)
logger.info(f"[SUCCES] Contact modifie: CT_No={contact_numero}")
logger.info("=" * 80)
contact_dict = _contact_to_dict(
contact,
numero_client=numero,
contact_numero=contact_numero,
n_contact=None
)
contact_dict["est_defaut"] = est_actuellement_defaut
return contact_dict
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def definir_contact_defaut(self, numero: str, contact_numero: int) -> Dict:
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:
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:
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(f"Contact non trouvable via ReadNomPrenom")
contact = win32com.client.CastTo(persist, "IBOTiersContact3")
contact.Read()
logger.info(f" OK Contact charge: {contact.Nom}")
except Exception as e:
raise ValueError(f"Contact introuvable: {e}")
logger.info("[3] Suppression")
try:
contact.Remove()
logger.info(" OK Remove() reussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
except:
pass
logger.error(f" ERROR Remove: {error_detail}")
raise RuntimeError(f"Echec suppression contact: {error_detail}")
logger.info("=" * 80)
logger.info(f"[SUCCES] Contact supprime: {prenom_contact} {nom_contact}")
logger.info("=" * 80)
return {
"numero": numero,
"contact_numero": contact_numero,
"nom": nom_contact,
"prenom": prenom_contact,
"supprime": True,
"date_suppression": datetime.now().isoformat()
}
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def creer_client(self, client_data: Dict) -> Dict:
"""
Creation client Sage - Version corrigée pour erreur cohérence
"""
if not self.cial:
raise RuntimeError("Connexion Sage non etablie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info("[CREATION CLIENT SAGE - DIAGNOSTIC COMPLET]")
logger.info("=" * 80)
def clean_str(value, max_len: int) -> str:
if value is None or str(value).lower() in ('none', 'null', ''):
return ""
return str(value)[:max_len].strip()
def safe_int(value, default=None):
if value is None:
return default
try:
return int(value)
except (ValueError, TypeError):
return default
def safe_float(value, default=None):
if value is None:
return default
try:
return float(value)
except (ValueError, TypeError):
return default
def try_set_attribute(obj, attr_name, value, variants=None):
"""Essaie de definir un attribut avec plusieurs variantes de noms"""
if variants is None:
variants = [attr_name]
else:
variants = [attr_name] + variants
for variant in variants:
try:
if hasattr(obj, variant):
setattr(obj, variant, value)
logger.debug(f" {variant} = {value} [OK]")
return True
except Exception as e:
logger.debug(f" {variant} echec: {str(e)[:50]}")
return False
if not client_data.get("intitule"):
raise ValueError("intitule obligatoire")
if not client_data.get("numero"):
raise ValueError("numero obligatoire")
intitule = clean_str(client_data["intitule"], 69)
numero = clean_str(client_data["numero"], 17).upper()
type_tiers = safe_int(client_data.get("type_tiers"), 0)
logger.info("[ETAPE 1] CREATION OBJET")
factory_map = {
0: ("FactoryClient", "IBOClient3"),
1: ("FactoryFourniss", "IBOFournisseur3"),
2: ("FactorySalarie", "IBOSalarie3"),
3: ("FactoryAutre", "IBOAutre3"),
}
factory_name, interface_name = factory_map[type_tiers]
factory = getattr(self.cial.CptaApplication, factory_name)
persist = factory.Create()
client = win32com.client.CastTo(persist, interface_name)
logger.info(f" Objet cree: {interface_name}")
logger.info("[ETAPE 2] CONFIGURATION OBLIGATOIRE")
client.CT_Intitule = intitule
client.CT_Num = numero
logger.info(f" CT_Num = {numero}")
logger.info(f" CT_Intitule = {intitule}")
qualite = clean_str(client_data.get("qualite", "CLI"), 17)
if qualite:
client.CT_Qualite = qualite
logger.info(f" CT_Qualite = {qualite}")
client.SetDefault()
logger.info(" SetDefault() applique")
if client_data.get("raccourci"):
raccourci = clean_str(client_data["raccourci"], 7).upper().strip()
try:
factory_client = self.cial.CptaApplication.FactoryClient
exist_client = factory_client.ReadRaccourci(raccourci)
if exist_client:
logger.warning(f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]")
else:
client.CT_Raccourci = raccourci
logger.info(f" CT_Raccourci = {raccourci} [OK]")
except Exception as e:
try:
client.CT_Raccourci = raccourci
logger.info(f" CT_Raccourci = {raccourci} [OK]")
except Exception as e2:
logger.warning(f" CT_Raccourci = {raccourci} [ECHEC: {e2}]")
try:
if not hasattr(client, 'CT_Type') or client.CT_Type is None:
client.CT_Type = type_tiers
logger.info(f" CT_Type force a {type_tiers}")
except:
pass
COMPTES_DEFAUT = {0: "4110000", 1: "4010000", 2: "421", 3: "471"}
compte = clean_str(
client_data.get("compte_general") or COMPTES_DEFAUT.get(type_tiers, "4110000"),
13
)
factory_compte = self.cial.CptaApplication.FactoryCompteG
compte_trouve = False
comptes_a_tester = [
compte,
COMPTES_DEFAUT.get(type_tiers, "4110000"),
"4110000", "411000", "411", # Clients
"4010000", "401000", "401", # Fournisseurs
]
for test_compte in comptes_a_tester:
try:
persist_compte = factory_compte.ReadNumero(test_compte)
if persist_compte:
compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3")
compte_obj.Read()
type_compte = getattr(compte_obj, 'CG_Type', None)
if type_compte == 0:
client.CompteGPrinc = compte_obj
compte = test_compte
compte_trouve = True
logger.info(f" CompteGPrinc = {test_compte} (Type: {type_compte}) [OK]")
break
else:
logger.debug(f" Compte {test_compte} - Type {type_compte} incompatible")
except Exception as e:
logger.debug(f" Compte {test_compte} - erreur: {e}")
if not compte_trouve:
raise RuntimeError("Aucun compte general valide trouve")
logger.info(" Configuration categories:")
try:
factory_cat_tarif = self.cial.CptaApplication.FactoryCategorieTarif
for cat_id in ["0", "1"]:
try:
persist_cat = factory_cat_tarif.ReadIntitule(cat_id)
if persist_cat:
cat_tarif_obj = win32com.client.CastTo(persist_cat, "IBOCategorieTarif3")
cat_tarif_obj.Read()
client.CatTarif = cat_tarif_obj
logger.info(f" CatTarif = {cat_id} [OK]")
break
except:
continue
except Exception as e:
logger.warning(f" CatTarif erreur: {e}")
try:
factory_cat_compta = self.cial.CptaApplication.FactoryCategorieCompta
for cat_id in ["0", "1"]:
try:
persist_cat = factory_cat_compta.ReadIntitule(cat_id)
if persist_cat:
cat_compta_obj = win32com.client.CastTo(persist_cat, "IBOCategorieCompta3")
cat_compta_obj.Read()
client.CatCompta = cat_compta_obj
logger.info(f" CatCompta = {cat_id} [OK]")
break
except:
continue
except Exception as e:
logger.warning(f" CatCompta erreur: {e}")
logger.info("[ETAPE 3] IDENTIFICATION")
if client_data.get("classement"):
try_set_attribute(client, "CT_Classement", clean_str(client_data["classement"], 17))
if client_data.get("raccourci"):
raccourci = clean_str(client_data["raccourci"], 7).upper()
try_set_attribute(client, "CT_Raccourci", raccourci)
if client_data.get("siret"):
try_set_attribute(client, "CT_Siret", clean_str(client_data["siret"], 15))
if client_data.get("tva_intra"):
try_set_attribute(client, "CT_Identifiant", clean_str(client_data["tva_intra"], 25))
if client_data.get("code_naf"):
try_set_attribute(client, "CT_Ape", clean_str(client_data["code_naf"], 7))
logger.info("[ETAPE 4] ADRESSE")
if client_data.get("contact"):
contact_nom = clean_str(client_data["contact"], 35)
try:
client.CT_Contact = contact_nom
logger.info(f" CT_Contact (client) = {contact_nom} [OK]")
except Exception as e:
logger.warning(f" CT_Contact (client) [ECHEC: {e}]")
try:
adresse_obj = client.Adresse
adresse_obj.Contact = contact_nom
logger.info(f" Contact (adresse) = {contact_nom} [OK]")
except Exception as e:
logger.warning(f" Contact (adresse) [ECHEC: {e}]")
try:
adresse_obj = client.Adresse
logger.info(" Objet Adresse OK")
if client_data.get("adresse"):
adresse_obj.Adresse = clean_str(client_data["adresse"], 35)
if client_data.get("complement"):
adresse_obj.Complement = clean_str(client_data["complement"], 35)
if client_data.get("code_postal"):
adresse_obj.CodePostal = clean_str(client_data["code_postal"], 9)
if client_data.get("ville"):
adresse_obj.Ville = clean_str(client_data["ville"], 35)
if client_data.get("region"):
adresse_obj.CodeRegion = clean_str(client_data["region"], 25)
if client_data.get("pays"):
adresse_obj.Pays = clean_str(client_data["pays"], 35)
except Exception as e:
logger.error(f" Adresse erreur: {e}")
logger.info("[ETAPE 5] TELECOM")
try:
telecom_obj = client.Telecom
logger.info(" Objet Telecom OK")
if client_data.get("telephone"):
telecom_obj.Telephone = clean_str(client_data["telephone"], 21)
if client_data.get("telecopie"):
telecom_obj.Telecopie = clean_str(client_data["telecopie"], 21)
if client_data.get("email"):
telecom_obj.EMail = clean_str(client_data["email"], 69)
if client_data.get("site_web"):
telecom_obj.Site = clean_str(client_data["site_web"], 69)
if client_data.get("portable"):
portable = clean_str(client_data["portable"], 21)
try_set_attribute(telecom_obj, "Portable", portable)
logger.info(f" Portable = {portable}")
if client_data.get("facebook"):
facebook = clean_str(client_data["facebook"], 69) # URL ou @username
if not try_set_attribute(telecom_obj, "Facebook", facebook):
try_set_attribute(client, "CT_Facebook", facebook)
logger.info(f" Facebook = {facebook}")
if client_data.get("linkedin"):
linkedin = clean_str(client_data["linkedin"], 69) # URL ou profil
if not try_set_attribute(telecom_obj, "LinkedIn", linkedin):
try_set_attribute(client, "CT_LinkedIn", linkedin)
logger.info(f" LinkedIn = {linkedin}")
except Exception as e:
logger.error(f" Telecom erreur: {e}")
logger.info("[ETAPE 6] TAUX")
for i in range(1, 5):
val = client_data.get(f"taux{i:02d}")
if val is not None:
try_set_attribute(client, f"CT_Taux{i:02d}", safe_float(val))
logger.info("[ETAPE 7] STATISTIQUES")
stat01 = client_data.get("statistique01") or client_data.get("secteur")
if stat01:
try_set_attribute(client, "CT_Statistique01", clean_str(stat01, 21))
for i in range(2, 11):
val = client_data.get(f"statistique{i:02d}")
if val:
try_set_attribute(client, f"CT_Statistique{i:02d}", clean_str(val, 21))
logger.info("[ETAPE 8] COMMERCIAL")
if client_data.get("encours_autorise"):
try_set_attribute(client, "CT_Encours", safe_float(client_data["encours_autorise"]))
if client_data.get("assurance_credit"):
try_set_attribute(client, "CT_Assurance", safe_float(client_data["assurance_credit"]))
if client_data.get("langue") is not None:
try_set_attribute(client, "CT_Langue", safe_int(client_data["langue"]))
if client_data.get("commercial_code") is not None:
co_no = safe_int(client_data["commercial_code"])
if not try_set_attribute(client, "CO_No", co_no):
try:
factory_collab = self.cial.CptaApplication.FactoryCollaborateur
persist_collab = factory_collab.ReadIntitule(str(co_no))
if persist_collab:
collab_obj = win32com.client.CastTo(persist_collab, "IBOCollaborateur3")
collab_obj.Read()
client.Collaborateur = collab_obj
logger.debug(f" Collaborateur (objet) = {co_no} [OK]")
except Exception as e:
logger.debug(f" Collaborateur echec: {e}")
logger.info("[ETAPE 9] FACTURATION")
try_set_attribute(client, "CT_Lettrage", 1 if client_data.get("lettrage_auto", True) else 0)
try_set_attribute(client, "CT_Sommeil", 0 if client_data.get("est_actif", True) else 1)
try_set_attribute(client, "CT_Facture", safe_int(client_data.get("type_facture", 1)))
if client_data.get("est_prospect") is not None:
try_set_attribute(client, "CT_Prospect", 1 if client_data["est_prospect"] else 0)
factu_map = {
"CT_BLFact": "bl_en_facture",
"CT_Saut": "saut_page",
"CT_ValidEch": "validation_echeance",
"CT_ControlEnc": "controle_encours",
"CT_NotRappel": "exclure_relance",
"CT_NotPenal": "exclure_penalites",
"CT_BonAPayer": "bon_a_payer",
}
for attr, key in factu_map.items():
if client_data.get(key) is not None:
try_set_attribute(client, attr, safe_int(client_data[key]))
logger.info("[ETAPE 10] LOGISTIQUE")
logistique_map = {
"CT_PrioriteLivr": "priorite_livraison",
"CT_LivrPartielle": "livraison_partielle",
"CT_DelaiTransport": "delai_transport",
"CT_DelaiAppro": "delai_appro",
}
for attr, key in logistique_map.items():
if client_data.get(key) is not None:
try_set_attribute(client, attr, safe_int(client_data[key]))
if client_data.get("commentaire"):
try_set_attribute(client, "CT_Commentaire", clean_str(client_data["commentaire"], 35))
logger.info("[ETAPE 12] ANALYTIQUE")
if client_data.get("section_analytique"):
try_set_attribute(client, "CA_Num", clean_str(client_data["section_analytique"], 13))
logger.info("[ETAPE 13] ORGANISATION")
if client_data.get("mode_reglement_code") is not None:
mr_no = safe_int(client_data["mode_reglement_code"])
if not try_set_attribute(client, "MR_No", mr_no):
try:
factory_mr = self.cial.CptaApplication.FactoryModeRegl
persist_mr = factory_mr.ReadIntitule(str(mr_no))
if persist_mr:
mr_obj = win32com.client.CastTo(persist_mr, "IBOModeRegl3")
mr_obj.Read()
client.ModeRegl = mr_obj
logger.debug(f" ModeRegl (objet) = {mr_no} [OK]")
except Exception as e:
logger.debug(f" ModeRegl echec: {e}")
if client_data.get("surveillance_active") is not None:
surveillance = 1 if client_data["surveillance_active"] else 0
try:
client.CT_Surveillance = surveillance
logger.info(f" CT_Surveillance = {surveillance} [OK]")
except Exception as e:
logger.warning(f" CT_Surveillance [ECHEC: {e}]")
if client_data.get("coface"):
coface = clean_str(client_data["coface"], 25)
try:
client.CT_Coface = coface
logger.info(f" CT_Coface = {coface} [OK]")
except Exception as e:
logger.warning(f" CT_Coface [ECHEC: {e}]")
if client_data.get("forme_juridique"):
try_set_attribute(client, "CT_SvFormeJuri", clean_str(client_data["forme_juridique"], 33))
if client_data.get("effectif"):
try_set_attribute(client, "CT_SvEffectif", clean_str(client_data["effectif"], 11))
if client_data.get("sv_regularite"):
try_set_attribute(client, "CT_SvRegul", clean_str(client_data["sv_regularite"], 3))
if client_data.get("sv_cotation"):
try_set_attribute(client, "CT_SvCotation", clean_str(client_data["sv_cotation"], 5))
if client_data.get("sv_objet_maj"):
try_set_attribute(client, "CT_SvObjetMaj", clean_str(client_data["sv_objet_maj"], 61))
ca = client_data.get("ca_annuel") or client_data.get("sv_chiffre_affaires")
if ca:
try_set_attribute(client, "CT_SvCA", safe_float(ca))
if client_data.get("sv_resultat"):
try_set_attribute(client, "CT_SvResultat", safe_float(client_data["sv_resultat"]))
logger.info("=" * 80)
logger.info("[DIAGNOSTIC PRE-WRITE]")
champs_diagnostic = [
'CT_Num', 'CT_Intitule', 'CT_Type', 'CT_Qualite',
'CT_Facture', 'CT_Lettrage', 'CT_Sommeil',
]
for champ in champs_diagnostic:
try:
valeur = getattr(client, champ, "ATTRIBUT_INEXISTANT")
logger.info(f" {champ}: {valeur}")
except Exception as e:
logger.info(f" {champ}: ERREUR ({str(e)[:50]})")
try:
compte_obj = client.CompteGPrinc
if compte_obj:
logger.info(f" CompteGPrinc.CG_Num: {compte_obj.CG_Num}")
logger.info(f" CompteGPrinc.CG_Type: {compte_obj.CG_Type}")
else:
logger.error(" CompteGPrinc: NULL !!!")
except Exception as e:
logger.error(f" CompteGPrinc: ERREUR - {e}")
logger.info("=" * 80)
logger.info("[WRITE]")
try:
client.Write()
client.Read()
logger.info("[OK] Write reussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
except:
pass
logger.error(f"[ERREUR] {error_detail}")
raise RuntimeError(f"Echec Write(): {error_detail}")
num_final = getattr(client, "CT_Num", numero)
logger.info("=" * 80)
logger.info(f"[SUCCES] CLIENT CREE: {num_final}")
logger.info("=" * 80)
return {
"numero": num_final,
"intitule": intitule,
"type_tiers": type_tiers,
"qualite": qualite,
"compte_general": compte,
"date_creation": datetime.now().isoformat(),
}
except ValueError as e:
logger.error(f"[ERREUR VALIDATION] {e}")
raise
except Exception as e:
logger.error(f"[ERREUR] {e}", exc_info=True)
raise RuntimeError(f"Erreur technique: {e}")
def modifier_client(self, code: str, client_data: Dict) -> Dict:
"""
Modification client Sage - Version complète alignée sur creer_client
"""
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info("=" * 80)
logger.info(f"[MODIFICATION CLIENT SAGE - {code}]")
logger.info("=" * 80)
def clean_str(value, max_len: int) -> str:
if value is None or str(value).lower() in ('none', 'null', ''):
return ""
return str(value)[:max_len].strip()
def safe_int(value, default=None):
if value is None:
return default
try:
return int(value)
except (ValueError, TypeError):
return default
def safe_float(value, default=None):
if value is None:
return default
try:
return float(value)
except (ValueError, TypeError):
return default
def try_set_attribute(obj, attr_name, value, variants=None):
"""Essaie de définir un attribut avec plusieurs variantes de noms"""
if variants is None:
variants = [attr_name]
else:
variants = [attr_name] + variants
for variant in variants:
try:
if hasattr(obj, variant):
setattr(obj, variant, value)
logger.debug(f" {variant} = {value} [OK]")
return True
except Exception as e:
logger.debug(f" {variant} echec: {str(e)[:50]}")
return False
champs_modifies = []
logger.info("[ETAPE 1] CHARGEMENT CLIENT")
factory_client = self.cial.CptaApplication.FactoryClient
persist = factory_client.ReadNumero(code)
if not persist:
raise ValueError(f"Client {code} introuvable")
client = win32com.client.CastTo(persist, "IBOClient3")
client.Read()
logger.info(f" Client chargé: {getattr(client, 'CT_Intitule', '')}")
logger.info("[ETAPE 2] IDENTIFICATION")
if "intitule" in client_data:
intitule = clean_str(client_data["intitule"], 69)
client.CT_Intitule = intitule
champs_modifies.append("intitule")
logger.info(f" CT_Intitule = {intitule}")
if "qualite" in client_data:
qualite = clean_str(client_data["qualite"], 17)
if try_set_attribute(client, "CT_Qualite", qualite):
champs_modifies.append("qualite")
if "classement" in client_data:
if try_set_attribute(client, "CT_Classement", clean_str(client_data["classement"], 17)):
champs_modifies.append("classement")
if "raccourci" in client_data:
raccourci = clean_str(client_data["raccourci"], 7).upper()
try:
exist_client = factory_client.ReadRaccourci(raccourci)
if exist_client and exist_client.CT_Num != code:
logger.warning(f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]")
else:
if try_set_attribute(client, "CT_Raccourci", raccourci):
champs_modifies.append("raccourci")
except:
if try_set_attribute(client, "CT_Raccourci", raccourci):
champs_modifies.append("raccourci")
if "siret" in client_data:
if try_set_attribute(client, "CT_Siret", clean_str(client_data["siret"], 15)):
champs_modifies.append("siret")
if "tva_intra" in client_data:
if try_set_attribute(client, "CT_Identifiant", clean_str(client_data["tva_intra"], 25)):
champs_modifies.append("tva_intra")
if "code_naf" in client_data:
if try_set_attribute(client, "CT_Ape", clean_str(client_data["code_naf"], 7)):
champs_modifies.append("code_naf")
adresse_keys = ["contact", "adresse", "complement", "code_postal", "ville", "region", "pays"]
if any(k in client_data for k in adresse_keys):
logger.info("[ETAPE 3] ADRESSE")
try:
if "contact" in client_data:
contact_nom = clean_str(client_data["contact"], 35)
try:
client.CT_Contact = contact_nom
champs_modifies.append("contact (client)")
logger.info(f" CT_Contact (client) = {contact_nom} [OK]")
except Exception as e:
logger.warning(f" CT_Contact (client) [ECHEC: {e}]")
try:
adresse_obj = client.Adresse
adresse_obj.Contact = contact_nom
champs_modifies.append("contact (adresse)")
logger.info(f" Contact (adresse) = {contact_nom} [OK]")
except Exception as e:
logger.warning(f" Contact (adresse) [ECHEC: {e}]")
adresse_obj = client.Adresse
if "adresse" in client_data:
adresse_obj.Adresse = clean_str(client_data["adresse"], 35)
champs_modifies.append("adresse")
if "complement" in client_data:
adresse_obj.Complement = clean_str(client_data["complement"], 35)
champs_modifies.append("complement")
if "code_postal" in client_data:
adresse_obj.CodePostal = clean_str(client_data["code_postal"], 9)
champs_modifies.append("code_postal")
if "ville" in client_data:
adresse_obj.Ville = clean_str(client_data["ville"], 35)
champs_modifies.append("ville")
if "region" in client_data:
adresse_obj.CodeRegion = clean_str(client_data["region"], 25)
champs_modifies.append("region")
if "pays" in client_data:
adresse_obj.Pays = clean_str(client_data["pays"], 35)
champs_modifies.append("pays")
logger.info(f" Adresse mise à jour ({len([k for k in adresse_keys if k in client_data])} champs)")
except Exception as e:
logger.error(f" Adresse erreur: {e}")
telecom_keys = ["telephone", "telecopie", "email", "site_web", "portable", "facebook", "linkedin"]
if any(k in client_data for k in telecom_keys):
logger.info("[ETAPE 4] TELECOM")
try:
telecom_obj = client.Telecom
if "telephone" in client_data:
telecom_obj.Telephone = clean_str(client_data["telephone"], 21)
champs_modifies.append("telephone")
if "telecopie" in client_data:
telecom_obj.Telecopie = clean_str(client_data["telecopie"], 21)
champs_modifies.append("telecopie")
if "email" in client_data:
telecom_obj.EMail = clean_str(client_data["email"], 69)
champs_modifies.append("email")
if "site_web" in client_data:
telecom_obj.Site = clean_str(client_data["site_web"], 69)
champs_modifies.append("site_web")
if "portable" in client_data:
portable = clean_str(client_data["portable"], 21)
if try_set_attribute(telecom_obj, "Portable", portable):
champs_modifies.append("portable")
if "facebook" in client_data:
facebook = clean_str(client_data["facebook"], 69)
if not try_set_attribute(telecom_obj, "Facebook", facebook):
try_set_attribute(client, "CT_Facebook", facebook)
champs_modifies.append("facebook")
if "linkedin" in client_data:
linkedin = clean_str(client_data["linkedin"], 69)
if not try_set_attribute(telecom_obj, "LinkedIn", linkedin):
try_set_attribute(client, "CT_LinkedIn", linkedin)
champs_modifies.append("linkedin")
logger.info(f" Telecom mis à jour ({len([k for k in telecom_keys if k in client_data])} champs)")
except Exception as e:
logger.error(f" Telecom erreur: {e}")
if "compte_general" in client_data:
logger.info("[ETAPE 5] COMPTE GENERAL")
compte = clean_str(client_data["compte_general"], 13)
factory_compte = self.cial.CptaApplication.FactoryCompteG
try:
persist_compte = factory_compte.ReadNumero(compte)
if persist_compte:
compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3")
compte_obj.Read()
type_compte = getattr(compte_obj, 'CG_Type', None)
if type_compte == 0:
client.CompteGPrinc = compte_obj
champs_modifies.append("compte_general")
logger.info(f" CompteGPrinc = {compte} [OK]")
else:
logger.warning(f" Compte {compte} - Type {type_compte} incompatible")
except Exception as e:
logger.warning(f" CompteGPrinc erreur: {e}")
if "categorie_tarifaire" in client_data or "categorie_comptable" in client_data:
logger.info("[ETAPE 6] CATEGORIES")
if "categorie_tarifaire" in client_data:
try:
cat_id = str(client_data["categorie_tarifaire"])
factory_cat_tarif = self.cial.CptaApplication.FactoryCategorieTarif
persist_cat = factory_cat_tarif.ReadIntitule(cat_id)
if persist_cat:
cat_tarif_obj = win32com.client.CastTo(persist_cat, "IBOCategorieTarif3")
cat_tarif_obj.Read()
client.CatTarif = cat_tarif_obj
champs_modifies.append("categorie_tarifaire")
logger.info(f" CatTarif = {cat_id} [OK]")
except Exception as e:
logger.warning(f" CatTarif erreur: {e}")
if "categorie_comptable" in client_data:
try:
cat_id = str(client_data["categorie_comptable"])
factory_cat_compta = self.cial.CptaApplication.FactoryCategorieCompta
persist_cat = factory_cat_compta.ReadIntitule(cat_id)
if persist_cat:
cat_compta_obj = win32com.client.CastTo(persist_cat, "IBOCategorieCompta3")
cat_compta_obj.Read()
client.CatCompta = cat_compta_obj
champs_modifies.append("categorie_comptable")
logger.info(f" CatCompta = {cat_id} [OK]")
except Exception as e:
logger.warning(f" CatCompta erreur: {e}")
taux_modifies = False
for i in range(1, 5):
key = f"taux{i:02d}"
if key in client_data:
if not taux_modifies:
logger.info("[ETAPE 7] TAUX")
taux_modifies = True
val = safe_float(client_data[key])
if try_set_attribute(client, f"CT_Taux{i:02d}", val):
champs_modifies.append(key)
stat_keys = ["statistique01", "secteur"] + [f"statistique{i:02d}" for i in range(2, 11)]
stat_modifies = False
stat01 = client_data.get("statistique01") or client_data.get("secteur")
if stat01:
if not stat_modifies:
logger.info("[ETAPE 8] STATISTIQUES")
stat_modifies = True
if try_set_attribute(client, "CT_Statistique01", clean_str(stat01, 21)):
champs_modifies.append("statistique01")
for i in range(2, 11):
key = f"statistique{i:02d}"
if key in client_data:
if not stat_modifies:
logger.info("[ETAPE 8] STATISTIQUES")
stat_modifies = True
if try_set_attribute(client, f"CT_Statistique{i:02d}", clean_str(client_data[key], 21)):
champs_modifies.append(key)
commercial_keys = ["encours_autorise", "assurance_credit", "langue", "commercial_code"]
if any(k in client_data for k in commercial_keys):
logger.info("[ETAPE 9] COMMERCIAL")
if "encours_autorise" in client_data:
if try_set_attribute(client, "CT_Encours", safe_float(client_data["encours_autorise"])):
champs_modifies.append("encours_autorise")
if "assurance_credit" in client_data:
if try_set_attribute(client, "CT_Assurance", safe_float(client_data["assurance_credit"])):
champs_modifies.append("assurance_credit")
if "langue" in client_data:
if try_set_attribute(client, "CT_Langue", safe_int(client_data["langue"])):
champs_modifies.append("langue")
if "commercial_code" in client_data:
co_no = safe_int(client_data["commercial_code"])
if not try_set_attribute(client, "CO_No", co_no):
try:
factory_collab = self.cial.CptaApplication.FactoryCollaborateur
persist_collab = factory_collab.ReadIntitule(str(co_no))
if persist_collab:
collab_obj = win32com.client.CastTo(persist_collab, "IBOCollaborateur3")
collab_obj.Read()
client.Collaborateur = collab_obj
champs_modifies.append("commercial_code")
logger.info(f" Collaborateur = {co_no} [OK]")
except Exception as e:
logger.warning(f" Collaborateur erreur: {e}")
facturation_keys = [
"lettrage_auto", "est_actif", "type_facture", "est_prospect",
"bl_en_facture", "saut_page", "validation_echeance", "controle_encours",
"exclure_relance", "exclure_penalites", "bon_a_payer"
]
if any(k in client_data for k in facturation_keys):
logger.info("[ETAPE 10] FACTURATION")
if "lettrage_auto" in client_data:
if try_set_attribute(client, "CT_Lettrage", 1 if client_data["lettrage_auto"] else 0):
champs_modifies.append("lettrage_auto")
if "est_actif" in client_data:
if try_set_attribute(client, "CT_Sommeil", 0 if client_data["est_actif"] else 1):
champs_modifies.append("est_actif")
if "type_facture" in client_data:
if try_set_attribute(client, "CT_Facture", safe_int(client_data["type_facture"])):
champs_modifies.append("type_facture")
if "est_prospect" in client_data:
if try_set_attribute(client, "CT_Prospect", 1 if client_data["est_prospect"] else 0):
champs_modifies.append("est_prospect")
factu_map = {
"CT_BLFact": "bl_en_facture",
"CT_Saut": "saut_page",
"CT_ValidEch": "validation_echeance",
"CT_ControlEnc": "controle_encours",
"CT_NotRappel": "exclure_relance",
"CT_NotPenal": "exclure_penalites",
"CT_BonAPayer": "bon_a_payer",
}
for attr, key in factu_map.items():
if key in client_data:
if try_set_attribute(client, attr, safe_int(client_data[key])):
champs_modifies.append(key)
logistique_keys = ["priorite_livraison", "livraison_partielle", "delai_transport", "delai_appro"]
if any(k in client_data for k in logistique_keys):
logger.info("[ETAPE 11] LOGISTIQUE")
logistique_map = {
"CT_PrioriteLivr": "priorite_livraison",
"CT_LivrPartielle": "livraison_partielle",
"CT_DelaiTransport": "delai_transport",
"CT_DelaiAppro": "delai_appro",
}
for attr, key in logistique_map.items():
if key in client_data:
if try_set_attribute(client, attr, safe_int(client_data[key])):
champs_modifies.append(key)
if "commentaire" in client_data:
logger.info("[ETAPE 12] COMMENTAIRE")
if try_set_attribute(client, "CT_Commentaire", clean_str(client_data["commentaire"], 35)):
champs_modifies.append("commentaire")
if "section_analytique" in client_data:
logger.info("[ETAPE 13] ANALYTIQUE")
if try_set_attribute(client, "CA_Num", clean_str(client_data["section_analytique"], 13)):
champs_modifies.append("section_analytique")
organisation_keys = [
"mode_reglement_code", "surveillance_active", "coface",
"forme_juridique", "effectif", "sv_regularite", "sv_cotation",
"sv_objet_maj", "ca_annuel", "sv_chiffre_affaires", "sv_resultat"
]
if any(k in client_data for k in organisation_keys):
logger.info("[ETAPE 14] ORGANISATION & SURVEILLANCE")
if "mode_reglement_code" in client_data:
mr_no = safe_int(client_data["mode_reglement_code"])
if not try_set_attribute(client, "MR_No", mr_no):
try:
factory_mr = self.cial.CptaApplication.FactoryModeRegl
persist_mr = factory_mr.ReadIntitule(str(mr_no))
if persist_mr:
mr_obj = win32com.client.CastTo(persist_mr, "IBOModeRegl3")
mr_obj.Read()
client.ModeRegl = mr_obj
champs_modifies.append("mode_reglement_code")
logger.info(f" ModeRegl = {mr_no} [OK]")
except Exception as e:
logger.warning(f" ModeRegl erreur: {e}")
if "surveillance_active" in client_data:
surveillance = 1 if client_data["surveillance_active"] else 0
try:
client.CT_Surveillance = surveillance
champs_modifies.append("surveillance_active")
logger.info(f" CT_Surveillance = {surveillance} [OK]")
except Exception as e:
logger.warning(f" CT_Surveillance [ECHEC: {e}]")
if "coface" in client_data:
coface = clean_str(client_data["coface"], 25)
try:
client.CT_Coface = coface
champs_modifies.append("coface")
logger.info(f" CT_Coface = {coface} [OK]")
except Exception as e:
logger.warning(f" CT_Coface [ECHEC: {e}]")
if "forme_juridique" in client_data:
if try_set_attribute(client, "CT_SvFormeJuri", clean_str(client_data["forme_juridique"], 33)):
champs_modifies.append("forme_juridique")
if "effectif" in client_data:
if try_set_attribute(client, "CT_SvEffectif", clean_str(client_data["effectif"], 11)):
champs_modifies.append("effectif")
if "sv_regularite" in client_data:
if try_set_attribute(client, "CT_SvRegul", clean_str(client_data["sv_regularite"], 3)):
champs_modifies.append("sv_regularite")
if "sv_cotation" in client_data:
if try_set_attribute(client, "CT_SvCotation", clean_str(client_data["sv_cotation"], 5)):
champs_modifies.append("sv_cotation")
if "sv_objet_maj" in client_data:
if try_set_attribute(client, "CT_SvObjetMaj", clean_str(client_data["sv_objet_maj"], 61)):
champs_modifies.append("sv_objet_maj")
ca = client_data.get("ca_annuel") or client_data.get("sv_chiffre_affaires")
if ca:
if try_set_attribute(client, "CT_SvCA", safe_float(ca)):
champs_modifies.append("ca_annuel/sv_chiffre_affaires")
if "sv_resultat" in client_data:
if try_set_attribute(client, "CT_SvResultat", safe_float(client_data["sv_resultat"])):
champs_modifies.append("sv_resultat")
if not champs_modifies:
logger.warning("Aucun champ à modifier")
return _extraire_client(client)
logger.info("=" * 80)
logger.info(f"[WRITE] {len(champs_modifies)} champs modifiés:")
for i, champ in enumerate(champs_modifies, 1):
logger.info(f" {i}. {champ}")
logger.info("=" * 80)
try:
client.Write()
client.Read()
logger.info("[OK] Write réussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
except:
pass
logger.error(f"[ERREUR] {error_detail}")
raise RuntimeError(f"Echec Write(): {error_detail}")
logger.info("=" * 80)
logger.info(f"[SUCCES] CLIENT MODIFIÉ: {code} ({len(champs_modifies)} champs)")
logger.info("=" * 80)
return _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:
pass
try:
process = self.cial.CreateProcess_Document(
settings.SAGE_TYPE_BON_COMMANDE
)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
logger.info(" Document commande créé")
doc.DO_Date = pywintypes.Time(
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(f"Impossible de charger le client")
doc.SetDefaultClient(client_obj)
doc.Write()
logger.info(f" Client {commande_data['client']['code']} associé")
if commande_data.get("reference"):
try:
doc.DO_Ref = commande_data["reference"]
logger.info(f" Référence: {commande_data['reference']}")
except:
pass
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
logger.info(f" Ajout de {len(commande_data['lignes'])} lignes...")
for idx, ligne_data in enumerate(commande_data["lignes"], 1):
logger.info(
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f" Article {ligne_data['article_code']} introuvable dans Sage"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
designation_sage = getattr(article_obj, "AR_Design", "")
logger.info(f" Prix Sage: {prix_sage}")
if prix_sage == 0:
logger.warning(
f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
f" Article associé via SetDefaultArticleReference"
)
except Exception as e:
logger.warning(
f"SetDefaultArticleReference échoué: {e}, tentative avec objet"
)
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(f" Article associé via SetDefaultArticle")
except Exception as e2:
logger.error(f" Toutes les méthodes ont échoué")
ligne_obj.DL_Design = (
designation_sage
or ligne_data.get("designation", "")
)
ligne_obj.DL_Qte = quantite
logger.warning("Configuration manuelle appliquée")
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
logger.info(f" Prix auto chargé: {prix_auto}")
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
if prix_a_utiliser is not None and prix_a_utiliser > 0:
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
logger.info(f" Prix personnalisé: {prix_a_utiliser}")
elif prix_auto == 0 and prix_sage > 0:
ligne_obj.DL_PrixUnitaire = float(prix_sage)
logger.info(f" Prix Sage forcé: {prix_sage}")
elif prix_auto > 0:
logger.info(f" Prix auto conservé: {prix_auto}")
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
montant_ligne = quantite * prix_final
logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}")
remise = ligne_data.get("remise_pourcentage", 0)
if remise > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(remise)
ligne_obj.DL_Remise01REM_Type = 0
montant_apres_remise = montant_ligne * (
1 - remise / 100
)
logger.info(
f" Remise {remise}% → {montant_apres_remise}"
)
except Exception as e:
logger.warning(f"Remise non appliquée: {e}")
ligne_obj.Write()
logger.info(f" Ligne {idx} écrite")
try:
ligne_obj.Read()
prix_enregistre = float(
getattr(ligne_obj, "DL_PrixUnitaire", 0.0)
)
montant_enregistre = float(
getattr(ligne_obj, "DL_MontantHT", 0.0)
)
logger.info(
f" Vérif: Prix={prix_enregistre}€, Montant HT={montant_enregistre}"
)
except Exception as e:
logger.warning(f"Impossible de vérifier: {e}")
doc.Write()
process.Process()
if transaction_active:
self.cial.CptaApplication.CommitTrans()
time.sleep(2)
numero_commande = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_commande = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_commande:
numero_commande = getattr(doc, "DO_Piece", "")
factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(
settings.SAGE_TYPE_BON_COMMANDE, numero_commande
)
if persist_reread:
doc_final = win32com.client.CastTo(
persist_reread, "IBODocumentVente3"
)
doc_final.Read()
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc_final, "DO_Ref", "")
date_livraison_final = None
try:
date_livr = getattr(doc_final, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except:
pass
else:
total_ht = 0.0
total_ttc = 0.0
reference_finale = commande_data.get("reference", "")
date_livraison_final = commande_data.get("date_livraison")
logger.info(
f" COMMANDE CRÉÉE: {numero_commande} - {total_ttc}€ TTC "
)
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
return {
"numero_commande": numero_commande,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(commande_data["lignes"]),
"client_code": commande_data["client"]["code"],
"date_commande": str(
normaliser_date(commande_data.get("date_commande"))
),
"date_livraison": date_livraison_final,
"reference": reference_finale,
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except:
pass
raise
except Exception as e:
logger.error(f" Erreur création commande: {e}", exc_info=True)
raise RuntimeError(f"Échec création commande: {str(e)}")
def modifier_commande(self, numero: str, commande_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f" === MODIFICATION COMMANDE {numero} ===")
logger.info(" Chargement document...")
factory = self.cial.FactoryDocumentVente
persist = None
for type_test in [10, 3, settings.SAGE_TYPE_BON_COMMANDE]:
try:
persist_test = factory.ReadPiece(type_test, numero)
if persist_test:
persist = persist_test
logger.info(f" Document trouvé (type={type_test})")
break
except:
continue
if not persist:
raise ValueError(f" Commande {numero} INTROUVABLE")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
type_reel = getattr(doc, "DO_Type", -1)
logger.info(f" Type={type_reel}, Statut={statut_actuel}")
client_code_initial = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code_initial = getattr(client_obj, "CT_Num", "").strip()
logger.info(f" Client initial: {client_code_initial}")
else:
logger.error(" Objet Client NULL à l'état initial !")
except Exception as e:
logger.error(f" Erreur lecture client initial: {e}")
if not client_code_initial:
raise ValueError(" Client introuvable dans le document")
nb_lignes_initial = 0
try:
factory_lignes = getattr(
doc, "FactoryDocumentLigne", None
) or getattr(doc, "FactoryDocumentVenteLigne", None)
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
nb_lignes_initial += 1
index += 1
except:
break
logger.info(f" Lignes initiales: {nb_lignes_initial}")
except Exception as e:
logger.warning(f" Erreur comptage lignes: {e}")
champs_modifies = []
modif_date = "date_commande" in commande_data
modif_date_livraison = "date_livraison" in commande_data
modif_statut = "statut" in commande_data
modif_ref = "reference" in commande_data
modif_lignes = (
"lignes" in commande_data and commande_data["lignes"] is not None
)
logger.info(f"Modifications demandées:")
logger.info(f" Date commande: {modif_date}")
logger.info(f" Date livraison: {modif_date_livraison}")
logger.info(f" Statut: {modif_statut}")
logger.info(f" Référence: {modif_ref}")
logger.info(f" Lignes: {modif_lignes}")
commande_data_temp = commande_data.copy()
reference_a_modifier = None
statut_a_modifier = None
if modif_lignes:
if modif_ref:
reference_a_modifier = commande_data_temp.pop("reference")
logger.info(
" Modification de la référence reportée après les lignes"
)
modif_ref = False
if modif_statut:
statut_a_modifier = commande_data_temp.pop("statut")
logger.info(
" Modification du statut reportée après les lignes"
)
modif_statut = False
logger.info(" Test Write() basique (sans modification)...")
try:
doc.Write()
logger.info(" Write() basique OK")
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
if client_apres == client_code_initial:
logger.info(f" Client préservé: {client_apres}")
else:
logger.error(
f" Client a changé: {client_code_initial}{client_apres}"
)
else:
logger.error(" Client devenu NULL après Write() basique")
except Exception as e:
logger.error(f" Write() basique ÉCHOUE: {e}")
logger.error(f" ABANDON: Le document est VERROUILLÉ")
raise ValueError(
f"Document verrouillé, impossible de modifier: {e}"
)
if not modif_lignes and (
modif_date
or modif_date_livraison
or modif_statut
or modif_ref
):
logger.info(" Modifications simples (sans lignes)...")
if modif_date:
logger.info(" Modification date commande...")
doc.DO_Date = pywintypes.Time(
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:
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:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
if nb_lignes_initial > 0:
logger.info(
f" Suppression de {nb_lignes_initial} lignes existantes..."
)
for idx in range(nb_lignes_initial, 0, -1):
try:
ligne_p = factory_lignes.List(idx)
if ligne_p:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
ligne.Read()
ligne.Remove()
logger.debug(f" Ligne {idx} supprimée")
except Exception as e:
logger.warning(
f" Impossible de supprimer ligne {idx}: {e}"
)
logger.info(" Toutes les lignes existantes supprimées")
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
logger.info(
f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f"Article {ligne_data['article_code']} introuvable"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Design = ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(
ligne_data["prix_unitaire_ht"]
)
if ligne_data.get("remise_pourcentage", 0) > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(
ligne_data["remise_pourcentage"]
)
ligne_obj.DL_Remise01REM_Type = 0
except:
pass
ligne_obj.Write()
logger.info(f" Ligne {idx} ajoutée")
logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées")
logger.info(" Write() document après remplacement lignes...")
doc.Write()
logger.info(" Document écrit")
import time
time.sleep(0.5)
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
logger.info(f" Client après remplacement: {client_apres}")
else:
logger.error(" Client NULL après remplacement")
champs_modifies.append("lignes")
if reference_a_modifier is not None:
try:
ancienne_reference = getattr(doc, "DO_Ref", "")
nouvelle_reference = (
str(reference_a_modifier) if reference_a_modifier else ""
)
doc.DO_Ref = nouvelle_reference
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("reference")
logger.info(
f" Référence modifiée: '{ancienne_reference}''{nouvelle_reference}'"
)
except Exception as e:
logger.warning(f"Impossible de modifier la référence: {e}")
if statut_a_modifier is not None:
try:
statut_actuel = getattr(doc, "DO_Statut", 0)
nouveau_statut = int(statut_a_modifier)
if nouveau_statut != statut_actuel:
doc.DO_Statut = nouveau_statut
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("statut")
logger.info(
f" Statut modifié: {statut_actuel}{nouveau_statut}"
)
except Exception as e:
logger.warning(f"Impossible de modifier le statut: {e}")
logger.info(" Relecture finale...")
import time
time.sleep(0.5)
doc.Read()
client_obj_final = getattr(doc, "Client", None)
if client_obj_final:
client_obj_final.Read()
client_final = getattr(client_obj_final, "CT_Num", "")
else:
client_final = ""
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc, "DO_Ref", "")
date_livraison_final = None
try:
date_livr = getattr(doc, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except:
pass
logger.info(f" SUCCÈS: {numero} modifiée ")
logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
logger.info(f" Client final: {client_final}")
logger.info(f" Référence: {reference_finale}")
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
logger.info(f" Champs modifiés: {champs_modifies}")
return {
"numero": numero,
"total_ht": total_ht,
"total_ttc": total_ttc,
"reference": reference_finale,
"date_livraison": date_livraison_final,
"champs_modifies": champs_modifies,
"statut": getattr(doc, "DO_Statut", 0),
"client_code": client_final,
}
except ValueError as e:
logger.error(f" ERREUR MÉTIER: {e}")
raise
except Exception as e:
logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = (
f"Erreur Sage: {err.Description} (Code: {err.Number})"
)
except:
pass
raise RuntimeError(f"Erreur Sage: {error_message}")
def creer_livraison_enrichi(self, livraison_data: dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(
f" Début création livraison pour client {livraison_data['client']['code']}"
)
try:
with self._com_context(), self._lock_com:
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug(" Transaction Sage démarrée")
except:
pass
try:
process = self.cial.CreateProcess_Document(
settings.SAGE_TYPE_BON_LIVRAISON
)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
logger.info(" Document livraison créé")
doc.DO_Date = pywintypes.Time(
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(f"Impossible de charger le client")
doc.SetDefaultClient(client_obj)
doc.Write()
logger.info(f" Client {livraison_data['client']['code']} associé")
if livraison_data.get("reference"):
try:
doc.DO_Ref = livraison_data["reference"]
logger.info(f" Référence: {livraison_data['reference']}")
except:
pass
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
logger.info(
f" Ajout de {len(livraison_data['lignes'])} lignes..."
)
for idx, ligne_data in enumerate(livraison_data["lignes"], 1):
logger.info(
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f" Article {ligne_data['article_code']} introuvable dans Sage"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
designation_sage = getattr(article_obj, "AR_Design", "")
logger.info(f" Prix Sage: {prix_sage}")
if prix_sage == 0:
logger.warning(
f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
f" Article associé via SetDefaultArticleReference"
)
except Exception as e:
logger.warning(
f"SetDefaultArticleReference échoué: {e}, tentative avec objet"
)
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(f" Article associé via SetDefaultArticle")
except Exception as e2:
logger.error(f" Toutes les méthodes ont échoué")
ligne_obj.DL_Design = (
designation_sage
or ligne_data.get("designation", "")
)
ligne_obj.DL_Qte = quantite
logger.warning("Configuration manuelle appliquée")
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
logger.info(f" Prix auto chargé: {prix_auto}")
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
if prix_a_utiliser is not None and prix_a_utiliser > 0:
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
logger.info(f" Prix personnalisé: {prix_a_utiliser}")
elif prix_auto == 0 and prix_sage > 0:
ligne_obj.DL_PrixUnitaire = float(prix_sage)
logger.info(f" Prix Sage forcé: {prix_sage}")
elif prix_auto > 0:
logger.info(f" Prix auto conservé: {prix_auto}")
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
montant_ligne = quantite * prix_final
logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}")
remise = ligne_data.get("remise_pourcentage", 0)
if remise > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(remise)
ligne_obj.DL_Remise01REM_Type = 0
montant_apres_remise = montant_ligne * (
1 - remise / 100
)
logger.info(
f" Remise {remise}% → {montant_apres_remise}"
)
except Exception as e:
logger.warning(f"Remise non appliquée: {e}")
ligne_obj.Write()
logger.info(f" Ligne {idx} écrite")
doc.Write()
process.Process()
if transaction_active:
self.cial.CptaApplication.CommitTrans()
time.sleep(2)
numero_livraison = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_livraison = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_livraison:
numero_livraison = getattr(doc, "DO_Piece", "")
factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(
settings.SAGE_TYPE_BON_LIVRAISON, numero_livraison
)
if persist_reread:
doc_final = win32com.client.CastTo(
persist_reread, "IBODocumentVente3"
)
doc_final.Read()
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc_final, "DO_Ref", "")
date_livraison_prevue_final = None
try:
date_livr = getattr(doc_final, "DO_DateLivr", None)
if date_livr:
date_livraison_prevue_final = date_livr.strftime(
"%Y-%m-%d"
)
except:
pass
else:
total_ht = 0.0
total_ttc = 0.0
reference_finale = livraison_data.get("reference", "")
date_livraison_prevue_final = livraison_data.get(
"date_livraison_prevue"
)
logger.info(
f" LIVRAISON CRÉÉE: {numero_livraison} - {total_ttc}€ TTC "
)
if date_livraison_prevue_final:
logger.info(
f" Date livraison prévue: {date_livraison_prevue_final}"
)
return {
"numero_livraison": numero_livraison,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(livraison_data["lignes"]),
"client_code": livraison_data["client"]["code"],
"date_livraison": str(
normaliser_date(livraison_data.get("date_livraison"))
),
"date_livraison_prevue": date_livraison_prevue_final,
"reference": reference_finale,
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except:
pass
raise
except Exception as e:
logger.error(f" Erreur création livraison: {e}", exc_info=True)
raise RuntimeError(f"Échec création livraison: {str(e)}")
def modifier_livraison(self, numero: str, livraison_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f" === MODIFICATION LIVRAISON {numero} ===")
logger.info(" Chargement document...")
factory = self.cial.FactoryDocumentVente
persist = None
for type_test in [30, settings.SAGE_TYPE_BON_LIVRAISON]:
try:
persist_test = factory.ReadPiece(type_test, numero)
if persist_test:
persist = persist_test
logger.info(f" Document trouvé (type={type_test})")
break
except:
continue
if not persist:
raise ValueError(f" Livraison {numero} INTROUVABLE")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
logger.info(f" Statut={statut_actuel}")
if statut_actuel == 5:
raise ValueError(f"La livraison {numero} a déjà été transformée")
if statut_actuel == 6:
raise ValueError(f"La livraison {numero} est annulée")
nb_lignes_initial = 0
try:
factory_lignes = getattr(
doc, "FactoryDocumentLigne", None
) or getattr(doc, "FactoryDocumentVenteLigne", None)
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
nb_lignes_initial += 1
index += 1
except:
break
logger.info(f" Lignes initiales: {nb_lignes_initial}")
except Exception as e:
logger.warning(f" Erreur comptage lignes: {e}")
champs_modifies = []
modif_date = "date_livraison" in livraison_data
modif_date_livraison_prevue = "date_livraison_prevue" in livraison_data
modif_statut = "statut" in livraison_data
modif_ref = "reference" in livraison_data
modif_lignes = (
"lignes" in livraison_data and livraison_data["lignes"] is not None
)
logger.info(f"Modifications demandées:")
logger.info(f" Date livraison: {modif_date}")
logger.info(f" Date livraison prévue: {modif_date_livraison_prevue}")
logger.info(f" Statut: {modif_statut}")
logger.info(f" Référence: {modif_ref}")
logger.info(f" Lignes: {modif_lignes}")
livraison_data_temp = livraison_data.copy()
reference_a_modifier = None
statut_a_modifier = None
if modif_lignes:
if modif_ref:
reference_a_modifier = livraison_data_temp.pop("reference")
logger.info(
" Modification de la référence reportée après les lignes"
)
modif_ref = False
if modif_statut:
statut_a_modifier = livraison_data_temp.pop("statut")
logger.info(
" Modification du statut reportée après les lignes"
)
modif_statut = False
if not modif_lignes and (
modif_date
or modif_date_livraison_prevue
or modif_statut
or modif_ref
):
logger.info(" Modifications simples (sans lignes)...")
if modif_date:
logger.info(" Modification date livraison...")
doc.DO_Date = pywintypes.Time(
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:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
if nb_lignes_initial > 0:
logger.info(f" Suppression de {nb_lignes_initial} lignes...")
for idx in range(nb_lignes_initial, 0, -1):
try:
ligne_p = factory_lignes.List(idx)
if ligne_p:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
ligne.Read()
ligne.Remove()
logger.debug(f" Ligne {idx} supprimée")
except Exception as e:
logger.warning(
f" Erreur suppression ligne {idx}: {e}"
)
logger.info(" Toutes les lignes supprimées")
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
logger.info(
f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f"Article {ligne_data['article_code']} introuvable"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Design = ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(
ligne_data["prix_unitaire_ht"]
)
if ligne_data.get("remise_pourcentage", 0) > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(
ligne_data["remise_pourcentage"]
)
ligne_obj.DL_Remise01REM_Type = 0
except:
pass
ligne_obj.Write()
logger.info(f" Ligne {idx} ajoutée")
logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées")
logger.info(" Write() document après remplacement lignes...")
doc.Write()
logger.info(" Document écrit")
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("lignes")
if reference_a_modifier is not None:
try:
ancienne_reference = getattr(doc, "DO_Ref", "")
nouvelle_reference = (
str(reference_a_modifier) if reference_a_modifier else ""
)
logger.info(
f" Modification référence: '{ancienne_reference}''{nouvelle_reference}'"
)
doc.DO_Ref = nouvelle_reference
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("reference")
logger.info(f" Référence modifiée avec succès")
except Exception as e:
logger.warning(f"Impossible de modifier la référence: {e}")
if statut_a_modifier is not None:
try:
statut_actuel = getattr(doc, "DO_Statut", 0)
nouveau_statut = int(statut_a_modifier)
if nouveau_statut != statut_actuel:
logger.info(
f" Modification statut: {statut_actuel}{nouveau_statut}"
)
doc.DO_Statut = nouveau_statut
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("statut")
logger.info(f" Statut modifié avec succès")
except Exception as e:
logger.warning(f"Impossible de modifier le statut: {e}")
logger.info(" Relecture finale...")
import time
time.sleep(0.5)
doc.Read()
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc, "DO_Ref", "")
statut_final = getattr(doc, "DO_Statut", 0)
date_livraison_prevue_final = None
try:
date_livr = getattr(doc, "DO_DateLivr", None)
if date_livr:
date_livraison_prevue_final = date_livr.strftime("%Y-%m-%d")
except:
pass
logger.info(f" SUCCÈS: {numero} modifiée ")
logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
logger.info(f" Référence: {reference_finale}")
logger.info(f" Statut: {statut_final}")
if date_livraison_prevue_final:
logger.info(
f" Date livraison prévue: {date_livraison_prevue_final}"
)
logger.info(f" Champs modifiés: {champs_modifies}")
return {
"numero": numero,
"total_ht": total_ht,
"total_ttc": total_ttc,
"reference": reference_finale,
"date_livraison_prevue": date_livraison_prevue_final,
"champs_modifies": champs_modifies,
"statut": statut_final,
}
except ValueError as e:
logger.error(f" ERREUR MÉTIER: {e}")
raise
except Exception as e:
logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = (
f"Erreur Sage: {err.Description} (Code: {err.Number})"
)
except:
pass
raise RuntimeError(f"Erreur Sage: {error_message}")
def creer_avoir_enrichi(self, avoir_data: dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(
f" Début création avoir pour client {avoir_data['client']['code']}"
)
try:
with self._com_context(), self._lock_com:
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug(" Transaction Sage démarrée")
except:
pass
try:
process = self.cial.CreateProcess_Document(
settings.SAGE_TYPE_BON_AVOIR
)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
logger.info(" Document avoir créé")
doc.DO_Date = pywintypes.Time(
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(f"Impossible de charger le client")
doc.SetDefaultClient(client_obj)
doc.Write()
logger.info(f" Client {avoir_data['client']['code']} associé")
if avoir_data.get("reference"):
try:
doc.DO_Ref = avoir_data["reference"]
logger.info(f" Référence: {avoir_data['reference']}")
except:
pass
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
logger.info(f" Ajout de {len(avoir_data['lignes'])} lignes...")
for idx, ligne_data in enumerate(avoir_data["lignes"], 1):
logger.info(
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f" Article {ligne_data['article_code']} introuvable dans Sage"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
designation_sage = getattr(article_obj, "AR_Design", "")
logger.info(f" Prix Sage: {prix_sage}")
if prix_sage == 0:
logger.warning(
f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
f" Article associé via SetDefaultArticleReference"
)
except Exception as e:
logger.warning(
f"SetDefaultArticleReference échoué: {e}, tentative avec objet"
)
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(f" Article associé via SetDefaultArticle")
except Exception as e2:
logger.error(f" Toutes les méthodes ont échoué")
ligne_obj.DL_Design = (
designation_sage
or ligne_data.get("designation", "")
)
ligne_obj.DL_Qte = quantite
logger.warning("Configuration manuelle appliquée")
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
logger.info(f" Prix auto chargé: {prix_auto}")
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
if prix_a_utiliser is not None and prix_a_utiliser > 0:
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
logger.info(f" Prix personnalisé: {prix_a_utiliser}")
elif prix_auto == 0 and prix_sage > 0:
ligne_obj.DL_PrixUnitaire = float(prix_sage)
logger.info(f" Prix Sage forcé: {prix_sage}")
elif prix_auto > 0:
logger.info(f" Prix auto conservé: {prix_auto}")
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
montant_ligne = quantite * prix_final
logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}")
remise = ligne_data.get("remise_pourcentage", 0)
if remise > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(remise)
ligne_obj.DL_Remise01REM_Type = 0
montant_apres_remise = montant_ligne * (
1 - remise / 100
)
logger.info(
f" Remise {remise}% → {montant_apres_remise}"
)
except Exception as e:
logger.warning(f"Remise non appliquée: {e}")
ligne_obj.Write()
logger.info(f" Ligne {idx} écrite")
doc.Write()
process.Process()
if transaction_active:
self.cial.CptaApplication.CommitTrans()
time.sleep(2)
numero_avoir = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_avoir = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_avoir:
numero_avoir = getattr(doc, "DO_Piece", "")
factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(
settings.SAGE_TYPE_BON_AVOIR, numero_avoir
)
if persist_reread:
doc_final = win32com.client.CastTo(
persist_reread, "IBODocumentVente3"
)
doc_final.Read()
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc_final, "DO_Ref", "")
date_livraison_final = None
try:
date_livr = getattr(doc_final, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except:
pass
else:
total_ht = 0.0
total_ttc = 0.0
reference_finale = avoir_data.get("reference", "")
date_livraison_final = avoir_data.get("date_livraison")
logger.info(
f" AVOIR CRÉÉ: {numero_avoir} - {total_ttc}€ TTC "
)
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
return {
"numero_avoir": numero_avoir,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(avoir_data["lignes"]),
"client_code": avoir_data["client"]["code"],
"date_avoir": str(
normaliser_date(avoir_data.get("date_avoir"))
),
"date_livraison": date_livraison_final,
"reference": reference_finale,
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except:
pass
raise
except Exception as e:
logger.error(f" Erreur création avoir: {e}", exc_info=True)
raise RuntimeError(f"Échec création avoir: {str(e)}")
def modifier_avoir(self, numero: str, avoir_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f" === MODIFICATION AVOIR {numero} ===")
logger.info(" Chargement document...")
factory = self.cial.FactoryDocumentVente
persist = None
for type_test in [50, settings.SAGE_TYPE_BON_AVOIR]:
try:
persist_test = factory.ReadPiece(type_test, numero)
if persist_test:
persist = persist_test
logger.info(f" Document trouvé (type={type_test})")
break
except:
continue
if not persist:
raise ValueError(f" Avoir {numero} INTROUVABLE")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
type_reel = getattr(doc, "DO_Type", -1)
logger.info(f" Type={type_reel}, Statut={statut_actuel}")
if statut_actuel == 5:
raise ValueError(f"L'avoir {numero} a déjà été transformé")
if statut_actuel == 6:
raise ValueError(f"L'avoir {numero} est annulé")
client_code_initial = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code_initial = getattr(client_obj, "CT_Num", "").strip()
logger.info(f" Client initial: {client_code_initial}")
else:
logger.error(" Objet Client NULL à l'état initial !")
except Exception as e:
logger.error(f" Erreur lecture client initial: {e}")
if not client_code_initial:
raise ValueError(" Client introuvable dans le document")
nb_lignes_initial = 0
try:
factory_lignes = getattr(
doc, "FactoryDocumentLigne", None
) or getattr(doc, "FactoryDocumentVenteLigne", None)
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
nb_lignes_initial += 1
index += 1
except:
break
logger.info(f" Lignes initiales: {nb_lignes_initial}")
except Exception as e:
logger.warning(f" Erreur comptage lignes: {e}")
champs_modifies = []
modif_date = "date_avoir" in avoir_data
modif_date_livraison = "date_livraison" in avoir_data
modif_statut = "statut" in avoir_data
modif_ref = "reference" in avoir_data
modif_lignes = (
"lignes" in avoir_data and avoir_data["lignes"] is not None
)
logger.info(f"Modifications demandées:")
logger.info(f" Date avoir: {modif_date}")
logger.info(f" Date livraison: {modif_date_livraison}")
logger.info(f" Statut: {modif_statut}")
logger.info(f" Référence: {modif_ref}")
logger.info(f" Lignes: {modif_lignes}")
avoir_data_temp = avoir_data.copy()
reference_a_modifier = None
statut_a_modifier = None
if modif_lignes:
if modif_ref:
reference_a_modifier = avoir_data_temp.pop("reference")
logger.info(
" Modification de la référence reportée après les lignes"
)
modif_ref = False
if modif_statut:
statut_a_modifier = avoir_data_temp.pop("statut")
logger.info(
" Modification du statut reportée après les lignes"
)
modif_statut = False
logger.info(" Test Write() basique (sans modification)...")
try:
doc.Write()
logger.info(" Write() basique OK")
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
if client_apres == client_code_initial:
logger.info(f" Client préservé: {client_apres}")
else:
logger.error(
f" Client a changé: {client_code_initial}{client_apres}"
)
else:
logger.error(" Client devenu NULL après Write() basique")
except Exception as e:
logger.error(f" Write() basique ÉCHOUE: {e}")
logger.error(f" ABANDON: Le document est VERROUILLÉ")
raise ValueError(
f"Document verrouillé, impossible de modifier: {e}"
)
if not modif_lignes and (
modif_date
or modif_date_livraison
or modif_statut
or modif_ref
):
logger.info(" Modifications simples (sans lignes)...")
if modif_date:
logger.info(" Modification date avoir...")
doc.DO_Date = pywintypes.Time(
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:
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:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
if nb_lignes_initial > 0:
logger.info(
f" Suppression de {nb_lignes_initial} lignes existantes..."
)
for idx in range(nb_lignes_initial, 0, -1):
try:
ligne_p = factory_lignes.List(idx)
if ligne_p:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
ligne.Read()
ligne.Remove()
logger.debug(f" Ligne {idx} supprimée")
except Exception as e:
logger.warning(
f" Impossible de supprimer ligne {idx}: {e}"
)
logger.info(" Toutes les lignes existantes supprimées")
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
logger.info(
f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f"Article {ligne_data['article_code']} introuvable"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Design = ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(
ligne_data["prix_unitaire_ht"]
)
if ligne_data.get("remise_pourcentage", 0) > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(
ligne_data["remise_pourcentage"]
)
ligne_obj.DL_Remise01REM_Type = 0
except:
pass
ligne_obj.Write()
logger.info(f" Ligne {idx} ajoutée")
logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées")
logger.info(" Write() document après remplacement lignes...")
doc.Write()
logger.info(" Document écrit")
import time
time.sleep(0.5)
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
logger.info(f" Client après remplacement: {client_apres}")
else:
logger.error(" Client NULL après remplacement")
champs_modifies.append("lignes")
if reference_a_modifier is not None:
try:
ancienne_reference = getattr(doc, "DO_Ref", "")
nouvelle_reference = (
str(reference_a_modifier) if reference_a_modifier else ""
)
logger.info(
f" Modification référence: '{ancienne_reference}''{nouvelle_reference}'"
)
doc.DO_Ref = nouvelle_reference
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("reference")
logger.info(f" Référence modifiée avec succès")
except Exception as e:
logger.warning(f"Impossible de modifier la référence: {e}")
if statut_a_modifier is not None:
try:
statut_actuel = getattr(doc, "DO_Statut", 0)
nouveau_statut = int(statut_a_modifier)
if nouveau_statut != statut_actuel:
logger.info(
f" Modification statut: {statut_actuel}{nouveau_statut}"
)
doc.DO_Statut = nouveau_statut
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("statut")
logger.info(f" Statut modifié avec succès")
except Exception as e:
logger.warning(f"Impossible de modifier le statut: {e}")
logger.info(" Relecture finale...")
import time
time.sleep(0.5)
doc.Read()
client_obj_final = getattr(doc, "Client", None)
if client_obj_final:
client_obj_final.Read()
client_final = getattr(client_obj_final, "CT_Num", "")
else:
client_final = ""
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc, "DO_Ref", "")
statut_final = getattr(doc, "DO_Statut", 0)
date_livraison_final = None
try:
date_livr = getattr(doc, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except:
pass
logger.info(f" SUCCÈS: {numero} modifié ")
logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
logger.info(f" Client final: {client_final}")
logger.info(f" Référence: {reference_finale}")
logger.info(f" Statut: {statut_final}")
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
logger.info(f" Champs modifiés: {champs_modifies}")
return {
"numero": numero,
"total_ht": total_ht,
"total_ttc": total_ttc,
"reference": reference_finale,
"date_livraison": date_livraison_final,
"champs_modifies": champs_modifies,
"statut": statut_final,
"client_code": client_final,
}
except ValueError as e:
logger.error(f" ERREUR MÉTIER: {e}")
raise
except Exception as e:
logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = (
f"Erreur Sage: {err.Description} (Code: {err.Number})"
)
except:
pass
raise RuntimeError(f"Erreur Sage: {error_message}")
def creer_facture_enrichi(self, facture_data: dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(
f" Début création facture pour client {facture_data['client']['code']}"
)
try:
with self._com_context(), self._lock_com:
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug(" Transaction Sage démarrée")
except:
pass
try:
process = self.cial.CreateProcess_Document(
settings.SAGE_TYPE_FACTURE
)
doc = process.Document
try:
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
except:
pass
logger.info(" Document facture créé")
doc.DO_Date = pywintypes.Time(
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(f"Impossible de charger le client")
doc.SetDefaultClient(client_obj)
doc.Write()
logger.info(f" Client {facture_data['client']['code']} associé")
if facture_data.get("reference"):
try:
doc.DO_Ref = facture_data["reference"]
logger.info(f" Référence: {facture_data['reference']}")
except:
pass
logger.info(" Configuration champs spécifiques factures...")
try:
if hasattr(doc, "DO_CodeJournal"):
try:
param_societe = (
self.cial.CptaApplication.ParametreSociete
)
journal_defaut = getattr(
param_societe, "P_CodeJournalVte", "VTE"
)
doc.DO_CodeJournal = journal_defaut
logger.info(f" Code journal: {journal_defaut}")
except:
doc.DO_CodeJournal = "VTE"
logger.info(" Code journal: VTE (défaut)")
except Exception as e:
logger.debug(f" Code journal: {e}")
try:
if hasattr(doc, "DO_Souche"):
doc.DO_Souche = 0
logger.debug(" Souche: 0 (défaut)")
except:
pass
try:
if hasattr(doc, "DO_Regime"):
doc.DO_Regime = 0
logger.debug(" Régime: 0 (défaut)")
except:
pass
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
logger.info(f" Ajout de {len(facture_data['lignes'])} lignes...")
for idx, ligne_data in enumerate(facture_data["lignes"], 1):
logger.info(
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f" Article {ligne_data['article_code']} introuvable dans Sage"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
designation_sage = getattr(article_obj, "AR_Design", "")
logger.info(f" Prix Sage: {prix_sage}")
if prix_sage == 0:
logger.warning(
f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
logger.info(
f" Article associé via SetDefaultArticleReference"
)
except Exception as e:
logger.warning(
f"SetDefaultArticleReference échoué: {e}, tentative avec objet"
)
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
logger.info(f" Article associé via SetDefaultArticle")
except Exception as e2:
logger.error(f" Toutes les méthodes ont échoué")
ligne_obj.DL_Design = (
designation_sage
or ligne_data.get("designation", "")
)
ligne_obj.DL_Qte = quantite
logger.warning("Configuration manuelle appliquée")
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
logger.info(f" Prix auto chargé: {prix_auto}")
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
if prix_a_utiliser is not None and prix_a_utiliser > 0:
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
logger.info(f" Prix personnalisé: {prix_a_utiliser}")
elif prix_auto == 0 and prix_sage > 0:
ligne_obj.DL_PrixUnitaire = float(prix_sage)
logger.info(f" Prix Sage forcé: {prix_sage}")
elif prix_auto > 0:
logger.info(f" Prix auto conservé: {prix_auto}")
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
montant_ligne = quantite * prix_final
logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}")
remise = ligne_data.get("remise_pourcentage", 0)
if remise > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(remise)
ligne_obj.DL_Remise01REM_Type = 0
montant_apres_remise = montant_ligne * (
1 - remise / 100
)
logger.info(
f" Remise {remise}% → {montant_apres_remise}"
)
except Exception as e:
logger.warning(f"Remise non appliquée: {e}")
ligne_obj.Write()
logger.info(f" Ligne {idx} écrite")
logger.info(" Validation facture...")
try:
doc.SetClient(client_obj)
logger.debug(" Client réassocié avant validation")
except:
try:
doc.SetDefaultClient(client_obj)
except:
pass
doc.Write()
logger.info(" Process()...")
process.Process()
if transaction_active:
self.cial.CptaApplication.CommitTrans()
logger.info(" Transaction committée")
time.sleep(2)
numero_facture = None
try:
doc_result = process.DocumentResult
if doc_result:
doc_result = win32com.client.CastTo(
doc_result, "IBODocumentVente3"
)
doc_result.Read()
numero_facture = getattr(doc_result, "DO_Piece", "")
except:
pass
if not numero_facture:
numero_facture = getattr(doc, "DO_Piece", "")
if not numero_facture:
raise RuntimeError("Numéro facture vide après création")
logger.info(f" Numéro facture: {numero_facture}")
factory_doc = self.cial.FactoryDocumentVente
persist_reread = factory_doc.ReadPiece(
settings.SAGE_TYPE_FACTURE, numero_facture
)
if persist_reread:
doc_final = win32com.client.CastTo(
persist_reread, "IBODocumentVente3"
)
doc_final.Read()
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc_final, "DO_Ref", "")
date_livraison_final = None
try:
date_livr = getattr(doc_final, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except:
pass
else:
total_ht = 0.0
total_ttc = 0.0
reference_finale = facture_data.get("reference", "")
date_livraison_final = facture_data.get("date_livraison")
logger.info(
f" FACTURE CRÉÉE: {numero_facture} - {total_ttc}€ TTC "
)
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
return {
"numero_facture": numero_facture,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(facture_data["lignes"]),
"client_code": facture_data["client"]["code"],
"date_facture": str(
normaliser_date(facture_data.get("date_facture"))
),
"date_livraison": date_livraison_final,
"reference": reference_finale,
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.error(" Transaction annulée (rollback)")
except:
pass
raise
except Exception as e:
logger.error(f" Erreur création facture: {e}", exc_info=True)
raise RuntimeError(f"Échec création facture: {str(e)}")
def modifier_facture(self, numero: str, facture_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f" === MODIFICATION FACTURE {numero} ===")
logger.info(" Chargement document...")
factory = self.cial.FactoryDocumentVente
persist = None
for type_test in [60, 5, settings.SAGE_TYPE_FACTURE]:
try:
persist_test = factory.ReadPiece(type_test, numero)
if persist_test:
persist = persist_test
logger.info(f" Document trouvé (type={type_test})")
break
except:
continue
if not persist:
raise ValueError(f" Facture {numero} INTROUVABLE")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
type_reel = getattr(doc, "DO_Type", -1)
logger.info(f" Type={type_reel}, Statut={statut_actuel}")
if statut_actuel == 5:
raise ValueError(f"La facture {numero} a déjà été transformée")
if statut_actuel == 6:
raise ValueError(f"La facture {numero} est annulée")
client_code_initial = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code_initial = getattr(client_obj, "CT_Num", "").strip()
logger.info(f" Client initial: {client_code_initial}")
else:
logger.error(" Objet Client NULL à l'état initial !")
except Exception as e:
logger.error(f" Erreur lecture client initial: {e}")
if not client_code_initial:
raise ValueError(" Client introuvable dans le document")
nb_lignes_initial = 0
try:
factory_lignes = getattr(
doc, "FactoryDocumentLigne", None
) or getattr(doc, "FactoryDocumentVenteLigne", None)
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
nb_lignes_initial += 1
index += 1
except:
break
logger.info(f" Lignes initiales: {nb_lignes_initial}")
except Exception as e:
logger.warning(f" Erreur comptage lignes: {e}")
champs_modifies = []
modif_date = "date_facture" in facture_data
modif_date_livraison = "date_livraison" in facture_data
modif_statut = "statut" in facture_data
modif_ref = "reference" in facture_data
modif_lignes = (
"lignes" in facture_data and facture_data["lignes"] is not None
)
logger.info(f"Modifications demandées:")
logger.info(f" Date facture: {modif_date}")
logger.info(f" Date livraison: {modif_date_livraison}")
logger.info(f" Statut: {modif_statut}")
logger.info(f" Référence: {modif_ref}")
logger.info(f" Lignes: {modif_lignes}")
facture_data_temp = facture_data.copy()
reference_a_modifier = None
statut_a_modifier = None
if modif_lignes:
if modif_ref:
reference_a_modifier = facture_data_temp.pop("reference")
logger.info(
" Modification de la référence reportée après les lignes"
)
modif_ref = False
if modif_statut:
statut_a_modifier = facture_data_temp.pop("statut")
logger.info(
" Modification du statut reportée après les lignes"
)
modif_statut = False
logger.info(" Test Write() basique (sans modification)...")
try:
doc.Write()
logger.info(" Write() basique OK")
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
if client_apres == client_code_initial:
logger.info(f" Client préservé: {client_apres}")
else:
logger.error(
f" Client a changé: {client_code_initial}{client_apres}"
)
else:
logger.error(" Client devenu NULL après Write() basique")
except Exception as e:
logger.error(f" Write() basique ÉCHOUE: {e}")
logger.error(f" ABANDON: Le document est VERROUILLÉ")
raise ValueError(
f"Document verrouillé, impossible de modifier: {e}"
)
if not modif_lignes and (
modif_date
or modif_date_livraison
or modif_statut
or modif_ref
):
logger.info(" Modifications simples (sans lignes)...")
if modif_date:
logger.info(" Modification date facture...")
doc.DO_Date = pywintypes.Time(
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:
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:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
if nb_lignes_initial > 0:
logger.info(
f" Suppression de {nb_lignes_initial} lignes existantes..."
)
for idx in range(nb_lignes_initial, 0, -1):
try:
ligne_p = factory_lignes.List(idx)
if ligne_p:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
ligne.Read()
ligne.Remove()
logger.debug(f" Ligne {idx} supprimée")
except Exception as e:
logger.warning(
f" Impossible de supprimer ligne {idx}: {e}"
)
logger.info(" Toutes les lignes existantes supprimées")
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
logger.info(
f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---"
)
persist_article = factory_article.ReadReference(
ligne_data["article_code"]
)
if not persist_article:
raise ValueError(
f"Article {ligne_data['article_code']} introuvable"
)
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentVenteLigne3"
)
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(
ligne_data["article_code"], quantite
)
except:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except:
ligne_obj.DL_Design = ligne_data.get("designation", "")
ligne_obj.DL_Qte = quantite
if ligne_data.get("prix_unitaire_ht"):
ligne_obj.DL_PrixUnitaire = float(
ligne_data["prix_unitaire_ht"]
)
if ligne_data.get("remise_pourcentage", 0) > 0:
try:
ligne_obj.DL_Remise01REM_Valeur = float(
ligne_data["remise_pourcentage"]
)
ligne_obj.DL_Remise01REM_Type = 0
except:
pass
ligne_obj.Write()
logger.info(f" Ligne {idx} ajoutée")
logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées")
logger.info(" Write() document après remplacement lignes...")
doc.Write()
logger.info(" Document écrit")
import time
time.sleep(0.5)
doc.Read()
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_apres = getattr(client_obj, "CT_Num", "")
logger.info(f" Client après remplacement: {client_apres}")
else:
logger.error(" Client NULL après remplacement")
champs_modifies.append("lignes")
if reference_a_modifier is not None:
try:
ancienne_reference = getattr(doc, "DO_Ref", "")
nouvelle_reference = (
str(reference_a_modifier) if reference_a_modifier else ""
)
logger.info(
f" Modification référence: '{ancienne_reference}''{nouvelle_reference}'"
)
doc.DO_Ref = nouvelle_reference
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("reference")
logger.info(f" Référence modifiée avec succès")
except Exception as e:
logger.warning(f"Impossible de modifier la référence: {e}")
if statut_a_modifier is not None:
try:
statut_actuel = getattr(doc, "DO_Statut", 0)
nouveau_statut = int(statut_a_modifier)
if nouveau_statut != statut_actuel:
logger.info(
f" Modification statut: {statut_actuel}{nouveau_statut}"
)
doc.DO_Statut = nouveau_statut
doc.Write()
import time
time.sleep(0.5)
doc.Read()
champs_modifies.append("statut")
logger.info(f" Statut modifié avec succès")
except Exception as e:
logger.warning(f"Impossible de modifier le statut: {e}")
logger.info(" Relecture finale...")
import time
time.sleep(0.5)
doc.Read()
client_obj_final = getattr(doc, "Client", None)
if client_obj_final:
client_obj_final.Read()
client_final = getattr(client_obj_final, "CT_Num", "")
else:
client_final = ""
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
reference_finale = getattr(doc, "DO_Ref", "")
statut_final = getattr(doc, "DO_Statut", 0)
date_livraison_final = None
try:
date_livr = getattr(doc, "DO_DateLivr", None)
if date_livr:
date_livraison_final = date_livr.strftime("%Y-%m-%d")
except:
pass
logger.info(f" SUCCÈS: {numero} modifiée ")
logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
logger.info(f" Client final: {client_final}")
logger.info(f" Référence: {reference_finale}")
logger.info(f" Statut: {statut_final}")
if date_livraison_final:
logger.info(f" Date livraison: {date_livraison_final}")
logger.info(f" Champs modifiés: {champs_modifies}")
return {
"numero": numero,
"total_ht": total_ht,
"total_ttc": total_ttc,
"reference": reference_finale,
"date_livraison": date_livraison_final,
"champs_modifies": champs_modifies,
"statut": statut_final,
"client_code": client_final,
}
except ValueError as e:
logger.error(f" ERREUR MÉTIER: {e}")
raise
except Exception as e:
logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = (
f"Erreur Sage: {err.Description} (Code: {err.Number})"
)
except:
pass
raise RuntimeError(f"Erreur Sage: {error_message}")
def creer_article(self, article_data: dict) -> dict:
with self._com_context(), self._lock_com:
try:
logger.info("[ARTICLE] === CREATION ARTICLE ===")
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug("Transaction Sage démarrée")
except Exception as e:
logger.debug(f"BeginTrans non disponible : {e}")
try:
depots_disponibles = []
depot_a_utiliser = None
depot_code_demande = article_data.get("depot_code")
try:
factory_depot = self.cial.FactoryDepot
index = 1
while index <= 100:
try:
persist = factory_depot.List(index)
if persist is None:
break
depot_obj = win32com.client.CastTo(persist, "IBODepot3")
depot_obj.Read()
code = getattr(depot_obj, "DE_Code", "").strip()
if not code:
index += 1
continue
numero = int(getattr(depot_obj, "Compteur", 0))
intitule = getattr(
depot_obj, "DE_Intitule", f"Depot {code}"
)
depot_info = {
"code": code,
"numero": numero,
"intitule": intitule,
"objet": depot_obj,
}
depots_disponibles.append(depot_info)
if depot_code_demande and code == depot_code_demande:
depot_a_utiliser = depot_info
elif not depot_code_demande and not depot_a_utiliser:
depot_a_utiliser = depot_info
index += 1
except Exception as e:
if "Acces refuse" in str(e):
break
index += 1
except Exception as e:
logger.warning(f"[DEPOT] Erreur découverte dépôts : {e}")
if not depots_disponibles:
raise ValueError(
"Aucun dépôt trouvé dans Sage. Créez d'abord un dépôt."
)
if not depot_a_utiliser:
depot_a_utiliser = depots_disponibles[0]
logger.info(
f"[DEPOT] Utilisation : '{depot_a_utiliser['code']}' ({depot_a_utiliser['intitule']})"
)
reference = article_data.get("reference", "").upper().strip()
if not reference:
raise ValueError("La référence est obligatoire")
if len(reference) > 18:
raise ValueError(
"La référence ne peut pas dépasser 18 caractères"
)
designation = article_data.get("designation", "").strip()
if not designation:
raise ValueError("La désignation est obligatoire")
if len(designation) > 69:
designation = designation[:69]
stock_reel = article_data.get("stock_reel", 0.0)
stock_mini = article_data.get("stock_mini", 0.0)
stock_maxi = article_data.get("stock_maxi", 0.0)
logger.info(f"[ARTICLE] Référence : {reference}")
logger.info(f"[ARTICLE] Désignation : {designation}")
logger.info(f"[ARTICLE] Stock réel demandé : {stock_reel}")
logger.info(f"[ARTICLE] Stock mini demandé : {stock_mini}")
logger.info(f"[ARTICLE] Stock maxi demandé : {stock_maxi}")
factory = self.cial.FactoryArticle
try:
article_existant = factory.ReadReference(reference)
if article_existant:
raise ValueError(f"L'article {reference} existe déjà")
except Exception as e:
error_msg = str(e)
if (
"Enregistrement non trouve" in error_msg
or "non trouve" in error_msg
or "-2607" in error_msg
):
logger.debug(
f"[ARTICLE] {reference} n'existe pas encore, création possible"
)
else:
logger.error(f"[ARTICLE] Erreur vérification : {e}")
raise
persist = factory.Create()
article = win32com.client.CastTo(persist, "IBOArticle3")
article.SetDefault()
article.AR_Ref = reference
article.AR_Design = designation
logger.info("[MODELE] Recherche article modèle via SQL...")
article_modele_ref = None
article_modele = None
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT TOP 1 AR_Ref
FROM F_ARTICLE
WHERE AR_Sommeil = 0
ORDER BY AR_Ref
"""
)
row = cursor.fetchone()
if row:
article_modele_ref = _safe_strip(row.AR_Ref)
logger.info(
f" [SQL] Article modèle trouvé : {article_modele_ref}"
)
except Exception as e:
logger.warning(f" [SQL] Erreur recherche article : {e}")
if article_modele_ref:
try:
persist_modele = factory.ReadReference(article_modele_ref)
if persist_modele:
article_modele = win32com.client.CastTo(
persist_modele, "IBOArticle3"
)
article_modele.Read()
logger.info(
f" [OK] Article modèle chargé : {article_modele_ref}"
)
except Exception as e:
logger.warning(f" [WARN] Erreur chargement modèle : {e}")
article_modele = None
if not article_modele:
raise ValueError(
"Aucun article modèle trouvé dans Sage.\n"
"Créez au moins un article manuellement dans Sage pour servir de modèle."
)
logger.info("[OBJETS] Copie Unite + Famille depuis modèle...")
unite_trouvee = False
try:
unite_obj = getattr(article_modele, "Unite", None)
if unite_obj:
article.Unite = unite_obj
logger.info(
f" [OK] Objet Unite copié depuis {article_modele_ref}"
)
unite_trouvee = True
except Exception as e:
logger.debug(f" Unite non copiable : {str(e)[:80]}")
if not unite_trouvee:
raise ValueError(
"Impossible de copier l'unité de vente depuis le modèle"
)
famille_trouvee = False
famille_code_personnalise = article_data.get("famille")
famille_obj = None
if famille_code_personnalise:
logger.info(
f" [FAMILLE] Code personnalisé demandé : {famille_code_personnalise}"
)
try:
famille_existe_sql = False
famille_code_exact = None
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT FA_CodeFamille, FA_Type
FROM F_FAMILLE
WHERE UPPER(FA_CodeFamille) = ?
""",
(famille_code_personnalise.upper(),),
)
row = cursor.fetchone()
if row:
famille_code_exact = _safe_strip(
row.FA_CodeFamille
)
famille_existe_sql = True
logger.info(
f" [SQL] Famille trouvée : {famille_code_exact}"
)
else:
raise ValueError(
f"Famille '{famille_code_personnalise}' introuvable"
)
except ValueError:
raise
except Exception as e_sql:
logger.warning(f" [SQL] Erreur : {e_sql}")
if famille_existe_sql and famille_code_exact:
factory_famille = self.cial.FactoryFamille
try:
index = 1
max_scan = 1000
while index <= max_scan:
try:
persist_test = factory_famille.List(index)
if persist_test is None:
break
fam_test = win32com.client.CastTo(
persist_test, "IBOFamille3"
)
fam_test.Read()
code_test = (
getattr(fam_test, "FA_CodeFamille", "")
.strip()
.upper()
)
if code_test == famille_code_exact.upper():
famille_obj = fam_test
famille_trouvee = True
logger.info(
f" [OK] Famille trouvée à l'index {index}"
)
break
index += 1
except Exception as e:
if "Accès refusé" in str(e):
break
index += 1
if famille_obj:
famille_obj.Read()
article.Famille = famille_obj
logger.info(
f" [OK] Famille '{famille_code_personnalise}' assignée"
)
else:
raise ValueError(
f"Famille '{famille_code_personnalise}' inaccessible via COM"
)
except Exception as e:
logger.warning(f" [COM] Erreur scanner : {e}")
raise
except ValueError:
raise
except Exception as e:
logger.warning(
f" [WARN] Famille personnalisée non chargée : {str(e)[:100]}"
)
if not famille_trouvee:
try:
famille_obj = getattr(article_modele, "Famille", None)
if famille_obj:
article.Famille = famille_obj
logger.info(
f" [OK] Objet Famille copié depuis {article_modele_ref}"
)
famille_trouvee = True
except Exception as e:
logger.debug(f" Famille non copiable : {str(e)[:80]}")
logger.info("[CHAMPS] Copie champs obligatoires depuis modèle...")
article.AR_Type = int(getattr(article_modele, "AR_Type", 0))
article.AR_Nature = int(getattr(article_modele, "AR_Nature", 0))
article.AR_Nomencl = int(getattr(article_modele, "AR_Nomencl", 0))
article.AR_SuiviStock = 2
logger.info(f" [OK] AR_SuiviStock=2 (FIFO/LIFO)")
prix_vente = article_data.get("prix_vente")
if prix_vente is not None:
try:
article.AR_PrixVen = float(prix_vente)
logger.info(f" Prix vente : {prix_vente} EUR")
except Exception as e:
logger.warning(f" Prix vente erreur : {str(e)[:100]}")
prix_achat = article_data.get("prix_achat")
if prix_achat is not None:
try:
try:
article.AR_PrixAch = float(prix_achat)
logger.info(
f" Prix achat (AR_PrixAch) : {prix_achat} EUR"
)
except:
article.AR_PrixAchat = float(prix_achat)
logger.info(
f" Prix achat (AR_PrixAchat) : {prix_achat} EUR"
)
except Exception as e:
logger.warning(f" Prix achat erreur : {str(e)[:100]}")
code_ean = article_data.get("code_ean")
if code_ean:
article.AR_CodeBarre = str(code_ean)
logger.info(f" Code EAN/Barre : {code_ean}")
description = article_data.get("description")
if description:
try:
article.AR_Commentaire = description
logger.info(f" Description définie")
except:
pass
logger.info("[ARTICLE] Écriture dans Sage...")
try:
article.Write()
logger.info(" [OK] Write() réussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
except:
pass
logger.error(f" [ERREUR] Write() échoué : {error_detail}")
raise RuntimeError(f"Échec création article : {error_detail}")
stock_defini = False
stock_erreur = None
has_stock_values = stock_reel or stock_mini or stock_maxi
if has_stock_values:
logger.info(
f"[STOCK] Définition stock dans F_ARTSTOCK (dépôt '{depot_a_utiliser['code']}')..."
)
try:
depot_obj = depot_a_utiliser["objet"]
factory_stock = None
for factory_name in [
"FactoryArticleStock",
"FactoryDepotStock",
]:
try:
factory_stock = getattr(
depot_obj, factory_name, None
)
if factory_stock:
logger.info(
f" Factory trouvée : {factory_name}"
)
break
except:
continue
if not factory_stock:
raise RuntimeError(
"Factory de stock introuvable sur le dépôt"
)
stock_persist = factory_stock.Create()
stock_obj = win32com.client.CastTo(
stock_persist, "IBODepotStock3"
)
stock_obj.SetDefault()
stock_obj.AR_Ref = reference
if stock_reel:
stock_obj.AS_QteSto = float(stock_reel)
logger.info(f" AS_QteSto = {stock_reel}")
if stock_mini:
try:
stock_obj.AS_QteMini = float(stock_mini)
logger.info(f" AS_QteMini = {stock_mini}")
except Exception as e:
logger.warning(f" AS_QteMini non défini : {e}")
if stock_maxi:
try:
stock_obj.AS_QteMaxi = float(stock_maxi)
logger.info(f" AS_QteMaxi = {stock_maxi}")
except Exception as e:
logger.warning(f" AS_QteMaxi non défini : {e}")
stock_obj.Write()
stock_defini = True
logger.info(
f" [OK] Stock défini dans F_ARTSTOCK : réel={stock_reel}, min={stock_mini}, max={stock_maxi}"
)
except Exception as e:
stock_erreur = str(e)
logger.error(
f" [ERREUR] Stock non défini dans F_ARTSTOCK : {e}",
exc_info=True,
)
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.info(
"[COMMIT] Transaction committée - Article persiste dans Sage"
)
except Exception as e:
logger.warning(f"[COMMIT] Erreur commit : {e}")
logger.info("[VERIF] Relecture article créé...")
article_cree_persist = factory.ReadReference(reference)
if not article_cree_persist:
raise RuntimeError(
"Article créé mais introuvable à la relecture"
)
article_cree = win32com.client.CastTo(
article_cree_persist, "IBOArticle3"
)
article_cree.Read()
stocks_par_depot = []
stock_total = 0.0
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
d.DE_Code,
s.AS_QteSto,
s.AS_QteMini,
s.AS_QteMaxi
FROM F_ARTSTOCK s
LEFT JOIN F_DEPOT d ON s.DE_No = d.DE_No
WHERE s.AR_Ref = ?
""",
(reference.upper(),),
)
depot_rows = cursor.fetchall()
for depot_row in depot_rows:
if len(depot_row) >= 4:
qte = float(depot_row[1]) if depot_row[1] else 0.0
stock_total += qte
stocks_par_depot.append(
{
"depot_code": _safe_strip(
depot_row[0]
),
"quantite": qte,
"qte_mini": (
float(depot_row[2])
if depot_row[2]
else 0.0
),
"qte_maxi": (
float(depot_row[3])
if depot_row[3]
else 0.0
),
}
)
logger.info(
f"[VERIF] Stock dans F_ARTSTOCK : {stock_total} unités dans {len(stocks_par_depot)} dépôt(s)"
)
except Exception as e:
logger.warning(
f"[VERIF] Impossible de vérifier le stock dans F_ARTSTOCK : {e}"
)
logger.info(
f"[OK] ARTICLE CREE : {reference} - Stock total : {stock_total}"
)
logger.info("[EXTRACTION] Extraction complète de l'article créé...")
resultat = _extraire_article(article_cree)
if not resultat:
resultat = {
"reference": reference,
"designation": designation,
}
resultat["stock_reel"] = stock_total
if stock_mini:
resultat["stock_mini"] = float(stock_mini)
if stock_maxi:
resultat["stock_maxi"] = float(stock_maxi)
resultat["stock_disponible"] = stock_total
resultat["stock_reserve"] = 0.0
resultat["stock_commande"] = 0.0
if prix_vente is not None:
resultat["prix_vente"] = float(prix_vente)
if prix_achat is not None:
resultat["prix_achat"] = float(prix_achat)
if description:
resultat["description"] = description
if code_ean:
resultat["code_ean"] = str(code_ean)
resultat["code_barre"] = str(code_ean)
if famille_code_personnalise and famille_trouvee:
resultat["famille_code"] = famille_code_personnalise
try:
if famille_obj:
famille_obj.Read()
resultat["famille_libelle"] = getattr(
famille_obj, "FA_Intitule", ""
)
except:
pass
if stocks_par_depot:
resultat["stocks_par_depot"] = stocks_par_depot
resultat["depot_principal"] = {
"code": depot_a_utiliser["code"],
"intitule": depot_a_utiliser["intitule"],
}
resultat["suivi_stock_active"] = stock_defini
if has_stock_values and not stock_defini and stock_erreur:
resultat["avertissement"] = (
f"Stock demandé mais non défini dans F_ARTSTOCK : {stock_erreur}"
)
logger.info(
f"[EXTRACTION] Article extrait et enrichi avec {len(resultat)} champs"
)
return resultat
except ValueError:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except:
pass
raise
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
except:
pass
logger.error(f"Erreur creation article : {e}", exc_info=True)
raise RuntimeError(f"Erreur creation article : {str(e)}")
except Exception as e:
logger.error(f"Erreur globale : {e}", exc_info=True)
raise
def modifier_article(self, reference: str, article_data: Dict) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
try:
with self._com_context(), self._lock_com:
logger.info(f"[ARTICLE] === MODIFICATION {reference} ===")
factory_article = self.cial.FactoryArticle
persist = factory_article.ReadReference(reference.upper())
if not persist:
raise ValueError(f"Article {reference} introuvable")
article = win32com.client.CastTo(persist, "IBOArticle3")
article.Read()
designation_actuelle = getattr(article, "AR_Design", "")
logger.info(f"[ARTICLE] Trouvé : {reference} - {designation_actuelle}")
logger.info("[ARTICLE] Mise à jour des champs...")
champs_modifies = []
if "famille" in article_data and article_data["famille"]:
famille_code_demande = article_data["famille"].upper().strip()
logger.info(
f"[FAMILLE] Changement demandé : {famille_code_demande}"
)
try:
famille_existe_sql = False
famille_code_exact = None
famille_type = None
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT FA_CodeFamille, FA_Type
FROM F_FAMILLE
WHERE UPPER(FA_CodeFamille) = ?
""",
(famille_code_demande,),
)
row = cursor.fetchone()
if row:
famille_code_exact = _safe_strip(
row.FA_CodeFamille
)
famille_type = row.FA_Type if len(row) > 1 else 0
famille_existe_sql = True
if famille_type == 1:
raise ValueError(
f"La famille '{famille_code_demande}' est de type 'Total' "
f"et ne peut pas contenir d'articles. "
f"Utilisez une famille de type Détail."
)
logger.info(
f" [SQL] Famille trouvée : {famille_code_exact} (type={famille_type})"
)
else:
raise ValueError(
f"Famille '{famille_code_demande}' introuvable dans Sage"
)
except ValueError:
raise
except Exception as e:
logger.warning(f" [SQL] Erreur : {e}")
raise ValueError(f"Impossible de vérifier la famille : {e}")
if famille_existe_sql and famille_code_exact:
logger.info(f" [COM] Recherche via scanner...")
factory_famille = self.cial.FactoryFamille
famille_obj = None
try:
index = 1
max_scan = 1000
while index <= max_scan:
try:
persist_test = factory_famille.List(index)
if persist_test is None:
break
fam_test = win32com.client.CastTo(
persist_test, "IBOFamille3"
)
fam_test.Read()
code_test = (
getattr(fam_test, "FA_CodeFamille", "")
.strip()
.upper()
)
if code_test == famille_code_exact.upper():
famille_obj = fam_test
logger.info(
f" [OK] Famille trouvée à l'index {index}"
)
break
index += 1
except Exception as e:
if "Accès refusé" in str(e) or "Access" in str(
e
):
break
index += 1
except Exception as e:
logger.warning(
f" [COM] Scanner échoué : {str(e)[:200]}"
)
if famille_obj:
famille_obj.Read()
article.Famille = famille_obj
champs_modifies.append(f"famille={famille_code_exact}")
logger.info(
f" [OK] Famille changée : {famille_code_exact}"
)
else:
raise ValueError(
f"Famille '{famille_code_demande}' trouvée en SQL mais inaccessible via COM. "
f"Essayez avec une autre famille."
)
except ValueError:
raise
except Exception as e:
logger.error(f" [ERREUR] Changement famille : {e}")
raise ValueError(f"Impossible de changer la famille : {str(e)}")
if "designation" in article_data:
designation = str(article_data["designation"])[:69].strip()
article.AR_Design = designation
champs_modifies.append(f"designation")
logger.info(f" [OK] Désignation : {designation}")
if "prix_vente" in article_data:
try:
prix_vente = float(article_data["prix_vente"])
article.AR_PrixVen = prix_vente
champs_modifies.append("prix_vente")
logger.info(f" [OK] Prix vente : {prix_vente} EUR")
except Exception as e:
logger.warning(f" [WARN] Prix vente : {e}")
if "prix_achat" in article_data:
try:
prix_achat = float(article_data["prix_achat"])
try:
article.AR_PrixAch = prix_achat
champs_modifies.append("prix_achat")
logger.info(
f" [OK] Prix achat (AR_PrixAch) : {prix_achat} EUR"
)
except:
article.AR_PrixAchat = prix_achat
champs_modifies.append("prix_achat")
logger.info(
f" [OK] Prix achat (AR_PrixAchat) : {prix_achat} EUR"
)
except Exception as e:
logger.warning(f" [WARN] Prix achat : {e}")
if "stock_reel" in article_data:
try:
stock_reel = float(article_data["stock_reel"])
ancien_stock = float(getattr(article, "AR_Stock", 0.0))
article.AR_Stock = stock_reel
champs_modifies.append("stock_reel")
logger.info(f" [OK] Stock : {ancien_stock} -> {stock_reel}")
if stock_reel > ancien_stock:
logger.info(
f" [+] Stock augmenté de {stock_reel - ancien_stock}"
)
except Exception as e:
logger.error(f" [ERREUR] Stock : {e}")
raise ValueError(f"Impossible de modifier le stock: {e}")
if "stock_mini" in article_data:
try:
stock_mini = float(article_data["stock_mini"])
article.AR_StockMini = stock_mini
champs_modifies.append("stock_mini")
logger.info(f" [OK] Stock mini : {stock_mini}")
except Exception as e:
logger.warning(f" [WARN] Stock mini : {e}")
if "stock_maxi" in article_data:
try:
stock_maxi = float(article_data["stock_maxi"])
article.AR_StockMaxi = stock_maxi
champs_modifies.append("stock_maxi")
logger.info(f" [OK] Stock maxi : {stock_maxi}")
except Exception as e:
logger.warning(f" [WARN] Stock maxi : {e}")
if "code_ean" in article_data:
try:
code_ean = str(article_data["code_ean"])[:13].strip()
article.AR_CodeBarre = code_ean
champs_modifies.append("code_ean")
logger.info(f" [OK] Code EAN : {code_ean}")
except Exception as e:
logger.warning(f" [WARN] Code EAN : {e}")
if "description" in article_data:
try:
description = str(article_data["description"])[:255].strip()
article.AR_Commentaire = description
champs_modifies.append("description")
logger.info(f" [OK] Description définie")
except Exception as e:
logger.warning(f" [WARN] Description : {e}")
if not champs_modifies:
logger.warning("[ARTICLE] Aucun champ à modifier")
return _extraire_article(article)
logger.info(
f"[ARTICLE] Champs à modifier : {', '.join(champs_modifies)}"
)
logger.info("[ARTICLE] Écriture des modifications...")
try:
article.Write()
logger.info("[ARTICLE] Write() réussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except:
pass
logger.error(f"[ARTICLE] Erreur Write() : {error_detail}")
raise RuntimeError(f"Échec modification : {error_detail}")
article.Read()
logger.info(
f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)"
)
resultat = _extraire_article(article)
if not resultat:
resultat = {
"reference": reference,
"designation": getattr(article, "AR_Design", ""),
}
if "prix_vente" in article_data:
resultat["prix_vente"] = float(article_data["prix_vente"])
if "prix_achat" in article_data:
resultat["prix_achat"] = float(article_data["prix_achat"])
if "stock_reel" in article_data:
resultat["stock_reel"] = float(article_data["stock_reel"])
if "stock_mini" in article_data:
resultat["stock_mini"] = float(article_data["stock_mini"])
if "stock_maxi" in article_data:
resultat["stock_maxi"] = float(article_data["stock_maxi"])
if "code_ean" in article_data:
resultat["code_ean"] = str(article_data["code_ean"])
resultat["code_barre"] = str(article_data["code_ean"])
if "description" in article_data:
resultat["description"] = str(article_data["description"])
if "famille" in article_data:
resultat["famille_code"] = (
famille_code_exact if "famille_code_exact" in locals() else ""
)
return resultat
except ValueError as e:
logger.error(f"[ARTICLE] Erreur métier : {e}")
raise
except Exception as e:
logger.error(f"[ARTICLE] Erreur technique : {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = f"Erreur Sage: {err.Description}"
except:
pass
raise RuntimeError(f"Erreur technique Sage : {error_message}")
def creer_famille(self, famille_data: dict) -> dict:
with self._com_context(), self._lock_com:
try:
logger.info("[FAMILLE] === CRÉATION FAMILLE (TYPE DÉTAIL) ===")
code = famille_data.get("code", "").upper().strip()
if not code:
raise ValueError("Le code famille est obligatoire")
if len(code) > 18:
raise ValueError(
"Le code famille ne peut pas dépasser 18 caractères"
)
intitule = famille_data.get("intitule", "").strip()
if not intitule:
raise ValueError("L'intitulé est obligatoire")
if len(intitule) > 69:
intitule = intitule[:69]
logger.info(f"[FAMILLE] Code : {code}")
logger.info(f"[FAMILLE] Intitulé : {intitule}")
type_demande = famille_data.get("type", 0)
if type_demande == 1:
logger.warning(
"[FAMILLE] Type 'Total' demandé mais IGNORÉ - Création en type Détail uniquement"
)
factory_famille = self.cial.FactoryFamille
try:
index = 1
while index <= 1000:
try:
persist_test = factory_famille.List(index)
if persist_test is None:
break
fam_test = win32com.client.CastTo(
persist_test, "IBOFamille3"
)
fam_test.Read()
code_existant = (
getattr(fam_test, "FA_CodeFamille", "").strip().upper()
)
if code_existant == code:
raise ValueError(f"La famille {code} existe déjà")
index += 1
except ValueError:
raise # Re-raise si c'est notre erreur
except:
index += 1
except ValueError:
raise
persist = factory_famille.Create()
famille = win32com.client.CastTo(persist, "IBOFamille3")
famille.SetDefault()
famille.FA_CodeFamille = code
famille.FA_Intitule = intitule
try:
famille.FA_Type = 0 # Toujours Détail
logger.info(f"[FAMILLE] Type : 0 (Détail)")
except Exception as e:
logger.warning(f"[FAMILLE] FA_Type non défini : {e}")
compte_achat = famille_data.get("compte_achat")
if compte_achat:
try:
factory_compte = self.cial.CptaApplication.FactoryCompteG
persist_compte = factory_compte.ReadNumero(compte_achat)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
famille.CompteGAchat = compte_obj
logger.info(f"[FAMILLE] Compte achat : {compte_achat}")
except Exception as e:
logger.warning(f"[FAMILLE] Compte achat non défini : {e}")
compte_vente = famille_data.get("compte_vente")
if compte_vente:
try:
factory_compte = self.cial.CptaApplication.FactoryCompteG
persist_compte = factory_compte.ReadNumero(compte_vente)
if persist_compte:
compte_obj = win32com.client.CastTo(
persist_compte, "IBOCompteG3"
)
compte_obj.Read()
famille.CompteGVente = compte_obj
logger.info(f"[FAMILLE] Compte vente : {compte_vente}")
except Exception as e:
logger.warning(f"[FAMILLE] Compte vente non défini : {e}")
logger.info("[FAMILLE] Écriture dans Sage...")
try:
famille.Write()
logger.info("[FAMILLE] Write() réussi")
except Exception as e:
error_detail = str(e)
try:
sage_error = self.cial.CptaApplication.LastError
if sage_error:
error_detail = (
f"{sage_error.Description} (Code: {sage_error.Number})"
)
except:
pass
logger.error(f"[FAMILLE] Erreur Write() : {error_detail}")
raise RuntimeError(f"Échec création famille : {error_detail}")
famille.Read()
resultat = {
"code": getattr(famille, "FA_CodeFamille", "").strip(),
"intitule": getattr(famille, "FA_Intitule", "").strip(),
"type": 0, # Toujours Détail
"type_libelle": "Détail",
}
logger.info(f"[FAMILLE] FAMILLE CRÉÉE : {code} - {intitule} (Détail)")
return resultat
except ValueError as e:
logger.error(f"[FAMILLE] Erreur métier : {e}")
raise
except Exception as e:
logger.error(f"[FAMILLE] Erreur technique : {e}", exc_info=True)
error_message = str(e)
if self.cial:
try:
err = self.cial.CptaApplication.LastError
if err:
error_message = f"Erreur Sage: {err.Description}"
except:
pass
raise RuntimeError(f"Erreur technique Sage : {error_message}")
def lister_toutes_familles(
self, filtre: str = "", inclure_totaux: bool = True
) -> List[Dict]:
"""Liste toutes les familles avec leurs comptes comptables et fournisseur principal"""
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
logger.info("[SQL] Chargement des familles avec F_FAMCOMPTA et F_FAMFOURNISS...")
query = """
SELECT
-- F_FAMILLE - Identification
f.FA_CodeFamille,
f.FA_Type,
f.FA_Intitule,
f.FA_UniteVen,
f.FA_Coef,
f.FA_SuiviStock,
f.FA_Garantie,
f.FA_Central,
-- F_FAMILLE - Statistiques
f.FA_Stat01,
f.FA_Stat02,
f.FA_Stat03,
f.FA_Stat04,
f.FA_Stat05,
-- F_FAMILLE - Fiscal et gestion
f.FA_CodeFiscal,
f.FA_Pays,
f.FA_UnitePoids,
f.FA_Escompte,
f.FA_Delai,
f.FA_HorsStat,
f.FA_VteDebit,
f.FA_NotImp,
-- F_FAMILLE - Frais annexes (3 frais avec 3 remises chacun)
f.FA_Frais01FR_Denomination,
f.FA_Frais01FR_Rem01REM_Valeur,
f.FA_Frais01FR_Rem01REM_Type,
f.FA_Frais01FR_Rem02REM_Valeur,
f.FA_Frais01FR_Rem02REM_Type,
f.FA_Frais01FR_Rem03REM_Valeur,
f.FA_Frais01FR_Rem03REM_Type,
f.FA_Frais02FR_Denomination,
f.FA_Frais02FR_Rem01REM_Valeur,
f.FA_Frais02FR_Rem01REM_Type,
f.FA_Frais02FR_Rem02REM_Valeur,
f.FA_Frais02FR_Rem02REM_Type,
f.FA_Frais02FR_Rem03REM_Valeur,
f.FA_Frais02FR_Rem03REM_Type,
f.FA_Frais03FR_Denomination,
f.FA_Frais03FR_Rem01REM_Valeur,
f.FA_Frais03FR_Rem01REM_Type,
f.FA_Frais03FR_Rem02REM_Valeur,
f.FA_Frais03FR_Rem02REM_Type,
f.FA_Frais03FR_Rem03REM_Valeur,
f.FA_Frais03FR_Rem03REM_Type,
-- F_FAMILLE - Options diverses
f.FA_Contremarque,
f.FA_FactPoids,
f.FA_FactForfait,
f.FA_Publie,
f.FA_RacineRef,
f.FA_RacineCB,
-- F_FAMILLE - Catégories
f.CL_No1,
f.CL_No2,
f.CL_No3,
f.CL_No4,
-- F_FAMILLE - Gestion avancée
f.FA_Nature,
f.FA_NbColis,
f.FA_SousTraitance,
f.FA_Fictif,
f.FA_Criticite,
-- F_FAMILLE - Métadonnées système
f.cbMarq,
f.cbCreateur,
f.cbModification,
f.cbCreation,
f.cbCreationUser,
-- F_FAMCOMPTA Vente (FCP_Type = 0)
vte.FCP_ComptaCPT_CompteG,
vte.FCP_ComptaCPT_CompteA,
vte.FCP_ComptaCPT_Taxe1,
vte.FCP_ComptaCPT_Taxe2,
vte.FCP_ComptaCPT_Taxe3,
vte.FCP_ComptaCPT_Date1,
vte.FCP_ComptaCPT_Date2,
vte.FCP_ComptaCPT_Date3,
vte.FCP_TypeFacture,
-- F_FAMCOMPTA Achat (FCP_Type = 1)
ach.FCP_ComptaCPT_CompteG,
ach.FCP_ComptaCPT_CompteA,
ach.FCP_ComptaCPT_Taxe1,
ach.FCP_ComptaCPT_Taxe2,
ach.FCP_ComptaCPT_Taxe3,
ach.FCP_ComptaCPT_Date1,
ach.FCP_ComptaCPT_Date2,
ach.FCP_ComptaCPT_Date3,
ach.FCP_TypeFacture,
-- F_FAMCOMPTA Stock (FCP_Type = 2)
sto.FCP_ComptaCPT_CompteG,
sto.FCP_ComptaCPT_CompteA,
-- F_FAMFOURNISS (fournisseur principal FF_Principal=1)
ff.CT_Num,
ff.FF_Unite,
ff.FF_Conversion,
ff.FF_DelaiAppro,
ff.FF_Garantie,
ff.FF_Colisage,
ff.FF_QteMini,
ff.FF_QteMont,
ff.EG_Champ,
ff.FF_Devise,
ff.FF_Remise,
ff.FF_ConvDiv,
ff.FF_TypeRem,
-- Nombre d'articles
ISNULL(COUNT(DISTINCT a.AR_Ref), 0) as nb_articles
FROM F_FAMILLE f
-- Jointures comptables
LEFT JOIN F_FAMCOMPTA vte ON f.FA_CodeFamille = vte.FA_CodeFamille
AND vte.FCP_Type = 0 -- Vente
AND vte.FCP_Champ = 1 -- Compte principal
LEFT JOIN F_FAMCOMPTA ach ON f.FA_CodeFamille = ach.FA_CodeFamille
AND ach.FCP_Type = 1 -- Achat
AND ach.FCP_Champ = 1
LEFT JOIN F_FAMCOMPTA sto ON f.FA_CodeFamille = sto.FA_CodeFamille
AND sto.FCP_Type = 2 -- Stock
AND sto.FCP_Champ = 1
-- Fournisseur principal
LEFT JOIN F_FAMFOURNISS ff ON f.FA_CodeFamille = ff.FA_CodeFamille
AND ff.FF_Principal = 1
-- Nombre d'articles
LEFT JOIN F_ARTICLE a ON f.FA_CodeFamille = a.FA_CodeFamille
WHERE 1=1
"""
params = []
if not inclure_totaux:
query += " AND f.FA_Type = 0"
logger.info("[SQL] Filtre : FA_Type = 0 (Détail uniquement)")
if filtre:
query += """
AND (
f.FA_CodeFamille LIKE ?
OR f.FA_Intitule LIKE ?
)
"""
params.extend([f"%{filtre}%", f"%{filtre}%"])
query += """
GROUP BY
f.FA_CodeFamille, f.FA_Type, f.FA_Intitule, f.FA_UniteVen, f.FA_Coef,
f.FA_SuiviStock, f.FA_Garantie, f.FA_Central,
f.FA_Stat01, f.FA_Stat02, f.FA_Stat03, f.FA_Stat04, f.FA_Stat05,
f.FA_CodeFiscal, f.FA_Pays, f.FA_UnitePoids, f.FA_Escompte, f.FA_Delai,
f.FA_HorsStat, f.FA_VteDebit, f.FA_NotImp,
f.FA_Frais01FR_Denomination, f.FA_Frais01FR_Rem01REM_Valeur, f.FA_Frais01FR_Rem01REM_Type,
f.FA_Frais01FR_Rem02REM_Valeur, f.FA_Frais01FR_Rem02REM_Type,
f.FA_Frais01FR_Rem03REM_Valeur, f.FA_Frais01FR_Rem03REM_Type,
f.FA_Frais02FR_Denomination, f.FA_Frais02FR_Rem01REM_Valeur, f.FA_Frais02FR_Rem01REM_Type,
f.FA_Frais02FR_Rem02REM_Valeur, f.FA_Frais02FR_Rem02REM_Type,
f.FA_Frais02FR_Rem03REM_Valeur, f.FA_Frais02FR_Rem03REM_Type,
f.FA_Frais03FR_Denomination, f.FA_Frais03FR_Rem01REM_Valeur, f.FA_Frais03FR_Rem01REM_Type,
f.FA_Frais03FR_Rem02REM_Valeur, f.FA_Frais03FR_Rem02REM_Type,
f.FA_Frais03FR_Rem03REM_Valeur, f.FA_Frais03FR_Rem03REM_Type,
f.FA_Contremarque, f.FA_FactPoids, f.FA_FactForfait, f.FA_Publie,
f.FA_RacineRef, f.FA_RacineCB,
f.CL_No1, f.CL_No2, f.CL_No3, f.CL_No4,
f.FA_Nature, f.FA_NbColis, f.FA_SousTraitance, f.FA_Fictif, f.FA_Criticite,
f.cbMarq, f.cbCreateur, f.cbModification, f.cbCreation, f.cbCreationUser,
vte.FCP_ComptaCPT_CompteG, vte.FCP_ComptaCPT_CompteA,
vte.FCP_ComptaCPT_Taxe1, vte.FCP_ComptaCPT_Taxe2, vte.FCP_ComptaCPT_Taxe3,
vte.FCP_ComptaCPT_Date1, vte.FCP_ComptaCPT_Date2, vte.FCP_ComptaCPT_Date3,
vte.FCP_TypeFacture,
ach.FCP_ComptaCPT_CompteG, ach.FCP_ComptaCPT_CompteA,
ach.FCP_ComptaCPT_Taxe1, ach.FCP_ComptaCPT_Taxe2, ach.FCP_ComptaCPT_Taxe3,
ach.FCP_ComptaCPT_Date1, ach.FCP_ComptaCPT_Date2, ach.FCP_ComptaCPT_Date3,
ach.FCP_TypeFacture,
sto.FCP_ComptaCPT_CompteG, sto.FCP_ComptaCPT_CompteA,
ff.CT_Num, ff.FF_Unite, ff.FF_Conversion, ff.FF_DelaiAppro,
ff.FF_Garantie, ff.FF_Colisage, ff.FF_QteMini, ff.FF_QteMont,
ff.EG_Champ, ff.FF_Devise, ff.FF_Remise, ff.FF_ConvDiv, ff.FF_TypeRem
ORDER BY f.FA_Intitule
"""
cursor.execute(query, params)
rows = cursor.fetchall()
def to_str(val):
"""Convertit en string, gère None et int"""
if val is None:
return ""
return str(val).strip() if isinstance(val, str) else str(val)
def to_float(val):
"""Convertit en float, gère None"""
if val is None or val == "":
return 0.0
try:
return float(val)
except (ValueError, TypeError):
return 0.0
def to_int(val):
"""Convertit en int, gère None"""
if val is None or val == "":
return 0
try:
return int(val)
except (ValueError, TypeError):
return 0
def to_bool(val):
"""Convertit en bool"""
if val is None:
return False
if isinstance(val, bool):
return val
if isinstance(val, int):
return val != 0
return bool(val)
familles = []
for row in rows:
idx = 0
famille = {
"code": to_str(row[idx]),
"type": to_int(row[idx+1]),
"intitule": to_str(row[idx+2]),
"unite_vente": to_str(row[idx+3]),
"coef": to_float(row[idx+4]),
"suivi_stock": to_bool(row[idx+5]),
"garantie": to_int(row[idx+6]),
"est_centrale": to_bool(row[idx+7]),
}
idx += 8
famille.update({
"stat_01": to_str(row[idx]),
"stat_02": to_str(row[idx+1]),
"stat_03": to_str(row[idx+2]),
"stat_04": to_str(row[idx+3]),
"stat_05": to_str(row[idx+4]),
})
idx += 5
famille.update({
"code_fiscal": to_str(row[idx]),
"pays": to_str(row[idx+1]),
"unite_poids": to_str(row[idx+2]),
"escompte": to_bool(row[idx+3]),
"delai": to_int(row[idx+4]),
"hors_statistique": to_bool(row[idx+5]),
"vente_debit": to_bool(row[idx+6]),
"non_imprimable": to_bool(row[idx+7]),
})
idx += 8
famille.update({
"frais_01_libelle": to_str(row[idx]),
"frais_01_remise_1_valeur": to_float(row[idx+1]),
"frais_01_remise_1_type": to_int(row[idx+2]),
"frais_01_remise_2_valeur": to_float(row[idx+3]),
"frais_01_remise_2_type": to_int(row[idx+4]),
"frais_01_remise_3_valeur": to_float(row[idx+5]),
"frais_01_remise_3_type": to_int(row[idx+6]),
"frais_02_libelle": to_str(row[idx+7]),
"frais_02_remise_1_valeur": to_float(row[idx+8]),
"frais_02_remise_1_type": to_int(row[idx+9]),
"frais_02_remise_2_valeur": to_float(row[idx+10]),
"frais_02_remise_2_type": to_int(row[idx+11]),
"frais_02_remise_3_valeur": to_float(row[idx+12]),
"frais_02_remise_3_type": to_int(row[idx+13]),
"frais_03_libelle": to_str(row[idx+14]),
"frais_03_remise_1_valeur": to_float(row[idx+15]),
"frais_03_remise_1_type": to_int(row[idx+16]),
"frais_03_remise_2_valeur": to_float(row[idx+17]),
"frais_03_remise_2_type": to_int(row[idx+18]),
"frais_03_remise_3_valeur": to_float(row[idx+19]),
"frais_03_remise_3_type": to_int(row[idx+20]),
})
idx += 21
famille.update({
"contremarque": to_bool(row[idx]),
"fact_poids": to_bool(row[idx+1]),
"fact_forfait": to_bool(row[idx+2]),
"publie": to_bool(row[idx+3]),
"racine_reference": to_str(row[idx+4]),
"racine_code_barre": to_str(row[idx+5]),
})
idx += 6
famille.update({
"categorie_1": to_int(row[idx]),
"categorie_2": to_int(row[idx+1]),
"categorie_3": to_int(row[idx+2]),
"categorie_4": to_int(row[idx+3]),
})
idx += 4
famille.update({
"nature": to_int(row[idx]),
"nb_colis": to_int(row[idx+1]),
"sous_traitance": to_bool(row[idx+2]),
"fictif": to_bool(row[idx+3]),
"criticite": to_int(row[idx+4]),
})
idx += 5
famille.update({
"cb_marq": to_int(row[idx]),
"cb_createur": to_str(row[idx+1]),
"cb_modification": row[idx+2], # datetime - garder tel quel
"cb_creation": row[idx+3], # datetime - garder tel quel
"cb_creation_user": to_str(row[idx+4]),
})
idx += 5
famille.update({
"compte_vente": to_str(row[idx]),
"compte_auxiliaire_vente": to_str(row[idx+1]),
"tva_vente_1": to_str(row[idx+2]),
"tva_vente_2": to_str(row[idx+3]),
"tva_vente_3": to_str(row[idx+4]),
"tva_vente_date_1": row[idx+5], # datetime
"tva_vente_date_2": row[idx+6],
"tva_vente_date_3": row[idx+7],
"type_facture_vente": to_int(row[idx+8]),
})
idx += 9
famille.update({
"compte_achat": to_str(row[idx]),
"compte_auxiliaire_achat": to_str(row[idx+1]),
"tva_achat_1": to_str(row[idx+2]),
"tva_achat_2": to_str(row[idx+3]),
"tva_achat_3": to_str(row[idx+4]),
"tva_achat_date_1": row[idx+5],
"tva_achat_date_2": row[idx+6],
"tva_achat_date_3": row[idx+7],
"type_facture_achat": to_int(row[idx+8]),
})
idx += 9
famille.update({
"compte_stock": to_str(row[idx]),
"compte_auxiliaire_stock": to_str(row[idx+1]),
})
idx += 2
famille.update({
"fournisseur_principal": to_str(row[idx]),
"fournisseur_unite": to_str(row[idx+1]),
"fournisseur_conversion": to_float(row[idx+2]),
"fournisseur_delai_appro": to_int(row[idx+3]),
"fournisseur_garantie": to_int(row[idx+4]),
"fournisseur_colisage": to_int(row[idx+5]),
"fournisseur_qte_mini": to_float(row[idx+6]),
"fournisseur_qte_mont": to_float(row[idx+7]),
"fournisseur_enumere_gamme": to_int(row[idx+8]),
"fournisseur_devise": to_int(row[idx+9]),
"fournisseur_remise": to_float(row[idx+10]),
"fournisseur_conv_div": to_float(row[idx+11]),
"fournisseur_type_remise": to_int(row[idx+12]),
})
idx += 13
famille["nb_articles"] = to_int(row[idx])
famille["type_libelle"] = "Total" if famille["type"] == 1 else "Détail"
famille["est_total"] = famille["type"] == 1
famille["est_detail"] = famille["type"] == 0
famille["FA_CodeFamille"] = famille["code"]
famille["FA_Intitule"] = famille["intitule"]
famille["FA_Type"] = famille["type"]
famille["CG_NumVte"] = famille["compte_vente"]
famille["CG_NumAch"] = famille["compte_achat"]
familles.append(famille)
type_msg = "DÉTAIL uniquement" if not inclure_totaux else "TOUS types"
logger.info(f" {len(familles)} familles chargées ({type_msg})")
return familles
except Exception as e:
logger.error(f"Erreur SQL familles: {e}", exc_info=True)
raise RuntimeError(f"Erreur lecture familles: {str(e)}")
def lire_famille(self, code: str) -> Dict:
try:
with self._get_sql_connection() as conn:
cursor = conn.cursor()
logger.info(f"[SQL] Lecture famille : {code}")
cursor.execute("SELECT TOP 1 * FROM F_FAMILLE")
colonnes_disponibles = [column[0] for column in cursor.description]
logger.info(f"[SQL] Colonnes trouvées : {len(colonnes_disponibles)}")
colonnes_souhaitees = [
"FA_CodeFamille",
"FA_Intitule",
"FA_Type",
"FA_UniteVen",
"FA_Coef",
"FA_SuiviStock",
"FA_Garantie",
"FA_UnitePoids",
"FA_Delai",
"FA_NbColis",
"CG_NumAch",
"CG_NumVte",
"FA_CodeFiscal",
"FA_Escompte",
"FA_Central",
"FA_Nature",
"CL_No1",
"CL_No2",
"CL_No3",
"CL_No4",
"FA_Stat01",
"FA_Stat02",
"FA_Stat03",
"FA_Stat04",
"FA_Stat05",
"FA_HorsStat",
"FA_Pays",
"FA_VteDebit",
"FA_NotImp",
"FA_Contremarque",
"FA_FactPoids",
"FA_FactForfait",
"FA_Publie",
"FA_RacineRef",
"FA_RacineCB",
"FA_Raccourci",
"FA_SousTraitance",
"FA_Fictif",
"FA_Criticite"
]
colonnes_a_lire = [
col for col in colonnes_souhaitees if col in colonnes_disponibles
]
if not colonnes_a_lire:
colonnes_a_lire = colonnes_disponibles
logger.info(f"[SQL] Colonnes sélectionnées : {len(colonnes_a_lire)}")
colonnes_str = ", ".join([f"f.{col}" for col in colonnes_a_lire])
query = f"""
SELECT {colonnes_str},
ISNULL(COUNT(a.AR_Ref), 0) as nb_articles
FROM F_FAMILLE f
LEFT JOIN F_ARTICLE a ON f.FA_CodeFamille = a.FA_CodeFamille
WHERE UPPER(f.FA_CodeFamille) = ?
GROUP BY {colonnes_str}
"""
cursor.execute(query, (code.upper().strip(),))
row = cursor.fetchone()
if not row:
raise ValueError(f"Famille '{code}' introuvable dans Sage")
famille = {}
for idx, colonne in enumerate(colonnes_a_lire):
valeur = row[idx]
if isinstance(valeur, str):
valeur = valeur.strip()
famille[colonne] = valeur
famille["nb_articles"] = row[-1]
if "FA_CodeFamille" in famille:
famille["code"] = famille["FA_CodeFamille"]
if "FA_Intitule" in famille:
famille["intitule"] = famille["FA_Intitule"]
if "FA_Type" in famille:
type_val = famille["FA_Type"]
famille["type"] = type_val
famille["type_libelle"] = "Total" if type_val == 1 else "Détail"
famille["est_total"] = type_val == 1
else:
famille["type"] = 0
famille["type_libelle"] = "Détail"
famille["est_total"] = False
famille["unite_vente"] = str(famille.get("FA_UniteVen", ""))
famille["unite_poids"] = str(famille.get("FA_UnitePoids", ""))
famille["coef"] = (
float(famille.get("FA_Coef", 0.0))
if famille.get("FA_Coef") is not None
else 0.0
)
famille["suivi_stock"] = bool(famille.get("FA_SuiviStock", 0))
famille["garantie"] = int(famille.get("FA_Garantie", 0))
famille["delai"] = int(famille.get("FA_Delai", 0))
famille["nb_colis"] = int(famille.get("FA_NbColis", 0))
famille["compte_achat"] = famille.get("CG_NumAch", "")
famille["compte_vente"] = famille.get("CG_NumVte", "")
famille["code_fiscal"] = famille.get("FA_CodeFiscal", "")
famille["escompte"] = bool(famille.get("FA_Escompte", 0))
famille["est_centrale"] = bool(famille.get("FA_Central", 0))
famille["nature"] = famille.get("FA_Nature", 0)
famille["pays"] = famille.get("FA_Pays", "")
famille["categorie_1"] = famille.get("CL_No1", 0)
famille["categorie_2"] = famille.get("CL_No2", 0)
famille["categorie_3"] = famille.get("CL_No3", 0)
famille["categorie_4"] = famille.get("CL_No4", 0)
famille["stat_01"] = famille.get("FA_Stat01", "")
famille["stat_02"] = famille.get("FA_Stat02", "")
famille["stat_03"] = famille.get("FA_Stat03", "")
famille["stat_04"] = famille.get("FA_Stat04", "")
famille["stat_05"] = famille.get("FA_Stat05", "")
famille["hors_statistique"] = bool(famille.get("FA_HorsStat", 0))
famille["vente_debit"] = bool(famille.get("FA_VteDebit", 0))
famille["non_imprimable"] = bool(famille.get("FA_NotImp", 0))
famille["contremarque"] = bool(famille.get("FA_Contremarque", 0))
famille["fact_poids"] = bool(famille.get("FA_FactPoids", 0))
famille["fact_forfait"] = bool(famille.get("FA_FactForfait", 0))
famille["publie"] = bool(famille.get("FA_Publie", 0))
famille["racine_reference"] = famille.get("FA_RacineRef", "")
famille["racine_code_barre"] = famille.get("FA_RacineCB", "")
famille["raccourci"] = famille.get("FA_Raccourci", "")
famille["sous_traitance"] = bool(famille.get("FA_SousTraitance", 0))
famille["fictif"] = bool(famille.get("FA_Fictif", 0))
famille["criticite"] = int(famille.get("FA_Criticite", 0))
logger.info(f"SQL: Famille '{famille['code']}' chargée ({famille['nb_articles']} articles)")
return famille
except ValueError as e:
logger.error(f"Erreur famille: {e}")
raise
except Exception as e:
logger.error(f"Erreur SQL famille: {e}", exc_info=True)
raise RuntimeError(f"Erreur lecture famille: {str(e)}")
def creer_entree_stock(self, entree_data: Dict) -> Dict:
try:
with self._com_context(), self._lock_com:
logger.info(f"[STOCK] === CRÉATION ENTRÉE STOCK (COM pur) ===")
transaction_active = False
try:
self.cial.CptaApplication.BeginTrans()
transaction_active = True
logger.debug("Transaction démarrée")
except:
pass
try:
factory_doc = self.cial.FactoryDocumentStock
persist_doc = factory_doc.CreateType(180) # 180 = Entrée
doc = win32com.client.CastTo(persist_doc, "IBODocumentStock3")
doc.SetDefault()
date_mouv = entree_data.get("date_mouvement")
if isinstance(date_mouv, date):
doc.DO_Date = pywintypes.Time(
datetime.combine(date_mouv, datetime.min.time())
)
else:
doc.DO_Date = pywintypes.Time(datetime.now())
if entree_data.get("reference"):
doc.DO_Ref = entree_data["reference"]
doc.Write()
logger.info(f"[STOCK] Document créé")
factory_article = self.cial.FactoryArticle
factory_depot = self.cial.FactoryDepot
stocks_mis_a_jour = []
depot_principal = None
try:
persist_depot = factory_depot.List(1)
if persist_depot:
depot_principal = win32com.client.CastTo(
persist_depot, "IBODepot3"
)
depot_principal.Read()
logger.info(
f"Dépôt principal: {getattr(depot_principal, 'DE_Code', '?')}"
)
except Exception as e:
logger.warning(f"Erreur chargement dépôt: {e}")
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentStockLigne
for idx, ligne_data in enumerate(entree_data["lignes"], 1):
article_ref = ligne_data["article_ref"].upper()
quantite = ligne_data["quantite"]
stock_mini = ligne_data.get("stock_mini")
stock_maxi = ligne_data.get("stock_maxi")
logger.info(f"[STOCK] Ligne {idx}: {article_ref} x {quantite}")
persist_article = factory_article.ReadReference(article_ref)
if not persist_article:
raise ValueError(f"Article {article_ref} introuvable")
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentStockLigne3"
)
ligne_obj.SetDefault()
try:
ligne_obj.SetDefaultArticleReference(
article_ref, float(quantite)
)
except:
try:
ligne_obj.SetDefaultArticle(
article_obj, float(quantite)
)
except:
raise ValueError(
f"Impossible de lier l'article {article_ref}"
)
prix = ligne_data.get("prix_unitaire")
if prix:
try:
ligne_obj.DL_PrixUnitaire = float(prix)
except:
pass
ligne_obj.Write()
if stock_mini is not None or stock_maxi is not None:
logger.info(
f"[STOCK] Ajustement stock pour {article_ref}..."
)
try:
logger.info(
f" [COM] Méthode A : Article.FactoryArticleStock"
)
factory_article = self.cial.FactoryArticle
persist_article_full = factory_article.ReadReference(
article_ref
)
article_full = win32com.client.CastTo(
persist_article_full, "IBOArticle3"
)
article_full.Read()
factory_article_stock = None
try:
factory_article_stock = (
article_full.FactoryArticleStock
)
logger.info(" FactoryArticleStock trouvée")
except AttributeError:
logger.warning(
" FactoryArticleStock non disponible"
)
if factory_article_stock:
stock_trouve = None
index_stock = 1
while index_stock <= 100:
try:
stock_persist = factory_article_stock.List(
index_stock
)
if stock_persist is None:
break
stock_obj = win32com.client.CastTo(
stock_persist, "IBOArticleStock3"
)
stock_obj.Read()
depot_stock = None
try:
depot_stock = getattr(
stock_obj, "Depot", None
)
if depot_stock:
depot_stock.Read()
depot_code = getattr(
depot_stock, "DE_Code", ""
).strip()
logger.debug(
f" Dépôt {index_stock}: {depot_code}"
)
if (
not stock_trouve
or depot_code
== getattr(
depot_principal,
"DE_Code",
"",
)
):
stock_trouve = stock_obj
logger.info(
f" Stock trouvé pour dépôt {depot_code}"
)
except:
pass
index_stock += 1
except Exception as e:
logger.debug(
f" Erreur stock {index_stock}: {e}"
)
index_stock += 1
if not stock_trouve:
try:
stock_persist = (
factory_article_stock.Create()
)
stock_trouve = win32com.client.CastTo(
stock_persist, "IBOArticleStock3"
)
stock_trouve.SetDefault()
if depot_principal:
try:
stock_trouve.Depot = depot_principal
logger.info(
" Dépôt principal lié"
)
except:
pass
logger.info(" Nouvel ArticleStock créé")
except Exception as e:
logger.error(
f" Impossible de créer ArticleStock: {e}"
)
raise
if stock_trouve:
try:
stock_trouve.Read()
except:
pass
if stock_mini is not None:
try:
for prop_name in [
"AS_QteMini",
"AS_Mini",
"AR_StockMini",
"StockMini",
]:
try:
setattr(
stock_trouve,
prop_name,
float(stock_mini),
)
logger.info(
f" Stock mini défini via {prop_name}: {stock_mini}"
)
break
except AttributeError:
continue
except Exception as e:
logger.debug(
f" {prop_name} échoué: {e}"
)
continue
except Exception as e:
logger.warning(
f" Stock mini non défini: {e}"
)
if stock_maxi is not None:
try:
for prop_name in [
"AS_QteMaxi",
"AS_Maxi",
"AR_StockMaxi",
"StockMaxi",
]:
try:
setattr(
stock_trouve,
prop_name,
float(stock_maxi),
)
logger.info(
f" Stock maxi défini via {prop_name}: {stock_maxi}"
)
break
except AttributeError:
continue
except Exception as e:
logger.debug(
f" {prop_name} échoué: {e}"
)
continue
except Exception as e:
logger.warning(
f" Stock maxi non défini: {e}"
)
try:
stock_trouve.Write()
logger.info(
f" ArticleStock sauvegardé"
)
except Exception as e:
logger.error(
f" Erreur Write() ArticleStock: {e}"
)
raise
if depot_principal and (
stock_mini is not None or stock_maxi is not None
):
logger.info(
f" [COM] Méthode B : Depot.FactoryDepotStock (alternative)"
)
try:
factory_depot_stock = None
for factory_name in [
"FactoryDepotStock",
"FactoryArticleStock",
]:
try:
factory_depot_stock = getattr(
depot_principal, factory_name, None
)
if factory_depot_stock:
logger.info(
f" Factory trouvée: {factory_name}"
)
break
except:
continue
if factory_depot_stock:
stock_depot_trouve = None
index_ds = 1
while index_ds <= 100:
try:
stock_ds_persist = (
factory_depot_stock.List(
index_ds
)
)
if stock_ds_persist is None:
break
stock_ds = win32com.client.CastTo(
stock_ds_persist,
"IBODepotStock3",
)
stock_ds.Read()
ar_ref_ds = (
getattr(stock_ds, "AR_Ref", "")
.strip()
.upper()
)
if ar_ref_ds == article_ref:
stock_depot_trouve = stock_ds
break
index_ds += 1
except:
index_ds += 1
if not stock_depot_trouve:
try:
stock_ds_persist = (
factory_depot_stock.Create()
)
stock_depot_trouve = (
win32com.client.CastTo(
stock_ds_persist,
"IBODepotStock3",
)
)
stock_depot_trouve.SetDefault()
stock_depot_trouve.AR_Ref = (
article_ref
)
logger.info(
" Nouveau DepotStock créé"
)
except Exception as e:
logger.error(
f" Impossible de créer DepotStock: {e}"
)
if stock_depot_trouve:
if stock_mini is not None:
try:
stock_depot_trouve.AS_QteMini = float(
stock_mini
)
logger.info(
f" DepotStock.AS_QteMini = {stock_mini}"
)
except Exception as e:
logger.warning(
f" DepotStock mini échoué: {e}"
)
if stock_maxi is not None:
try:
stock_depot_trouve.AS_QteMaxi = float(
stock_maxi
)
logger.info(
f" DepotStock.AS_QteMaxi = {stock_maxi}"
)
except Exception as e:
logger.warning(
f" DepotStock maxi échoué: {e}"
)
try:
stock_depot_trouve.Write()
logger.info(
" DepotStock sauvegardé"
)
except Exception as e:
logger.error(
f" DepotStock Write() échoué: {e}"
)
except Exception as e:
logger.warning(f" Méthode B échouée: {e}")
except Exception as e:
logger.error(
f"[STOCK] Erreur ajustement stock: {e}",
exc_info=True,
)
stocks_mis_a_jour.append(
{
"article_ref": article_ref,
"quantite_ajoutee": quantite,
"stock_mini_defini": stock_mini,
"stock_maxi_defini": stock_maxi,
}
)
doc.Write()
doc.Read()
numero = getattr(doc, "DO_Piece", "")
logger.info(f"[STOCK] Document finalisé: {numero}")
logger.info(f"[STOCK] Vérification finale via COM...")
for stock_info in stocks_mis_a_jour:
article_ref = stock_info["article_ref"]
try:
persist_article = factory_article.ReadReference(article_ref)
article_verif = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_verif.Read()
stock_total = 0.0
stock_mini_lu = 0.0
stock_maxi_lu = 0.0
for attr in ["AR_Stock", "AS_QteSto", "Stock"]:
try:
val = getattr(article_verif, attr, None)
if val is not None:
stock_total = float(val)
break
except:
pass
for attr in ["AR_StockMini", "AS_QteMini", "StockMini"]:
try:
val = getattr(article_verif, attr, None)
if val is not None:
stock_mini_lu = float(val)
break
except:
pass
for attr in ["AR_StockMaxi", "AS_QteMaxi", "StockMaxi"]:
try:
val = getattr(article_verif, attr, None)
if val is not None:
stock_maxi_lu = float(val)
break
except:
pass
logger.info(
f"[VERIF] {article_ref}: "
f"Total={stock_total}, "
f"Mini={stock_mini_lu}, "
f"Maxi={stock_maxi_lu}"
)
stock_info["stock_total_verifie"] = stock_total
stock_info["stock_mini_verifie"] = stock_mini_lu
stock_info["stock_maxi_verifie"] = stock_maxi_lu
except Exception as e:
logger.warning(
f"[VERIF] Erreur vérification {article_ref}: {e}"
)
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.info(f"[STOCK] Transaction committée")
except:
logger.info(f"[STOCK] Changements sauvegardés")
return {
"article_ref": article_ref,
"numero": numero,
"type": 180,
"type_libelle": "Entrée en stock",
"date": str(getattr(doc, "DO_Date", "")),
"nb_lignes": len(stocks_mis_a_jour),
"stocks_mis_a_jour": stocks_mis_a_jour,
}
except Exception as e:
if transaction_active:
try:
self.cial.CptaApplication.RollbackTrans()
logger.info(f"[STOCK] Transaction annulée")
except:
pass
logger.error(f"[STOCK] ERREUR : {e}", exc_info=True)
raise ValueError(f"Erreur création entrée stock : {str(e)}")
except Exception as e:
logger.error(f"[STOCK] ERREUR GLOBALE : {e}", exc_info=True)
raise ValueError(f"Erreur création entrée stock : {str(e)}")
def lire_stock_article(self, reference: str, calcul_complet: bool = False) -> Dict:
try:
with self._com_context(), self._lock_com:
logger.info(f"[STOCK] Lecture stock : {reference}")
factory_article = self.cial.FactoryArticle
persist_article = factory_article.ReadReference(reference.upper())
if not persist_article:
raise ValueError(f"Article {reference} introuvable")
article = win32com.client.CastTo(persist_article, "IBOArticle3")
article.Read()
ar_suivi = getattr(article, "AR_SuiviStock", 0)
ar_design = getattr(article, "AR_Design", reference)
stock_info = {
"article": reference.upper(),
"designation": ar_design,
"stock_total": 0.0,
"suivi_stock": ar_suivi,
"suivi_libelle": {
0: "Aucun suivi",
1: "CMUP (sans lot)",
2: "FIFO/LIFO (avec lot)",
}.get(ar_suivi, f"Code {ar_suivi}"),
"depots": [],
"methode_lecture": None,
}
logger.info("[STOCK] Tentative Méthode 1 : Depot.FactoryDepotStock...")
try:
factory_depot = self.cial.FactoryDepot
index_depot = 1
stocks_trouves = []
while index_depot <= 20:
try:
persist_depot = factory_depot.List(index_depot)
if persist_depot is None:
break
depot = win32com.client.CastTo(persist_depot, "IBODepot3")
depot.Read()
depot_code = ""
depot_intitule = ""
try:
depot_code = getattr(depot, "DE_Code", "").strip()
depot_intitule = getattr(
depot, "DE_Intitule", f"Dépôt {depot_code}"
)
except:
pass
if not depot_code:
index_depot += 1
continue
factory_depot_stock = None
for factory_name in [
"FactoryDepotStock",
"FactoryArticleStock",
]:
try:
factory_depot_stock = getattr(
depot, factory_name, None
)
if factory_depot_stock:
break
except:
pass
if factory_depot_stock:
index_stock = 1
while index_stock <= 1000:
try:
stock_persist = factory_depot_stock.List(
index_stock
)
if stock_persist is None:
break
stock = win32com.client.CastTo(
stock_persist, "IBODepotStock3"
)
stock.Read()
article_ref_stock = ""
for attr_ref in [
"AR_Ref",
"AS_Article",
"Article_Ref",
]:
try:
val = getattr(stock, attr_ref, None)
if val:
article_ref_stock = (
str(val).strip().upper()
)
break
except:
pass
if not article_ref_stock:
try:
article_obj = getattr(
stock, "Article", None
)
if article_obj:
article_obj.Read()
article_ref_stock = (
getattr(
article_obj, "AR_Ref", ""
)
.strip()
.upper()
)
except:
pass
if article_ref_stock == reference.upper():
quantite = 0.0
qte_mini = 0.0
qte_maxi = 0.0
for attr_qte in [
"AS_QteSto",
"AS_Qte",
"QteSto",
"Quantite",
]:
try:
val = getattr(stock, attr_qte, None)
if val is not None:
quantite = float(val)
break
except:
pass
try:
qte_mini = float(
getattr(stock, "AS_QteMini", 0.0)
)
except:
pass
try:
qte_maxi = float(
getattr(stock, "AS_QteMaxi", 0.0)
)
except:
pass
stocks_trouves.append(
{
"code": depot_code,
"intitule": depot_intitule,
"quantite": quantite,
"qte_mini": qte_mini,
"qte_maxi": qte_maxi,
}
)
logger.info(
f"[STOCK] Trouvé dépôt {depot_code} : {quantite} unités"
)
break
index_stock += 1
except Exception as e:
if "Accès refusé" in str(e):
break
index_stock += 1
index_depot += 1
except Exception as e:
if "Accès refusé" in str(e):
break
index_depot += 1
if stocks_trouves:
stock_info["depots"] = stocks_trouves
stock_info["stock_total"] = sum(
d["quantite"] for d in stocks_trouves
)
stock_info["methode_lecture"] = (
"Depot.FactoryDepotStock (RAPIDE)"
)
logger.info(
f"[STOCK] Méthode 1 réussie : {stock_info['stock_total']} unités"
)
return stock_info
except Exception as e:
logger.warning(f"[STOCK] Méthode 1 échouée : {e}")
logger.info("[STOCK] Tentative Méthode 2 : Attributs Article...")
try:
stock_trouve = False
for attr_stock in ["AR_Stock", "AR_QteSto", "AR_QteStock"]:
try:
val = getattr(article, attr_stock, None)
if val is not None:
stock_info["stock_total"] = float(val)
stock_info["methode_lecture"] = (
f"Article.{attr_stock} (RAPIDE)"
)
stock_trouve = True
logger.info(
f"[STOCK] Méthode 2 réussie via {attr_stock}"
)
break
except:
pass
if stock_trouve:
return stock_info
except Exception as e:
logger.warning(f"[STOCK] Méthode 2 échouée : {e}")
if not calcul_complet:
logger.warning(
f"[STOCK] Méthodes rapides échouées pour {reference}"
)
stock_info["methode_lecture"] = "ÉCHEC - Méthodes rapides échouées"
stock_info["stock_total"] = 0.0
stock_info["note"] = (
"Les méthodes rapides de lecture de stock ont échoué. "
"Options disponibles :\n"
"1. Utiliser GET /sage/diagnostic/stock-rapide-test/{reference} pour un diagnostic détaillé\n"
"2. Appeler lire_stock_article(reference, calcul_complet=True) pour un calcul depuis mouvements (LENT - 20+ min)\n"
"3. Vérifier que l'article a bien un suivi de stock activé (AR_SuiviStock)"
)
return stock_info
logger.warning(
"[STOCK] CALCUL DEPUIS MOUVEMENTS ACTIVÉ - PEUT PRENDRE 20+ MINUTES"
)
except ValueError:
raise
except Exception as e:
logger.error(f"[STOCK] Erreur lecture : {e}", exc_info=True)
raise ValueError(f"Erreur lecture stock : {str(e)}")
def creer_sortie_stock(self, sortie_data: Dict) -> Dict:
try:
with self._com_context(), self._lock_com, self._get_sql_connection() as conn:
cursor = conn.cursor()
logger.info(f"[STOCK] === CRÉATION SORTIE STOCK ===")
logger.info(f"[STOCK] {len(sortie_data.get('lignes', []))} ligne(s)")
try:
self.cial.CptaApplication.BeginTrans()
except:
pass
try:
factory = self.cial.FactoryDocumentStock
persist = factory.CreateType(181) # 181 = Sortie
doc = win32com.client.CastTo(persist, "IBODocumentStock3")
doc.SetDefault()
date_mouv = sortie_data.get("date_mouvement")
if isinstance(date_mouv, date):
doc.DO_Date = pywintypes.Time(
datetime.combine(date_mouv, datetime.min.time())
)
else:
doc.DO_Date = pywintypes.Time(datetime.now())
if sortie_data.get("reference"):
doc.DO_Ref = sortie_data["reference"]
doc.Write()
logger.info(
f"[STOCK] Document créé : {getattr(doc, 'DO_Piece', '?')}"
)
try:
factory_lignes = doc.FactoryDocumentLigne
except:
factory_lignes = doc.FactoryDocumentStockLigne
factory_article = self.cial.FactoryArticle
stocks_mis_a_jour = []
for idx, ligne_data in enumerate(sortie_data["lignes"], 1):
article_ref = ligne_data["article_ref"].upper()
quantite = ligne_data["quantite"]
logger.info(
f"[STOCK] ======== LIGNE {idx} : {article_ref} x {quantite} ========"
)
persist_article = factory_article.ReadReference(article_ref)
if not persist_article:
raise ValueError(f"Article {article_ref} introuvable")
article_obj = win32com.client.CastTo(
persist_article, "IBOArticle3"
)
article_obj.Read()
ar_suivi = getattr(article_obj, "AR_SuiviStock", 0)
ar_design = getattr(article_obj, "AR_Design", article_ref)
logger.info(f"[STOCK] Article : {ar_design}")
logger.info(f"[STOCK] AR_SuiviStock : {ar_suivi}")
stock_dispo = 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: # CMUP
if numero_lot:
logger.warning(f"[STOCK] CMUP : Suppression du lot")
numero_lot = None
elif ar_suivi == 2: # FIFO/LIFO
if not numero_lot:
import uuid
numero_lot = f"AUTO-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6].upper()}"
logger.info(
f"[STOCK] FIFO/LIFO : Lot auto-généré '{numero_lot}'"
)
ligne_persist = factory_lignes.Create()
try:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentLigne3"
)
except:
ligne_obj = win32com.client.CastTo(
ligne_persist, "IBODocumentStockLigne3"
)
ligne_obj.SetDefault()
article_lie = False
try:
ligne_obj.SetDefaultArticleReference(
article_ref, float(quantite)
)
article_lie = True
logger.info(f"[STOCK] SetDefaultArticleReference()")
except:
try:
ligne_obj.SetDefaultArticle(
article_obj, float(quantite)
)
article_lie = True
logger.info(f"[STOCK] SetDefaultArticle()")
except:
pass
if not article_lie:
raise ValueError(
f"Impossible de lier l'article {article_ref}"
)
if numero_lot and ar_suivi == 2:
try:
ligne_obj.SetDefaultLot(numero_lot)
logger.info(f"[STOCK] Lot défini")
except:
try:
ligne_obj.LS_NoSerie = numero_lot
logger.info(f"[STOCK] Lot via LS_NoSerie")
except:
pass
prix = ligne_data.get("prix_unitaire")
if prix:
try:
ligne_obj.DL_PrixUnitaire = float(prix)
except:
pass
ligne_obj.Write()
logger.info(f"[STOCK] Write() réussi")
ligne_obj.Read()
ref_verifiee = article_ref # Supposer OK si Write() réussi
try:
article_lie_obj = getattr(ligne_obj, "Article", None)
if article_lie_obj:
article_lie_obj.Read()
ref_verifiee = (
getattr(article_lie_obj, "AR_Ref", "").strip()
or article_ref
)
except:
pass
logger.info(f"[STOCK] LIGNE {idx} COMPLÈTE")
stocks_mis_a_jour.append(
{
"article_ref": article_ref,
"quantite_retiree": quantite,
"reference_verifiee": ref_verifiee,
"stock_avant": stock_dispo["stock_disponible"],
"stock_apres": stock_dispo["stock_apres"],
"numero_lot": numero_lot if ar_suivi == 2 else None,
}
)
doc.Write()
doc.Read()
numero = getattr(doc, "DO_Piece", "")
logger.info(f"[STOCK] Document finalisé : {numero}")
try:
self.cial.CptaApplication.CommitTrans()
logger.info(f"[STOCK] Transaction committée")
except:
pass
return {
"numero": numero,
"type": 1,
"date": str(getattr(doc, "DO_Date", "")),
"nb_lignes": len(stocks_mis_a_jour),
"reference": sortie_data.get("reference"),
"stocks_mis_a_jour": stocks_mis_a_jour,
}
except Exception as e:
logger.error(f"[STOCK] ERREUR : {e}", exc_info=True)
try:
self.cial.CptaApplication.RollbackTrans()
except:
pass
raise ValueError(f"Erreur création sortie stock : {str(e)}")
except Exception as e:
logger.error(f"[STOCK] ERREUR GLOBALE : {e}", exc_info=True)
raise ValueError(f"Erreur création sortie stock : {str(e)}")
def lire_mouvement_stock(self, numero: str) -> Dict:
try:
with self._com_context(), self._lock_com:
factory = self.cial.FactoryDocumentStock
persist = None
index = 1
logger.info(f"[MOUVEMENT] Recherche de {numero}...")
while index < 10000:
try:
persist_test = factory.List(index)
if persist_test is None:
break
doc_test = win32com.client.CastTo(
persist_test, "IBODocumentStock3"
)
doc_test.Read()
if getattr(doc_test, "DO_Piece", "") == numero:
persist = persist_test
logger.info(f"[MOUVEMENT] Trouvé à l'index {index}")
break
index += 1
except:
index += 1
if not persist:
raise ValueError(f"Mouvement {numero} introuvable")
doc = win32com.client.CastTo(persist, "IBODocumentStock3")
doc.Read()
do_type = getattr(doc, "DO_Type", -1)
types_mouvements = {
180: "Entrée",
181: "Sortie",
182: "Transfert",
183: "Inventaire",
}
mouvement = {
"numero": numero,
"type": do_type,
"type_libelle": types_mouvements.get(do_type, f"Type {do_type}"),
"date": str(getattr(doc, "DO_Date", "")),
"reference": getattr(doc, "DO_Ref", ""),
"lignes": [],
}
try:
factory_lignes = getattr(
doc, "FactoryDocumentLigne", None
) or getattr(doc, "FactoryDocumentStockLigne", None)
if factory_lignes:
idx = 1
while idx <= 100:
try:
ligne_p = factory_lignes.List(idx)
if ligne_p is None:
break
try:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
except:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentStockLigne3"
)
ligne.Read()
article_ref = ""
try:
article_obj = getattr(ligne, "Article", None)
if article_obj:
article_obj.Read()
article_ref = getattr(
article_obj, "AR_Ref", ""
).strip()
except:
pass
ligne_info = {
"article_ref": article_ref,
"designation": getattr(ligne, "DL_Design", ""),
"quantite": float(getattr(ligne, "DL_Qte", 0.0)),
"prix_unitaire": float(
getattr(ligne, "DL_PrixUnitaire", 0.0)
),
"montant_ht": float(
getattr(ligne, "DL_MontantHT", 0.0)
),
"numero_lot": getattr(ligne, "LS_NoSerie", ""),
}
mouvement["lignes"].append(ligne_info)
idx += 1
except:
break
except Exception as e:
logger.warning(f"[MOUVEMENT] Erreur lecture lignes : {e}")
mouvement["nb_lignes"] = len(mouvement["lignes"])
logger.info(
f"[MOUVEMENT] Lu : {numero} - {mouvement['nb_lignes']} ligne(s)"
)
return mouvement
except ValueError:
raise
except Exception as e:
logger.error(f"[MOUVEMENT] Erreur : {e}", exc_info=True)
raise ValueError(f"Erreur lecture mouvement : {str(e)}")
def 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