14441 lines
626 KiB
Python
14441 lines
626 KiB
Python
import win32com.client
|
|
import pythoncom # AJOUT CRITIQUE
|
|
from datetime import datetime, timedelta, date
|
|
from typing import Dict, List, Optional, Any
|
|
import threading
|
|
import time
|
|
import logging
|
|
from config import settings, validate_settings
|
|
import pyodbc
|
|
from contextlib import contextmanager
|
|
import pywintypes
|
|
import os
|
|
import glob
|
|
import tempfile
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
import zlib
|
|
import struct
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SageConnector:
|
|
def __init__(self, chemin_base, utilisateur="<Administrateur>", mot_de_passe=""):
|
|
self.chemin_base = chemin_base
|
|
self.utilisateur = utilisateur
|
|
self.mot_de_passe = mot_de_passe
|
|
self.cial = None
|
|
|
|
self.sql_server = "OV-FDDDC6\\SAGE100"
|
|
self.sql_database = "BIJOU"
|
|
self.sql_conn_string = (
|
|
f"DRIVER={{ODBC Driver 17 for SQL Server}};"
|
|
f"SERVER={self.sql_server};"
|
|
f"DATABASE={self.sql_database};"
|
|
f"Trusted_Connection=yes;"
|
|
f"Encrypt=no;"
|
|
)
|
|
|
|
self._lock_com = threading.RLock()
|
|
|
|
self._thread_local = threading.local()
|
|
|
|
|
|
@contextmanager
|
|
def _com_context(self):
|
|
if not hasattr(self._thread_local, "com_initialized"):
|
|
try:
|
|
pythoncom.CoInitialize()
|
|
self._thread_local.com_initialized = True
|
|
logger.debug(
|
|
f"COM initialisé pour thread {threading.current_thread().name}"
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Erreur initialisation COM: {e}")
|
|
raise
|
|
|
|
try:
|
|
yield
|
|
finally:
|
|
pass
|
|
|
|
@contextmanager
|
|
def _get_sql_connection(self):
|
|
"""Context manager pour connexions SQL"""
|
|
conn = None
|
|
try:
|
|
conn = pyodbc.connect(self.sql_conn_string, timeout=10)
|
|
yield conn
|
|
except pyodbc.Error as e:
|
|
logger.error(f" Erreur SQL: {e}")
|
|
raise RuntimeError(f"Erreur SQL: {str(e)}")
|
|
finally:
|
|
if conn:
|
|
conn.close()
|
|
|
|
# def _safe_strip(self, value):
|
|
# """Strip sécurisé pour valeurs SQL"""
|
|
# if value is None:
|
|
# return None
|
|
# if isinstance(value, str):
|
|
# return value.strip()
|
|
# return value
|
|
|
|
def _cleanup_com_thread(self):
|
|
"""Nettoie COM pour le thread actuel (à appeler à la fin)"""
|
|
if hasattr(self._thread_local, "com_initialized"):
|
|
try:
|
|
pythoncom.CoUninitialize()
|
|
delattr(self._thread_local, "com_initialized")
|
|
logger.debug(
|
|
f"COM nettoyé pour thread {threading.current_thread().name}"
|
|
)
|
|
except:
|
|
pass
|
|
|
|
|
|
def connecter(self):
|
|
"""Connexion initiale à Sage - VERSION HYBRIDE"""
|
|
try:
|
|
with self._com_context():
|
|
self.cial = win32com.client.gencache.EnsureDispatch(
|
|
"Objets100c.Cial.Stream"
|
|
)
|
|
self.cial.Name = self.chemin_base
|
|
self.cial.Loggable.UserName = self.utilisateur
|
|
self.cial.Loggable.UserPwd = self.mot_de_passe
|
|
self.cial.Open()
|
|
|
|
logger.info(f" Connexion COM Sage réussie: {self.chemin_base}")
|
|
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
except Exception as e:
|
|
logger.warning(f"SQL non disponible: {e}")
|
|
logger.warning(" Les lectures utiliseront COM (plus lent)")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur connexion Sage: {e}", exc_info=True)
|
|
return False
|
|
|
|
def deconnecter(self):
|
|
"""Déconnexion propre"""
|
|
if self.cial:
|
|
try:
|
|
with self._com_context():
|
|
self.cial.Close()
|
|
logger.info("Connexion Sage fermée")
|
|
except:
|
|
pass
|
|
|
|
def lister_tous_fournisseurs(self, filtre=""):
|
|
"""
|
|
Liste tous les fournisseurs avec TOUS les champs
|
|
Symétrie complète avec lister_tous_clients
|
|
"""
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = """
|
|
SELECT
|
|
-- IDENTIFICATION (9)
|
|
CT_Num, CT_Intitule, CT_Type, CT_Qualite,
|
|
CT_Classement, CT_Raccourci, CT_Siret, CT_Identifiant,
|
|
CT_Ape,
|
|
|
|
-- ADRESSE (7)
|
|
CT_Contact, CT_Adresse, CT_Complement,
|
|
CT_CodePostal, CT_Ville, CT_CodeRegion, CT_Pays,
|
|
|
|
-- TELECOM (6)
|
|
CT_Telephone, CT_Telecopie, CT_EMail, CT_Site,
|
|
CT_Facebook, CT_LinkedIn,
|
|
|
|
-- TAUX (4)
|
|
CT_Taux01, CT_Taux02, CT_Taux03, CT_Taux04,
|
|
|
|
-- STATISTIQUES (10)
|
|
CT_Statistique01, CT_Statistique02, CT_Statistique03,
|
|
CT_Statistique04, CT_Statistique05, CT_Statistique06,
|
|
CT_Statistique07, CT_Statistique08, CT_Statistique09,
|
|
CT_Statistique10,
|
|
|
|
-- COMMERCIAL (4)
|
|
CT_Encours, CT_Assurance, CT_Langue, CO_No,
|
|
|
|
-- FACTURATION (11)
|
|
CT_Lettrage, CT_Sommeil, CT_Facture, CT_Prospect,
|
|
CT_BLFact, CT_Saut, CT_ValidEch, CT_ControlEnc,
|
|
CT_NotRappel, CT_NotPenal, CT_BonAPayer,
|
|
|
|
-- LOGISTIQUE (4)
|
|
CT_PrioriteLivr, CT_LivrPartielle,
|
|
CT_DelaiTransport, CT_DelaiAppro,
|
|
|
|
-- COMMENTAIRE (1)
|
|
CT_Commentaire,
|
|
|
|
-- ANALYTIQUE (1)
|
|
CA_Num,
|
|
|
|
-- ORGANISATION / SURVEILLANCE (10)
|
|
MR_No, CT_Surveillance, CT_Coface,
|
|
CT_SvFormeJuri, CT_SvEffectif, CT_SvRegul,
|
|
CT_SvCotation, CT_SvObjetMaj, CT_SvCA, CT_SvResultat,
|
|
|
|
-- COMPTE GENERAL ET CATEGORIES (3)
|
|
CG_NumPrinc, N_CatTarif, N_CatCompta
|
|
|
|
FROM F_COMPTET
|
|
WHERE CT_Type = 1
|
|
"""
|
|
|
|
params = []
|
|
|
|
if filtre:
|
|
query += " AND (CT_Num LIKE ? OR CT_Intitule LIKE ?)"
|
|
params.extend([f"%{filtre}%", f"%{filtre}%"])
|
|
|
|
query += " ORDER BY CT_Intitule"
|
|
|
|
cursor.execute(query, params)
|
|
rows = cursor.fetchall()
|
|
|
|
fournisseurs = []
|
|
for row in rows:
|
|
fournisseur = {
|
|
"numero": self._safe_strip(row.CT_Num),
|
|
"intitule": self._safe_strip(row.CT_Intitule),
|
|
"type_tiers": row.CT_Type,
|
|
"qualite": self._safe_strip(row.CT_Qualite),
|
|
"classement": self._safe_strip(row.CT_Classement),
|
|
"raccourci": self._safe_strip(row.CT_Raccourci),
|
|
"siret": self._safe_strip(row.CT_Siret),
|
|
"tva_intra": self._safe_strip(row.CT_Identifiant),
|
|
"code_naf": self._safe_strip(row.CT_Ape),
|
|
|
|
"contact": self._safe_strip(row.CT_Contact),
|
|
"adresse": self._safe_strip(row.CT_Adresse),
|
|
"complement": self._safe_strip(row.CT_Complement),
|
|
"code_postal": self._safe_strip(row.CT_CodePostal),
|
|
"ville": self._safe_strip(row.CT_Ville),
|
|
"region": self._safe_strip(row.CT_CodeRegion),
|
|
"pays": self._safe_strip(row.CT_Pays),
|
|
|
|
"telephone": self._safe_strip(row.CT_Telephone),
|
|
"telecopie": self._safe_strip(row.CT_Telecopie),
|
|
"email": self._safe_strip(row.CT_EMail),
|
|
"site_web": self._safe_strip(row.CT_Site),
|
|
"facebook": self._safe_strip(row.CT_Facebook),
|
|
"linkedin": self._safe_strip(row.CT_LinkedIn),
|
|
|
|
"taux01": row.CT_Taux01,
|
|
"taux02": row.CT_Taux02,
|
|
"taux03": row.CT_Taux03,
|
|
"taux04": row.CT_Taux04,
|
|
|
|
"statistique01": self._safe_strip(row.CT_Statistique01),
|
|
"statistique02": self._safe_strip(row.CT_Statistique02),
|
|
"statistique03": self._safe_strip(row.CT_Statistique03),
|
|
"statistique04": self._safe_strip(row.CT_Statistique04),
|
|
"statistique05": self._safe_strip(row.CT_Statistique05),
|
|
"statistique06": self._safe_strip(row.CT_Statistique06),
|
|
"statistique07": self._safe_strip(row.CT_Statistique07),
|
|
"statistique08": self._safe_strip(row.CT_Statistique08),
|
|
"statistique09": self._safe_strip(row.CT_Statistique09),
|
|
"statistique10": self._safe_strip(row.CT_Statistique10),
|
|
|
|
"encours_autorise": row.CT_Encours,
|
|
"assurance_credit": row.CT_Assurance,
|
|
"langue": row.CT_Langue,
|
|
"commercial_code": row.CO_No,
|
|
|
|
"lettrage_auto": (row.CT_Lettrage == 1),
|
|
"est_actif": (row.CT_Sommeil == 0),
|
|
"type_facture": row.CT_Facture,
|
|
"est_prospect": (row.CT_Prospect == 1),
|
|
"bl_en_facture": row.CT_BLFact,
|
|
"saut_page": row.CT_Saut,
|
|
"validation_echeance": row.CT_ValidEch,
|
|
"controle_encours": row.CT_ControlEnc,
|
|
"exclure_relance": (row.CT_NotRappel == 1),
|
|
"exclure_penalites": (row.CT_NotPenal == 1),
|
|
"bon_a_payer": row.CT_BonAPayer,
|
|
|
|
"priorite_livraison": row.CT_PrioriteLivr,
|
|
"livraison_partielle": row.CT_LivrPartielle,
|
|
"delai_transport": row.CT_DelaiTransport,
|
|
"delai_appro": row.CT_DelaiAppro,
|
|
|
|
"commentaire": self._safe_strip(row.CT_Commentaire),
|
|
|
|
"section_analytique": self._safe_strip(row.CA_Num),
|
|
|
|
"mode_reglement_code": row.MR_No,
|
|
"surveillance_active": (row.CT_Surveillance == 1),
|
|
"coface": self._safe_strip(row.CT_Coface),
|
|
"forme_juridique": self._safe_strip(row.CT_SvFormeJuri),
|
|
"effectif": self._safe_strip(row.CT_SvEffectif),
|
|
"sv_regularite": self._safe_strip(row.CT_SvRegul),
|
|
"sv_cotation": self._safe_strip(row.CT_SvCotation),
|
|
"sv_objet_maj": self._safe_strip(row.CT_SvObjetMaj),
|
|
"sv_chiffre_affaires": row.CT_SvCA,
|
|
"sv_resultat": row.CT_SvResultat,
|
|
|
|
"compte_general": self._safe_strip(row.CG_NumPrinc),
|
|
"categorie_tarif": row.N_CatTarif,
|
|
"categorie_compta": row.N_CatCompta,
|
|
}
|
|
|
|
fournisseur["contacts"] = self._get_contacts_client(row.CT_Num, conn)
|
|
|
|
fournisseurs.append(fournisseur)
|
|
|
|
logger.info(f"✅ SQL: {len(fournisseurs)} fournisseurs avec {len(fournisseur)} champs")
|
|
return fournisseurs
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Erreur SQL fournisseurs: {e}")
|
|
raise RuntimeError(f"Erreur lecture fournisseurs: {str(e)}")
|
|
|
|
|
|
def lire_fournisseur(self, code_fournisseur):
|
|
"""
|
|
Lit un fournisseur avec TOUS les champs (identique à lister_tous_fournisseurs)
|
|
Symétrie complète GET/POST
|
|
"""
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = """
|
|
SELECT
|
|
-- IDENTIFICATION (9)
|
|
CT_Num, CT_Intitule, CT_Type, CT_Qualite,
|
|
CT_Classement, CT_Raccourci, CT_Siret, CT_Identifiant,
|
|
CT_Ape,
|
|
|
|
-- ADRESSE (7)
|
|
CT_Contact, CT_Adresse, CT_Complement,
|
|
CT_CodePostal, CT_Ville, CT_CodeRegion, CT_Pays,
|
|
|
|
-- TELECOM (6)
|
|
CT_Telephone, CT_Telecopie, CT_EMail, CT_Site,
|
|
CT_Facebook, CT_LinkedIn,
|
|
|
|
-- TAUX (4)
|
|
CT_Taux01, CT_Taux02, CT_Taux03, CT_Taux04,
|
|
|
|
-- STATISTIQUES (10)
|
|
CT_Statistique01, CT_Statistique02, CT_Statistique03,
|
|
CT_Statistique04, CT_Statistique05, CT_Statistique06,
|
|
CT_Statistique07, CT_Statistique08, CT_Statistique09,
|
|
CT_Statistique10,
|
|
|
|
-- COMMERCIAL (4)
|
|
CT_Encours, CT_Assurance, CT_Langue, CO_No,
|
|
|
|
-- FACTURATION (11)
|
|
CT_Lettrage, CT_Sommeil, CT_Facture, CT_Prospect,
|
|
CT_BLFact, CT_Saut, CT_ValidEch, CT_ControlEnc,
|
|
CT_NotRappel, CT_NotPenal, CT_BonAPayer,
|
|
|
|
-- LOGISTIQUE (4)
|
|
CT_PrioriteLivr, CT_LivrPartielle,
|
|
CT_DelaiTransport, CT_DelaiAppro,
|
|
|
|
-- COMMENTAIRE (1)
|
|
CT_Commentaire,
|
|
|
|
-- ANALYTIQUE (1)
|
|
CA_Num,
|
|
|
|
-- ORGANISATION / SURVEILLANCE (10)
|
|
MR_No, CT_Surveillance, CT_Coface,
|
|
CT_SvFormeJuri, CT_SvEffectif, CT_SvRegul,
|
|
CT_SvCotation, CT_SvObjetMaj, CT_SvCA, CT_SvResultat,
|
|
|
|
-- COMPTE GENERAL ET CATEGORIES (3)
|
|
CG_NumPrinc, N_CatTarif, N_CatCompta
|
|
|
|
FROM F_COMPTET
|
|
WHERE CT_Num = ? AND CT_Type = 1
|
|
"""
|
|
|
|
cursor.execute(query, (code_fournisseur.upper(),))
|
|
row = cursor.fetchone()
|
|
|
|
if not row:
|
|
return None
|
|
|
|
fournisseur = {
|
|
"numero": self._safe_strip(row.CT_Num),
|
|
"intitule": self._safe_strip(row.CT_Intitule),
|
|
"type_tiers": row.CT_Type,
|
|
"qualite": self._safe_strip(row.CT_Qualite),
|
|
"classement": self._safe_strip(row.CT_Classement),
|
|
"raccourci": self._safe_strip(row.CT_Raccourci),
|
|
"siret": self._safe_strip(row.CT_Siret),
|
|
"tva_intra": self._safe_strip(row.CT_Identifiant),
|
|
"code_naf": self._safe_strip(row.CT_Ape),
|
|
|
|
"contact": self._safe_strip(row.CT_Contact),
|
|
"adresse": self._safe_strip(row.CT_Adresse),
|
|
"complement": self._safe_strip(row.CT_Complement),
|
|
"code_postal": self._safe_strip(row.CT_CodePostal),
|
|
"ville": self._safe_strip(row.CT_Ville),
|
|
"region": self._safe_strip(row.CT_CodeRegion),
|
|
"pays": self._safe_strip(row.CT_Pays),
|
|
|
|
"telephone": self._safe_strip(row.CT_Telephone),
|
|
"telecopie": self._safe_strip(row.CT_Telecopie),
|
|
"email": self._safe_strip(row.CT_EMail),
|
|
"site_web": self._safe_strip(row.CT_Site),
|
|
"facebook": self._safe_strip(row.CT_Facebook),
|
|
"linkedin": self._safe_strip(row.CT_LinkedIn),
|
|
|
|
"taux01": row.CT_Taux01,
|
|
"taux02": row.CT_Taux02,
|
|
"taux03": row.CT_Taux03,
|
|
"taux04": row.CT_Taux04,
|
|
|
|
"statistique01": self._safe_strip(row.CT_Statistique01),
|
|
"statistique02": self._safe_strip(row.CT_Statistique02),
|
|
"statistique03": self._safe_strip(row.CT_Statistique03),
|
|
"statistique04": self._safe_strip(row.CT_Statistique04),
|
|
"statistique05": self._safe_strip(row.CT_Statistique05),
|
|
"statistique06": self._safe_strip(row.CT_Statistique06),
|
|
"statistique07": self._safe_strip(row.CT_Statistique07),
|
|
"statistique08": self._safe_strip(row.CT_Statistique08),
|
|
"statistique09": self._safe_strip(row.CT_Statistique09),
|
|
"statistique10": self._safe_strip(row.CT_Statistique10),
|
|
|
|
"encours_autorise": row.CT_Encours,
|
|
"assurance_credit": row.CT_Assurance,
|
|
"langue": row.CT_Langue,
|
|
"commercial_code": row.CO_No,
|
|
|
|
"lettrage_auto": (row.CT_Lettrage == 1),
|
|
"est_actif": (row.CT_Sommeil == 0),
|
|
"type_facture": row.CT_Facture,
|
|
"est_prospect": (row.CT_Prospect == 1),
|
|
"bl_en_facture": row.CT_BLFact,
|
|
"saut_page": row.CT_Saut,
|
|
"validation_echeance": row.CT_ValidEch,
|
|
"controle_encours": row.CT_ControlEnc,
|
|
"exclure_relance": (row.CT_NotRappel == 1),
|
|
"exclure_penalites": (row.CT_NotPenal == 1),
|
|
"bon_a_payer": row.CT_BonAPayer,
|
|
|
|
"priorite_livraison": row.CT_PrioriteLivr,
|
|
"livraison_partielle": row.CT_LivrPartielle,
|
|
"delai_transport": row.CT_DelaiTransport,
|
|
"delai_appro": row.CT_DelaiAppro,
|
|
|
|
"commentaire": self._safe_strip(row.CT_Commentaire),
|
|
|
|
"section_analytique": self._safe_strip(row.CA_Num),
|
|
|
|
"mode_reglement_code": row.MR_No,
|
|
"surveillance_active": (row.CT_Surveillance == 1),
|
|
"coface": self._safe_strip(row.CT_Coface),
|
|
"forme_juridique": self._safe_strip(row.CT_SvFormeJuri),
|
|
"effectif": self._safe_strip(row.CT_SvEffectif),
|
|
"sv_regularite": self._safe_strip(row.CT_SvRegul),
|
|
"sv_cotation": self._safe_strip(row.CT_SvCotation),
|
|
"sv_objet_maj": self._safe_strip(row.CT_SvObjetMaj),
|
|
"sv_chiffre_affaires": row.CT_SvCA,
|
|
"sv_resultat": row.CT_SvResultat,
|
|
|
|
"compte_general": self._safe_strip(row.CG_NumPrinc),
|
|
"categorie_tarif": row.N_CatTarif,
|
|
"categorie_compta": row.N_CatCompta,
|
|
}
|
|
|
|
fournisseur["contacts"] = self._get_contacts_client(row.CT_Num, conn)
|
|
|
|
logger.info(f"✅ SQL: Fournisseur {code_fournisseur} avec {len(fournisseur)} champs")
|
|
return fournisseur
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Erreur SQL fournisseur {code_fournisseur}: {e}")
|
|
return None
|
|
|
|
|
|
|
|
def creer_fournisseur(self, fournisseur_data: Dict) -> Dict:
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info(" === VALIDATION DONNÉES FOURNISSEUR ===")
|
|
|
|
if not fournisseur_data.get("intitule"):
|
|
raise ValueError("Le champ 'intitule' est obligatoire")
|
|
|
|
intitule = str(fournisseur_data["intitule"])[:69].strip()
|
|
num_prop = (
|
|
str(fournisseur_data.get("num", "")).upper()[:17].strip()
|
|
if fournisseur_data.get("num")
|
|
else ""
|
|
)
|
|
compte = str(fournisseur_data.get("compte_collectif", "4010000"))[
|
|
:13
|
|
].strip()
|
|
|
|
adresse = str(fournisseur_data.get("adresse", ""))[:35].strip()
|
|
code_postal = str(fournisseur_data.get("code_postal", ""))[:9].strip()
|
|
ville = str(fournisseur_data.get("ville", ""))[:35].strip()
|
|
pays = str(fournisseur_data.get("pays", ""))[:35].strip()
|
|
|
|
telephone = str(fournisseur_data.get("telephone", ""))[:21].strip()
|
|
email = str(fournisseur_data.get("email", ""))[:69].strip()
|
|
|
|
siret = str(fournisseur_data.get("siret", ""))[:14].strip()
|
|
tva_intra = str(fournisseur_data.get("tva_intra", ""))[:25].strip()
|
|
|
|
logger.info(f" intitule: '{intitule}' (len={len(intitule)})")
|
|
logger.info(f" num: '{num_prop or 'AUTO'}' (len={len(num_prop)})")
|
|
logger.info(f" compte: '{compte}' (len={len(compte)})")
|
|
|
|
factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur
|
|
|
|
persist = factory_fournisseur.Create()
|
|
fournisseur = win32com.client.CastTo(persist, "IBOFournisseur3")
|
|
|
|
fournisseur.SetDefault()
|
|
|
|
logger.info(" Objet fournisseur créé et initialisé")
|
|
|
|
logger.info(" Définition des champs obligatoires...")
|
|
|
|
fournisseur.CT_Intitule = intitule
|
|
logger.debug(f" CT_Intitule: '{intitule}'")
|
|
|
|
try:
|
|
fournisseur.CT_Type = 1 # 1 = Fournisseur
|
|
logger.debug(" CT_Type: 1 (Fournisseur)")
|
|
except:
|
|
logger.debug(" CT_Type non défini (géré par FactoryFournisseur)")
|
|
|
|
try:
|
|
fournisseur.CT_Qualite = "FOU"
|
|
logger.debug(" CT_Qualite: 'FOU'")
|
|
except:
|
|
logger.debug(" CT_Qualite non défini (pas critique)")
|
|
|
|
try:
|
|
factory_compte = self.cial.CptaApplication.FactoryCompteG
|
|
persist_compte = factory_compte.ReadNumero(compte)
|
|
|
|
if persist_compte:
|
|
compte_obj = win32com.client.CastTo(
|
|
persist_compte, "IBOCompteG3"
|
|
)
|
|
compte_obj.Read()
|
|
|
|
fournisseur.CompteGPrinc = compte_obj
|
|
logger.debug(f" CompteGPrinc: objet '{compte}' assigné")
|
|
else:
|
|
logger.warning(
|
|
f" Compte {compte} introuvable - utilisation défaut"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f" Erreur CompteGPrinc: {e}")
|
|
|
|
if num_prop:
|
|
fournisseur.CT_Num = num_prop
|
|
logger.debug(f" CT_Num fourni: '{num_prop}'")
|
|
else:
|
|
try:
|
|
if hasattr(fournisseur, "SetDefaultNumPiece"):
|
|
fournisseur.SetDefaultNumPiece()
|
|
num_genere = getattr(fournisseur, "CT_Num", "")
|
|
logger.debug(f" CT_Num auto-généré: '{num_genere}'")
|
|
else:
|
|
num_genere = factory_fournisseur.GetNextNumero()
|
|
if num_genere:
|
|
fournisseur.CT_Num = num_genere
|
|
logger.debug(
|
|
f" CT_Num auto (GetNextNumero): '{num_genere}'"
|
|
)
|
|
else:
|
|
import time
|
|
|
|
num_genere = f"FOUR{int(time.time()) % 1000000}"
|
|
fournisseur.CT_Num = num_genere
|
|
logger.warning(f" CT_Num fallback: '{num_genere}'")
|
|
except Exception as e:
|
|
logger.error(f" Impossible de générer CT_Num: {e}")
|
|
raise ValueError(
|
|
"Impossible de générer le numéro fournisseur automatiquement"
|
|
)
|
|
|
|
try:
|
|
if hasattr(fournisseur, "N_CatTarif"):
|
|
fournisseur.N_CatTarif = 1
|
|
if hasattr(fournisseur, "N_CatCompta"):
|
|
fournisseur.N_CatCompta = 1
|
|
if hasattr(fournisseur, "N_Period"):
|
|
fournisseur.N_Period = 1
|
|
logger.debug(" Catégories (N_*) initialisées")
|
|
except Exception as e:
|
|
logger.warning(f" Catégories: {e}")
|
|
|
|
logger.info(" Définition champs optionnels...")
|
|
|
|
if any([adresse, code_postal, ville, pays]):
|
|
try:
|
|
adresse_obj = fournisseur.Adresse
|
|
|
|
if adresse:
|
|
adresse_obj.Adresse = adresse
|
|
if code_postal:
|
|
adresse_obj.CodePostal = code_postal
|
|
if ville:
|
|
adresse_obj.Ville = ville
|
|
if pays:
|
|
adresse_obj.Pays = pays
|
|
|
|
logger.debug(" Adresse définie")
|
|
except Exception as e:
|
|
logger.warning(f" Adresse: {e}")
|
|
|
|
if telephone or email:
|
|
try:
|
|
telecom_obj = fournisseur.Telecom
|
|
|
|
if telephone:
|
|
telecom_obj.Telephone = telephone
|
|
if email:
|
|
telecom_obj.EMail = email
|
|
|
|
logger.debug(" Télécom défini")
|
|
except Exception as e:
|
|
logger.warning(f" Télécom: {e}")
|
|
|
|
if siret:
|
|
try:
|
|
fournisseur.CT_Siret = siret
|
|
logger.debug(f" SIRET: '{siret}'")
|
|
except Exception as e:
|
|
logger.warning(f" SIRET: {e}")
|
|
|
|
if tva_intra:
|
|
try:
|
|
fournisseur.CT_Identifiant = tva_intra
|
|
logger.debug(f" TVA intra: '{tva_intra}'")
|
|
except Exception as e:
|
|
logger.warning(f" TVA: {e}")
|
|
|
|
try:
|
|
if hasattr(fournisseur, "CT_Lettrage"):
|
|
fournisseur.CT_Lettrage = True
|
|
if hasattr(fournisseur, "CT_Sommeil"):
|
|
fournisseur.CT_Sommeil = False
|
|
logger.debug(" Options par défaut définies")
|
|
except Exception as e:
|
|
logger.debug(f" Options: {e}")
|
|
|
|
logger.info(" === DIAGNOSTIC PRÉ-WRITE ===")
|
|
|
|
num_avant_write = getattr(fournisseur, "CT_Num", "")
|
|
if not num_avant_write:
|
|
logger.error(" CRITIQUE: CT_Num toujours vide !")
|
|
raise ValueError("Le numéro fournisseur (CT_Num) est obligatoire")
|
|
|
|
logger.info(f" CT_Num confirmé: '{num_avant_write}'")
|
|
|
|
logger.info(" Écriture du fournisseur dans Sage...")
|
|
|
|
try:
|
|
fournisseur.Write()
|
|
logger.info(" Write() réussi !")
|
|
|
|
except Exception as e:
|
|
error_detail = str(e)
|
|
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_detail = (
|
|
f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
)
|
|
logger.error(f" Erreur Sage: {error_detail}")
|
|
except:
|
|
pass
|
|
|
|
if (
|
|
"doublon" in error_detail.lower()
|
|
or "existe" in error_detail.lower()
|
|
):
|
|
raise ValueError(f"Ce fournisseur existe déjà : {error_detail}")
|
|
|
|
raise RuntimeError(f"Échec Write(): {error_detail}")
|
|
|
|
try:
|
|
fournisseur.Read()
|
|
except Exception as e:
|
|
logger.warning(f"Impossible de relire: {e}")
|
|
|
|
num_final = getattr(fournisseur, "CT_Num", "")
|
|
|
|
if not num_final:
|
|
raise RuntimeError("CT_Num vide après Write()")
|
|
|
|
logger.info(f" FOURNISSEUR CRÉÉ: {num_final} - {intitule} ")
|
|
|
|
resultat = {
|
|
"numero": num_final,
|
|
"intitule": intitule,
|
|
"compte_collectif": compte,
|
|
"type": 1, # Fournisseur
|
|
"est_fournisseur": True,
|
|
"adresse": adresse or None,
|
|
"code_postal": code_postal or None,
|
|
"ville": ville or None,
|
|
"pays": pays or None,
|
|
"email": email or None,
|
|
"telephone": telephone or None,
|
|
"siret": siret or None,
|
|
"tva_intra": tva_intra or None,
|
|
}
|
|
|
|
|
|
return resultat
|
|
|
|
except ValueError as e:
|
|
logger.error(f" Erreur métier: {e}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur création fournisseur: {e}", exc_info=True)
|
|
|
|
error_message = str(e)
|
|
if self.cial:
|
|
try:
|
|
err = self.cial.CptaApplication.LastError
|
|
if err:
|
|
error_message = f"Erreur Sage: {err.Description}"
|
|
except:
|
|
pass
|
|
|
|
raise RuntimeError(f"Erreur technique Sage: {error_message}")
|
|
|
|
def modifier_fournisseur(self, code: str, fournisseur_data: Dict) -> Dict:
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info(f" Recherche fournisseur {code}...")
|
|
|
|
factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur
|
|
persist = factory_fournisseur.ReadNumero(code)
|
|
|
|
if not persist:
|
|
raise ValueError(f"Fournisseur {code} introuvable")
|
|
|
|
fournisseur = self._cast_client(persist) # Réutiliser _cast_client
|
|
if not fournisseur:
|
|
raise ValueError(f"Impossible de charger le fournisseur {code}")
|
|
|
|
logger.info(
|
|
f" Fournisseur {code} trouvé: {getattr(fournisseur, 'CT_Intitule', '')}"
|
|
)
|
|
|
|
logger.info(" Mise à jour des champs...")
|
|
|
|
champs_modifies = []
|
|
|
|
if "intitule" in fournisseur_data:
|
|
intitule = str(fournisseur_data["intitule"])[:69].strip()
|
|
fournisseur.CT_Intitule = intitule
|
|
champs_modifies.append(f"intitule='{intitule}'")
|
|
|
|
if any(
|
|
k in fournisseur_data
|
|
for k in ["adresse", "code_postal", "ville", "pays"]
|
|
):
|
|
try:
|
|
adresse_obj = fournisseur.Adresse
|
|
|
|
if "adresse" in fournisseur_data:
|
|
adresse = str(fournisseur_data["adresse"])[:35].strip()
|
|
adresse_obj.Adresse = adresse
|
|
champs_modifies.append("adresse")
|
|
|
|
if "code_postal" in fournisseur_data:
|
|
cp = str(fournisseur_data["code_postal"])[:9].strip()
|
|
adresse_obj.CodePostal = cp
|
|
champs_modifies.append("code_postal")
|
|
|
|
if "ville" in fournisseur_data:
|
|
ville = str(fournisseur_data["ville"])[:35].strip()
|
|
adresse_obj.Ville = ville
|
|
champs_modifies.append("ville")
|
|
|
|
if "pays" in fournisseur_data:
|
|
pays = str(fournisseur_data["pays"])[:35].strip()
|
|
adresse_obj.Pays = pays
|
|
champs_modifies.append("pays")
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Erreur mise à jour adresse: {e}")
|
|
|
|
if "email" in fournisseur_data or "telephone" in fournisseur_data:
|
|
try:
|
|
telecom_obj = fournisseur.Telecom
|
|
|
|
if "email" in fournisseur_data:
|
|
email = str(fournisseur_data["email"])[:69].strip()
|
|
telecom_obj.EMail = email
|
|
champs_modifies.append("email")
|
|
|
|
if "telephone" in fournisseur_data:
|
|
tel = str(fournisseur_data["telephone"])[:21].strip()
|
|
telecom_obj.Telephone = tel
|
|
champs_modifies.append("telephone")
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Erreur mise à jour télécom: {e}")
|
|
|
|
if "siret" in fournisseur_data:
|
|
try:
|
|
siret = str(fournisseur_data["siret"])[:14].strip()
|
|
fournisseur.CT_Siret = siret
|
|
champs_modifies.append("siret")
|
|
except Exception as e:
|
|
logger.warning(f"Erreur mise à jour SIRET: {e}")
|
|
|
|
if "tva_intra" in fournisseur_data:
|
|
try:
|
|
tva = str(fournisseur_data["tva_intra"])[:25].strip()
|
|
fournisseur.CT_Identifiant = tva
|
|
champs_modifies.append("tva_intra")
|
|
except Exception as e:
|
|
logger.warning(f"Erreur mise à jour TVA: {e}")
|
|
|
|
if not champs_modifies:
|
|
logger.warning("Aucun champ à modifier")
|
|
return {
|
|
"numero": getattr(fournisseur, "CT_Num", "").strip(),
|
|
"intitule": getattr(fournisseur, "CT_Intitule", "").strip(),
|
|
"type": 1,
|
|
"est_fournisseur": True,
|
|
}
|
|
|
|
logger.info(f" Champs à modifier: {', '.join(champs_modifies)}")
|
|
|
|
logger.info(" Écriture des modifications...")
|
|
|
|
try:
|
|
fournisseur.Write()
|
|
logger.info(" Write() réussi !")
|
|
|
|
except Exception as e:
|
|
error_detail = str(e)
|
|
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_detail = (
|
|
f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
)
|
|
except:
|
|
pass
|
|
|
|
logger.error(f" Erreur Write(): {error_detail}")
|
|
raise RuntimeError(f"Échec modification: {error_detail}")
|
|
|
|
fournisseur.Read()
|
|
|
|
logger.info(
|
|
f" FOURNISSEUR MODIFIÉ: {code} ({len(champs_modifies)} champs) "
|
|
)
|
|
|
|
numero = getattr(fournisseur, "CT_Num", "").strip()
|
|
intitule = getattr(fournisseur, "CT_Intitule", "").strip()
|
|
|
|
data = {
|
|
"numero": numero,
|
|
"intitule": intitule,
|
|
"type": 1,
|
|
"est_fournisseur": True,
|
|
}
|
|
|
|
try:
|
|
adresse_obj = getattr(fournisseur, "Adresse", None)
|
|
if adresse_obj:
|
|
data["adresse"] = getattr(adresse_obj, "Adresse", "").strip()
|
|
data["code_postal"] = getattr(
|
|
adresse_obj, "CodePostal", ""
|
|
).strip()
|
|
data["ville"] = getattr(adresse_obj, "Ville", "").strip()
|
|
except:
|
|
data["adresse"] = ""
|
|
data["code_postal"] = ""
|
|
data["ville"] = ""
|
|
|
|
try:
|
|
telecom_obj = getattr(fournisseur, "Telecom", None)
|
|
if telecom_obj:
|
|
data["telephone"] = getattr(
|
|
telecom_obj, "Telephone", ""
|
|
).strip()
|
|
data["email"] = getattr(telecom_obj, "EMail", "").strip()
|
|
except:
|
|
data["telephone"] = ""
|
|
data["email"] = ""
|
|
|
|
return data
|
|
|
|
except ValueError as e:
|
|
logger.error(f" Erreur métier: {e}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur modification fournisseur: {e}", exc_info=True)
|
|
|
|
error_message = str(e)
|
|
if self.cial:
|
|
try:
|
|
err = self.cial.CptaApplication.LastError
|
|
if err:
|
|
error_message = f"Erreur Sage: {err.Description}"
|
|
except:
|
|
pass
|
|
|
|
raise RuntimeError(f"Erreur technique Sage: {error_message}")
|
|
|
|
|
|
def _get_contacts_client(self, numero: str, conn) -> list:
|
|
"""
|
|
Récupère tous les contacts d'un client avec indication du contact par défaut
|
|
"""
|
|
try:
|
|
cursor = conn.cursor()
|
|
|
|
query = """
|
|
SELECT
|
|
CT_Num, CT_No, N_Contact,
|
|
CT_Civilite, CT_Nom, CT_Prenom, CT_Fonction,
|
|
N_Service,
|
|
CT_Telephone, CT_TelPortable, CT_Telecopie, CT_EMail,
|
|
CT_Facebook, CT_LinkedIn, CT_Skype
|
|
FROM F_CONTACTT
|
|
WHERE CT_Num = ?
|
|
ORDER BY N_Contact, CT_Nom, CT_Prenom
|
|
"""
|
|
|
|
cursor.execute(query, [numero])
|
|
rows = cursor.fetchall()
|
|
|
|
# Récupérer le contact par défaut du client
|
|
query_client = """
|
|
SELECT CT_Contact
|
|
FROM F_COMPTET
|
|
WHERE CT_Num = ?
|
|
"""
|
|
cursor.execute(query_client, [numero])
|
|
client_row = cursor.fetchone()
|
|
|
|
nom_contact_defaut = None
|
|
if client_row:
|
|
nom_contact_defaut = self._safe_strip(client_row.CT_Contact)
|
|
|
|
contacts = []
|
|
for row in rows:
|
|
contact = self._row_to_contact_dict(row)
|
|
|
|
# Vérifier si c'est le contact par défaut
|
|
if nom_contact_defaut:
|
|
nom_complet = f"{contact.get('prenom', '')} {contact['nom']}".strip()
|
|
contact["est_defaut"] = (
|
|
nom_complet == nom_contact_defaut or
|
|
contact['nom'] == nom_contact_defaut
|
|
)
|
|
else:
|
|
contact["est_defaut"] = False
|
|
|
|
contacts.append(contact)
|
|
|
|
return contacts
|
|
|
|
except Exception as e:
|
|
logger.warning(f"⚠️ Impossible de récupérer contacts pour {numero}: {e}")
|
|
return []
|
|
|
|
def lister_tous_clients(self, filtre=""):
|
|
"""
|
|
Liste tous les clients avec TOUS les champs gérés par creer_client
|
|
Symétrie complète GET/POST
|
|
"""
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = """
|
|
SELECT
|
|
-- IDENTIFICATION (8)
|
|
CT_Num, CT_Intitule, CT_Type, CT_Qualite,
|
|
CT_Classement, CT_Raccourci, CT_Siret, CT_Identifiant,
|
|
CT_Ape,
|
|
|
|
-- ADRESSE (7)
|
|
CT_Contact, CT_Adresse, CT_Complement,
|
|
CT_CodePostal, CT_Ville, CT_CodeRegion, CT_Pays,
|
|
|
|
-- TELECOM (7)
|
|
CT_Telephone, CT_Telecopie, CT_EMail, CT_Site,
|
|
CT_Facebook, CT_LinkedIn,
|
|
|
|
-- TAUX (4)
|
|
CT_Taux01, CT_Taux02, CT_Taux03, CT_Taux04,
|
|
|
|
-- STATISTIQUES (10)
|
|
CT_Statistique01, CT_Statistique02, CT_Statistique03,
|
|
CT_Statistique04, CT_Statistique05, CT_Statistique06,
|
|
CT_Statistique07, CT_Statistique08, CT_Statistique09,
|
|
CT_Statistique10,
|
|
|
|
-- COMMERCIAL (4)
|
|
CT_Encours, CT_Assurance, CT_Langue, CO_No,
|
|
|
|
-- FACTURATION (11)
|
|
CT_Lettrage, CT_Sommeil, CT_Facture, CT_Prospect,
|
|
CT_BLFact, CT_Saut, CT_ValidEch, CT_ControlEnc,
|
|
CT_NotRappel, CT_NotPenal, CT_BonAPayer,
|
|
|
|
-- LOGISTIQUE (4)
|
|
CT_PrioriteLivr, CT_LivrPartielle,
|
|
CT_DelaiTransport, CT_DelaiAppro,
|
|
|
|
-- COMMENTAIRE (1)
|
|
CT_Commentaire,
|
|
|
|
-- ANALYTIQUE (1)
|
|
CA_Num,
|
|
|
|
-- ORGANISATION / SURVEILLANCE (10)
|
|
MR_No, CT_Surveillance, CT_Coface,
|
|
CT_SvFormeJuri, CT_SvEffectif, CT_SvRegul,
|
|
CT_SvCotation, CT_SvObjetMaj, CT_SvCA, CT_SvResultat,
|
|
|
|
-- COMPTE GENERAL ET CATEGORIES (3)
|
|
CG_NumPrinc, N_CatTarif, N_CatCompta
|
|
|
|
FROM F_COMPTET
|
|
WHERE CT_Type = 0
|
|
"""
|
|
|
|
params = []
|
|
|
|
if filtre:
|
|
query += " AND (CT_Num LIKE ? OR CT_Intitule LIKE ?)"
|
|
params.extend([f"%{filtre}%", f"%{filtre}%"])
|
|
|
|
query += " ORDER BY CT_Intitule"
|
|
|
|
cursor.execute(query, params)
|
|
rows = cursor.fetchall()
|
|
|
|
clients = []
|
|
for row in rows:
|
|
client = {
|
|
"numero": self._safe_strip(row.CT_Num),
|
|
"intitule": self._safe_strip(row.CT_Intitule),
|
|
"type_tiers": row.CT_Type,
|
|
"qualite": self._safe_strip(row.CT_Qualite),
|
|
"classement": self._safe_strip(row.CT_Classement),
|
|
"raccourci": self._safe_strip(row.CT_Raccourci),
|
|
"siret": self._safe_strip(row.CT_Siret),
|
|
"tva_intra": self._safe_strip(row.CT_Identifiant),
|
|
"code_naf": self._safe_strip(row.CT_Ape),
|
|
|
|
"contact": self._safe_strip(row.CT_Contact),
|
|
"adresse": self._safe_strip(row.CT_Adresse),
|
|
"complement": self._safe_strip(row.CT_Complement),
|
|
"code_postal": self._safe_strip(row.CT_CodePostal),
|
|
"ville": self._safe_strip(row.CT_Ville),
|
|
"region": self._safe_strip(row.CT_CodeRegion),
|
|
"pays": self._safe_strip(row.CT_Pays),
|
|
|
|
"telephone": self._safe_strip(row.CT_Telephone),
|
|
"telecopie": self._safe_strip(row.CT_Telecopie),
|
|
"email": self._safe_strip(row.CT_EMail),
|
|
"site_web": self._safe_strip(row.CT_Site),
|
|
"facebook": self._safe_strip(row.CT_Facebook),
|
|
"linkedin": self._safe_strip(row.CT_LinkedIn),
|
|
|
|
"taux01": row.CT_Taux01,
|
|
"taux02": row.CT_Taux02,
|
|
"taux03": row.CT_Taux03,
|
|
"taux04": row.CT_Taux04,
|
|
|
|
"statistique01": self._safe_strip(row.CT_Statistique01),
|
|
"statistique02": self._safe_strip(row.CT_Statistique02),
|
|
"statistique03": self._safe_strip(row.CT_Statistique03),
|
|
"statistique04": self._safe_strip(row.CT_Statistique04),
|
|
"statistique05": self._safe_strip(row.CT_Statistique05),
|
|
"statistique06": self._safe_strip(row.CT_Statistique06),
|
|
"statistique07": self._safe_strip(row.CT_Statistique07),
|
|
"statistique08": self._safe_strip(row.CT_Statistique08),
|
|
"statistique09": self._safe_strip(row.CT_Statistique09),
|
|
"statistique10": self._safe_strip(row.CT_Statistique10),
|
|
|
|
"encours_autorise": row.CT_Encours,
|
|
"assurance_credit": row.CT_Assurance,
|
|
"langue": row.CT_Langue,
|
|
"commercial_code": row.CO_No,
|
|
|
|
"lettrage_auto": (row.CT_Lettrage == 1),
|
|
"est_actif": (row.CT_Sommeil == 0),
|
|
"type_facture": row.CT_Facture,
|
|
"est_prospect": (row.CT_Prospect == 1),
|
|
"bl_en_facture": row.CT_BLFact,
|
|
"saut_page": row.CT_Saut,
|
|
"validation_echeance": row.CT_ValidEch,
|
|
"controle_encours": row.CT_ControlEnc,
|
|
"exclure_relance": (row.CT_NotRappel == 1),
|
|
"exclure_penalites": (row.CT_NotPenal == 1),
|
|
"bon_a_payer": row.CT_BonAPayer,
|
|
|
|
"priorite_livraison": row.CT_PrioriteLivr,
|
|
"livraison_partielle": row.CT_LivrPartielle,
|
|
"delai_transport": row.CT_DelaiTransport,
|
|
"delai_appro": row.CT_DelaiAppro,
|
|
|
|
"commentaire": self._safe_strip(row.CT_Commentaire),
|
|
|
|
"section_analytique": self._safe_strip(row.CA_Num),
|
|
|
|
"mode_reglement_code": row.MR_No,
|
|
"surveillance_active": (row.CT_Surveillance == 1),
|
|
"coface": self._safe_strip(row.CT_Coface),
|
|
"forme_juridique": self._safe_strip(row.CT_SvFormeJuri),
|
|
"effectif": self._safe_strip(row.CT_SvEffectif),
|
|
"sv_regularite": self._safe_strip(row.CT_SvRegul),
|
|
"sv_cotation": self._safe_strip(row.CT_SvCotation),
|
|
"sv_objet_maj": self._safe_strip(row.CT_SvObjetMaj),
|
|
"sv_chiffre_affaires": row.CT_SvCA,
|
|
"sv_resultat": row.CT_SvResultat,
|
|
|
|
"compte_general": self._safe_strip(row.CG_NumPrinc),
|
|
"categorie_tarif": row.N_CatTarif,
|
|
"categorie_compta": row.N_CatCompta,
|
|
}
|
|
|
|
client["contacts"] = self._get_contacts_client(row.CT_Num, conn)
|
|
|
|
clients.append(client)
|
|
|
|
logger.info(f"✅ SQL: {len(clients)} clients avec {len(client)} champs")
|
|
return clients
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Erreur SQL clients: {e}")
|
|
raise RuntimeError(f"Erreur lecture clients: {str(e)}")
|
|
|
|
|
|
def lire_client(self, code_client):
|
|
"""
|
|
Lit un client avec TOUS les champs (identique à lister_tous_clients)
|
|
Symétrie complète GET/POST
|
|
"""
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = """
|
|
SELECT
|
|
-- IDENTIFICATION (8)
|
|
CT_Num, CT_Intitule, CT_Type, CT_Qualite,
|
|
CT_Classement, CT_Raccourci, CT_Siret, CT_Identifiant,
|
|
CT_Ape,
|
|
|
|
-- ADRESSE (7)
|
|
CT_Contact, CT_Adresse, CT_Complement,
|
|
CT_CodePostal, CT_Ville, CT_CodeRegion, CT_Pays,
|
|
|
|
-- TELECOM (7)
|
|
CT_Telephone, CT_Telecopie, CT_EMail, CT_Site,
|
|
CT_Facebook, CT_LinkedIn,
|
|
|
|
-- TAUX (4)
|
|
CT_Taux01, CT_Taux02, CT_Taux03, CT_Taux04,
|
|
|
|
-- STATISTIQUES (10)
|
|
CT_Statistique01, CT_Statistique02, CT_Statistique03,
|
|
CT_Statistique04, CT_Statistique05, CT_Statistique06,
|
|
CT_Statistique07, CT_Statistique08, CT_Statistique09,
|
|
CT_Statistique10,
|
|
|
|
-- COMMERCIAL (4)
|
|
CT_Encours, CT_Assurance, CT_Langue, CO_No,
|
|
|
|
-- FACTURATION (11)
|
|
CT_Lettrage, CT_Sommeil, CT_Facture, CT_Prospect,
|
|
CT_BLFact, CT_Saut, CT_ValidEch, CT_ControlEnc,
|
|
CT_NotRappel, CT_NotPenal, CT_BonAPayer,
|
|
|
|
-- LOGISTIQUE (4)
|
|
CT_PrioriteLivr, CT_LivrPartielle,
|
|
CT_DelaiTransport, CT_DelaiAppro,
|
|
|
|
-- COMMENTAIRE (1)
|
|
CT_Commentaire,
|
|
|
|
-- ANALYTIQUE (1)
|
|
CA_Num,
|
|
|
|
-- ORGANISATION / SURVEILLANCE (10)
|
|
MR_No, CT_Surveillance, CT_Coface,
|
|
CT_SvFormeJuri, CT_SvEffectif, CT_SvRegul,
|
|
CT_SvCotation, CT_SvObjetMaj, CT_SvCA, CT_SvResultat,
|
|
|
|
-- COMPTE GENERAL ET CATEGORIES (3)
|
|
CG_NumPrinc, N_CatTarif, N_CatCompta
|
|
|
|
FROM F_COMPTET
|
|
WHERE CT_Num = ? AND CT_Type = 0
|
|
"""
|
|
|
|
cursor.execute(query, (code_client.upper(),))
|
|
row = cursor.fetchone()
|
|
|
|
if not row:
|
|
return None
|
|
|
|
client = {
|
|
"numero": self._safe_strip(row.CT_Num),
|
|
"intitule": self._safe_strip(row.CT_Intitule),
|
|
"type_tiers": row.CT_Type,
|
|
"qualite": self._safe_strip(row.CT_Qualite),
|
|
"classement": self._safe_strip(row.CT_Classement),
|
|
"raccourci": self._safe_strip(row.CT_Raccourci),
|
|
"siret": self._safe_strip(row.CT_Siret),
|
|
"tva_intra": self._safe_strip(row.CT_Identifiant),
|
|
"code_naf": self._safe_strip(row.CT_Ape),
|
|
|
|
"contact": self._safe_strip(row.CT_Contact),
|
|
"adresse": self._safe_strip(row.CT_Adresse),
|
|
"complement": self._safe_strip(row.CT_Complement),
|
|
"code_postal": self._safe_strip(row.CT_CodePostal),
|
|
"ville": self._safe_strip(row.CT_Ville),
|
|
"region": self._safe_strip(row.CT_CodeRegion),
|
|
"pays": self._safe_strip(row.CT_Pays),
|
|
|
|
"telephone": self._safe_strip(row.CT_Telephone),
|
|
"telecopie": self._safe_strip(row.CT_Telecopie),
|
|
"email": self._safe_strip(row.CT_EMail),
|
|
"site_web": self._safe_strip(row.CT_Site),
|
|
"facebook": self._safe_strip(row.CT_Facebook),
|
|
"linkedin": self._safe_strip(row.CT_LinkedIn),
|
|
|
|
"taux01": row.CT_Taux01,
|
|
"taux02": row.CT_Taux02,
|
|
"taux03": row.CT_Taux03,
|
|
"taux04": row.CT_Taux04,
|
|
|
|
"statistique01": self._safe_strip(row.CT_Statistique01),
|
|
"statistique02": self._safe_strip(row.CT_Statistique02),
|
|
"statistique03": self._safe_strip(row.CT_Statistique03),
|
|
"statistique04": self._safe_strip(row.CT_Statistique04),
|
|
"statistique05": self._safe_strip(row.CT_Statistique05),
|
|
"statistique06": self._safe_strip(row.CT_Statistique06),
|
|
"statistique07": self._safe_strip(row.CT_Statistique07),
|
|
"statistique08": self._safe_strip(row.CT_Statistique08),
|
|
"statistique09": self._safe_strip(row.CT_Statistique09),
|
|
"statistique10": self._safe_strip(row.CT_Statistique10),
|
|
|
|
"encours_autorise": row.CT_Encours,
|
|
"assurance_credit": row.CT_Assurance,
|
|
"langue": row.CT_Langue,
|
|
"commercial_code": row.CO_No,
|
|
|
|
"lettrage_auto": (row.CT_Lettrage == 1),
|
|
"est_actif": (row.CT_Sommeil == 0),
|
|
"type_facture": row.CT_Facture,
|
|
"est_prospect": (row.CT_Prospect == 1),
|
|
"bl_en_facture": row.CT_BLFact,
|
|
"saut_page": row.CT_Saut,
|
|
"validation_echeance": row.CT_ValidEch,
|
|
"controle_encours": row.CT_ControlEnc,
|
|
"exclure_relance": (row.CT_NotRappel == 1),
|
|
"exclure_penalites": (row.CT_NotPenal == 1),
|
|
"bon_a_payer": row.CT_BonAPayer,
|
|
|
|
"priorite_livraison": row.CT_PrioriteLivr,
|
|
"livraison_partielle": row.CT_LivrPartielle,
|
|
"delai_transport": row.CT_DelaiTransport,
|
|
"delai_appro": row.CT_DelaiAppro,
|
|
|
|
"commentaire": self._safe_strip(row.CT_Commentaire),
|
|
|
|
"section_analytique": self._safe_strip(row.CA_Num),
|
|
|
|
"mode_reglement_code": row.MR_No,
|
|
"surveillance_active": (row.CT_Surveillance == 1),
|
|
"coface": self._safe_strip(row.CT_Coface),
|
|
"forme_juridique": self._safe_strip(row.CT_SvFormeJuri),
|
|
"effectif": self._safe_strip(row.CT_SvEffectif),
|
|
"sv_regularite": self._safe_strip(row.CT_SvRegul),
|
|
"sv_cotation": self._safe_strip(row.CT_SvCotation),
|
|
"sv_objet_maj": self._safe_strip(row.CT_SvObjetMaj),
|
|
"sv_chiffre_affaires": row.CT_SvCA,
|
|
"sv_resultat": row.CT_SvResultat,
|
|
|
|
"compte_general": self._safe_strip(row.CG_NumPrinc),
|
|
"categorie_tarif": row.N_CatTarif,
|
|
"categorie_compta": row.N_CatCompta,
|
|
}
|
|
|
|
client["contacts"] = self._get_contacts_client(row.CT_Num, conn)
|
|
|
|
logger.info(f"✅ SQL: Client {code_client} avec {len(client)} champs")
|
|
return client
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Erreur SQL client {code_client}: {e}")
|
|
return None
|
|
|
|
|
|
def lister_tous_articles(self, filtre=""):
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
logger.info("[SQL] Détection des colonnes de F_ARTICLE...")
|
|
cursor.execute("SELECT TOP 1 * FROM F_ARTICLE")
|
|
colonnes_disponibles = [column[0] for column in cursor.description]
|
|
logger.info(f"[SQL] Colonnes F_ARTICLE trouvées : {len(colonnes_disponibles)}")
|
|
|
|
colonnes_config = {
|
|
"AR_Ref": "reference",
|
|
"AR_Design": "designation",
|
|
"AR_CodeBarre": "code_barre",
|
|
"AR_EdiCode": "edi_code",
|
|
"AR_Raccourci": "raccourci",
|
|
|
|
"AR_PrixVen": "prix_vente",
|
|
"AR_PrixAch": "prix_achat",
|
|
"AR_Coef": "coef",
|
|
"AR_PUNet": "prix_net",
|
|
"AR_PrixAchNouv": "prix_achat_nouveau",
|
|
"AR_CoefNouv": "coef_nouveau",
|
|
"AR_PrixVenNouv": "prix_vente_nouveau",
|
|
"AR_DateApplication": "date_application_prix",
|
|
"AR_CoutStd": "cout_standard",
|
|
|
|
"AR_UniteVen": "unite_vente",
|
|
"AR_UnitePoids": "unite_poids",
|
|
"AR_PoidsNet": "poids_net",
|
|
"AR_PoidsBrut": "poids_brut",
|
|
|
|
"AR_Gamme1": "gamme_1",
|
|
"AR_Gamme2": "gamme_2",
|
|
|
|
"FA_CodeFamille": "famille_code",
|
|
"AR_Type": "type_article",
|
|
"AR_Nature": "nature",
|
|
"AR_Garantie": "garantie",
|
|
"AR_CodeFiscal": "code_fiscal",
|
|
"AR_Pays": "pays",
|
|
|
|
"CO_No": "fournisseur_principal",
|
|
"AR_Condition": "conditionnement",
|
|
"AR_NbColis": "nb_colis",
|
|
"AR_Prevision": "prevision",
|
|
|
|
"AR_SuiviStock": "suivi_stock",
|
|
"AR_Nomencl": "nomenclature",
|
|
"AR_QteComp": "qte_composant",
|
|
"AR_QteOperatoire": "qte_operatoire",
|
|
|
|
"AR_Sommeil": "sommeil",
|
|
"AR_Substitut": "article_substitut",
|
|
"AR_Escompte": "soumis_escompte",
|
|
"AR_Delai": "delai",
|
|
|
|
"AR_Stat01": "stat_01",
|
|
"AR_Stat02": "stat_02",
|
|
"AR_Stat03": "stat_03",
|
|
"AR_Stat04": "stat_04",
|
|
"AR_Stat05": "stat_05",
|
|
"AR_HorsStat": "hors_statistique",
|
|
|
|
"CL_No1": "categorie_1",
|
|
"CL_No2": "categorie_2",
|
|
"CL_No3": "categorie_3",
|
|
"CL_No4": "categorie_4",
|
|
|
|
"AR_DateModif": "date_modification",
|
|
|
|
"AR_VteDebit": "vente_debit",
|
|
"AR_NotImp": "non_imprimable",
|
|
"AR_Transfere": "transfere",
|
|
"AR_Publie": "publie",
|
|
"AR_Contremarque": "contremarque",
|
|
"AR_FactPoids": "fact_poids",
|
|
"AR_FactForfait": "fact_forfait",
|
|
"AR_SaisieVar": "saisie_variable",
|
|
"AR_Fictif": "fictif",
|
|
"AR_SousTraitance": "sous_traitance",
|
|
"AR_Criticite": "criticite",
|
|
|
|
"RP_CodeDefaut": "reprise_code_defaut",
|
|
"AR_DelaiFabrication": "delai_fabrication",
|
|
"AR_DelaiPeremption": "delai_peremption",
|
|
"AR_DelaiSecurite": "delai_securite",
|
|
"AR_TypeLancement": "type_lancement",
|
|
"AR_Cycle": "cycle",
|
|
|
|
"AR_Photo": "photo",
|
|
"AR_Langue1": "langue_1",
|
|
"AR_Langue2": "langue_2",
|
|
|
|
"AR_Frais01FR_Denomination": "frais_01_denomination",
|
|
"AR_Frais02FR_Denomination": "frais_02_denomination",
|
|
"AR_Frais03FR_Denomination": "frais_03_denomination",
|
|
|
|
"Marque commerciale": "marque_commerciale",
|
|
"Objectif / Qtés vendues": "objectif_qtes_vendues",
|
|
"Pourcentage teneur en or": "pourcentage_or",
|
|
"1ère commercialisation": "premiere_commercialisation",
|
|
"AR_InterdireCommande": "interdire_commande",
|
|
"AR_Exclure": "exclure",
|
|
}
|
|
|
|
colonnes_a_lire = [
|
|
col_sql for col_sql in colonnes_config.keys()
|
|
if col_sql in colonnes_disponibles
|
|
]
|
|
|
|
if not colonnes_a_lire:
|
|
logger.error("[SQL] Aucune colonne mappée trouvée !")
|
|
colonnes_a_lire = ["AR_Ref", "AR_Design", "AR_PrixVen"]
|
|
|
|
logger.info(f"[SQL] Colonnes sélectionnées : {len(colonnes_a_lire)}")
|
|
|
|
colonnes_sql = []
|
|
for col in colonnes_a_lire:
|
|
if " " in col or "/" in col or "è" in col:
|
|
colonnes_sql.append(f"[{col}]")
|
|
else:
|
|
colonnes_sql.append(col)
|
|
|
|
colonnes_str = ", ".join(colonnes_sql)
|
|
query = f"SELECT {colonnes_str} FROM F_ARTICLE WHERE 1=1"
|
|
|
|
params = []
|
|
|
|
if filtre:
|
|
conditions = []
|
|
if "AR_Ref" in colonnes_a_lire:
|
|
conditions.append("AR_Ref LIKE ?")
|
|
params.append(f"%{filtre}%")
|
|
if "AR_Design" in colonnes_a_lire:
|
|
conditions.append("AR_Design LIKE ?")
|
|
params.append(f"%{filtre}%")
|
|
if "AR_CodeBarre" in colonnes_a_lire:
|
|
conditions.append("AR_CodeBarre LIKE ?")
|
|
params.append(f"%{filtre}%")
|
|
|
|
if conditions:
|
|
query += " AND (" + " OR ".join(conditions) + ")"
|
|
|
|
query += " ORDER BY AR_Ref"
|
|
|
|
logger.debug(f"[SQL] Requête : {query[:200]}...")
|
|
cursor.execute(query, params)
|
|
rows = cursor.fetchall()
|
|
|
|
logger.info(f"[SQL] {len(rows)} lignes récupérées")
|
|
|
|
articles = []
|
|
|
|
for row in rows:
|
|
row_data = {}
|
|
for idx, col_sql in enumerate(colonnes_a_lire):
|
|
valeur = row[idx]
|
|
if isinstance(valeur, str):
|
|
valeur = valeur.strip()
|
|
row_data[col_sql] = valeur
|
|
|
|
if "Marque commerciale" in row_data:
|
|
logger.debug(f"[DEBUG] Marque commerciale trouvée: {row_data['Marque commerciale']}")
|
|
|
|
article_data = self._mapper_article_depuis_row(row_data, colonnes_config)
|
|
articles.append(article_data)
|
|
|
|
articles = self._enrichir_stocks_articles(articles, cursor)
|
|
articles = self._enrichir_familles_articles(articles, cursor)
|
|
articles = self._enrichir_fournisseurs_articles(articles, cursor)
|
|
articles = self._enrichir_tva_articles(articles, cursor)
|
|
|
|
articles = self._enrichir_stock_emplacements(articles, cursor)
|
|
articles = self._enrichir_gammes_articles(articles, cursor)
|
|
articles = self._enrichir_tarifs_clients(articles, cursor)
|
|
articles = self._enrichir_nomenclature(articles, cursor)
|
|
articles = self._enrichir_compta_articles(articles, cursor)
|
|
articles = self._enrichir_fournisseurs_multiples(articles, cursor)
|
|
articles = self._enrichir_depots_details(articles, cursor)
|
|
articles = self._enrichir_emplacements_details(articles, cursor)
|
|
articles = self._enrichir_gammes_enumeres(articles, cursor)
|
|
articles = self._enrichir_references_enumerees(articles, cursor)
|
|
articles = self._enrichir_medias_articles(articles, cursor)
|
|
articles = self._enrichir_prix_gammes(articles, cursor)
|
|
articles = self._enrichir_conditionnements(articles, cursor)
|
|
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f"✗ Erreur SQL articles: {e}", exc_info=True)
|
|
raise RuntimeError(f"Erreur lecture articles: {str(e)}")
|
|
|
|
|
|
def _enrichir_stock_emplacements(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit avec le détail du stock par emplacement
|
|
Structure: articles[i]["emplacements"] = [{"depot": "01", "emplacement": "A1", "qte": 10}, ...]
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement stock emplacements...")
|
|
|
|
references = [a["reference"] for a in articles if a["reference"]]
|
|
if not references:
|
|
return articles
|
|
|
|
placeholders = ",".join(["?"] * len(references))
|
|
query = f"""
|
|
SELECT
|
|
AR_Ref,
|
|
DE_No,
|
|
DP_No,
|
|
AE_QteSto,
|
|
AE_QtePrepa,
|
|
AE_QteAControler,
|
|
cbCreation,
|
|
cbModification
|
|
FROM F_ARTSTOCKEMPL
|
|
WHERE AR_Ref IN ({placeholders})
|
|
ORDER BY AR_Ref, DE_No, DP_No
|
|
"""
|
|
|
|
cursor.execute(query, references)
|
|
rows = cursor.fetchall()
|
|
|
|
emplacements_map = {}
|
|
for row in rows:
|
|
ref = self._safe_strip(row[0])
|
|
if not ref:
|
|
continue
|
|
|
|
if ref not in emplacements_map:
|
|
emplacements_map[ref] = []
|
|
|
|
emplacements_map[ref].append({
|
|
"depot": self._safe_strip(row[1]),
|
|
"emplacement": self._safe_strip(row[2]),
|
|
"qte_stockee": float(row[3]) if row[3] else 0.0,
|
|
"qte_preparee": float(row[4]) if row[4] else 0.0,
|
|
"qte_a_controler": float(row[5]) if row[5] else 0.0,
|
|
"date_creation": row[6],
|
|
"date_modification": row[7],
|
|
})
|
|
|
|
for article in articles:
|
|
article["emplacements"] = emplacements_map.get(article["reference"], [])
|
|
article["nb_emplacements"] = len(article["emplacements"])
|
|
|
|
logger.info(f" ✓ {len(emplacements_map)} articles avec emplacements")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur stock emplacements: {e}")
|
|
for article in articles:
|
|
article["emplacements"] = []
|
|
article["nb_emplacements"] = 0
|
|
return articles
|
|
|
|
def _enrichir_gammes_articles(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit avec les gammes (taille, couleur, etc.)
|
|
Structure: articles[i]["gammes"] = [{"numero": 1, "enumere": "001", "type": 0}, ...]
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement gammes articles...")
|
|
|
|
references = [a["reference"] for a in articles if a["reference"]]
|
|
if not references:
|
|
return articles
|
|
|
|
placeholders = ",".join(["?"] * len(references))
|
|
query = f"""
|
|
SELECT
|
|
AR_Ref,
|
|
AG_No,
|
|
EG_Enumere,
|
|
AG_Type,
|
|
cbCreation,
|
|
cbModification
|
|
FROM F_ARTGAMME
|
|
WHERE AR_Ref IN ({placeholders})
|
|
ORDER BY AR_Ref, AG_No, EG_Enumere
|
|
"""
|
|
|
|
cursor.execute(query, references)
|
|
rows = cursor.fetchall()
|
|
|
|
gammes_map = {}
|
|
for row in rows:
|
|
ref = self._safe_strip(row[0])
|
|
if not ref:
|
|
continue
|
|
|
|
if ref not in gammes_map:
|
|
gammes_map[ref] = []
|
|
|
|
gammes_map[ref].append({
|
|
"numero_gamme": int(row[1]) if row[1] else 0,
|
|
"enumere": self._safe_strip(row[2]),
|
|
"type_gamme": int(row[3]) if row[3] else 0,
|
|
"date_creation": row[4],
|
|
"date_modification": row[5],
|
|
})
|
|
|
|
for article in articles:
|
|
article["gammes"] = gammes_map.get(article["reference"], [])
|
|
article["nb_gammes"] = len(article["gammes"])
|
|
|
|
logger.info(f" ✓ {len(gammes_map)} articles avec gammes")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur gammes: {e}")
|
|
for article in articles:
|
|
article["gammes"] = []
|
|
article["nb_gammes"] = 0
|
|
return articles
|
|
|
|
def _enrichir_tarifs_clients(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit avec les tarifs spécifiques par client/catégorie tarifaire
|
|
Structure: articles[i]["tarifs_clients"] = [{"client": "CLI001", "prix": 125.5}, ...]
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement tarifs clients...")
|
|
|
|
references = [a["reference"] for a in articles if a["reference"]]
|
|
if not references:
|
|
return articles
|
|
|
|
placeholders = ",".join(["?"] * len(references))
|
|
query = f"""
|
|
SELECT
|
|
AR_Ref,
|
|
AC_Categorie,
|
|
CT_Num,
|
|
AC_PrixVen,
|
|
AC_Coef,
|
|
AC_PrixTTC,
|
|
AC_Arrondi,
|
|
AC_QteMont,
|
|
EG_Champ,
|
|
AC_PrixDev,
|
|
AC_Devise,
|
|
AC_Remise,
|
|
AC_Calcul,
|
|
AC_TypeRem,
|
|
AC_RefClient,
|
|
AC_CoefNouv,
|
|
AC_PrixVenNouv,
|
|
AC_PrixDevNouv,
|
|
AC_RemiseNouv,
|
|
AC_DateApplication,
|
|
cbCreation,
|
|
cbModification
|
|
FROM F_ARTCLIENT
|
|
WHERE AR_Ref IN ({placeholders})
|
|
ORDER BY AR_Ref, AC_Categorie, CT_Num
|
|
"""
|
|
|
|
cursor.execute(query, references)
|
|
rows = cursor.fetchall()
|
|
|
|
tarifs_map = {}
|
|
for row in rows:
|
|
ref = self._safe_strip(row[0])
|
|
if not ref:
|
|
continue
|
|
|
|
if ref not in tarifs_map:
|
|
tarifs_map[ref] = []
|
|
|
|
tarifs_map[ref].append({
|
|
"categorie": int(row[1]) if row[1] else 0,
|
|
"client_num": self._safe_strip(row[2]),
|
|
"prix_vente": float(row[3]) if row[3] else 0.0,
|
|
"coefficient": float(row[4]) if row[4] else 0.0,
|
|
"prix_ttc": float(row[5]) if row[5] else 0.0,
|
|
"arrondi": float(row[6]) if row[6] else 0.0,
|
|
"qte_montant": float(row[7]) if row[7] else 0.0,
|
|
"enumere_gamme": int(row[8]) if row[8] else 0,
|
|
"prix_devise": float(row[9]) if row[9] else 0.0,
|
|
"devise": int(row[10]) if row[10] else 0,
|
|
"remise": float(row[11]) if row[11] else 0.0,
|
|
"mode_calcul": int(row[12]) if row[12] else 0,
|
|
"type_remise": int(row[13]) if row[13] else 0,
|
|
"ref_client": self._safe_strip(row[14]),
|
|
"coef_nouveau": float(row[15]) if row[15] else 0.0,
|
|
"prix_vente_nouveau": float(row[16]) if row[16] else 0.0,
|
|
"prix_devise_nouveau": float(row[17]) if row[17] else 0.0,
|
|
"remise_nouvelle": float(row[18]) if row[18] else 0.0,
|
|
"date_application": row[19],
|
|
"date_creation": row[20],
|
|
"date_modification": row[21],
|
|
})
|
|
|
|
for article in articles:
|
|
article["tarifs_clients"] = tarifs_map.get(article["reference"], [])
|
|
article["nb_tarifs_clients"] = len(article["tarifs_clients"])
|
|
|
|
logger.info(f" ✓ {len(tarifs_map)} articles avec tarifs clients")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur tarifs clients: {e}")
|
|
for article in articles:
|
|
article["tarifs_clients"] = []
|
|
article["nb_tarifs_clients"] = 0
|
|
return articles
|
|
|
|
def _enrichir_nomenclature(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit avec la nomenclature de production (composants, opérations)
|
|
Structure: articles[i]["composants"] = [{"operation": "OP10", "ressource": "RES01"}, ...]
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement nomenclature...")
|
|
|
|
references = [a["reference"] for a in articles if a["reference"]]
|
|
if not references:
|
|
return articles
|
|
|
|
placeholders = ",".join(["?"] * len(references))
|
|
query = f"""
|
|
SELECT
|
|
AR_Ref,
|
|
AT_Operation,
|
|
RP_Code,
|
|
AT_Temps,
|
|
AT_Type,
|
|
AT_Description,
|
|
AT_Ordre,
|
|
AG_No1Comp,
|
|
AG_No2Comp,
|
|
AT_TypeRessource,
|
|
AT_Chevauche,
|
|
AT_Demarre,
|
|
AT_OperationChevauche,
|
|
AT_ValeurChevauche,
|
|
AT_TypeChevauche,
|
|
cbCreation,
|
|
cbModification
|
|
FROM F_ARTCOMPO
|
|
WHERE AR_Ref IN ({placeholders})
|
|
ORDER BY AR_Ref, AT_Ordre, AT_Operation
|
|
"""
|
|
|
|
cursor.execute(query, references)
|
|
rows = cursor.fetchall()
|
|
|
|
composants_map = {}
|
|
for row in rows:
|
|
ref = self._safe_strip(row[0])
|
|
if not ref:
|
|
continue
|
|
|
|
if ref not in composants_map:
|
|
composants_map[ref] = []
|
|
|
|
composants_map[ref].append({
|
|
"operation": self._safe_strip(row[1]),
|
|
"code_ressource": self._safe_strip(row[2]),
|
|
"temps": float(row[3]) if row[3] else 0.0,
|
|
"type": int(row[4]) if row[4] else 0,
|
|
"description": self._safe_strip(row[5]),
|
|
"ordre": int(row[6]) if row[6] else 0,
|
|
"gamme_1_comp": int(row[7]) if row[7] else 0,
|
|
"gamme_2_comp": int(row[8]) if row[8] else 0,
|
|
"type_ressource": int(row[9]) if row[9] else 0,
|
|
"chevauche": int(row[10]) if row[10] else 0,
|
|
"demarre": int(row[11]) if row[11] else 0,
|
|
"operation_chevauche": self._safe_strip(row[12]),
|
|
"valeur_chevauche": float(row[13]) if row[13] else 0.0,
|
|
"type_chevauche": int(row[14]) if row[14] else 0,
|
|
"date_creation": row[15],
|
|
"date_modification": row[16],
|
|
})
|
|
|
|
for article in articles:
|
|
article["composants"] = composants_map.get(article["reference"], [])
|
|
article["nb_composants"] = len(article["composants"])
|
|
|
|
logger.info(f" ✓ {len(composants_map)} articles avec nomenclature")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur nomenclature: {e}")
|
|
for article in articles:
|
|
article["composants"] = []
|
|
article["nb_composants"] = 0
|
|
return articles
|
|
|
|
def _enrichir_compta_articles(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit avec les comptes comptables spécifiques par article
|
|
Structure: articles[i]["compta_vente/achat/stock"] = {...}
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement comptabilité articles...")
|
|
|
|
references = [a["reference"] for a in articles if a["reference"]]
|
|
if not references:
|
|
return articles
|
|
|
|
placeholders = ",".join(["?"] * len(references))
|
|
query = f"""
|
|
SELECT
|
|
AR_Ref,
|
|
ACP_Type,
|
|
ACP_Champ,
|
|
ACP_ComptaCPT_CompteG,
|
|
ACP_ComptaCPT_CompteA,
|
|
ACP_ComptaCPT_Taxe1,
|
|
ACP_ComptaCPT_Taxe2,
|
|
ACP_ComptaCPT_Taxe3,
|
|
ACP_ComptaCPT_Date1,
|
|
ACP_ComptaCPT_Date2,
|
|
ACP_ComptaCPT_Date3,
|
|
ACP_ComptaCPT_TaxeAnc1,
|
|
ACP_ComptaCPT_TaxeAnc2,
|
|
ACP_ComptaCPT_TaxeAnc3,
|
|
ACP_TypeFacture,
|
|
cbCreation,
|
|
cbModification
|
|
FROM F_ARTCOMPTA
|
|
WHERE AR_Ref IN ({placeholders})
|
|
ORDER BY AR_Ref, ACP_Type, ACP_Champ
|
|
"""
|
|
|
|
cursor.execute(query, references)
|
|
rows = cursor.fetchall()
|
|
|
|
compta_map = {}
|
|
for row in rows:
|
|
ref = self._safe_strip(row[0])
|
|
if not ref:
|
|
continue
|
|
|
|
if ref not in compta_map:
|
|
compta_map[ref] = {"vente": [], "achat": [], "stock": []}
|
|
|
|
type_compta = int(row[1]) if row[1] else 0
|
|
type_key = {0: "vente", 1: "achat", 2: "stock"}.get(type_compta, "autre")
|
|
|
|
compta_entry = {
|
|
"champ": int(row[2]) if row[2] else 0,
|
|
"compte_general": self._safe_strip(row[3]),
|
|
"compte_auxiliaire": self._safe_strip(row[4]),
|
|
"taxe_1": self._safe_strip(row[5]),
|
|
"taxe_2": self._safe_strip(row[6]),
|
|
"taxe_3": self._safe_strip(row[7]),
|
|
"taxe_date_1": row[8],
|
|
"taxe_date_2": row[9],
|
|
"taxe_date_3": row[10],
|
|
"taxe_anc_1": self._safe_strip(row[11]),
|
|
"taxe_anc_2": self._safe_strip(row[12]),
|
|
"taxe_anc_3": self._safe_strip(row[13]),
|
|
"type_facture": int(row[14]) if row[14] else 0,
|
|
"date_creation": row[15],
|
|
"date_modification": row[16],
|
|
}
|
|
|
|
if type_key in compta_map[ref]:
|
|
compta_map[ref][type_key].append(compta_entry)
|
|
|
|
for article in articles:
|
|
compta = compta_map.get(article["reference"], {"vente": [], "achat": [], "stock": []})
|
|
article["compta_vente"] = compta["vente"]
|
|
article["compta_achat"] = compta["achat"]
|
|
article["compta_stock"] = compta["stock"]
|
|
|
|
logger.info(f" ✓ {len(compta_map)} articles avec compta spécifique")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur comptabilité articles: {e}")
|
|
for article in articles:
|
|
article["compta_vente"] = []
|
|
article["compta_achat"] = []
|
|
article["compta_stock"] = []
|
|
return articles
|
|
|
|
def _enrichir_fournisseurs_multiples(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit avec TOUS les fournisseurs (pas seulement le principal)
|
|
Structure: articles[i]["fournisseurs"] = [{"num": "F001", "ref": "REF123", "prix": 45.5}, ...]
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement fournisseurs multiples...")
|
|
|
|
references = [a["reference"] for a in articles if a["reference"]]
|
|
if not references:
|
|
return articles
|
|
|
|
placeholders = ",".join(["?"] * len(references))
|
|
query = f"""
|
|
SELECT
|
|
AR_Ref,
|
|
CT_Num,
|
|
AF_RefFourniss,
|
|
AF_PrixAch,
|
|
AF_Unite,
|
|
AF_Conversion,
|
|
AF_DelaiAppro,
|
|
AF_Garantie,
|
|
AF_Colisage,
|
|
AF_QteMini,
|
|
AF_QteMont,
|
|
EG_Champ,
|
|
AF_Principal,
|
|
AF_PrixDev,
|
|
AF_Devise,
|
|
AF_Remise,
|
|
AF_ConvDiv,
|
|
AF_TypeRem,
|
|
AF_CodeBarre,
|
|
AF_PrixAchNouv,
|
|
AF_PrixDevNouv,
|
|
AF_RemiseNouv,
|
|
AF_DateApplication,
|
|
cbCreation,
|
|
cbModification
|
|
FROM F_ARTFOURNISS
|
|
WHERE AR_Ref IN ({placeholders})
|
|
ORDER BY AR_Ref, AF_Principal DESC, CT_Num
|
|
"""
|
|
|
|
cursor.execute(query, references)
|
|
rows = cursor.fetchall()
|
|
|
|
fournisseurs_map = {}
|
|
for row in rows:
|
|
ref = self._safe_strip(row[0])
|
|
if not ref:
|
|
continue
|
|
|
|
if ref not in fournisseurs_map:
|
|
fournisseurs_map[ref] = []
|
|
|
|
fournisseurs_map[ref].append({
|
|
"fournisseur_num": self._safe_strip(row[1]),
|
|
"ref_fournisseur": self._safe_strip(row[2]),
|
|
"prix_achat": float(row[3]) if row[3] else 0.0,
|
|
"unite": self._safe_strip(row[4]),
|
|
"conversion": float(row[5]) if row[5] else 0.0,
|
|
"delai_appro": int(row[6]) if row[6] else 0,
|
|
"garantie": int(row[7]) if row[7] else 0,
|
|
"colisage": int(row[8]) if row[8] else 0,
|
|
"qte_mini": float(row[9]) if row[9] else 0.0,
|
|
"qte_montant": float(row[10]) if row[10] else 0.0,
|
|
"enumere_gamme": int(row[11]) if row[11] else 0,
|
|
"est_principal": bool(row[12]),
|
|
"prix_devise": float(row[13]) if row[13] else 0.0,
|
|
"devise": int(row[14]) if row[14] else 0,
|
|
"remise": float(row[15]) if row[15] else 0.0,
|
|
"conversion_devise": float(row[16]) if row[16] else 0.0,
|
|
"type_remise": int(row[17]) if row[17] else 0,
|
|
"code_barre_fournisseur": self._safe_strip(row[18]),
|
|
"prix_achat_nouveau": float(row[19]) if row[19] else 0.0,
|
|
"prix_devise_nouveau": float(row[20]) if row[20] else 0.0,
|
|
"remise_nouvelle": float(row[21]) if row[21] else 0.0,
|
|
"date_application": row[22],
|
|
"date_creation": row[23],
|
|
"date_modification": row[24],
|
|
})
|
|
|
|
for article in articles:
|
|
article["fournisseurs"] = fournisseurs_map.get(article["reference"], [])
|
|
article["nb_fournisseurs"] = len(article["fournisseurs"])
|
|
|
|
logger.info(f" ✓ {len(fournisseurs_map)} articles avec fournisseurs multiples")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur fournisseurs multiples: {e}")
|
|
for article in articles:
|
|
article["fournisseurs"] = []
|
|
article["nb_fournisseurs"] = 0
|
|
return articles
|
|
|
|
def _enrichir_depots_details(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit les stocks avec les informations détaillées des dépôts
|
|
Ajoute le nom du dépôt à chaque ligne de stock
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement détails dépôts...")
|
|
|
|
query = """
|
|
SELECT
|
|
DE_No,
|
|
DE_Intitule,
|
|
DE_Code,
|
|
DE_Adresse,
|
|
DE_Complement,
|
|
DE_CodePostal,
|
|
DE_Ville,
|
|
DE_Contact,
|
|
DE_Principal,
|
|
DE_CatCompta,
|
|
DE_Region,
|
|
DE_Pays,
|
|
DE_EMail,
|
|
DE_Telephone,
|
|
DE_Telecopie,
|
|
DP_NoDefaut,
|
|
DE_Exclure
|
|
FROM F_DEPOT
|
|
"""
|
|
|
|
cursor.execute(query)
|
|
rows = cursor.fetchall()
|
|
|
|
depots_map = {}
|
|
for row in rows:
|
|
de_no = self._safe_strip(row[0])
|
|
if not de_no:
|
|
continue
|
|
|
|
depots_map[de_no] = {
|
|
"depot_num": de_no,
|
|
"depot_nom": self._safe_strip(row[1]),
|
|
"depot_code": self._safe_strip(row[2]),
|
|
"depot_adresse": self._safe_strip(row[3]),
|
|
"depot_complement": self._safe_strip(row[4]),
|
|
"depot_code_postal": self._safe_strip(row[5]),
|
|
"depot_ville": self._safe_strip(row[6]),
|
|
"depot_contact": self._safe_strip(row[7]),
|
|
"depot_est_principal": bool(row[8]),
|
|
"depot_categorie_compta": int(row[9]) if row[9] else 0,
|
|
"depot_region": self._safe_strip(row[10]),
|
|
"depot_pays": self._safe_strip(row[11]),
|
|
"depot_email": self._safe_strip(row[12]),
|
|
"depot_telephone": self._safe_strip(row[13]),
|
|
"depot_fax": self._safe_strip(row[14]),
|
|
"depot_emplacement_defaut": self._safe_strip(row[15]),
|
|
"depot_exclu": bool(row[16]),
|
|
}
|
|
|
|
logger.info(f" → {len(depots_map)} dépôts chargés")
|
|
|
|
for article in articles:
|
|
for empl in article.get("emplacements", []):
|
|
depot_num = empl.get("depot")
|
|
if depot_num and depot_num in depots_map:
|
|
empl.update(depots_map[depot_num])
|
|
|
|
logger.info(f" ✓ Emplacements enrichis avec détails dépôts")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur détails dépôts: {e}")
|
|
return articles
|
|
|
|
def _enrichir_emplacements_details(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit les emplacements avec leurs détails (zone, type, etc.)
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement détails emplacements...")
|
|
|
|
query = """
|
|
SELECT
|
|
DE_No,
|
|
DP_No,
|
|
DP_Code,
|
|
DP_Intitule,
|
|
DP_Zone,
|
|
DP_Type
|
|
FROM F_DEPOTEMPL
|
|
"""
|
|
|
|
cursor.execute(query)
|
|
rows = cursor.fetchall()
|
|
|
|
emplacements_map = {}
|
|
for row in rows:
|
|
de_no = self._safe_strip(row[0])
|
|
dp_no = self._safe_strip(row[1])
|
|
|
|
if not de_no or not dp_no:
|
|
continue
|
|
|
|
key = f"{de_no}_{dp_no}"
|
|
emplacements_map[key] = {
|
|
"emplacement_code": self._safe_strip(row[2]),
|
|
"emplacement_libelle": self._safe_strip(row[3]),
|
|
"emplacement_zone": self._safe_strip(row[4]),
|
|
"emplacement_type": int(row[5]) if row[5] else 0,
|
|
}
|
|
|
|
logger.info(f" → {len(emplacements_map)} emplacements détaillés chargés")
|
|
|
|
for article in articles:
|
|
for empl in article.get("emplacements", []):
|
|
depot = empl.get("depot")
|
|
emplacement = empl.get("emplacement")
|
|
if depot and emplacement:
|
|
key = f"{depot}_{emplacement}"
|
|
if key in emplacements_map:
|
|
empl.update(emplacements_map[key])
|
|
|
|
logger.info(f" ✓ Emplacements enrichis avec détails")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur détails emplacements: {e}")
|
|
return articles
|
|
|
|
def _enrichir_gammes_enumeres(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit les gammes avec leurs libellés depuis F_ENUMGAMME et P_GAMME
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement énumérés gammes...")
|
|
|
|
query_pgamme = "SELECT G_Intitule, G_Type FROM P_GAMME ORDER BY G_Type"
|
|
cursor.execute(query_pgamme)
|
|
pgamme_rows = cursor.fetchall()
|
|
|
|
gammes_config = {}
|
|
for idx, row in enumerate(pgamme_rows):
|
|
gammes_config[idx + 1] = {
|
|
"nom": self._safe_strip(row[0]),
|
|
"type": int(row[1]) if row[1] else 0,
|
|
}
|
|
|
|
logger.info(f" → Configuration gammes: {gammes_config}")
|
|
|
|
query_enum = """
|
|
SELECT
|
|
EG_Champ,
|
|
EG_Ligne,
|
|
EG_Enumere,
|
|
EG_BorneSup
|
|
FROM F_ENUMGAMME
|
|
ORDER BY EG_Champ, EG_Ligne
|
|
"""
|
|
|
|
cursor.execute(query_enum)
|
|
enum_rows = cursor.fetchall()
|
|
|
|
enumeres_map = {}
|
|
for row in enum_rows:
|
|
champ = int(row[0]) if row[0] else 0
|
|
enumere = self._safe_strip(row[2])
|
|
|
|
if not enumere:
|
|
continue
|
|
|
|
key = f"{champ}_{enumere}"
|
|
enumeres_map[key] = {
|
|
"ligne": int(row[1]) if row[1] else 0,
|
|
"enumere": enumere,
|
|
"borne_sup": float(row[3]) if row[3] else 0.0,
|
|
"gamme_nom": gammes_config.get(champ, {}).get("nom", f"Gamme {champ}"),
|
|
}
|
|
|
|
logger.info(f" → {len(enumeres_map)} énumérés chargés")
|
|
|
|
for article in articles:
|
|
for gamme in article.get("gammes", []):
|
|
num_gamme = gamme.get("numero_gamme")
|
|
enumere = gamme.get("enumere")
|
|
|
|
if num_gamme and enumere:
|
|
key = f"{num_gamme}_{enumere}"
|
|
if key in enumeres_map:
|
|
gamme.update(enumeres_map[key])
|
|
|
|
logger.info(f" ✓ Gammes enrichies avec énumérés")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur énumérés gammes: {e}")
|
|
return articles
|
|
|
|
def _enrichir_references_enumerees(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit avec les références énumérées (articles avec gammes)
|
|
Structure: articles[i]["refs_enumerees"] = [{"gamme1": 1, "gamme2": 3, "ref": "ART-R-B"}, ...]
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement références énumérées...")
|
|
|
|
references = [a["reference"] for a in articles if a["reference"]]
|
|
if not references:
|
|
return articles
|
|
|
|
placeholders = ",".join(["?"] * len(references))
|
|
query = f"""
|
|
SELECT
|
|
AR_Ref,
|
|
AG_No1,
|
|
AG_No2,
|
|
AE_Ref,
|
|
AE_PrixAch,
|
|
AE_CodeBarre,
|
|
AE_PrixAchNouv,
|
|
AE_EdiCode,
|
|
AE_Sommeil,
|
|
cbCreation,
|
|
cbModification
|
|
FROM F_ARTENUMREF
|
|
WHERE AR_Ref IN ({placeholders})
|
|
ORDER BY AR_Ref, AG_No1, AG_No2
|
|
"""
|
|
|
|
cursor.execute(query, references)
|
|
rows = cursor.fetchall()
|
|
|
|
refs_enum_map = {}
|
|
for row in rows:
|
|
ref = self._safe_strip(row[0])
|
|
if not ref:
|
|
continue
|
|
|
|
if ref not in refs_enum_map:
|
|
refs_enum_map[ref] = []
|
|
|
|
refs_enum_map[ref].append({
|
|
"gamme_1": int(row[1]) if row[1] else 0,
|
|
"gamme_2": int(row[2]) if row[2] else 0,
|
|
"reference_enumeree": self._safe_strip(row[3]),
|
|
"prix_achat": float(row[4]) if row[4] else 0.0,
|
|
"code_barre": self._safe_strip(row[5]),
|
|
"prix_achat_nouveau": float(row[6]) if row[6] else 0.0,
|
|
"edi_code": self._safe_strip(row[7]),
|
|
"en_sommeil": bool(row[8]),
|
|
"date_creation": row[9],
|
|
"date_modification": row[10],
|
|
})
|
|
|
|
for article in articles:
|
|
article["refs_enumerees"] = refs_enum_map.get(article["reference"], [])
|
|
article["nb_refs_enumerees"] = len(article["refs_enumerees"])
|
|
|
|
logger.info(f" ✓ {len(refs_enum_map)} articles avec références énumérées")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur références énumérées: {e}")
|
|
for article in articles:
|
|
article["refs_enumerees"] = []
|
|
article["nb_refs_enumerees"] = 0
|
|
return articles
|
|
|
|
def _enrichir_medias_articles(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit avec les médias attachés (photos, documents, etc.)
|
|
Structure: articles[i]["medias"] = [{"fichier": "photo.jpg", "type": "image/jpeg"}, ...]
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement médias articles...")
|
|
|
|
references = [a["reference"] for a in articles if a["reference"]]
|
|
if not references:
|
|
return articles
|
|
|
|
placeholders = ",".join(["?"] * len(references))
|
|
query = f"""
|
|
SELECT
|
|
AR_Ref,
|
|
ME_Commentaire,
|
|
ME_Fichier,
|
|
ME_TypeMIME,
|
|
ME_Origine,
|
|
ME_GedId,
|
|
cbCreation,
|
|
cbModification
|
|
FROM F_ARTICLEMEDIA
|
|
WHERE AR_Ref IN ({placeholders})
|
|
ORDER BY AR_Ref, cbCreation
|
|
"""
|
|
|
|
cursor.execute(query, references)
|
|
rows = cursor.fetchall()
|
|
|
|
medias_map = {}
|
|
for row in rows:
|
|
ref = self._safe_strip(row[0])
|
|
if not ref:
|
|
continue
|
|
|
|
if ref not in medias_map:
|
|
medias_map[ref] = []
|
|
|
|
medias_map[ref].append({
|
|
"commentaire": self._safe_strip(row[1]),
|
|
"fichier": self._safe_strip(row[2]),
|
|
"type_mime": self._safe_strip(row[3]),
|
|
"origine": int(row[4]) if row[4] else 0,
|
|
"ged_id": self._safe_strip(row[5]),
|
|
"date_creation": row[6],
|
|
"date_modification": row[7],
|
|
})
|
|
|
|
for article in articles:
|
|
article["medias"] = medias_map.get(article["reference"], [])
|
|
article["nb_medias"] = len(article["medias"])
|
|
|
|
logger.info(f" ✓ {len(medias_map)} articles avec médias")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur médias: {e}")
|
|
for article in articles:
|
|
article["medias"] = []
|
|
article["nb_medias"] = 0
|
|
return articles
|
|
|
|
def _enrichir_prix_gammes(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit avec les prix spécifiques par combinaison de gammes
|
|
Structure: articles[i]["prix_gammes"] = [{"gamme1": 1, "gamme2": 3, "prix_net": 125.5}, ...]
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement prix par gammes...")
|
|
|
|
references = [a["reference"] for a in articles if a["reference"]]
|
|
if not references:
|
|
return articles
|
|
|
|
placeholders = ",".join(["?"] * len(references))
|
|
query = f"""
|
|
SELECT
|
|
AR_Ref,
|
|
AG_No1,
|
|
AG_No2,
|
|
AR_PUNet,
|
|
AR_CoutStd,
|
|
cbCreation,
|
|
cbModification
|
|
FROM F_ARTPRIX
|
|
WHERE AR_Ref IN ({placeholders})
|
|
ORDER BY AR_Ref, AG_No1, AG_No2
|
|
"""
|
|
|
|
cursor.execute(query, references)
|
|
rows = cursor.fetchall()
|
|
|
|
prix_gammes_map = {}
|
|
for row in rows:
|
|
ref = self._safe_strip(row[0])
|
|
if not ref:
|
|
continue
|
|
|
|
if ref not in prix_gammes_map:
|
|
prix_gammes_map[ref] = []
|
|
|
|
prix_gammes_map[ref].append({
|
|
"gamme_1": int(row[1]) if row[1] else 0,
|
|
"gamme_2": int(row[2]) if row[2] else 0,
|
|
"prix_net": float(row[3]) if row[3] else 0.0,
|
|
"cout_standard": float(row[4]) if row[4] else 0.0,
|
|
"date_creation": row[5],
|
|
"date_modification": row[6],
|
|
})
|
|
|
|
for article in articles:
|
|
article["prix_gammes"] = prix_gammes_map.get(article["reference"], [])
|
|
article["nb_prix_gammes"] = len(article["prix_gammes"])
|
|
|
|
logger.info(f" ✓ {len(prix_gammes_map)} articles avec prix par gammes")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur prix gammes: {e}")
|
|
for article in articles:
|
|
article["prix_gammes"] = []
|
|
article["nb_prix_gammes"] = 0
|
|
return articles
|
|
|
|
def _enrichir_conditionnements(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""
|
|
Enrichit avec les conditionnements disponibles
|
|
"""
|
|
try:
|
|
logger.info(f" → Enrichissement conditionnements...")
|
|
|
|
query = """
|
|
SELECT
|
|
EC_Champ,
|
|
EC_Enumere,
|
|
EC_Quantite,
|
|
EC_EdiCode
|
|
FROM F_ENUMCOND
|
|
ORDER BY EC_Champ, EC_Enumere
|
|
"""
|
|
|
|
cursor.execute(query)
|
|
rows = cursor.fetchall()
|
|
|
|
cond_map = {}
|
|
for row in rows:
|
|
champ = int(row[0]) if row[0] else 0
|
|
enumere = self._safe_strip(row[1])
|
|
|
|
if not enumere:
|
|
continue
|
|
|
|
key = f"{champ}_{enumere}"
|
|
cond_map[key] = {
|
|
"champ": champ,
|
|
"enumere": enumere,
|
|
"quantite": float(row[2]) if row[2] else 0.0,
|
|
"edi_code": self._safe_strip(row[3]),
|
|
}
|
|
|
|
logger.info(f" → {len(cond_map)} conditionnements chargés")
|
|
|
|
for article in articles:
|
|
conditionnement = article.get("conditionnement")
|
|
if conditionnement:
|
|
for key, cond_data in cond_map.items():
|
|
if cond_data["enumere"] == conditionnement:
|
|
article["conditionnement_qte"] = cond_data["quantite"]
|
|
article["conditionnement_edi"] = cond_data["edi_code"]
|
|
break
|
|
|
|
logger.info(f" ✓ Conditionnements enrichis")
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur conditionnements: {e}")
|
|
return articles
|
|
|
|
def _mapper_article_depuis_row(self, row_data: Dict, colonnes_config: Dict) -> Dict:
|
|
"""
|
|
Mappe une ligne SQL vers un dictionnaire article normalisé
|
|
|
|
Args:
|
|
row_data: Dictionnaire avec noms de colonnes SQL comme clés
|
|
colonnes_config: Mapping SQL -> noms normalisés
|
|
|
|
Returns:
|
|
Dictionnaire article avec noms normalisés
|
|
"""
|
|
article = {}
|
|
|
|
def get_val(sql_col, default=None, convert_type=None):
|
|
val = row_data.get(sql_col, default)
|
|
if val is None:
|
|
return default
|
|
|
|
if convert_type == float:
|
|
return float(val) if val not in (None, "") else (default or 0.0)
|
|
elif convert_type == int:
|
|
return int(val) if val not in (None, "") else (default or 0)
|
|
elif convert_type == bool:
|
|
return bool(val) if val not in (None, "") else (default or False)
|
|
elif convert_type == str:
|
|
return self._safe_strip(val)
|
|
|
|
return val
|
|
|
|
article["reference"] = get_val("AR_Ref", convert_type=str)
|
|
article["designation"] = get_val("AR_Design", convert_type=str)
|
|
article["code_ean"] = get_val("AR_CodeBarre", convert_type=str)
|
|
article["code_barre"] = get_val("AR_CodeBarre", convert_type=str)
|
|
article["edi_code"] = get_val("AR_EdiCode", convert_type=str)
|
|
article["raccourci"] = get_val("AR_Raccourci", convert_type=str)
|
|
|
|
article["prix_vente"] = get_val("AR_PrixVen", 0.0, float)
|
|
article["prix_achat"] = get_val("AR_PrixAch", 0.0, float)
|
|
article["coef"] = get_val("AR_Coef", 0.0, float)
|
|
article["prix_net"] = get_val("AR_PUNet", 0.0, float)
|
|
article["prix_achat_nouveau"] = get_val("AR_PrixAchNouv", 0.0, float)
|
|
article["coef_nouveau"] = get_val("AR_CoefNouv", 0.0, float)
|
|
article["prix_vente_nouveau"] = get_val("AR_PrixVenNouv", 0.0, float)
|
|
|
|
date_app = get_val("AR_DateApplication")
|
|
article["date_application_prix"] = str(date_app) if date_app else None
|
|
|
|
article["cout_standard"] = get_val("AR_CoutStd", 0.0, float)
|
|
|
|
article["unite_vente"] = get_val("AR_UniteVen", convert_type=str)
|
|
article["unite_poids"] = get_val("AR_UnitePoids", convert_type=str)
|
|
article["poids_net"] = get_val("AR_PoidsNet", 0.0, float)
|
|
article["poids_brut"] = get_val("AR_PoidsBrut", 0.0, float)
|
|
|
|
article["gamme_1"] = get_val("AR_Gamme1", convert_type=str)
|
|
article["gamme_2"] = get_val("AR_Gamme2", convert_type=str)
|
|
|
|
type_val = get_val("AR_Type", 0, int)
|
|
article["type_article"] = type_val
|
|
article["type_article_libelle"] = self._get_type_article_libelle(type_val)
|
|
article["famille_code"] = get_val("FA_CodeFamille", convert_type=str)
|
|
article["nature"] = get_val("AR_Nature", 0, int)
|
|
article["garantie"] = get_val("AR_Garantie", 0, int)
|
|
article["code_fiscal"] = get_val("AR_CodeFiscal", convert_type=str)
|
|
article["pays"] = get_val("AR_Pays", convert_type=str)
|
|
|
|
article["fournisseur_principal"] = get_val("CO_No", 0, int)
|
|
article["conditionnement"] = get_val("AR_Condition", convert_type=str)
|
|
article["nb_colis"] = get_val("AR_NbColis", 0, int)
|
|
article["prevision"] = get_val("AR_Prevision", False, bool)
|
|
|
|
article["suivi_stock"] = get_val("AR_SuiviStock", False, bool)
|
|
article["nomenclature"] = get_val("AR_Nomencl", False, bool)
|
|
article["qte_composant"] = get_val("AR_QteComp", 0.0, float)
|
|
article["qte_operatoire"] = get_val("AR_QteOperatoire", 0.0, float)
|
|
|
|
sommeil = get_val("AR_Sommeil", 0, int)
|
|
article["est_actif"] = (sommeil == 0)
|
|
article["en_sommeil"] = (sommeil == 1)
|
|
article["article_substitut"] = get_val("AR_Substitut", convert_type=str)
|
|
article["soumis_escompte"] = get_val("AR_Escompte", False, bool)
|
|
article["delai"] = get_val("AR_Delai", 0, int)
|
|
|
|
article["stat_01"] = get_val("AR_Stat01", convert_type=str)
|
|
article["stat_02"] = get_val("AR_Stat02", convert_type=str)
|
|
article["stat_03"] = get_val("AR_Stat03", convert_type=str)
|
|
article["stat_04"] = get_val("AR_Stat04", convert_type=str)
|
|
article["stat_05"] = get_val("AR_Stat05", convert_type=str)
|
|
article["hors_statistique"] = get_val("AR_HorsStat", False, bool)
|
|
|
|
article["categorie_1"] = get_val("CL_No1", 0, int)
|
|
article["categorie_2"] = get_val("CL_No2", 0, int)
|
|
article["categorie_3"] = get_val("CL_No3", 0, int)
|
|
article["categorie_4"] = get_val("CL_No4", 0, int)
|
|
|
|
date_modif = get_val("AR_DateModif")
|
|
article["date_modification"] = str(date_modif) if date_modif else None
|
|
|
|
article["vente_debit"] = get_val("AR_VteDebit", False, bool)
|
|
article["non_imprimable"] = get_val("AR_NotImp", False, bool)
|
|
article["transfere"] = get_val("AR_Transfere", False, bool)
|
|
article["publie"] = get_val("AR_Publie", False, bool)
|
|
article["contremarque"] = get_val("AR_Contremarque", False, bool)
|
|
article["fact_poids"] = get_val("AR_FactPoids", False, bool)
|
|
article["fact_forfait"] = get_val("AR_FactForfait", False, bool)
|
|
article["saisie_variable"] = get_val("AR_SaisieVar", False, bool)
|
|
article["fictif"] = get_val("AR_Fictif", False, bool)
|
|
article["sous_traitance"] = get_val("AR_SousTraitance", False, bool)
|
|
article["criticite"] = get_val("AR_Criticite", 0, int)
|
|
|
|
article["reprise_code_defaut"] = get_val("RP_CodeDefaut", convert_type=str)
|
|
article["delai_fabrication"] = get_val("AR_DelaiFabrication", 0, int)
|
|
article["delai_peremption"] = get_val("AR_DelaiPeremption", 0, int)
|
|
article["delai_securite"] = get_val("AR_DelaiSecurite", 0, int)
|
|
article["type_lancement"] = get_val("AR_TypeLancement", 0, int)
|
|
article["cycle"] = get_val("AR_Cycle", 1, int)
|
|
|
|
article["photo"] = get_val("AR_Photo", convert_type=str)
|
|
article["langue_1"] = get_val("AR_Langue1", convert_type=str)
|
|
article["langue_2"] = get_val("AR_Langue2", convert_type=str)
|
|
|
|
article["frais_01_denomination"] = get_val("AR_Frais01FR_Denomination", convert_type=str)
|
|
article["frais_02_denomination"] = get_val("AR_Frais02FR_Denomination", convert_type=str)
|
|
article["frais_03_denomination"] = get_val("AR_Frais03FR_Denomination", convert_type=str)
|
|
|
|
article["marque_commerciale"] = get_val("Marque commerciale", convert_type=str)
|
|
|
|
objectif_val = get_val("Objectif / Qtés vendues")
|
|
if objectif_val is not None:
|
|
article["objectif_qtes_vendues"] = str(float(objectif_val)) if objectif_val not in ("", 0, 0.0) else None
|
|
else:
|
|
article["objectif_qtes_vendues"] = None
|
|
|
|
pourcentage_val = get_val("Pourcentage teneur en or")
|
|
if pourcentage_val is not None:
|
|
article["pourcentage_or"] = str(float(pourcentage_val)) if pourcentage_val not in ("", 0, 0.0) else None
|
|
else:
|
|
article["pourcentage_or"] = None
|
|
|
|
date_com = get_val("1ère commercialisation")
|
|
article["premiere_commercialisation"] = str(date_com) if date_com else None
|
|
|
|
article["interdire_commande"] = get_val("AR_InterdireCommande", False, bool)
|
|
article["exclure"] = get_val("AR_Exclure", False, bool)
|
|
|
|
article["stock_reel"] = 0.0
|
|
article["stock_mini"] = 0.0
|
|
article["stock_maxi"] = 0.0
|
|
article["stock_reserve"] = 0.0
|
|
article["stock_commande"] = 0.0
|
|
article["stock_disponible"] = 0.0
|
|
|
|
article["famille_libelle"] = None
|
|
article["famille_type"] = None
|
|
article["famille_unite_vente"] = None
|
|
article["famille_coef"] = None
|
|
article["famille_suivi_stock"] = None
|
|
article["famille_garantie"] = None
|
|
article["famille_unite_poids"] = None
|
|
article["famille_delai"] = None
|
|
article["famille_nb_colis"] = None
|
|
article["famille_code_fiscal"] = None
|
|
article["famille_escompte"] = None
|
|
article["famille_centrale"] = None
|
|
article["famille_nature"] = None
|
|
article["famille_hors_stat"] = None
|
|
article["famille_pays"] = None
|
|
|
|
article["fournisseur_nom"] = None
|
|
article["tva_code"] = None
|
|
article["tva_taux"] = None
|
|
|
|
return article
|
|
|
|
def _enrichir_stocks_articles(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""Enrichit les articles avec les données de stock depuis F_ARTSTOCK"""
|
|
try:
|
|
logger.info(f" → Enrichissement stocks pour {len(articles)} articles...")
|
|
|
|
references = [a["reference"] for a in articles if a["reference"]]
|
|
|
|
if not references:
|
|
return articles
|
|
|
|
placeholders = ",".join(["?"] * len(references))
|
|
stock_query = f"""
|
|
SELECT
|
|
AR_Ref,
|
|
SUM(ISNULL(AS_QteSto, 0)) as Stock_Total,
|
|
MIN(ISNULL(AS_QteMini, 0)) as Stock_Mini,
|
|
MAX(ISNULL(AS_QteMaxi, 0)) as Stock_Maxi,
|
|
SUM(ISNULL(AS_QteRes, 0)) as Stock_Reserve,
|
|
SUM(ISNULL(AS_QteCom, 0)) as Stock_Commande
|
|
FROM F_ARTSTOCK
|
|
WHERE AR_Ref IN ({placeholders})
|
|
GROUP BY AR_Ref
|
|
"""
|
|
|
|
cursor.execute(stock_query, references)
|
|
stock_rows = cursor.fetchall()
|
|
|
|
stock_map = {}
|
|
for stock_row in stock_rows:
|
|
ref = self._safe_strip(stock_row[0])
|
|
if ref:
|
|
stock_map[ref] = {
|
|
"stock_reel": float(stock_row[1]) if stock_row[1] else 0.0,
|
|
"stock_mini": float(stock_row[2]) if stock_row[2] else 0.0,
|
|
"stock_maxi": float(stock_row[3]) if stock_row[3] else 0.0,
|
|
"stock_reserve": float(stock_row[4]) if stock_row[4] else 0.0,
|
|
"stock_commande": float(stock_row[5]) if stock_row[5] else 0.0,
|
|
}
|
|
|
|
logger.info(f" → {len(stock_map)} articles avec stock trouvés dans F_ARTSTOCK")
|
|
|
|
for article in articles:
|
|
if article["reference"] in stock_map:
|
|
stock_data = stock_map[article["reference"]]
|
|
article.update(stock_data)
|
|
article["stock_disponible"] = (
|
|
article["stock_reel"] - article["stock_reserve"]
|
|
)
|
|
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur enrichissement stocks: {e}", exc_info=True)
|
|
return articles
|
|
|
|
def _enrichir_fournisseurs_articles(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""Enrichit les articles avec le nom du fournisseur principal"""
|
|
try:
|
|
logger.info(f" → Enrichissement fournisseurs...")
|
|
|
|
nums_fournisseurs = list(set([
|
|
a["fournisseur_principal"] for a in articles
|
|
if a.get("fournisseur_principal") and a["fournisseur_principal"] > 0
|
|
]))
|
|
|
|
if not nums_fournisseurs:
|
|
logger.warning(" ⚠ Aucun numéro de fournisseur trouvé dans les articles")
|
|
for article in articles:
|
|
article["fournisseur_nom"] = None
|
|
return articles
|
|
|
|
logger.info(f" → {len(nums_fournisseurs)} fournisseurs uniques à chercher")
|
|
logger.info(f" → Exemples CO_No : {nums_fournisseurs[:5]}")
|
|
|
|
placeholders = ",".join(["?"] * len(nums_fournisseurs))
|
|
fournisseur_query = f"""
|
|
SELECT
|
|
CT_Num,
|
|
CT_Intitule,
|
|
CT_Type
|
|
FROM F_COMPTET
|
|
WHERE CT_Num IN ({placeholders})
|
|
AND CT_Type = 1
|
|
"""
|
|
|
|
cursor.execute(fournisseur_query, nums_fournisseurs)
|
|
fournisseur_rows = cursor.fetchall()
|
|
|
|
logger.info(f" → {len(fournisseur_rows)} fournisseurs trouvés dans F_COMPTET")
|
|
|
|
if len(fournisseur_rows) == 0:
|
|
logger.warning(f" ⚠ Aucun fournisseur trouvé pour CT_Type=1 et CT_Num IN {nums_fournisseurs[:5]}")
|
|
cursor.execute(f"SELECT CT_Num, CT_Type FROM F_COMPTET WHERE CT_Num IN ({placeholders})", nums_fournisseurs)
|
|
tous_types = cursor.fetchall()
|
|
if tous_types:
|
|
logger.info(f" → Trouvé {len(tous_types)} comptes (tous types) : {[(r[0], r[1]) for r in tous_types[:5]]}")
|
|
|
|
fournisseur_map = {}
|
|
for fourn_row in fournisseur_rows:
|
|
num = int(fourn_row[0]) # CT_Num
|
|
nom = self._safe_strip(fourn_row[1]) # CT_Intitule
|
|
type_ct = int(fourn_row[2]) # CT_Type
|
|
fournisseur_map[num] = nom
|
|
logger.debug(f" → Fournisseur mappé : {num} = {nom} (Type={type_ct})")
|
|
|
|
nb_enrichis = 0
|
|
for article in articles:
|
|
num_fourn = article.get("fournisseur_principal")
|
|
if num_fourn and num_fourn in fournisseur_map:
|
|
article["fournisseur_nom"] = fournisseur_map[num_fourn]
|
|
nb_enrichis += 1
|
|
else:
|
|
article["fournisseur_nom"] = None
|
|
|
|
logger.info(f" ✓ {nb_enrichis} articles enrichis avec nom fournisseur")
|
|
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur enrichissement fournisseurs: {e}", exc_info=True)
|
|
for article in articles:
|
|
article["fournisseur_nom"] = None
|
|
return articles
|
|
|
|
def _enrichir_familles_articles(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""Enrichit les articles avec les informations de famille depuis F_FAMILLE"""
|
|
try:
|
|
logger.info(f" → Enrichissement familles pour {len(articles)} articles...")
|
|
|
|
codes_familles_bruts = [
|
|
a.get("famille_code") for a in articles
|
|
if a.get("famille_code") not in (None, "", " ")
|
|
]
|
|
|
|
if codes_familles_bruts:
|
|
logger.info(f" → Exemples de codes familles : {codes_familles_bruts[:5]}")
|
|
|
|
codes_familles = list(set([
|
|
str(code).strip() for code in codes_familles_bruts if code
|
|
]))
|
|
|
|
if not codes_familles:
|
|
logger.warning(" ⚠ Aucun code famille trouvé dans les articles")
|
|
for article in articles:
|
|
self._init_champs_famille_vides(article)
|
|
return articles
|
|
|
|
logger.info(f" → {len(codes_familles)} codes famille uniques")
|
|
|
|
cursor.execute("SELECT TOP 1 * FROM F_FAMILLE")
|
|
colonnes_disponibles = [column[0] for column in cursor.description]
|
|
|
|
colonnes_souhaitees = [
|
|
"FA_CodeFamille",
|
|
"FA_Intitule",
|
|
"FA_Type",
|
|
"FA_UniteVen",
|
|
"FA_Coef",
|
|
"FA_SuiviStock",
|
|
"FA_Garantie",
|
|
"FA_UnitePoids",
|
|
"FA_Delai",
|
|
"FA_NbColis",
|
|
"FA_CodeFiscal",
|
|
"FA_Escompte",
|
|
"FA_Central",
|
|
"FA_Nature",
|
|
"FA_HorsStat",
|
|
"FA_Pays",
|
|
"FA_VteDebit",
|
|
"FA_NotImp",
|
|
"FA_Contremarque",
|
|
"FA_FactPoids",
|
|
"FA_FactForfait",
|
|
"FA_Publie",
|
|
"FA_RacineRef",
|
|
"FA_RacineCB",
|
|
"FA_Raccourci",
|
|
"FA_SousTraitance",
|
|
"FA_Fictif",
|
|
"FA_Criticite",
|
|
]
|
|
|
|
colonnes_a_lire = [col for col in colonnes_souhaitees if col in colonnes_disponibles]
|
|
|
|
if "FA_CodeFamille" not in colonnes_a_lire or "FA_Intitule" not in colonnes_a_lire:
|
|
logger.error(" ✗ Colonnes essentielles manquantes !")
|
|
return articles
|
|
|
|
logger.info(f" → Colonnes disponibles : {len(colonnes_a_lire)}")
|
|
|
|
colonnes_str = ", ".join(colonnes_a_lire)
|
|
placeholders = ",".join(["?"] * len(codes_familles))
|
|
|
|
famille_query = f"""
|
|
SELECT {colonnes_str}
|
|
FROM F_FAMILLE
|
|
WHERE FA_CodeFamille IN ({placeholders})
|
|
"""
|
|
|
|
cursor.execute(famille_query, codes_familles)
|
|
famille_rows = cursor.fetchall()
|
|
|
|
logger.info(f" → {len(famille_rows)} familles trouvées")
|
|
|
|
famille_map = {}
|
|
for fam_row in famille_rows:
|
|
famille_data = {}
|
|
for idx, col in enumerate(colonnes_a_lire):
|
|
famille_data[col] = fam_row[idx]
|
|
|
|
code = self._safe_strip(famille_data.get("FA_CodeFamille"))
|
|
if not code:
|
|
continue
|
|
|
|
famille_map[code] = {
|
|
"famille_libelle": self._safe_strip(famille_data.get("FA_Intitule")),
|
|
"famille_type": int(famille_data.get("FA_Type", 0) or 0),
|
|
"famille_unite_vente": self._safe_strip(famille_data.get("FA_UniteVen")),
|
|
"famille_coef": float(famille_data.get("FA_Coef", 0) or 0),
|
|
"famille_suivi_stock": bool(famille_data.get("FA_SuiviStock", 0)),
|
|
"famille_garantie": int(famille_data.get("FA_Garantie", 0) or 0),
|
|
"famille_unite_poids": self._safe_strip(famille_data.get("FA_UnitePoids")),
|
|
"famille_delai": int(famille_data.get("FA_Delai", 0) or 0),
|
|
"famille_nb_colis": int(famille_data.get("FA_NbColis", 0) or 0),
|
|
"famille_code_fiscal": self._safe_strip(famille_data.get("FA_CodeFiscal")),
|
|
"famille_escompte": bool(famille_data.get("FA_Escompte", 0)),
|
|
"famille_centrale": bool(famille_data.get("FA_Central", 0)),
|
|
"famille_nature": int(famille_data.get("FA_Nature", 0) or 0),
|
|
"famille_hors_stat": bool(famille_data.get("FA_HorsStat", 0)),
|
|
"famille_pays": self._safe_strip(famille_data.get("FA_Pays")),
|
|
}
|
|
|
|
logger.info(f" → {len(famille_map)} familles mappées")
|
|
|
|
nb_enrichis = 0
|
|
for article in articles:
|
|
code_fam = str(article.get("famille_code", "")).strip()
|
|
|
|
if code_fam and code_fam in famille_map:
|
|
article.update(famille_map[code_fam])
|
|
nb_enrichis += 1
|
|
else:
|
|
self._init_champs_famille_vides(article)
|
|
|
|
logger.info(f" ✓ {nb_enrichis} articles enrichis avec infos famille")
|
|
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur enrichissement familles: {e}", exc_info=True)
|
|
for article in articles:
|
|
self._init_champs_famille_vides(article)
|
|
return articles
|
|
|
|
def _init_champs_famille_vides(self, article: Dict):
|
|
"""Initialise les champs famille à None/0"""
|
|
article["famille_libelle"] = None
|
|
article["famille_type"] = None
|
|
article["famille_unite_vente"] = None
|
|
article["famille_coef"] = None
|
|
article["famille_suivi_stock"] = None
|
|
article["famille_garantie"] = None
|
|
article["famille_unite_poids"] = None
|
|
article["famille_delai"] = None
|
|
article["famille_nb_colis"] = None
|
|
article["famille_code_fiscal"] = None
|
|
article["famille_escompte"] = None
|
|
article["famille_centrale"] = None
|
|
article["famille_nature"] = None
|
|
article["famille_hors_stat"] = None
|
|
article["famille_pays"] = None
|
|
|
|
def _enrichir_tva_articles(self, articles: List[Dict], cursor) -> List[Dict]:
|
|
"""Enrichit les articles avec le taux de TVA"""
|
|
try:
|
|
logger.info(f" → Enrichissement TVA...")
|
|
|
|
codes_tva = list(set([
|
|
a["code_fiscal"] for a in articles
|
|
if a.get("code_fiscal")
|
|
]))
|
|
|
|
if not codes_tva:
|
|
for article in articles:
|
|
article["tva_code"] = None
|
|
article["tva_taux"] = None
|
|
return articles
|
|
|
|
placeholders = ",".join(["?"] * len(codes_tva))
|
|
tva_query = f"""
|
|
SELECT
|
|
TA_Code,
|
|
TA_Taux
|
|
FROM F_TAXE
|
|
WHERE TA_Code IN ({placeholders})
|
|
"""
|
|
|
|
cursor.execute(tva_query, codes_tva)
|
|
tva_rows = cursor.fetchall()
|
|
|
|
tva_map = {}
|
|
for tva_row in tva_rows:
|
|
code = self._safe_strip(tva_row[0])
|
|
tva_map[code] = float(tva_row[1]) if tva_row[1] else 0.0
|
|
|
|
logger.info(f" → {len(tva_map)} codes TVA trouvés")
|
|
|
|
for article in articles:
|
|
code_tva = article.get("code_fiscal")
|
|
if code_tva and code_tva in tva_map:
|
|
article["tva_code"] = code_tva
|
|
article["tva_taux"] = tva_map[code_tva]
|
|
else:
|
|
article["tva_code"] = code_tva
|
|
article["tva_taux"] = None
|
|
|
|
return articles
|
|
|
|
except Exception as e:
|
|
logger.error(f" ✗ Erreur enrichissement TVA: {e}", exc_info=True)
|
|
for article in articles:
|
|
article["tva_code"] = article.get("code_fiscal")
|
|
article["tva_taux"] = None
|
|
return articles
|
|
|
|
def _get_type_article_libelle(self, type_val: int) -> str:
|
|
"""Retourne le libellé du type d'article"""
|
|
types = {
|
|
0: "Article",
|
|
1: "Prestation",
|
|
2: "Divers / Frais",
|
|
3: "Nomenclature"
|
|
}
|
|
return types.get(type_val, f"Type {type_val}")
|
|
|
|
def _safe_strip(self, value) -> Optional[str]:
|
|
"""Nettoie une valeur string en toute sécurité"""
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, str):
|
|
stripped = value.strip()
|
|
return stripped if stripped else None
|
|
return str(value).strip() or None
|
|
|
|
|
|
def _convertir_type_pour_sql(self, type_doc: int) -> int:
|
|
"""COM → SQL : 0, 10, 20, 30... → 0, 1, 2, 3..."""
|
|
mapping = {0: 0, 10: 1, 20: 2, 30: 3, 40: 4, 50: 5, 60: 6}
|
|
return mapping.get(type_doc, type_doc)
|
|
|
|
def _convertir_type_depuis_sql(self, type_sql: int) -> int:
|
|
"""SQL → COM : 0, 1, 2, 3... → 0, 10, 20, 30..."""
|
|
mapping = {0: 0, 1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 6: 60}
|
|
return mapping.get(type_sql, type_sql)
|
|
|
|
def _lire_document_sql(self, numero: str, type_doc: int):
|
|
"""
|
|
Lit un document spécifique par son numéro.
|
|
PAS de filtre par préfixe car on cherche un document précis.
|
|
"""
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = """
|
|
SELECT
|
|
d.DO_Piece, d.DO_Date, d.DO_Ref, d.DO_TotalHT, d.DO_TotalTTC,
|
|
d.DO_Statut, d.DO_Tiers, d.DO_DateLivr, d.DO_DateExpedition,
|
|
d.DO_Contact, d.DO_TotalHTNet, d.DO_NetAPayer,
|
|
d.DO_MontantRegle, d.DO_Reliquat, d.DO_TxEscompte, d.DO_Escompte,
|
|
d.DO_Taxe1, d.DO_Taxe2, d.DO_Taxe3,
|
|
d.DO_CodeTaxe1, d.DO_CodeTaxe2, d.DO_CodeTaxe3,
|
|
d.DO_EStatut, d.DO_Imprim, d.DO_Valide, d.DO_Cloture,
|
|
d.DO_Transfere, d.DO_Souche, d.DO_PieceOrig, d.DO_GUID,
|
|
d.CA_Num, d.CG_Num, d.DO_Expedit, d.DO_Condition,
|
|
d.DO_Tarif, d.DO_TypeFrais, d.DO_ValFrais,
|
|
d.DO_TypeFranco, d.DO_ValFranco,
|
|
c.CT_Intitule, c.CT_Adresse, c.CT_CodePostal,
|
|
c.CT_Ville, c.CT_Telephone, c.CT_EMail
|
|
FROM F_DOCENTETE d
|
|
LEFT JOIN F_COMPTET c ON d.DO_Tiers = c.CT_Num
|
|
WHERE d.DO_Piece = ? AND d.DO_Type = ?
|
|
"""
|
|
|
|
logger.info(f"[SQL READ] Lecture directe de {numero} (type={type_doc})")
|
|
|
|
cursor.execute(query, (numero, type_doc))
|
|
row = cursor.fetchone()
|
|
|
|
if not row:
|
|
logger.warning(
|
|
f"[SQL READ] Document {numero} (type={type_doc}) INTROUVABLE dans F_DOCENTETE"
|
|
)
|
|
return None
|
|
|
|
numero_piece = self._safe_strip(row[0])
|
|
logger.info(f"[SQL READ] Document trouvé: {numero_piece}")
|
|
|
|
|
|
doc = {
|
|
"numero": numero_piece,
|
|
"reference": self._safe_strip(row[2]), # DO_Ref
|
|
"date": str(row[1]) if row[1] else "", # DO_Date
|
|
"date_livraison": (str(row[7]) if row[7] else ""), # DO_DateLivr
|
|
"date_expedition": (
|
|
str(row[8]) if row[8] else ""
|
|
), # DO_DateExpedition
|
|
"client_code": self._safe_strip(row[6]), # DO_Tiers
|
|
"client_intitule": self._safe_strip(row[39]), # CT_Intitule
|
|
"client_adresse": self._safe_strip(row[40]), # CT_Adresse
|
|
"client_code_postal": self._safe_strip(row[41]), # CT_CodePostal
|
|
"client_ville": self._safe_strip(row[42]), # CT_Ville
|
|
"client_telephone": self._safe_strip(row[43]), # CT_Telephone
|
|
"client_email": self._safe_strip(row[44]), # CT_EMail
|
|
"contact": self._safe_strip(row[9]), # DO_Contact
|
|
"total_ht": float(row[3]) if row[3] else 0.0, # DO_TotalHT
|
|
"total_ht_net": float(row[10]) if row[10] else 0.0, # DO_TotalHTNet
|
|
"total_ttc": float(row[4]) if row[4] else 0.0, # DO_TotalTTC
|
|
"net_a_payer": float(row[11]) if row[11] else 0.0, # DO_NetAPayer
|
|
"montant_regle": (
|
|
float(row[12]) if row[12] else 0.0
|
|
), # DO_MontantRegle
|
|
"reliquat": float(row[13]) if row[13] else 0.0, # DO_Reliquat
|
|
"taux_escompte": (
|
|
float(row[14]) if row[14] else 0.0
|
|
), # DO_TxEscompte
|
|
"escompte": float(row[15]) if row[15] else 0.0, # DO_Escompte
|
|
"taxe1": float(row[16]) if row[16] else 0.0, # DO_Taxe1
|
|
"taxe2": float(row[17]) if row[17] else 0.0, # DO_Taxe2
|
|
"taxe3": float(row[18]) if row[18] else 0.0, # DO_Taxe3
|
|
"code_taxe1": self._safe_strip(row[19]), # DO_CodeTaxe1
|
|
"code_taxe2": self._safe_strip(row[20]), # DO_CodeTaxe2
|
|
"code_taxe3": self._safe_strip(row[21]), # DO_CodeTaxe3
|
|
"statut": int(row[5]) if row[5] is not None else 0, # DO_Statut
|
|
"statut_estatut": (
|
|
int(row[22]) if row[22] is not None else 0
|
|
), # DO_EStatut
|
|
"imprime": int(row[23]) if row[23] is not None else 0, # DO_Imprim
|
|
"valide": int(row[24]) if row[24] is not None else 0, # DO_Valide
|
|
"cloture": int(row[25]) if row[25] is not None else 0, # DO_Cloture
|
|
"transfere": (
|
|
int(row[26]) if row[26] is not None else 0
|
|
), # DO_Transfere
|
|
"souche": int(row[27]) if row[27] is not None else 0, # DO_Souche
|
|
"piece_origine": self._safe_strip(row[28]), # DO_PieceOrig
|
|
"guid": self._safe_strip(row[29]), # DO_GUID
|
|
"ca_num": self._safe_strip(row[30]), # CA_Num
|
|
"cg_num": self._safe_strip(row[31]), # CG_Num
|
|
"expedition": (
|
|
int(row[32]) if row[32] is not None else 1
|
|
), # DO_Expedit
|
|
"condition": (
|
|
int(row[33]) if row[33] is not None else 1
|
|
), # DO_Condition
|
|
"tarif": int(row[34]) if row[34] is not None else 1, # DO_Tarif
|
|
"type_frais": (
|
|
int(row[35]) if row[35] is not None else 0
|
|
), # DO_TypeFrais
|
|
"valeur_frais": float(row[36]) if row[36] else 0.0, # DO_ValFrais
|
|
"type_franco": (
|
|
int(row[37]) if row[37] is not None else 0
|
|
), # DO_TypeFranco
|
|
"valeur_franco": float(row[38]) if row[38] else 0.0, # DO_ValFranco
|
|
}
|
|
|
|
cursor.execute(
|
|
"""
|
|
SELECT
|
|
dl.*,
|
|
a.AR_Design, a.FA_CodeFamille, a.AR_PrixTTC, a.AR_PrixVen, a.AR_PrixAch,
|
|
a.AR_Gamme1, a.AR_Gamme2, a.AR_CodeBarre, a.AR_CoutStd,
|
|
a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_UniteVen,
|
|
a.AR_Type, a.AR_Nature, a.AR_Escompte, a.AR_Garantie
|
|
FROM F_DOCLIGNE dl
|
|
LEFT JOIN F_ARTICLE a ON dl.AR_Ref = a.AR_Ref
|
|
WHERE dl.DO_Piece = ? AND dl.DO_Type = ?
|
|
ORDER BY dl.DL_Ligne
|
|
""",
|
|
(numero, type_doc),
|
|
)
|
|
|
|
lignes = []
|
|
for ligne_row in cursor.fetchall():
|
|
montant_ht = (
|
|
float(ligne_row.DL_MontantHT) if ligne_row.DL_MontantHT else 0.0
|
|
)
|
|
montant_net = (
|
|
float(ligne_row.DL_MontantNet)
|
|
if hasattr(ligne_row, "DL_MontantNet")
|
|
and ligne_row.DL_MontantNet
|
|
else montant_ht
|
|
)
|
|
|
|
taux_taxe1 = (
|
|
float(ligne_row.DL_Taxe1)
|
|
if hasattr(ligne_row, "DL_Taxe1") and ligne_row.DL_Taxe1
|
|
else 0.0
|
|
)
|
|
taux_taxe2 = (
|
|
float(ligne_row.DL_Taxe2)
|
|
if hasattr(ligne_row, "DL_Taxe2") and ligne_row.DL_Taxe2
|
|
else 0.0
|
|
)
|
|
taux_taxe3 = (
|
|
float(ligne_row.DL_Taxe3)
|
|
if hasattr(ligne_row, "DL_Taxe3") and ligne_row.DL_Taxe3
|
|
else 0.0
|
|
)
|
|
|
|
total_taux_taxes = taux_taxe1 + taux_taxe2 + taux_taxe3
|
|
montant_ttc = montant_net * (1 + total_taux_taxes / 100)
|
|
|
|
montant_taxe1 = montant_net * (taux_taxe1 / 100)
|
|
montant_taxe2 = montant_net * (taux_taxe2 / 100)
|
|
montant_taxe3 = montant_net * (taux_taxe3 / 100)
|
|
|
|
ligne = {
|
|
"numero_ligne": (
|
|
int(ligne_row.DL_Ligne) if ligne_row.DL_Ligne else 0
|
|
),
|
|
"article_code": self._safe_strip(ligne_row.AR_Ref),
|
|
"designation": self._safe_strip(ligne_row.DL_Design),
|
|
"designation_article": self._safe_strip(ligne_row.AR_Design),
|
|
"quantite": (
|
|
float(ligne_row.DL_Qte) if ligne_row.DL_Qte else 0.0
|
|
),
|
|
"quantite_livree": (
|
|
float(ligne_row.DL_QteLiv)
|
|
if hasattr(ligne_row, "DL_QteLiv") and ligne_row.DL_QteLiv
|
|
else 0.0
|
|
),
|
|
"quantite_reservee": (
|
|
float(ligne_row.DL_QteRes)
|
|
if hasattr(ligne_row, "DL_QteRes") and ligne_row.DL_QteRes
|
|
else 0.0
|
|
),
|
|
"unite": (
|
|
self._safe_strip(ligne_row.DL_Unite)
|
|
if hasattr(ligne_row, "DL_Unite")
|
|
else ""
|
|
),
|
|
"prix_unitaire_ht": (
|
|
float(ligne_row.DL_PrixUnitaire)
|
|
if ligne_row.DL_PrixUnitaire
|
|
else 0.0
|
|
),
|
|
"prix_unitaire_achat": (
|
|
float(ligne_row.AR_PrixAch) if ligne_row.AR_PrixAch else 0.0
|
|
),
|
|
"prix_unitaire_vente": (
|
|
float(ligne_row.AR_PrixVen) if ligne_row.AR_PrixVen else 0.0
|
|
),
|
|
"prix_unitaire_ttc": (
|
|
float(ligne_row.AR_PrixTTC) if ligne_row.AR_PrixTTC else 0.0
|
|
),
|
|
"montant_ligne_ht": montant_ht,
|
|
"montant_ligne_net": montant_net,
|
|
"montant_ligne_ttc": montant_ttc,
|
|
"remise_valeur1": (
|
|
float(ligne_row.DL_Remise01REM_Valeur)
|
|
if hasattr(ligne_row, "DL_Remise01REM_Valeur")
|
|
and ligne_row.DL_Remise01REM_Valeur
|
|
else 0.0
|
|
),
|
|
"remise_type1": (
|
|
int(ligne_row.DL_Remise01REM_Type)
|
|
if hasattr(ligne_row, "DL_Remise01REM_Type")
|
|
and ligne_row.DL_Remise01REM_Type
|
|
else 0
|
|
),
|
|
"remise_valeur2": (
|
|
float(ligne_row.DL_Remise02REM_Valeur)
|
|
if hasattr(ligne_row, "DL_Remise02REM_Valeur")
|
|
and ligne_row.DL_Remise02REM_Valeur
|
|
else 0.0
|
|
),
|
|
"remise_type2": (
|
|
int(ligne_row.DL_Remise02REM_Type)
|
|
if hasattr(ligne_row, "DL_Remise02REM_Type")
|
|
and ligne_row.DL_Remise02REM_Type
|
|
else 0
|
|
),
|
|
"remise_article": (
|
|
float(ligne_row.AR_Escompte)
|
|
if ligne_row.AR_Escompte
|
|
else 0.0
|
|
),
|
|
"taux_taxe1": taux_taxe1,
|
|
"montant_taxe1": montant_taxe1,
|
|
"taux_taxe2": taux_taxe2,
|
|
"montant_taxe2": montant_taxe2,
|
|
"taux_taxe3": taux_taxe3,
|
|
"montant_taxe3": montant_taxe3,
|
|
"total_taxes": montant_taxe1 + montant_taxe2 + montant_taxe3,
|
|
"famille_article": self._safe_strip(ligne_row.FA_CodeFamille),
|
|
"gamme1": self._safe_strip(ligne_row.AR_Gamme1),
|
|
"gamme2": self._safe_strip(ligne_row.AR_Gamme2),
|
|
"code_barre": self._safe_strip(ligne_row.AR_CodeBarre),
|
|
"type_article": self._safe_strip(ligne_row.AR_Type),
|
|
"nature_article": self._safe_strip(ligne_row.AR_Nature),
|
|
"garantie": self._safe_strip(ligne_row.AR_Garantie),
|
|
"cout_standard": (
|
|
float(ligne_row.AR_CoutStd) if ligne_row.AR_CoutStd else 0.0
|
|
),
|
|
"poids_net": (
|
|
float(ligne_row.AR_PoidsNet)
|
|
if ligne_row.AR_PoidsNet
|
|
else 0.0
|
|
),
|
|
"poids_brut": (
|
|
float(ligne_row.AR_PoidsBrut)
|
|
if ligne_row.AR_PoidsBrut
|
|
else 0.0
|
|
),
|
|
"unite_vente": self._safe_strip(ligne_row.AR_UniteVen),
|
|
"date_livraison_ligne": (
|
|
str(ligne_row.DL_DateLivr)
|
|
if hasattr(ligne_row, "DL_DateLivr")
|
|
and ligne_row.DL_DateLivr
|
|
else ""
|
|
),
|
|
"statut_ligne": (
|
|
int(ligne_row.DL_Statut)
|
|
if hasattr(ligne_row, "DL_Statut")
|
|
and ligne_row.DL_Statut is not None
|
|
else 0
|
|
),
|
|
"depot": (
|
|
self._safe_strip(ligne_row.DE_No)
|
|
if hasattr(ligne_row, "DE_No")
|
|
else ""
|
|
),
|
|
"numero_commande": (
|
|
self._safe_strip(ligne_row.DL_NoColis)
|
|
if hasattr(ligne_row, "DL_NoColis")
|
|
else ""
|
|
),
|
|
"num_colis": (
|
|
self._safe_strip(ligne_row.DL_Colis)
|
|
if hasattr(ligne_row, "DL_Colis")
|
|
else ""
|
|
),
|
|
}
|
|
lignes.append(ligne)
|
|
|
|
doc["lignes"] = lignes
|
|
doc["nb_lignes"] = len(lignes)
|
|
|
|
total_ht_calcule = sum(l.get("montant_ligne_ht", 0) for l in lignes)
|
|
total_ttc_calcule = sum(l.get("montant_ligne_ttc", 0) for l in lignes)
|
|
total_taxes_calcule = sum(l.get("total_taxes", 0) for l in lignes)
|
|
|
|
doc["total_ht_calcule"] = total_ht_calcule
|
|
doc["total_ttc_calcule"] = total_ttc_calcule
|
|
doc["total_taxes_calcule"] = total_taxes_calcule
|
|
|
|
return doc
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur SQL lecture document {numero}: {e}", exc_info=True)
|
|
return None
|
|
|
|
def _lister_documents_avec_lignes_sql(
|
|
self,
|
|
type_doc: int,
|
|
filtre: str = "",
|
|
limit: int = None,
|
|
inclure_liaisons: bool = False,
|
|
calculer_transformations: bool = True,
|
|
):
|
|
"""Liste les documents avec leurs lignes."""
|
|
try:
|
|
type_doc_sql = self._convertir_type_pour_sql(type_doc)
|
|
logger.info(f"[SQL LIST] ═══ Type COM {type_doc} → SQL {type_doc_sql} ═══")
|
|
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = """
|
|
SELECT DISTINCT
|
|
d.DO_Piece, d.DO_Type, d.DO_Date, d.DO_Ref, d.DO_Tiers,
|
|
d.DO_TotalHT, d.DO_TotalTTC, d.DO_NetAPayer, d.DO_Statut,
|
|
d.DO_DateLivr, d.DO_DateExpedition, d.DO_Contact, d.DO_TotalHTNet,
|
|
d.DO_MontantRegle, d.DO_Reliquat, d.DO_TxEscompte, d.DO_Escompte,
|
|
d.DO_Taxe1, d.DO_Taxe2, d.DO_Taxe3,
|
|
d.DO_CodeTaxe1, d.DO_CodeTaxe2, d.DO_CodeTaxe3,
|
|
d.DO_EStatut, d.DO_Imprim, d.DO_Valide, d.DO_Cloture, d.DO_Transfere,
|
|
d.DO_Souche, d.DO_PieceOrig, d.DO_GUID,
|
|
d.CA_Num, d.CG_Num, d.DO_Expedit, d.DO_Condition, d.DO_Tarif,
|
|
d.DO_TypeFrais, d.DO_ValFrais, d.DO_TypeFranco, d.DO_ValFranco,
|
|
c.CT_Intitule, c.CT_Adresse, c.CT_CodePostal,
|
|
c.CT_Ville, c.CT_Telephone, c.CT_EMail
|
|
FROM F_DOCENTETE d
|
|
LEFT JOIN F_COMPTET c ON d.DO_Tiers = c.CT_Num
|
|
WHERE d.DO_Type = ?
|
|
"""
|
|
|
|
params = [type_doc_sql]
|
|
|
|
if filtre:
|
|
query += " AND (d.DO_Piece LIKE ? OR c.CT_Intitule LIKE ? OR d.DO_Ref LIKE ?)"
|
|
params.extend([f"%{filtre}%", f"%{filtre}%", f"%{filtre}%"])
|
|
|
|
query += " ORDER BY d.DO_Date DESC"
|
|
|
|
if limit:
|
|
query = f"SELECT TOP ({limit}) * FROM ({query}) AS subquery"
|
|
|
|
cursor.execute(query, params)
|
|
entetes = cursor.fetchall()
|
|
|
|
logger.info(f"[SQL LIST] {len(entetes)} documents SQL")
|
|
|
|
documents = []
|
|
stats = {
|
|
"total": len(entetes),
|
|
"exclus_prefixe": 0,
|
|
"erreur_construction": 0,
|
|
"erreur_lignes": 0,
|
|
"erreur_transformations": 0,
|
|
"erreur_liaisons": 0,
|
|
"succes": 0,
|
|
}
|
|
|
|
for idx, entete in enumerate(entetes):
|
|
numero = self._safe_strip(entete.DO_Piece)
|
|
logger.info(
|
|
f"[SQL LIST] [{idx+1}/{len(entetes)}] Traitement {numero}..."
|
|
)
|
|
|
|
try:
|
|
prefixes_vente = {
|
|
0: ["DE"],
|
|
10: ["BC"],
|
|
30: ["BL"],
|
|
50: ["AV", "AR"],
|
|
60: ["FA", "FC"],
|
|
}
|
|
|
|
prefixes_acceptes = prefixes_vente.get(type_doc, [])
|
|
|
|
if prefixes_acceptes:
|
|
est_vente = any(
|
|
numero.upper().startswith(p) for p in prefixes_acceptes
|
|
)
|
|
if not est_vente:
|
|
logger.info(
|
|
f"[SQL LIST] {numero} : exclu (préfixe achat)"
|
|
)
|
|
stats["exclus_prefixe"] += 1
|
|
continue
|
|
|
|
logger.debug(f"[SQL LIST] {numero} : préfixe OK")
|
|
|
|
try:
|
|
type_doc_depuis_sql = self._convertir_type_depuis_sql(
|
|
int(entete.DO_Type)
|
|
)
|
|
|
|
doc = {
|
|
"numero": numero,
|
|
"type": type_doc_depuis_sql,
|
|
"reference": self._safe_strip(entete.DO_Ref),
|
|
"date": str(entete.DO_Date) if entete.DO_Date else "",
|
|
"date_livraison": (
|
|
str(entete.DO_DateLivr)
|
|
if entete.DO_DateLivr
|
|
else ""
|
|
),
|
|
"date_expedition": (
|
|
str(entete.DO_DateExpedition)
|
|
if entete.DO_DateExpedition
|
|
else ""
|
|
),
|
|
"client_code": self._safe_strip(entete.DO_Tiers),
|
|
"client_intitule": self._safe_strip(entete.CT_Intitule),
|
|
"client_adresse": self._safe_strip(entete.CT_Adresse),
|
|
"client_code_postal": self._safe_strip(
|
|
entete.CT_CodePostal
|
|
),
|
|
"client_ville": self._safe_strip(entete.CT_Ville),
|
|
"client_telephone": self._safe_strip(
|
|
entete.CT_Telephone
|
|
),
|
|
"client_email": self._safe_strip(entete.CT_EMail),
|
|
"contact": self._safe_strip(entete.DO_Contact),
|
|
"total_ht": (
|
|
float(entete.DO_TotalHT)
|
|
if entete.DO_TotalHT
|
|
else 0.0
|
|
),
|
|
"total_ht_net": (
|
|
float(entete.DO_TotalHTNet)
|
|
if entete.DO_TotalHTNet
|
|
else 0.0
|
|
),
|
|
"total_ttc": (
|
|
float(entete.DO_TotalTTC)
|
|
if entete.DO_TotalTTC
|
|
else 0.0
|
|
),
|
|
"net_a_payer": (
|
|
float(entete.DO_NetAPayer)
|
|
if entete.DO_NetAPayer
|
|
else 0.0
|
|
),
|
|
"montant_regle": (
|
|
float(entete.DO_MontantRegle)
|
|
if entete.DO_MontantRegle
|
|
else 0.0
|
|
),
|
|
"reliquat": (
|
|
float(entete.DO_Reliquat)
|
|
if entete.DO_Reliquat
|
|
else 0.0
|
|
),
|
|
"taux_escompte": (
|
|
float(entete.DO_TxEscompte)
|
|
if entete.DO_TxEscompte
|
|
else 0.0
|
|
),
|
|
"escompte": (
|
|
float(entete.DO_Escompte)
|
|
if entete.DO_Escompte
|
|
else 0.0
|
|
),
|
|
"taxe1": (
|
|
float(entete.DO_Taxe1) if entete.DO_Taxe1 else 0.0
|
|
),
|
|
"taxe2": (
|
|
float(entete.DO_Taxe2) if entete.DO_Taxe2 else 0.0
|
|
),
|
|
"taxe3": (
|
|
float(entete.DO_Taxe3) if entete.DO_Taxe3 else 0.0
|
|
),
|
|
"code_taxe1": self._safe_strip(entete.DO_CodeTaxe1),
|
|
"code_taxe2": self._safe_strip(entete.DO_CodeTaxe2),
|
|
"code_taxe3": self._safe_strip(entete.DO_CodeTaxe3),
|
|
"statut": (
|
|
int(entete.DO_Statut)
|
|
if entete.DO_Statut is not None
|
|
else 0
|
|
),
|
|
"statut_estatut": (
|
|
int(entete.DO_EStatut)
|
|
if entete.DO_EStatut is not None
|
|
else 0
|
|
),
|
|
"imprime": (
|
|
int(entete.DO_Imprim)
|
|
if entete.DO_Imprim is not None
|
|
else 0
|
|
),
|
|
"valide": (
|
|
int(entete.DO_Valide)
|
|
if entete.DO_Valide is not None
|
|
else 0
|
|
),
|
|
"cloture": (
|
|
int(entete.DO_Cloture)
|
|
if entete.DO_Cloture is not None
|
|
else 0
|
|
),
|
|
"transfere": (
|
|
int(entete.DO_Transfere)
|
|
if entete.DO_Transfere is not None
|
|
else 0
|
|
),
|
|
"souche": self._safe_strip(entete.DO_Souche),
|
|
"piece_origine": self._safe_strip(entete.DO_PieceOrig),
|
|
"guid": self._safe_strip(entete.DO_GUID),
|
|
"ca_num": self._safe_strip(entete.CA_Num),
|
|
"cg_num": self._safe_strip(entete.CG_Num),
|
|
"expedition": self._safe_strip(entete.DO_Expedit),
|
|
"condition": self._safe_strip(entete.DO_Condition),
|
|
"tarif": self._safe_strip(entete.DO_Tarif),
|
|
"type_frais": (
|
|
int(entete.DO_TypeFrais)
|
|
if entete.DO_TypeFrais is not None
|
|
else 0
|
|
),
|
|
"valeur_frais": (
|
|
float(entete.DO_ValFrais)
|
|
if entete.DO_ValFrais
|
|
else 0.0
|
|
),
|
|
"type_franco": (
|
|
int(entete.DO_TypeFranco)
|
|
if entete.DO_TypeFranco is not None
|
|
else 0
|
|
),
|
|
"valeur_franco": (
|
|
float(entete.DO_ValFranco)
|
|
if entete.DO_ValFranco
|
|
else 0.0
|
|
),
|
|
"lignes": [],
|
|
}
|
|
|
|
logger.debug(
|
|
f"[SQL LIST] {numero} : document de base créé"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
f"[SQL LIST] {numero} : ERREUR construction base: {e}",
|
|
exc_info=True,
|
|
)
|
|
stats["erreur_construction"] += 1
|
|
continue
|
|
|
|
try:
|
|
cursor.execute(
|
|
"""
|
|
SELECT dl.*,
|
|
a.AR_Design, a.FA_CodeFamille, a.AR_PrixTTC, a.AR_PrixVen, a.AR_PrixAch,
|
|
a.AR_Gamme1, a.AR_Gamme2, a.AR_CodeBarre, a.AR_CoutStd,
|
|
a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_UniteVen,
|
|
a.AR_Type, a.AR_Nature, a.AR_Escompte, a.AR_Garantie
|
|
FROM F_DOCLIGNE dl
|
|
LEFT JOIN F_ARTICLE a ON dl.AR_Ref = a.AR_Ref
|
|
WHERE dl.DO_Piece = ? AND dl.DO_Type = ?
|
|
ORDER BY dl.DL_Ligne
|
|
""",
|
|
(numero, type_doc_sql),
|
|
)
|
|
|
|
for ligne_row in cursor.fetchall():
|
|
montant_ht = (
|
|
float(ligne_row.DL_MontantHT)
|
|
if ligne_row.DL_MontantHT
|
|
else 0.0
|
|
)
|
|
montant_net = (
|
|
float(ligne_row.DL_MontantNet)
|
|
if hasattr(ligne_row, "DL_MontantNet")
|
|
and ligne_row.DL_MontantNet
|
|
else montant_ht
|
|
)
|
|
|
|
taux_taxe1 = (
|
|
float(ligne_row.DL_Taxe1)
|
|
if hasattr(ligne_row, "DL_Taxe1")
|
|
and ligne_row.DL_Taxe1
|
|
else 0.0
|
|
)
|
|
taux_taxe2 = (
|
|
float(ligne_row.DL_Taxe2)
|
|
if hasattr(ligne_row, "DL_Taxe2")
|
|
and ligne_row.DL_Taxe2
|
|
else 0.0
|
|
)
|
|
taux_taxe3 = (
|
|
float(ligne_row.DL_Taxe3)
|
|
if hasattr(ligne_row, "DL_Taxe3")
|
|
and ligne_row.DL_Taxe3
|
|
else 0.0
|
|
)
|
|
|
|
total_taux_taxes = taux_taxe1 + taux_taxe2 + taux_taxe3
|
|
montant_ttc = montant_net * (1 + total_taux_taxes / 100)
|
|
|
|
montant_taxe1 = montant_net * (taux_taxe1 / 100)
|
|
montant_taxe2 = montant_net * (taux_taxe2 / 100)
|
|
montant_taxe3 = montant_net * (taux_taxe3 / 100)
|
|
|
|
ligne = {
|
|
"numero_ligne": (
|
|
int(ligne_row.DL_Ligne)
|
|
if ligne_row.DL_Ligne
|
|
else 0
|
|
),
|
|
"article_code": self._safe_strip(ligne_row.AR_Ref),
|
|
"designation": self._safe_strip(
|
|
ligne_row.DL_Design
|
|
),
|
|
"designation_article": self._safe_strip(
|
|
ligne_row.AR_Design
|
|
),
|
|
"quantite": (
|
|
float(ligne_row.DL_Qte)
|
|
if ligne_row.DL_Qte
|
|
else 0.0
|
|
),
|
|
"quantite_livree": (
|
|
float(ligne_row.DL_QteLiv)
|
|
if hasattr(ligne_row, "DL_QteLiv")
|
|
and ligne_row.DL_QteLiv
|
|
else 0.0
|
|
),
|
|
"quantite_reservee": (
|
|
float(ligne_row.DL_QteRes)
|
|
if hasattr(ligne_row, "DL_QteRes")
|
|
and ligne_row.DL_QteRes
|
|
else 0.0
|
|
),
|
|
"unite": (
|
|
self._safe_strip(ligne_row.DL_Unite)
|
|
if hasattr(ligne_row, "DL_Unite")
|
|
else ""
|
|
),
|
|
"prix_unitaire_ht": (
|
|
float(ligne_row.DL_PrixUnitaire)
|
|
if ligne_row.DL_PrixUnitaire
|
|
else 0.0
|
|
),
|
|
"prix_unitaire_achat": (
|
|
float(ligne_row.AR_PrixAch)
|
|
if ligne_row.AR_PrixAch
|
|
else 0.0
|
|
),
|
|
"prix_unitaire_vente": (
|
|
float(ligne_row.AR_PrixVen)
|
|
if ligne_row.AR_PrixVen
|
|
else 0.0
|
|
),
|
|
"prix_unitaire_ttc": (
|
|
float(ligne_row.AR_PrixTTC)
|
|
if ligne_row.AR_PrixTTC
|
|
else 0.0
|
|
),
|
|
"montant_ligne_ht": montant_ht,
|
|
"montant_ligne_net": montant_net,
|
|
"montant_ligne_ttc": montant_ttc,
|
|
"remise_valeur1": (
|
|
float(ligne_row.DL_Remise01REM_Valeur)
|
|
if hasattr(ligne_row, "DL_Remise01REM_Valeur")
|
|
and ligne_row.DL_Remise01REM_Valeur
|
|
else 0.0
|
|
),
|
|
"remise_type1": (
|
|
int(ligne_row.DL_Remise01REM_Type)
|
|
if hasattr(ligne_row, "DL_Remise01REM_Type")
|
|
and ligne_row.DL_Remise01REM_Type
|
|
else 0
|
|
),
|
|
"remise_valeur2": (
|
|
float(ligne_row.DL_Remise02REM_Valeur)
|
|
if hasattr(ligne_row, "DL_Remise02REM_Valeur")
|
|
and ligne_row.DL_Remise02REM_Valeur
|
|
else 0.0
|
|
),
|
|
"remise_type2": (
|
|
int(ligne_row.DL_Remise02REM_Type)
|
|
if hasattr(ligne_row, "DL_Remise02REM_Type")
|
|
and ligne_row.DL_Remise02REM_Type
|
|
else 0
|
|
),
|
|
"remise_article": (
|
|
float(ligne_row.AR_Escompte)
|
|
if ligne_row.AR_Escompte
|
|
else 0.0
|
|
),
|
|
"taux_taxe1": taux_taxe1,
|
|
"montant_taxe1": montant_taxe1,
|
|
"taux_taxe2": taux_taxe2,
|
|
"montant_taxe2": montant_taxe2,
|
|
"taux_taxe3": taux_taxe3,
|
|
"montant_taxe3": montant_taxe3,
|
|
"total_taxes": montant_taxe1
|
|
+ montant_taxe2
|
|
+ montant_taxe3,
|
|
"famille_article": self._safe_strip(
|
|
ligne_row.FA_CodeFamille
|
|
),
|
|
"gamme1": self._safe_strip(ligne_row.AR_Gamme1),
|
|
"gamme2": self._safe_strip(ligne_row.AR_Gamme2),
|
|
"code_barre": self._safe_strip(
|
|
ligne_row.AR_CodeBarre
|
|
),
|
|
"type_article": self._safe_strip(ligne_row.AR_Type),
|
|
"nature_article": self._safe_strip(
|
|
ligne_row.AR_Nature
|
|
),
|
|
"garantie": self._safe_strip(ligne_row.AR_Garantie),
|
|
"cout_standard": (
|
|
float(ligne_row.AR_CoutStd)
|
|
if ligne_row.AR_CoutStd
|
|
else 0.0
|
|
),
|
|
"poids_net": (
|
|
float(ligne_row.AR_PoidsNet)
|
|
if ligne_row.AR_PoidsNet
|
|
else 0.0
|
|
),
|
|
"poids_brut": (
|
|
float(ligne_row.AR_PoidsBrut)
|
|
if ligne_row.AR_PoidsBrut
|
|
else 0.0
|
|
),
|
|
"unite_vente": self._safe_strip(
|
|
ligne_row.AR_UniteVen
|
|
),
|
|
"date_livraison_ligne": (
|
|
str(ligne_row.DL_DateLivr)
|
|
if hasattr(ligne_row, "DL_DateLivr")
|
|
and ligne_row.DL_DateLivr
|
|
else ""
|
|
),
|
|
"statut_ligne": (
|
|
int(ligne_row.DL_Statut)
|
|
if hasattr(ligne_row, "DL_Statut")
|
|
and ligne_row.DL_Statut is not None
|
|
else 0
|
|
),
|
|
"depot": (
|
|
self._safe_strip(ligne_row.DE_No)
|
|
if hasattr(ligne_row, "DE_No")
|
|
else ""
|
|
),
|
|
"numero_commande": (
|
|
self._safe_strip(ligne_row.DL_NoColis)
|
|
if hasattr(ligne_row, "DL_NoColis")
|
|
else ""
|
|
),
|
|
"num_colis": (
|
|
self._safe_strip(ligne_row.DL_Colis)
|
|
if hasattr(ligne_row, "DL_Colis")
|
|
else ""
|
|
),
|
|
}
|
|
doc["lignes"].append(ligne)
|
|
|
|
doc["nb_lignes"] = len(doc["lignes"])
|
|
doc["total_ht_calcule"] = sum(
|
|
l.get("montant_ligne_ht", 0) for l in doc["lignes"]
|
|
)
|
|
doc["total_ttc_calcule"] = sum(
|
|
l.get("montant_ligne_ttc", 0) for l in doc["lignes"]
|
|
)
|
|
doc["total_taxes_calcule"] = sum(
|
|
l.get("total_taxes", 0) for l in doc["lignes"]
|
|
)
|
|
|
|
logger.debug(
|
|
f"[SQL LIST] {numero} : {doc['nb_lignes']} lignes chargées"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
f"[SQL LIST] {numero} : ERREUR lignes: {e}",
|
|
exc_info=True,
|
|
)
|
|
stats["erreur_lignes"] += 1
|
|
|
|
documents.append(doc)
|
|
stats["succes"] += 1
|
|
logger.info(
|
|
f"[SQL LIST] {numero} : AJOUTÉ à la liste (total: {len(documents)})"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
f"[SQL LIST] {numero} : EXCEPTION GLOBALE - DOCUMENT EXCLU: {e}",
|
|
exc_info=True,
|
|
)
|
|
continue
|
|
|
|
logger.info(f"[SQL LIST] ═══════════════════════════")
|
|
logger.info(f"[SQL LIST] STATISTIQUES FINALES:")
|
|
logger.info(f"[SQL LIST] Total SQL: {stats['total']}")
|
|
logger.info(f"[SQL LIST] Exclus préfixe: {stats['exclus_prefixe']}")
|
|
logger.info(
|
|
f"[SQL LIST] Erreur construction: {stats['erreur_construction']}"
|
|
)
|
|
logger.info(f"[SQL LIST] Erreur lignes: {stats['erreur_lignes']}")
|
|
logger.info(
|
|
f"[SQL LIST] Erreur transformations: {stats['erreur_transformations']}"
|
|
)
|
|
logger.info(f"[SQL LIST] Erreur liaisons: {stats['erreur_liaisons']}")
|
|
logger.info(f"[SQL LIST] SUCCÈS: {stats['succes']}")
|
|
logger.info(f"[SQL LIST] Documents retournés: {len(documents)}")
|
|
logger.info(f"[SQL LIST] ═══════════════════════════")
|
|
|
|
return documents
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur GLOBALE listage: {e}", exc_info=True)
|
|
return []
|
|
|
|
def lister_tous_devis_cache(self, filtre=""):
|
|
return self._lister_documents_avec_lignes_sql(type_doc=0, filtre=filtre)
|
|
|
|
def lire_devis_cache(self, numero):
|
|
return self._lire_document_sql(numero, type_doc=0)
|
|
|
|
def lister_toutes_commandes_cache(self, filtre=""):
|
|
return self._lister_documents_avec_lignes_sql(type_doc=1, filtre=filtre)
|
|
|
|
def lire_commande_cache(self, numero):
|
|
return self._lire_document_sql(numero, type_doc=1)
|
|
|
|
def lister_toutes_factures_cache(self, filtre=""):
|
|
return self._lister_documents_avec_lignes_sql(type_doc=6, filtre=filtre)
|
|
|
|
def lire_facture_cache(self, numero):
|
|
return self._lire_document_sql(numero, type_doc=6)
|
|
|
|
def lister_tous_fournisseurs_cache(self, filtre=""):
|
|
return self.lister_tous_fournisseurs()
|
|
|
|
def lire_fournisseur_cache(self, code):
|
|
return self.lire_fournisseur()
|
|
|
|
def lister_toutes_livraisons_cache(self, filtre=""):
|
|
return self._lister_documents_avec_lignes_sql(type_doc=3, filtre=filtre)
|
|
|
|
def lire_livraison_cache(self, numero):
|
|
return self._lire_document_sql(numero, type_doc=3)
|
|
|
|
def lister_tous_avoirs_cache(self, filtre=""):
|
|
return self._lister_documents_avec_lignes_sql(type_doc=5, filtre=filtre)
|
|
|
|
def lire_avoir_cache(self, numero):
|
|
return self._lire_document_sql(numero, type_doc=5)
|
|
|
|
|
|
def _cast_client(self, persist_obj):
|
|
try:
|
|
obj = win32com.client.CastTo(persist_obj, "IBOClient3")
|
|
obj.Read()
|
|
return obj
|
|
except Exception as e:
|
|
logger.debug(f" _cast_client échoue: {e}") # AJOUTER CE LOG
|
|
return None
|
|
|
|
def _cast_article(self, persist_obj):
|
|
try:
|
|
obj = win32com.client.CastTo(persist_obj, "IBOArticle3")
|
|
obj.Read()
|
|
return obj
|
|
except:
|
|
return None
|
|
|
|
def _extraire_client(self, client_obj):
|
|
try:
|
|
try:
|
|
numero = getattr(client_obj, "CT_Num", "").strip()
|
|
if not numero:
|
|
logger.debug("Objet sans CT_Num, skip")
|
|
return None
|
|
except Exception as e:
|
|
logger.debug(f" Erreur lecture CT_Num: {e}")
|
|
return None
|
|
|
|
try:
|
|
intitule = getattr(client_obj, "CT_Intitule", "").strip()
|
|
if not intitule:
|
|
logger.debug(f"{numero} sans CT_Intitule")
|
|
except Exception as e:
|
|
logger.debug(f"Erreur CT_Intitule sur {numero}: {e}")
|
|
intitule = ""
|
|
|
|
data = {
|
|
"numero": numero,
|
|
"intitule": intitule,
|
|
}
|
|
|
|
try:
|
|
qualite_code = getattr(client_obj, "CT_Type", None)
|
|
|
|
qualite_map = {
|
|
0: "CLI", # Client
|
|
1: "FOU", # Fournisseur
|
|
2: "CLIFOU", # Client + Fournisseur
|
|
3: "SAL", # Salarié
|
|
4: "PRO", # Prospect
|
|
}
|
|
|
|
data["qualite"] = qualite_map.get(qualite_code, "CLI")
|
|
data["est_fournisseur"] = qualite_code in [1, 2]
|
|
|
|
except:
|
|
data["qualite"] = "CLI"
|
|
data["est_fournisseur"] = False
|
|
|
|
try:
|
|
data["est_prospect"] = getattr(client_obj, "CT_Prospect", 0) == 1
|
|
except:
|
|
data["est_prospect"] = False
|
|
|
|
if data["est_prospect"]:
|
|
data["type_tiers"] = "prospect"
|
|
elif data["est_fournisseur"] and data["qualite"] != "CLIFOU":
|
|
data["type_tiers"] = "fournisseur"
|
|
elif data["qualite"] == "CLIFOU":
|
|
data["type_tiers"] = "client_fournisseur"
|
|
else:
|
|
data["type_tiers"] = "client"
|
|
|
|
try:
|
|
sommeil = getattr(client_obj, "CT_Sommeil", 0)
|
|
data["est_actif"] = sommeil == 0
|
|
data["est_en_sommeil"] = sommeil == 1
|
|
except:
|
|
data["est_actif"] = True
|
|
data["est_en_sommeil"] = False
|
|
|
|
try:
|
|
forme_juridique = getattr(client_obj, "CT_FormeJuridique", "").strip()
|
|
data["forme_juridique"] = forme_juridique
|
|
data["est_entreprise"] = bool(forme_juridique)
|
|
data["est_particulier"] = not bool(forme_juridique)
|
|
except:
|
|
data["forme_juridique"] = ""
|
|
data["est_entreprise"] = False
|
|
data["est_particulier"] = True
|
|
|
|
try:
|
|
data["civilite"] = getattr(client_obj, "CT_Civilite", "").strip()
|
|
except:
|
|
data["civilite"] = ""
|
|
|
|
try:
|
|
data["nom"] = getattr(client_obj, "CT_Nom", "").strip()
|
|
except:
|
|
data["nom"] = ""
|
|
|
|
try:
|
|
data["prenom"] = getattr(client_obj, "CT_Prenom", "").strip()
|
|
except:
|
|
data["prenom"] = ""
|
|
|
|
if data.get("nom") or data.get("prenom"):
|
|
parts = []
|
|
if data.get("civilite"):
|
|
parts.append(data["civilite"])
|
|
if data.get("prenom"):
|
|
parts.append(data["prenom"])
|
|
if data.get("nom"):
|
|
parts.append(data["nom"])
|
|
data["nom_complet"] = " ".join(parts)
|
|
else:
|
|
data["nom_complet"] = ""
|
|
|
|
try:
|
|
data["contact"] = getattr(client_obj, "CT_Contact", "").strip()
|
|
except:
|
|
data["contact"] = ""
|
|
|
|
try:
|
|
adresse_obj = getattr(client_obj, "Adresse", None)
|
|
if adresse_obj:
|
|
try:
|
|
data["adresse"] = getattr(adresse_obj, "Adresse", "").strip()
|
|
except:
|
|
data["adresse"] = ""
|
|
|
|
try:
|
|
data["complement"] = getattr(
|
|
adresse_obj, "Complement", ""
|
|
).strip()
|
|
except:
|
|
data["complement"] = ""
|
|
|
|
try:
|
|
data["code_postal"] = getattr(
|
|
adresse_obj, "CodePostal", ""
|
|
).strip()
|
|
except:
|
|
data["code_postal"] = ""
|
|
|
|
try:
|
|
data["ville"] = getattr(adresse_obj, "Ville", "").strip()
|
|
except:
|
|
data["ville"] = ""
|
|
|
|
try:
|
|
data["region"] = getattr(adresse_obj, "Region", "").strip()
|
|
except:
|
|
data["region"] = ""
|
|
|
|
try:
|
|
data["pays"] = getattr(adresse_obj, "Pays", "").strip()
|
|
except:
|
|
data["pays"] = ""
|
|
else:
|
|
data["adresse"] = ""
|
|
data["complement"] = ""
|
|
data["code_postal"] = ""
|
|
data["ville"] = ""
|
|
data["region"] = ""
|
|
data["pays"] = ""
|
|
except Exception as e:
|
|
logger.debug(f"Erreur adresse sur {numero}: {e}")
|
|
data["adresse"] = ""
|
|
data["complement"] = ""
|
|
data["code_postal"] = ""
|
|
data["ville"] = ""
|
|
data["region"] = ""
|
|
data["pays"] = ""
|
|
|
|
try:
|
|
telecom = getattr(client_obj, "Telecom", None)
|
|
if telecom:
|
|
try:
|
|
data["telephone"] = getattr(telecom, "Telephone", "").strip()
|
|
except:
|
|
data["telephone"] = ""
|
|
|
|
try:
|
|
data["portable"] = getattr(telecom, "Portable", "").strip()
|
|
except:
|
|
data["portable"] = ""
|
|
|
|
try:
|
|
data["telecopie"] = getattr(telecom, "Telecopie", "").strip()
|
|
except:
|
|
data["telecopie"] = ""
|
|
|
|
try:
|
|
data["email"] = getattr(telecom, "EMail", "").strip()
|
|
except:
|
|
data["email"] = ""
|
|
|
|
try:
|
|
site = (
|
|
getattr(telecom, "Site", None)
|
|
or getattr(telecom, "Web", None)
|
|
or getattr(telecom, "SiteWeb", "")
|
|
)
|
|
data["site_web"] = str(site).strip() if site else ""
|
|
except:
|
|
data["site_web"] = ""
|
|
else:
|
|
data["telephone"] = ""
|
|
data["portable"] = ""
|
|
data["telecopie"] = ""
|
|
data["email"] = ""
|
|
data["site_web"] = ""
|
|
except Exception as e:
|
|
logger.debug(f"Erreur telecom sur {numero}: {e}")
|
|
data["telephone"] = ""
|
|
data["portable"] = ""
|
|
data["telecopie"] = ""
|
|
data["email"] = ""
|
|
data["site_web"] = ""
|
|
|
|
try:
|
|
data["siret"] = getattr(client_obj, "CT_Siret", "").strip()
|
|
except:
|
|
data["siret"] = ""
|
|
|
|
try:
|
|
data["siren"] = getattr(client_obj, "CT_Siren", "").strip()
|
|
except:
|
|
data["siren"] = ""
|
|
|
|
try:
|
|
data["tva_intra"] = getattr(client_obj, "CT_Identifiant", "").strip()
|
|
except:
|
|
data["tva_intra"] = ""
|
|
|
|
try:
|
|
data["code_naf"] = (
|
|
getattr(client_obj, "CT_CodeNAF", "").strip()
|
|
or getattr(client_obj, "CT_APE", "").strip()
|
|
)
|
|
except:
|
|
data["code_naf"] = ""
|
|
|
|
try:
|
|
data["secteur"] = getattr(client_obj, "CT_Secteur", "").strip()
|
|
except:
|
|
data["secteur"] = ""
|
|
|
|
try:
|
|
effectif = getattr(client_obj, "CT_Effectif", None)
|
|
data["effectif"] = int(effectif) if effectif is not None else None
|
|
except:
|
|
data["effectif"] = None
|
|
|
|
try:
|
|
ca = getattr(client_obj, "CT_ChiffreAffaire", None)
|
|
data["ca_annuel"] = float(ca) if ca is not None else None
|
|
except:
|
|
data["ca_annuel"] = None
|
|
|
|
try:
|
|
data["commercial_code"] = getattr(client_obj, "CO_No", "").strip()
|
|
except:
|
|
try:
|
|
data["commercial_code"] = getattr(
|
|
client_obj, "CT_Commercial", ""
|
|
).strip()
|
|
except:
|
|
data["commercial_code"] = ""
|
|
|
|
if data.get("commercial_code"):
|
|
try:
|
|
commercial_obj = getattr(client_obj, "Commercial", None)
|
|
if commercial_obj:
|
|
commercial_obj.Read()
|
|
data["commercial_nom"] = getattr(
|
|
commercial_obj, "CO_Nom", ""
|
|
).strip()
|
|
else:
|
|
data["commercial_nom"] = ""
|
|
except:
|
|
data["commercial_nom"] = ""
|
|
else:
|
|
data["commercial_nom"] = ""
|
|
|
|
try:
|
|
data["categorie_tarifaire"] = getattr(client_obj, "N_CatTarif", None)
|
|
except:
|
|
data["categorie_tarifaire"] = None
|
|
|
|
try:
|
|
data["categorie_comptable"] = getattr(client_obj, "N_CatCompta", None)
|
|
except:
|
|
data["categorie_comptable"] = None
|
|
|
|
try:
|
|
data["encours_autorise"] = float(getattr(client_obj, "CT_Encours", 0.0))
|
|
except:
|
|
data["encours_autorise"] = 0.0
|
|
|
|
try:
|
|
data["assurance_credit"] = float(
|
|
getattr(client_obj, "CT_Assurance", 0.0)
|
|
)
|
|
except:
|
|
data["assurance_credit"] = 0.0
|
|
|
|
try:
|
|
data["compte_general"] = getattr(client_obj, "CG_Num", "").strip()
|
|
except:
|
|
data["compte_general"] = ""
|
|
|
|
try:
|
|
date_creation = getattr(client_obj, "CT_DateCreate", None)
|
|
data["date_creation"] = str(date_creation) if date_creation else ""
|
|
except:
|
|
data["date_creation"] = ""
|
|
|
|
try:
|
|
date_modif = getattr(client_obj, "CT_DateModif", None)
|
|
data["date_modification"] = str(date_modif) if date_modif else ""
|
|
except:
|
|
data["date_modification"] = ""
|
|
|
|
return data
|
|
|
|
except Exception as e:
|
|
logger.error(f" ERREUR GLOBALE _extraire_client: {e}", exc_info=True)
|
|
return None
|
|
|
|
def _extraire_article(self, article_obj):
|
|
try:
|
|
data = {
|
|
"reference": getattr(article_obj, "AR_Ref", "").strip(),
|
|
"designation": getattr(article_obj, "AR_Design", "").strip(),
|
|
}
|
|
|
|
data["code_ean"] = ""
|
|
data["code_barre"] = ""
|
|
|
|
try:
|
|
code_barre = getattr(article_obj, "AR_CodeBarre", "").strip()
|
|
if code_barre:
|
|
data["code_ean"] = code_barre
|
|
data["code_barre"] = code_barre
|
|
|
|
if not data["code_ean"]:
|
|
code_barre1 = getattr(article_obj, "AR_CodeBarre1", "").strip()
|
|
if code_barre1:
|
|
data["code_ean"] = code_barre1
|
|
data["code_barre"] = code_barre1
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
data["prix_vente"] = float(getattr(article_obj, "AR_PrixVen", 0.0))
|
|
except:
|
|
data["prix_vente"] = 0.0
|
|
|
|
try:
|
|
data["prix_achat"] = float(getattr(article_obj, "AR_PrixAch", 0.0))
|
|
except:
|
|
data["prix_achat"] = 0.0
|
|
|
|
try:
|
|
data["prix_revient"] = float(
|
|
getattr(article_obj, "AR_PrixRevient", 0.0)
|
|
)
|
|
except:
|
|
data["prix_revient"] = 0.0
|
|
|
|
try:
|
|
data["stock_reel"] = float(getattr(article_obj, "AR_Stock", 0.0))
|
|
except:
|
|
data["stock_reel"] = 0.0
|
|
|
|
try:
|
|
data["stock_mini"] = float(getattr(article_obj, "AR_StockMini", 0.0))
|
|
except:
|
|
data["stock_mini"] = 0.0
|
|
|
|
try:
|
|
data["stock_maxi"] = float(getattr(article_obj, "AR_StockMaxi", 0.0))
|
|
except:
|
|
data["stock_maxi"] = 0.0
|
|
|
|
try:
|
|
data["stock_reserve"] = float(getattr(article_obj, "AR_QteCom", 0.0))
|
|
except:
|
|
data["stock_reserve"] = 0.0
|
|
|
|
try:
|
|
data["stock_commande"] = float(
|
|
getattr(article_obj, "AR_QteComFou", 0.0)
|
|
)
|
|
except:
|
|
data["stock_commande"] = 0.0
|
|
|
|
try:
|
|
data["stock_disponible"] = data["stock_reel"] - data["stock_reserve"]
|
|
except:
|
|
data["stock_disponible"] = data["stock_reel"]
|
|
|
|
try:
|
|
commentaire = getattr(article_obj, "AR_Commentaire", "").strip()
|
|
data["description"] = commentaire
|
|
except:
|
|
data["description"] = ""
|
|
|
|
try:
|
|
design2 = getattr(article_obj, "AR_Design2", "").strip()
|
|
data["designation_complementaire"] = design2
|
|
except:
|
|
data["designation_complementaire"] = ""
|
|
|
|
try:
|
|
type_art = getattr(article_obj, "AR_Type", 0)
|
|
data["type_article"] = type_art
|
|
data["type_article_libelle"] = {
|
|
0: "Article",
|
|
1: "Prestation",
|
|
2: "Divers",
|
|
}.get(type_art, "Inconnu")
|
|
except:
|
|
data["type_article"] = 0
|
|
data["type_article_libelle"] = "Article"
|
|
|
|
try:
|
|
famille_code = getattr(article_obj, "FA_CodeFamille", "").strip()
|
|
data["famille_code"] = famille_code
|
|
|
|
if famille_code:
|
|
try:
|
|
famille_obj = getattr(article_obj, "Famille", None)
|
|
if famille_obj:
|
|
famille_obj.Read()
|
|
data["famille_libelle"] = getattr(
|
|
famille_obj, "FA_Intitule", ""
|
|
).strip()
|
|
else:
|
|
data["famille_libelle"] = ""
|
|
except:
|
|
data["famille_libelle"] = ""
|
|
else:
|
|
data["famille_libelle"] = ""
|
|
except:
|
|
data["famille_code"] = ""
|
|
data["famille_libelle"] = ""
|
|
|
|
try:
|
|
fournisseur_code = getattr(article_obj, "CT_Num", "").strip()
|
|
data["fournisseur_principal"] = fournisseur_code
|
|
|
|
if fournisseur_code:
|
|
try:
|
|
fourn_obj = getattr(article_obj, "Fournisseur", None)
|
|
if fourn_obj:
|
|
fourn_obj.Read()
|
|
data["fournisseur_nom"] = getattr(
|
|
fourn_obj, "CT_Intitule", ""
|
|
).strip()
|
|
else:
|
|
data["fournisseur_nom"] = ""
|
|
except:
|
|
data["fournisseur_nom"] = ""
|
|
else:
|
|
data["fournisseur_nom"] = ""
|
|
except:
|
|
data["fournisseur_principal"] = ""
|
|
data["fournisseur_nom"] = ""
|
|
|
|
try:
|
|
data["unite_vente"] = getattr(article_obj, "AR_UniteVen", "").strip()
|
|
except:
|
|
data["unite_vente"] = ""
|
|
|
|
try:
|
|
data["unite_achat"] = getattr(article_obj, "AR_UniteAch", "").strip()
|
|
except:
|
|
data["unite_achat"] = ""
|
|
|
|
try:
|
|
data["poids"] = float(getattr(article_obj, "AR_Poids", 0.0))
|
|
except:
|
|
data["poids"] = 0.0
|
|
|
|
try:
|
|
data["volume"] = float(getattr(article_obj, "AR_Volume", 0.0))
|
|
except:
|
|
data["volume"] = 0.0
|
|
|
|
try:
|
|
sommeil = getattr(article_obj, "AR_Sommeil", 0)
|
|
data["est_actif"] = sommeil == 0
|
|
data["en_sommeil"] = sommeil == 1
|
|
except:
|
|
data["est_actif"] = True
|
|
data["en_sommeil"] = False
|
|
|
|
try:
|
|
tva_code = getattr(article_obj, "TA_Code", "").strip()
|
|
data["tva_code"] = tva_code
|
|
|
|
try:
|
|
tva_obj = getattr(article_obj, "Taxe1", None)
|
|
if tva_obj:
|
|
tva_obj.Read()
|
|
data["tva_taux"] = float(getattr(tva_obj, "TA_Taux", 20.0))
|
|
else:
|
|
data["tva_taux"] = 20.0
|
|
except:
|
|
data["tva_taux"] = 20.0
|
|
except:
|
|
data["tva_code"] = ""
|
|
data["tva_taux"] = 20.0
|
|
|
|
try:
|
|
date_creation = getattr(article_obj, "AR_DateCreate", None)
|
|
data["date_creation"] = str(date_creation) if date_creation else ""
|
|
except:
|
|
data["date_creation"] = ""
|
|
|
|
try:
|
|
date_modif = getattr(article_obj, "AR_DateModif", None)
|
|
data["date_modification"] = str(date_modif) if date_modif else ""
|
|
except:
|
|
data["date_modification"] = ""
|
|
|
|
return data
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur extraction article: {e}", exc_info=True)
|
|
return {
|
|
"reference": getattr(article_obj, "AR_Ref", "").strip(),
|
|
"designation": getattr(article_obj, "AR_Design", "").strip(),
|
|
"prix_vente": 0.0,
|
|
"stock_reel": 0.0,
|
|
"code_ean": "",
|
|
"description": "",
|
|
"designation_complementaire": "",
|
|
"prix_achat": 0.0,
|
|
"prix_revient": 0.0,
|
|
"stock_mini": 0.0,
|
|
"stock_maxi": 0.0,
|
|
"stock_reserve": 0.0,
|
|
"stock_commande": 0.0,
|
|
"stock_disponible": 0.0,
|
|
"code_barre": "",
|
|
"type_article": 0,
|
|
"type_article_libelle": "Article",
|
|
"famille_code": "",
|
|
"famille_libelle": "",
|
|
"fournisseur_principal": "",
|
|
"fournisseur_nom": "",
|
|
"unite_vente": "",
|
|
"unite_achat": "",
|
|
"poids": 0.0,
|
|
"volume": 0.0,
|
|
"est_actif": True,
|
|
"en_sommeil": False,
|
|
"tva_code": "",
|
|
"tva_taux": 20.0,
|
|
"date_creation": "",
|
|
"date_modification": "",
|
|
}
|
|
|
|
def _extraire_fournisseur_enrichi(self, fourn_obj):
|
|
try:
|
|
numero = getattr(fourn_obj, "CT_Num", "").strip()
|
|
if not numero:
|
|
return None
|
|
|
|
intitule = getattr(fourn_obj, "CT_Intitule", "").strip()
|
|
|
|
data = {
|
|
"numero": numero,
|
|
"intitule": intitule,
|
|
"type": 1, # Fournisseur
|
|
"est_fournisseur": True,
|
|
}
|
|
|
|
try:
|
|
sommeil = getattr(fourn_obj, "CT_Sommeil", 0)
|
|
data["est_actif"] = sommeil == 0
|
|
data["en_sommeil"] = sommeil == 1
|
|
except:
|
|
data["est_actif"] = True
|
|
data["en_sommeil"] = False
|
|
|
|
try:
|
|
adresse_obj = getattr(fourn_obj, "Adresse", None)
|
|
if adresse_obj:
|
|
data["adresse"] = getattr(adresse_obj, "Adresse", "").strip()
|
|
data["complement"] = getattr(adresse_obj, "Complement", "").strip()
|
|
data["code_postal"] = getattr(adresse_obj, "CodePostal", "").strip()
|
|
data["ville"] = getattr(adresse_obj, "Ville", "").strip()
|
|
data["region"] = getattr(adresse_obj, "Region", "").strip()
|
|
data["pays"] = getattr(adresse_obj, "Pays", "").strip()
|
|
|
|
parties_adresse = []
|
|
if data["adresse"]:
|
|
parties_adresse.append(data["adresse"])
|
|
if data["complement"]:
|
|
parties_adresse.append(data["complement"])
|
|
if data["code_postal"] or data["ville"]:
|
|
ville_cp = f"{data['code_postal']} {data['ville']}".strip()
|
|
if ville_cp:
|
|
parties_adresse.append(ville_cp)
|
|
if data["pays"]:
|
|
parties_adresse.append(data["pays"])
|
|
|
|
data["adresse_complete"] = ", ".join(parties_adresse)
|
|
else:
|
|
data["adresse"] = ""
|
|
data["complement"] = ""
|
|
data["code_postal"] = ""
|
|
data["ville"] = ""
|
|
data["region"] = ""
|
|
data["pays"] = ""
|
|
data["adresse_complete"] = ""
|
|
except Exception as e:
|
|
logger.debug(f"Erreur adresse fournisseur {numero}: {e}")
|
|
data["adresse"] = ""
|
|
data["complement"] = ""
|
|
data["code_postal"] = ""
|
|
data["ville"] = ""
|
|
data["region"] = ""
|
|
data["pays"] = ""
|
|
data["adresse_complete"] = ""
|
|
|
|
try:
|
|
telecom_obj = getattr(fourn_obj, "Telecom", None)
|
|
if telecom_obj:
|
|
data["telephone"] = getattr(telecom_obj, "Telephone", "").strip()
|
|
data["portable"] = getattr(telecom_obj, "Portable", "").strip()
|
|
data["telecopie"] = getattr(telecom_obj, "Telecopie", "").strip()
|
|
data["email"] = getattr(telecom_obj, "EMail", "").strip()
|
|
|
|
try:
|
|
site = (
|
|
getattr(telecom_obj, "Site", None)
|
|
or getattr(telecom_obj, "Web", None)
|
|
or getattr(telecom_obj, "SiteWeb", "")
|
|
)
|
|
data["site_web"] = str(site).strip() if site else ""
|
|
except:
|
|
data["site_web"] = ""
|
|
else:
|
|
data["telephone"] = ""
|
|
data["portable"] = ""
|
|
data["telecopie"] = ""
|
|
data["email"] = ""
|
|
data["site_web"] = ""
|
|
except Exception as e:
|
|
logger.debug(f"Erreur telecom fournisseur {numero}: {e}")
|
|
data["telephone"] = ""
|
|
data["portable"] = ""
|
|
data["telecopie"] = ""
|
|
data["email"] = ""
|
|
data["site_web"] = ""
|
|
|
|
try:
|
|
data["siret"] = getattr(fourn_obj, "CT_Siret", "").strip()
|
|
except:
|
|
data["siret"] = ""
|
|
|
|
try:
|
|
if data["siret"] and len(data["siret"]) >= 9:
|
|
data["siren"] = data["siret"][:9]
|
|
else:
|
|
data["siren"] = getattr(fourn_obj, "CT_Siren", "").strip()
|
|
except:
|
|
data["siren"] = ""
|
|
|
|
try:
|
|
data["tva_intra"] = getattr(fourn_obj, "CT_Identifiant", "").strip()
|
|
except:
|
|
data["tva_intra"] = ""
|
|
|
|
try:
|
|
data["code_naf"] = (
|
|
getattr(fourn_obj, "CT_CodeNAF", "").strip()
|
|
or getattr(fourn_obj, "CT_APE", "").strip()
|
|
)
|
|
except:
|
|
data["code_naf"] = ""
|
|
|
|
try:
|
|
data["forme_juridique"] = getattr(
|
|
fourn_obj, "CT_FormeJuridique", ""
|
|
).strip()
|
|
except:
|
|
data["forme_juridique"] = ""
|
|
|
|
try:
|
|
cat_tarif = getattr(fourn_obj, "N_CatTarif", None)
|
|
data["categorie_tarifaire"] = (
|
|
int(cat_tarif) if cat_tarif is not None else None
|
|
)
|
|
except:
|
|
data["categorie_tarifaire"] = None
|
|
|
|
try:
|
|
cat_compta = getattr(fourn_obj, "N_CatCompta", None)
|
|
data["categorie_comptable"] = (
|
|
int(cat_compta) if cat_compta is not None else None
|
|
)
|
|
except:
|
|
data["categorie_comptable"] = None
|
|
|
|
try:
|
|
cond_regl = getattr(fourn_obj, "CT_CondRegl", "").strip()
|
|
data["conditions_reglement_code"] = cond_regl
|
|
|
|
if cond_regl:
|
|
try:
|
|
cond_obj = getattr(fourn_obj, "ConditionReglement", None)
|
|
if cond_obj:
|
|
cond_obj.Read()
|
|
data["conditions_reglement_libelle"] = getattr(
|
|
cond_obj, "C_Intitule", ""
|
|
).strip()
|
|
else:
|
|
data["conditions_reglement_libelle"] = ""
|
|
except:
|
|
data["conditions_reglement_libelle"] = ""
|
|
else:
|
|
data["conditions_reglement_libelle"] = ""
|
|
except:
|
|
data["conditions_reglement_code"] = ""
|
|
data["conditions_reglement_libelle"] = ""
|
|
|
|
try:
|
|
mode_regl = getattr(fourn_obj, "CT_ModeRegl", "").strip()
|
|
data["mode_reglement_code"] = mode_regl
|
|
|
|
if mode_regl:
|
|
try:
|
|
mode_obj = getattr(fourn_obj, "ModeReglement", None)
|
|
if mode_obj:
|
|
mode_obj.Read()
|
|
data["mode_reglement_libelle"] = getattr(
|
|
mode_obj, "M_Intitule", ""
|
|
).strip()
|
|
else:
|
|
data["mode_reglement_libelle"] = ""
|
|
except:
|
|
data["mode_reglement_libelle"] = ""
|
|
else:
|
|
data["mode_reglement_libelle"] = ""
|
|
except:
|
|
data["mode_reglement_code"] = ""
|
|
data["mode_reglement_libelle"] = ""
|
|
|
|
data["coordonnees_bancaires"] = []
|
|
|
|
try:
|
|
factory_banque = getattr(fourn_obj, "FactoryBanque", None)
|
|
|
|
if factory_banque:
|
|
index = 1
|
|
while index <= 5: # Max 5 comptes bancaires
|
|
try:
|
|
banque_persist = factory_banque.List(index)
|
|
if banque_persist is None:
|
|
break
|
|
|
|
banque = win32com.client.CastTo(
|
|
banque_persist, "IBOBanque3"
|
|
)
|
|
banque.Read()
|
|
|
|
compte_bancaire = {
|
|
"banque_nom": getattr(
|
|
banque, "BI_Intitule", ""
|
|
).strip(),
|
|
"iban": getattr(banque, "RIB_Iban", "").strip(),
|
|
"bic": getattr(banque, "RIB_Bic", "").strip(),
|
|
"code_banque": getattr(
|
|
banque, "RIB_Banque", ""
|
|
).strip(),
|
|
"code_guichet": getattr(
|
|
banque, "RIB_Guichet", ""
|
|
).strip(),
|
|
"numero_compte": getattr(
|
|
banque, "RIB_Compte", ""
|
|
).strip(),
|
|
"cle_rib": getattr(banque, "RIB_Cle", "").strip(),
|
|
}
|
|
|
|
if (
|
|
compte_bancaire["iban"]
|
|
or compte_bancaire["numero_compte"]
|
|
):
|
|
data["coordonnees_bancaires"].append(compte_bancaire)
|
|
|
|
index += 1
|
|
except:
|
|
break
|
|
except Exception as e:
|
|
logger.debug(
|
|
f"Erreur coordonnées bancaires fournisseur {numero}: {e}"
|
|
)
|
|
|
|
if data["coordonnees_bancaires"]:
|
|
data["iban_principal"] = data["coordonnees_bancaires"][0].get(
|
|
"iban", ""
|
|
)
|
|
data["bic_principal"] = data["coordonnees_bancaires"][0].get("bic", "")
|
|
else:
|
|
data["iban_principal"] = ""
|
|
data["bic_principal"] = ""
|
|
|
|
data["contacts"] = []
|
|
|
|
try:
|
|
factory_contact = getattr(fourn_obj, "FactoryContact", None)
|
|
|
|
if factory_contact:
|
|
index = 1
|
|
while index <= 20: # Max 20 contacts
|
|
try:
|
|
contact_persist = factory_contact.List(index)
|
|
if contact_persist is None:
|
|
break
|
|
|
|
contact = win32com.client.CastTo(
|
|
contact_persist, "IBOContact3"
|
|
)
|
|
contact.Read()
|
|
|
|
contact_data = {
|
|
"nom": getattr(contact, "CO_Nom", "").strip(),
|
|
"prenom": getattr(contact, "CO_Prenom", "").strip(),
|
|
"fonction": getattr(contact, "CO_Fonction", "").strip(),
|
|
"service": getattr(contact, "CO_Service", "").strip(),
|
|
"telephone": getattr(
|
|
contact, "CO_Telephone", ""
|
|
).strip(),
|
|
"portable": getattr(contact, "CO_Portable", "").strip(),
|
|
"email": getattr(contact, "CO_EMail", "").strip(),
|
|
}
|
|
|
|
nom_complet = f"{contact_data['prenom']} {contact_data['nom']}".strip()
|
|
if nom_complet:
|
|
contact_data["nom_complet"] = nom_complet
|
|
else:
|
|
contact_data["nom_complet"] = contact_data["nom"]
|
|
|
|
if contact_data["nom"]:
|
|
data["contacts"].append(contact_data)
|
|
|
|
index += 1
|
|
except:
|
|
break
|
|
except Exception as e:
|
|
logger.debug(f"Erreur contacts fournisseur {numero}: {e}")
|
|
|
|
data["nb_contacts"] = len(data["contacts"])
|
|
|
|
if data["contacts"]:
|
|
data["contact_principal"] = data["contacts"][0]
|
|
else:
|
|
data["contact_principal"] = None
|
|
|
|
try:
|
|
data["encours_autorise"] = float(getattr(fourn_obj, "CT_Encours", 0.0))
|
|
except:
|
|
data["encours_autorise"] = 0.0
|
|
|
|
try:
|
|
data["ca_annuel"] = float(getattr(fourn_obj, "CT_ChiffreAffaire", 0.0))
|
|
except:
|
|
data["ca_annuel"] = 0.0
|
|
|
|
try:
|
|
data["compte_general"] = getattr(fourn_obj, "CG_Num", "").strip()
|
|
except:
|
|
data["compte_general"] = ""
|
|
|
|
try:
|
|
date_creation = getattr(fourn_obj, "CT_DateCreate", None)
|
|
data["date_creation"] = str(date_creation) if date_creation else ""
|
|
except:
|
|
data["date_creation"] = ""
|
|
|
|
try:
|
|
date_modif = getattr(fourn_obj, "CT_DateModif", None)
|
|
data["date_modification"] = str(date_modif) if date_modif else ""
|
|
except:
|
|
data["date_modification"] = ""
|
|
|
|
return data
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur extraction fournisseur: {e}", exc_info=True)
|
|
return {
|
|
"numero": getattr(fourn_obj, "CT_Num", "").strip(),
|
|
"intitule": getattr(fourn_obj, "CT_Intitule", "").strip(),
|
|
"type": 1,
|
|
"est_fournisseur": True,
|
|
"est_actif": True,
|
|
"en_sommeil": False,
|
|
"adresse": "",
|
|
"complement": "",
|
|
"code_postal": "",
|
|
"ville": "",
|
|
"region": "",
|
|
"pays": "",
|
|
"adresse_complete": "",
|
|
"telephone": "",
|
|
"portable": "",
|
|
"telecopie": "",
|
|
"email": "",
|
|
"site_web": "",
|
|
"siret": "",
|
|
"siren": "",
|
|
"tva_intra": "",
|
|
"code_naf": "",
|
|
"forme_juridique": "",
|
|
"categorie_tarifaire": None,
|
|
"categorie_comptable": None,
|
|
"conditions_reglement_code": "",
|
|
"conditions_reglement_libelle": "",
|
|
"mode_reglement_code": "",
|
|
"mode_reglement_libelle": "",
|
|
"iban_principal": "",
|
|
"bic_principal": "",
|
|
"coordonnees_bancaires": [],
|
|
"contacts": [],
|
|
"nb_contacts": 0,
|
|
"contact_principal": None,
|
|
"encours_autorise": 0.0,
|
|
"ca_annuel": 0.0,
|
|
"compte_general": "",
|
|
"date_creation": "",
|
|
"date_modification": "",
|
|
}
|
|
|
|
def normaliser_date(self, valeur):
|
|
if isinstance(valeur, str):
|
|
try:
|
|
return datetime.fromisoformat(valeur)
|
|
except ValueError:
|
|
return datetime.now()
|
|
|
|
elif isinstance(valeur, date):
|
|
return datetime.combine(valeur, datetime.min.time())
|
|
|
|
elif isinstance(valeur, datetime):
|
|
return valeur
|
|
|
|
else:
|
|
return datetime.now()
|
|
|
|
def creer_devis_enrichi(self, devis_data: dict, forcer_brouillon: bool = False):
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
logger.info(
|
|
f" Début création devis pour client {devis_data['client']['code']} "
|
|
f"(brouillon={forcer_brouillon})"
|
|
)
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
transaction_active = False
|
|
try:
|
|
self.cial.CptaApplication.BeginTrans()
|
|
transaction_active = True
|
|
logger.debug(" Transaction Sage démarrée")
|
|
except Exception as e:
|
|
logger.warning(f"BeginTrans échoué: {e}")
|
|
|
|
try:
|
|
process = self.cial.CreateProcess_Document(0)
|
|
doc = process.Document
|
|
|
|
try:
|
|
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
|
|
except:
|
|
pass
|
|
|
|
logger.info(" Document devis créé")
|
|
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(devis_data.get("date_devis"))
|
|
)
|
|
|
|
if "date_livraison" in devis_data and devis_data["date_livraison"]:
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(devis_data["date_livraison"])
|
|
)
|
|
|
|
factory_client = self.cial.CptaApplication.FactoryClient
|
|
persist_client = factory_client.ReadNumero(
|
|
devis_data["client"]["code"]
|
|
)
|
|
|
|
if not persist_client:
|
|
raise ValueError(
|
|
f" Client {devis_data['client']['code']} introuvable"
|
|
)
|
|
|
|
client_obj = self._cast_client(persist_client)
|
|
if not client_obj:
|
|
raise ValueError(
|
|
f" Impossible de charger le client {devis_data['client']['code']}"
|
|
)
|
|
|
|
doc.SetDefaultClient(client_obj)
|
|
logger.info(f" Client {devis_data['client']['code']} associé")
|
|
|
|
if forcer_brouillon:
|
|
doc.DO_Statut = 0
|
|
logger.info(" Statut défini: 0 (Brouillon)")
|
|
else:
|
|
doc.DO_Statut = 2
|
|
logger.info(" Statut défini: 2 (Accepté)")
|
|
|
|
doc.Write()
|
|
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
|
|
logger.info(f" Ajout de {len(devis_data['lignes'])} lignes...")
|
|
|
|
for idx, ligne_data in enumerate(devis_data["lignes"], 1):
|
|
logger.debug(
|
|
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
|
|
)
|
|
|
|
persist_article = factory_article.ReadReference(
|
|
ligne_data["article_code"]
|
|
)
|
|
|
|
if not persist_article:
|
|
raise ValueError(
|
|
f" Article {ligne_data['article_code']} introuvable"
|
|
)
|
|
|
|
article_obj = win32com.client.CastTo(
|
|
persist_article, "IBOArticle3"
|
|
)
|
|
article_obj.Read()
|
|
|
|
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
|
|
designation_sage = getattr(article_obj, "AR_Design", "")
|
|
|
|
if prix_sage == 0:
|
|
logger.warning(
|
|
f"Article {ligne_data['article_code']} a un prix = 0€"
|
|
)
|
|
|
|
ligne_persist = factory_lignes.Create()
|
|
|
|
try:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentLigne3"
|
|
)
|
|
except:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentVenteLigne3"
|
|
)
|
|
|
|
quantite = float(ligne_data["quantite"])
|
|
|
|
try:
|
|
ligne_obj.SetDefaultArticleReference(
|
|
ligne_data["article_code"], quantite
|
|
)
|
|
except:
|
|
try:
|
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
|
except:
|
|
ligne_obj.DL_Design = (
|
|
designation_sage
|
|
or ligne_data.get("designation", "")
|
|
)
|
|
ligne_obj.DL_Qte = quantite
|
|
|
|
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
|
|
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
|
|
|
|
if prix_a_utiliser is not None and prix_a_utiliser > 0:
|
|
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
|
|
elif prix_auto == 0:
|
|
if prix_sage == 0:
|
|
raise ValueError(
|
|
f"Prix nul pour article {ligne_data['article_code']}"
|
|
)
|
|
ligne_obj.DL_PrixUnitaire = float(prix_sage)
|
|
|
|
remise = ligne_data.get("remise_pourcentage", 0)
|
|
if remise > 0:
|
|
try:
|
|
ligne_obj.DL_Remise01REM_Valeur = float(remise)
|
|
ligne_obj.DL_Remise01REM_Type = 0
|
|
except Exception as e:
|
|
logger.warning(f"Remise non appliquée: {e}")
|
|
|
|
ligne_obj.Write()
|
|
|
|
logger.info(f" {len(devis_data['lignes'])} lignes écrites")
|
|
|
|
doc.Write()
|
|
|
|
if not forcer_brouillon:
|
|
logger.info(" Lancement Process()...")
|
|
process.Process()
|
|
else:
|
|
try:
|
|
process.Process()
|
|
logger.info(" Process() appelé (brouillon)")
|
|
except:
|
|
logger.debug("Process() ignoré pour brouillon")
|
|
|
|
numero_devis = self._recuperer_numero_devis(process, doc)
|
|
|
|
if not numero_devis:
|
|
raise RuntimeError(" Numéro devis vide après création")
|
|
|
|
logger.info(f" Numéro: {numero_devis}")
|
|
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.CommitTrans()
|
|
logger.info(" Transaction committée")
|
|
except:
|
|
pass
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
if "reference" in devis_data and devis_data["reference"]:
|
|
try:
|
|
logger.info(
|
|
f" Application de la référence: {devis_data['reference']}"
|
|
)
|
|
|
|
doc_reload = self._charger_devis(numero_devis)
|
|
|
|
nouvelle_reference = devis_data["reference"]
|
|
doc_reload.DO_Ref = (
|
|
str(nouvelle_reference) if nouvelle_reference else ""
|
|
)
|
|
doc_reload.Write()
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc_reload.Read()
|
|
|
|
logger.info(f" Référence définie: {nouvelle_reference}")
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"Impossible de définir la référence: {e}",
|
|
exc_info=True,
|
|
)
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc_final_data = self._relire_devis(
|
|
numero_devis, devis_data, forcer_brouillon
|
|
)
|
|
|
|
logger.info(
|
|
f" DEVIS CRÉÉ: {numero_devis} - {doc_final_data['total_ttc']}€ TTC "
|
|
)
|
|
|
|
return doc_final_data
|
|
|
|
except Exception as e:
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.RollbackTrans()
|
|
logger.error(" Transaction annulée (rollback)")
|
|
except:
|
|
pass
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f" ERREUR CRÉATION DEVIS: {e}", exc_info=True)
|
|
raise RuntimeError(f"Échec création devis: {str(e)}")
|
|
|
|
def _recuperer_numero_devis(self, process, doc):
|
|
"""Récupère le numéro du devis créé via plusieurs méthodes."""
|
|
numero_devis = None
|
|
|
|
try:
|
|
doc_result = process.DocumentResult
|
|
if doc_result:
|
|
doc_result = win32com.client.CastTo(doc_result, "IBODocumentVente3")
|
|
doc_result.Read()
|
|
numero_devis = getattr(doc_result, "DO_Piece", "")
|
|
except:
|
|
pass
|
|
|
|
if not numero_devis:
|
|
numero_devis = getattr(doc, "DO_Piece", "")
|
|
|
|
if not numero_devis:
|
|
try:
|
|
doc.SetDefaultNumPiece()
|
|
doc.Write()
|
|
doc.Read()
|
|
numero_devis = getattr(doc, "DO_Piece", "")
|
|
except:
|
|
pass
|
|
|
|
return numero_devis
|
|
|
|
def _relire_devis(self, numero_devis, devis_data, forcer_brouillon):
|
|
"""Relit le devis créé et extrait les informations finales."""
|
|
factory_doc = self.cial.FactoryDocumentVente
|
|
persist_reread = factory_doc.ReadPiece(0, numero_devis)
|
|
|
|
if not persist_reread:
|
|
logger.debug("ReadPiece échoué, recherche dans List()...")
|
|
persist_reread = self._rechercher_devis_dans_liste(
|
|
numero_devis, factory_doc
|
|
)
|
|
|
|
if persist_reread:
|
|
doc_final = win32com.client.CastTo(persist_reread, "IBODocumentVente3")
|
|
doc_final.Read()
|
|
|
|
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
|
|
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
|
|
statut_final = getattr(doc_final, "DO_Statut", 0)
|
|
reference_final = getattr(doc_final, "DO_Ref", "")
|
|
|
|
date_livraison_final = None
|
|
|
|
try:
|
|
date_livr = getattr(doc_final, "DO_DateLivr", None)
|
|
if date_livr:
|
|
date_livraison_final = date_livr.strftime("%Y-%m-%d")
|
|
except:
|
|
pass
|
|
else:
|
|
total_calcule = sum(
|
|
l.get("montant_ligne_ht", 0) for l in devis_data["lignes"]
|
|
)
|
|
total_ht = total_calcule
|
|
total_ttc = round(total_calcule * 1.20, 2)
|
|
statut_final = 0 if forcer_brouillon else 2
|
|
reference_final = devis_data.get("reference", "")
|
|
date_livraison_final = devis_data.get("date_livraison")
|
|
|
|
logger.info(f" Total HT: {total_ht}€")
|
|
logger.info(f" Total TTC: {total_ttc}€")
|
|
logger.info(f" Statut final: {statut_final}")
|
|
if reference_final:
|
|
logger.info(f" Référence: {reference_final}")
|
|
if date_livraison_final:
|
|
logger.info(f" Date livraison: {date_livraison_final}")
|
|
|
|
return {
|
|
"numero_devis": numero_devis,
|
|
"total_ht": total_ht,
|
|
"total_ttc": total_ttc,
|
|
"nb_lignes": len(devis_data["lignes"]),
|
|
"client_code": devis_data["client"]["code"],
|
|
"date_devis": str(devis_data.get("date_devis", "")),
|
|
"date_livraison": date_livraison_final,
|
|
"reference": reference_final,
|
|
"statut": statut_final,
|
|
}
|
|
|
|
def _rechercher_devis_dans_liste(self, numero_devis, factory_doc):
|
|
"""Recherche un devis dans les 100 premiers éléments de la liste."""
|
|
index = 1
|
|
while index < 100:
|
|
try:
|
|
persist_test = factory_doc.List(index)
|
|
if persist_test is None:
|
|
break
|
|
|
|
doc_test = win32com.client.CastTo(persist_test, "IBODocumentVente3")
|
|
doc_test.Read()
|
|
|
|
if (
|
|
getattr(doc_test, "DO_Type", -1) == 0
|
|
and getattr(doc_test, "DO_Piece", "") == numero_devis
|
|
):
|
|
logger.info(f" Document trouvé à l'index {index}")
|
|
return persist_test
|
|
|
|
index += 1
|
|
except:
|
|
index += 1
|
|
|
|
return None
|
|
|
|
def modifier_devis(self, numero: str, devis_data: Dict) -> Dict:
|
|
"""
|
|
Modifie un devis existant dans Sage - VERSION COMPLÈTE.
|
|
|
|
Args:
|
|
numero: Numéro du devis
|
|
devis_data: dict contenant les champs à modifier:
|
|
- date_devis: str ou date (optionnel)
|
|
- date_livraison: str ou date (optionnel)
|
|
- reference: str (optionnel)
|
|
- statut: int (optionnel)
|
|
- lignes: list[dict] (optionnel)
|
|
"""
|
|
logger.info("=" * 100)
|
|
logger.info("=" * 100)
|
|
logger.info(f" NOUVELLE MÉTHODE modifier_devis() APPELÉE POUR {numero} ")
|
|
logger.info(f" Données reçues: {devis_data}")
|
|
logger.info("=" * 100)
|
|
|
|
if not self.cial:
|
|
logger.error(" Connexion Sage non établie")
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info("")
|
|
logger.info("=" * 80)
|
|
logger.info(f" [ÉTAPE 1] CHARGEMENT DU DEVIS {numero}")
|
|
logger.info("=" * 80)
|
|
|
|
doc = self._charger_devis(numero)
|
|
logger.info(f" Devis {numero} chargé avec succès")
|
|
|
|
logger.info("")
|
|
self._afficher_etat_document(doc, "📸 ÉTAT INITIAL")
|
|
|
|
logger.info(" Vérification statut transformation...")
|
|
self._verifier_devis_non_transforme(numero, doc)
|
|
logger.info(" Devis non transformé - modification autorisée")
|
|
|
|
logger.info("")
|
|
logger.info("=" * 80)
|
|
logger.info(" [ÉTAPE 2] ANALYSE DOCUMENT ACTUEL")
|
|
logger.info("=" * 80)
|
|
|
|
client_code_initial = ""
|
|
try:
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_code_initial = getattr(client_obj, "CT_Num", "").strip()
|
|
logger.info(f" Client: {client_code_initial}")
|
|
else:
|
|
logger.warning(" Objet Client non trouvé")
|
|
except Exception as e:
|
|
logger.warning(f" Impossible de lire le client: {e}")
|
|
|
|
nb_lignes_initial = self._compter_lignes_document(doc)
|
|
logger.info(f" Nombre de lignes actuelles: {nb_lignes_initial}")
|
|
|
|
logger.info("")
|
|
logger.info("=" * 80)
|
|
logger.info(" [ÉTAPE 3] ANALYSE MODIFICATIONS DEMANDÉES")
|
|
logger.info("=" * 80)
|
|
|
|
modif_date = "date_devis" in devis_data
|
|
modif_date_livraison = "date_livraison" in devis_data
|
|
modif_statut = "statut" in devis_data
|
|
modif_ref = "reference" in devis_data
|
|
modif_lignes = "lignes" in devis_data and devis_data["lignes"] is not None
|
|
|
|
logger.info(f" Date devis: {modif_date}")
|
|
if modif_date:
|
|
logger.info(f" → Valeur: {devis_data['date_devis']}")
|
|
|
|
logger.info(f" Date livraison: {modif_date_livraison}")
|
|
if modif_date_livraison:
|
|
logger.info(f" → Valeur: {devis_data['date_livraison']}")
|
|
|
|
logger.info(f" Référence: {modif_ref}")
|
|
if modif_ref:
|
|
logger.info(f" → Valeur: '{devis_data['reference']}'")
|
|
|
|
logger.info(f" Statut: {modif_statut}")
|
|
if modif_statut:
|
|
logger.info(f" → Valeur: {devis_data['statut']}")
|
|
|
|
logger.info(f" Lignes: {modif_lignes}")
|
|
if modif_lignes:
|
|
logger.info(f" → Nombre: {len(devis_data['lignes'])}")
|
|
for i, ligne in enumerate(devis_data['lignes'], 1):
|
|
logger.info(f" → Ligne {i}: {ligne.get('article_code')} (Qté: {ligne.get('quantite')})")
|
|
|
|
devis_data_temp = devis_data.copy()
|
|
reference_a_modifier = None
|
|
statut_a_modifier = None
|
|
|
|
if modif_lignes:
|
|
logger.info("")
|
|
logger.info(" STRATÉGIE: Report référence/statut APRÈS modification lignes")
|
|
|
|
if modif_ref:
|
|
reference_a_modifier = devis_data_temp.pop("reference")
|
|
logger.info(f" Référence '{reference_a_modifier}' reportée")
|
|
modif_ref = False
|
|
|
|
if modif_statut:
|
|
statut_a_modifier = devis_data_temp.pop("statut")
|
|
logger.info(f" Statut {statut_a_modifier} reporté")
|
|
modif_statut = False
|
|
|
|
logger.info("")
|
|
logger.info("=" * 80)
|
|
logger.info(" [ÉTAPE 4] TEST WRITE() BASIQUE")
|
|
logger.info("=" * 80)
|
|
logger.info("Test sans modification pour vérifier le verrouillage...")
|
|
|
|
try:
|
|
doc.Write()
|
|
logger.info(" Write() basique OK - Document NON verrouillé")
|
|
|
|
time.sleep(0.3)
|
|
doc.Read()
|
|
logger.info(" Read() après Write() OK")
|
|
except Exception as e:
|
|
logger.error(f" Write() basique ÉCHOUE: {e}")
|
|
logger.error(" ABANDON: Document VERROUILLÉ ou problème COM")
|
|
raise ValueError(f"Document verrouillé: {e}")
|
|
|
|
champs_modifies = []
|
|
|
|
if not modif_lignes and (modif_date or modif_date_livraison or modif_statut or modif_ref):
|
|
logger.info("")
|
|
logger.info("=" * 80)
|
|
logger.info(" [ÉTAPE 5A] MODIFICATIONS SIMPLES (sans lignes)")
|
|
logger.info("=" * 80)
|
|
|
|
if modif_date:
|
|
logger.info("")
|
|
logger.info(" Modification DATE_DEVIS...")
|
|
try:
|
|
ancienne_date = getattr(doc, "DO_Date", None)
|
|
ancienne_date_str = ancienne_date.strftime("%Y-%m-%d") if ancienne_date else "None"
|
|
logger.info(f" Actuelle: {ancienne_date_str}")
|
|
|
|
nouvelle_date = self.normaliser_date(devis_data_temp["date_devis"])
|
|
nouvelle_date_str = nouvelle_date.strftime("%Y-%m-%d")
|
|
logger.info(f" Cible: {nouvelle_date_str}")
|
|
|
|
doc.DO_Date = pywintypes.Time(nouvelle_date)
|
|
logger.info(" ✓ doc.DO_Date affecté")
|
|
|
|
champs_modifies.append("date_devis")
|
|
logger.info(f" Date devis sera modifiée: {ancienne_date_str} → {nouvelle_date_str}")
|
|
except Exception as e:
|
|
logger.error(f" Erreur date devis: {e}", exc_info=True)
|
|
|
|
if modif_date_livraison:
|
|
logger.info("")
|
|
logger.info(" Modification DATE_LIVRAISON...")
|
|
try:
|
|
ancienne_date_livr = getattr(doc, "DO_DateLivr", None)
|
|
ancienne_date_livr_str = ancienne_date_livr.strftime("%Y-%m-%d") if ancienne_date_livr else "None"
|
|
logger.info(f" Actuelle: {ancienne_date_livr_str}")
|
|
|
|
if devis_data_temp["date_livraison"]:
|
|
nouvelle_date_livr = self.normaliser_date(devis_data_temp["date_livraison"])
|
|
nouvelle_date_livr_str = nouvelle_date_livr.strftime("%Y-%m-%d")
|
|
logger.info(f" Cible: {nouvelle_date_livr_str}")
|
|
|
|
doc.DO_DateLivr = pywintypes.Time(nouvelle_date_livr)
|
|
logger.info(" ✓ doc.DO_DateLivr affecté")
|
|
else:
|
|
logger.info(" Cible: Effacement (None)")
|
|
doc.DO_DateLivr = None
|
|
logger.info(" ✓ doc.DO_DateLivr = None")
|
|
|
|
champs_modifies.append("date_livraison")
|
|
logger.info(" Date livraison sera modifiée")
|
|
except Exception as e:
|
|
logger.error(f" Erreur date livraison: {e}", exc_info=True)
|
|
|
|
if modif_ref:
|
|
logger.info("")
|
|
logger.info(" Modification RÉFÉRENCE...")
|
|
try:
|
|
ancienne_ref = getattr(doc, "DO_Ref", "")
|
|
logger.info(f" Actuelle: '{ancienne_ref}'")
|
|
|
|
nouvelle_ref = str(devis_data_temp["reference"]) if devis_data_temp["reference"] else ""
|
|
logger.info(f" Cible: '{nouvelle_ref}'")
|
|
|
|
doc.DO_Ref = nouvelle_ref
|
|
logger.info(" ✓ doc.DO_Ref affecté")
|
|
|
|
champs_modifies.append("reference")
|
|
logger.info(f" Référence sera modifiée: '{ancienne_ref}' → '{nouvelle_ref}'")
|
|
except Exception as e:
|
|
logger.error(f" Erreur référence: {e}", exc_info=True)
|
|
|
|
if modif_statut:
|
|
logger.info("")
|
|
logger.info(" Modification STATUT...")
|
|
try:
|
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
|
logger.info(f" Actuel: {statut_actuel}")
|
|
|
|
nouveau_statut = int(devis_data_temp["statut"])
|
|
logger.info(f" Cible: {nouveau_statut}")
|
|
|
|
if nouveau_statut in [0, 1, 2, 3]:
|
|
doc.DO_Statut = nouveau_statut
|
|
logger.info(" ✓ doc.DO_Statut affecté")
|
|
|
|
champs_modifies.append("statut")
|
|
logger.info(f" Statut sera modifié: {statut_actuel} → {nouveau_statut}")
|
|
else:
|
|
logger.warning(f" Statut {nouveau_statut} invalide (doit être 0,1,2 ou 3)")
|
|
except Exception as e:
|
|
logger.error(f" Erreur statut: {e}", exc_info=True)
|
|
|
|
logger.info("")
|
|
logger.info(" Write() modifications simples...")
|
|
try:
|
|
doc.Write()
|
|
logger.info(" Write() réussi")
|
|
|
|
time.sleep(0.5)
|
|
doc.Read()
|
|
logger.info(" Read() après Write() OK")
|
|
except Exception as e:
|
|
logger.error(f" Write() a échoué: {e}", exc_info=True)
|
|
raise
|
|
|
|
elif modif_lignes:
|
|
logger.info("")
|
|
logger.info("=" * 80)
|
|
logger.info(" [ÉTAPE 5B] REMPLACEMENT COMPLET DES LIGNES")
|
|
logger.info("=" * 80)
|
|
|
|
if modif_date:
|
|
logger.info(" Modification date devis (avant lignes)...")
|
|
try:
|
|
nouvelle_date = self.normaliser_date(devis_data_temp["date_devis"])
|
|
doc.DO_Date = pywintypes.Time(nouvelle_date)
|
|
champs_modifies.append("date_devis")
|
|
logger.info(f" Date: {nouvelle_date.strftime('%Y-%m-%d')}")
|
|
except Exception as e:
|
|
logger.error(f" Erreur: {e}")
|
|
|
|
if modif_date_livraison:
|
|
logger.info(" Modification date livraison (avant lignes)...")
|
|
try:
|
|
if devis_data_temp["date_livraison"]:
|
|
nouvelle_date_livr = self.normaliser_date(devis_data_temp["date_livraison"])
|
|
doc.DO_DateLivr = pywintypes.Time(nouvelle_date_livr)
|
|
logger.info(f" Date livraison: {nouvelle_date_livr.strftime('%Y-%m-%d')}")
|
|
else:
|
|
doc.DO_DateLivr = None
|
|
logger.info(" Date livraison effacée")
|
|
champs_modifies.append("date_livraison")
|
|
except Exception as e:
|
|
logger.error(f" Erreur: {e}")
|
|
|
|
nouvelles_lignes = devis_data["lignes"]
|
|
nb_nouvelles = len(nouvelles_lignes)
|
|
|
|
logger.info("")
|
|
logger.info(f" Remplacement: {nb_lignes_initial} lignes → {nb_nouvelles} lignes")
|
|
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
|
|
if nb_lignes_initial > 0:
|
|
logger.info("")
|
|
logger.info(f" Suppression de {nb_lignes_initial} lignes existantes...")
|
|
|
|
for idx in range(nb_lignes_initial, 0, -1):
|
|
try:
|
|
ligne_p = factory_lignes.List(idx)
|
|
if ligne_p:
|
|
try:
|
|
ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3")
|
|
except:
|
|
ligne = win32com.client.CastTo(ligne_p, "IBODocumentVenteLigne3")
|
|
|
|
ligne.Read()
|
|
ligne.Remove()
|
|
logger.debug(f" Ligne {idx} supprimée")
|
|
except Exception as e:
|
|
logger.warning(f" Ligne {idx} non supprimée: {e}")
|
|
|
|
logger.info(f" {nb_lignes_initial} lignes supprimées")
|
|
|
|
logger.info("")
|
|
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
|
|
|
|
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
|
|
article_code = ligne_data["article_code"]
|
|
quantite = float(ligne_data["quantite"])
|
|
|
|
logger.info("")
|
|
logger.info(f" Ligne {idx}/{nb_nouvelles}: {article_code}")
|
|
logger.info(f" Quantité: {quantite}")
|
|
if ligne_data.get("prix_unitaire_ht"):
|
|
logger.info(f" Prix HT: {ligne_data['prix_unitaire_ht']}€")
|
|
if ligne_data.get("remise_pourcentage"):
|
|
logger.info(f" Remise: {ligne_data['remise_pourcentage']}%")
|
|
|
|
try:
|
|
persist_article = factory_article.ReadReference(article_code)
|
|
if not persist_article:
|
|
raise ValueError(f"Article {article_code} INTROUVABLE")
|
|
|
|
article_obj = win32com.client.CastTo(persist_article, "IBOArticle3")
|
|
article_obj.Read()
|
|
logger.info(f" ✓ Article chargé")
|
|
|
|
ligne_persist = factory_lignes.Create()
|
|
|
|
try:
|
|
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3")
|
|
except:
|
|
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3")
|
|
|
|
try:
|
|
ligne_obj.SetDefaultArticleReference(article_code, quantite)
|
|
logger.info(f" ✓ Article associé via SetDefaultArticleReference")
|
|
except:
|
|
try:
|
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
|
logger.info(f" ✓ Article associé via SetDefaultArticle")
|
|
except:
|
|
ligne_obj.DL_Design = ligne_data.get("designation", "")
|
|
ligne_obj.DL_Qte = quantite
|
|
logger.info(f" ✓ Article associé manuellement")
|
|
|
|
if ligne_data.get("prix_unitaire_ht"):
|
|
ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"])
|
|
logger.info(f" ✓ Prix unitaire défini")
|
|
|
|
if ligne_data.get("remise_pourcentage", 0) > 0:
|
|
try:
|
|
ligne_obj.DL_Remise01REM_Valeur = float(ligne_data["remise_pourcentage"])
|
|
ligne_obj.DL_Remise01REM_Type = 0
|
|
logger.info(f" ✓ Remise définie")
|
|
except:
|
|
logger.debug(f" Remise non supportée")
|
|
|
|
ligne_obj.Write()
|
|
logger.info(f" Ligne {idx} créée avec succès")
|
|
|
|
except Exception as e:
|
|
logger.error(f" ERREUR ligne {idx}: {e}", exc_info=True)
|
|
raise
|
|
|
|
logger.info("")
|
|
logger.info(f" {nb_nouvelles} lignes créées")
|
|
|
|
logger.info("")
|
|
logger.info(" Write() après remplacement lignes...")
|
|
try:
|
|
doc.Write()
|
|
logger.info(" Write() réussi")
|
|
|
|
time.sleep(0.5)
|
|
doc.Read()
|
|
logger.info(" Read() après Write() OK")
|
|
except Exception as e:
|
|
logger.error(f" Write() a échoué: {e}", exc_info=True)
|
|
raise
|
|
|
|
champs_modifies.append("lignes")
|
|
|
|
if reference_a_modifier is not None:
|
|
logger.info("")
|
|
logger.info("=" * 80)
|
|
logger.info(" [ÉTAPE 6] MODIFICATION RÉFÉRENCE (après lignes)")
|
|
logger.info("=" * 80)
|
|
|
|
try:
|
|
ancienne_ref = getattr(doc, "DO_Ref", "")
|
|
nouvelle_ref = str(reference_a_modifier) if reference_a_modifier else ""
|
|
|
|
logger.info(f" Actuelle: '{ancienne_ref}'")
|
|
logger.info(f" Cible: '{nouvelle_ref}'")
|
|
|
|
doc.DO_Ref = nouvelle_ref
|
|
logger.info(" ✓ doc.DO_Ref affecté")
|
|
|
|
doc.Write()
|
|
logger.info(" ✓ Write()")
|
|
|
|
time.sleep(0.5)
|
|
doc.Read()
|
|
logger.info(" ✓ Read()")
|
|
|
|
champs_modifies.append("reference")
|
|
logger.info(f" Référence modifiée: '{ancienne_ref}' → '{nouvelle_ref}'")
|
|
except Exception as e:
|
|
logger.error(f" Erreur référence: {e}", exc_info=True)
|
|
|
|
if statut_a_modifier is not None:
|
|
logger.info("")
|
|
logger.info("=" * 80)
|
|
logger.info(" [ÉTAPE 7] MODIFICATION STATUT (en dernier)")
|
|
logger.info("=" * 80)
|
|
|
|
try:
|
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
|
nouveau_statut = int(statut_a_modifier)
|
|
|
|
logger.info(f" Actuel: {statut_actuel}")
|
|
logger.info(f" Cible: {nouveau_statut}")
|
|
|
|
if nouveau_statut != statut_actuel and nouveau_statut in [0, 1, 2, 3]:
|
|
doc.DO_Statut = nouveau_statut
|
|
logger.info(" ✓ doc.DO_Statut affecté")
|
|
|
|
doc.Write()
|
|
logger.info(" ✓ Write()")
|
|
|
|
time.sleep(0.5)
|
|
doc.Read()
|
|
logger.info(" ✓ Read()")
|
|
|
|
champs_modifies.append("statut")
|
|
logger.info(f" Statut modifié: {statut_actuel} → {nouveau_statut}")
|
|
else:
|
|
logger.info(f" Pas de modification (identique ou invalide)")
|
|
except Exception as e:
|
|
logger.error(f" Erreur statut: {e}", exc_info=True)
|
|
|
|
logger.info("")
|
|
logger.info("=" * 80)
|
|
logger.info(" [ÉTAPE 8] VALIDATION FINALE")
|
|
logger.info("=" * 80)
|
|
|
|
try:
|
|
doc.Write()
|
|
logger.info(" Write() final")
|
|
except Exception as e:
|
|
logger.warning(f" Write() final: {e}")
|
|
|
|
time.sleep(0.5)
|
|
doc.Read()
|
|
logger.info(" Read() final")
|
|
|
|
logger.info("")
|
|
self._afficher_etat_document(doc, "📸 ÉTAT FINAL")
|
|
|
|
logger.info("")
|
|
logger.info("=" * 80)
|
|
logger.info(" [ÉTAPE 9] EXTRACTION RÉSULTAT")
|
|
logger.info("=" * 80)
|
|
|
|
resultat = self._extraire_infos_devis(doc, numero, champs_modifies)
|
|
|
|
logger.info(f" Résultat extrait:")
|
|
logger.info(f" Numéro: {resultat['numero']}")
|
|
logger.info(f" Référence: '{resultat['reference']}'")
|
|
logger.info(f" Date devis: {resultat['date_devis']}")
|
|
logger.info(f" Date livraison: {resultat['date_livraison']}")
|
|
logger.info(f" Statut: {resultat['statut']}")
|
|
logger.info(f" Total HT: {resultat['total_ht']}€")
|
|
logger.info(f" Total TTC: {resultat['total_ttc']}€")
|
|
logger.info(f" Champs modifiés: {resultat['champs_modifies']}")
|
|
|
|
logger.info("")
|
|
logger.info("=" * 100)
|
|
logger.info(f" MODIFICATION DEVIS {numero} TERMINÉE AVEC SUCCÈS ")
|
|
logger.info("=" * 100)
|
|
|
|
return resultat
|
|
|
|
except ValueError as e:
|
|
logger.error(f" ERREUR MÉTIER: {e}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True)
|
|
raise RuntimeError(f"Erreur technique Sage: {str(e)}")
|
|
|
|
def _afficher_etat_document(self, doc, titre: str):
|
|
"""Affiche l'état complet d'un document."""
|
|
logger.info("-" * 80)
|
|
logger.info(titre)
|
|
logger.info("-" * 80)
|
|
try:
|
|
logger.info(f" DO_Piece: {getattr(doc, 'DO_Piece', 'N/A')}")
|
|
logger.info(f" DO_Ref: '{getattr(doc, 'DO_Ref', 'N/A')}'")
|
|
logger.info(f" DO_Statut: {getattr(doc, 'DO_Statut', 'N/A')}")
|
|
|
|
date_doc = getattr(doc, 'DO_Date', None)
|
|
date_str = date_doc.strftime('%Y-%m-%d') if date_doc else 'None'
|
|
logger.info(f" DO_Date: {date_str}")
|
|
|
|
date_livr = getattr(doc, 'DO_DateLivr', None)
|
|
date_livr_str = date_livr.strftime('%Y-%m-%d') if date_livr else 'None'
|
|
logger.info(f" DO_DateLivr: {date_livr_str}")
|
|
|
|
logger.info(f" DO_TotalHT: {getattr(doc, 'DO_TotalHT', 0)}€")
|
|
logger.info(f" DO_TotalTTC: {getattr(doc, 'DO_TotalTTC', 0)}€")
|
|
except Exception as e:
|
|
logger.error(f" Erreur affichage état: {e}")
|
|
logger.info("-" * 80)
|
|
|
|
|
|
def _compter_lignes_document(self, doc) -> int:
|
|
"""Compte les lignes d'un document."""
|
|
try:
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
|
|
|
count = 0
|
|
index = 1
|
|
while index <= 100:
|
|
try:
|
|
ligne_p = factory_lignes.List(index)
|
|
if ligne_p is None:
|
|
break
|
|
count += 1
|
|
index += 1
|
|
except:
|
|
break
|
|
return count
|
|
except Exception as e:
|
|
logger.warning(f" Erreur comptage lignes: {e}")
|
|
return 0
|
|
|
|
|
|
def _charger_devis(self, numero: str):
|
|
"""Charge un devis depuis Sage."""
|
|
logger.info(f" Chargement devis {numero}...")
|
|
|
|
factory = self.cial.FactoryDocumentVente
|
|
|
|
logger.info(" Tentative ReadPiece(0, numero)...")
|
|
persist = factory.ReadPiece(0, numero)
|
|
|
|
if not persist:
|
|
logger.warning(" ReadPiece a échoué, recherche dans la liste...")
|
|
persist = self._rechercher_devis_par_numero(numero, factory)
|
|
|
|
if not persist:
|
|
raise ValueError(f" Devis {numero} INTROUVABLE")
|
|
|
|
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
|
|
doc.Read()
|
|
|
|
logger.info(f" Devis {numero} chargé")
|
|
return doc
|
|
|
|
|
|
def _rechercher_devis_par_numero(self, numero: str, factory):
|
|
"""Recherche un devis par numéro dans la liste."""
|
|
logger.info(f" Recherche de {numero} dans la liste...")
|
|
|
|
index = 1
|
|
while index < 10000:
|
|
try:
|
|
persist_test = factory.List(index)
|
|
if persist_test is None:
|
|
break
|
|
|
|
doc_test = win32com.client.CastTo(persist_test, "IBODocumentVente3")
|
|
doc_test.Read()
|
|
|
|
if (
|
|
getattr(doc_test, "DO_Type", -1) == 0
|
|
and getattr(doc_test, "DO_Piece", "") == numero
|
|
):
|
|
logger.info(f" Trouvé à l'index {index}")
|
|
return persist_test
|
|
|
|
index += 1
|
|
except:
|
|
index += 1
|
|
|
|
logger.error(f" Devis {numero} non trouvé dans la liste")
|
|
return None
|
|
|
|
|
|
def _verifier_devis_non_transforme(self, numero: str, doc):
|
|
"""Vérifie que le devis n'est pas transformé."""
|
|
verification = self.verifier_si_deja_transforme_sql(numero, 0)
|
|
|
|
if verification["deja_transforme"]:
|
|
docs_cibles = verification["documents_cibles"]
|
|
nums = [d["numero"] for d in docs_cibles]
|
|
raise ValueError(
|
|
f" Devis {numero} déjà transformé en {len(docs_cibles)} document(s): {', '.join(nums)}"
|
|
)
|
|
|
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
|
if statut_actuel == 5:
|
|
raise ValueError(f" Devis {numero} déjà transformé (statut=5)")
|
|
|
|
|
|
def _extraire_infos_devis(self, doc, numero: str, champs_modifies: list) -> Dict:
|
|
"""Extrait les informations complètes du devis."""
|
|
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
|
|
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
|
|
statut = getattr(doc, "DO_Statut", 0)
|
|
reference = getattr(doc, "DO_Ref", "")
|
|
|
|
date_devis = None
|
|
try:
|
|
date_doc = getattr(doc, "DO_Date", None)
|
|
if date_doc:
|
|
date_devis = date_doc.strftime("%Y-%m-%d")
|
|
except:
|
|
pass
|
|
|
|
date_livraison = None
|
|
try:
|
|
date_livr = getattr(doc, "DO_DateLivr", None)
|
|
if date_livr:
|
|
date_livraison = date_livr.strftime("%Y-%m-%d")
|
|
except:
|
|
pass
|
|
|
|
client_code = ""
|
|
try:
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_code = getattr(client_obj, "CT_Num", "")
|
|
except:
|
|
pass
|
|
|
|
return {
|
|
"numero": numero,
|
|
"total_ht": total_ht,
|
|
"total_ttc": total_ttc,
|
|
"reference": reference,
|
|
"date_devis": date_devis,
|
|
"date_livraison": date_livraison,
|
|
"champs_modifies": champs_modifies,
|
|
"statut": statut,
|
|
"client_code": client_code,
|
|
}
|
|
|
|
|
|
def lire_devis(self, numero_devis):
|
|
try:
|
|
devis = self._lire_document_sql(numero_devis, type_doc=0)
|
|
|
|
if not devis:
|
|
return None
|
|
|
|
return devis
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur SQL lecture devis {numero_devis}: {e}")
|
|
return None
|
|
|
|
def lire_document(self, numero, type_doc):
|
|
return self._lire_document_sql(numero, type_doc)
|
|
|
|
def verifier_si_deja_transforme_sql(self, numero_source, type_source):
|
|
"""Version corrigée avec normalisation des types"""
|
|
logger.info(
|
|
f"[VERIF] Vérification transformations de {numero_source} (type {type_source})"
|
|
)
|
|
|
|
logger.info(
|
|
f"[VERIF] Vérification transformations de {numero_source} (type {type_source})"
|
|
)
|
|
|
|
logger.info(f"[DEBUG] Type source brut: {type_source}")
|
|
logger.info(
|
|
f"[DEBUG] Type source après normalisation: {self._normaliser_type_document(type_source)}"
|
|
)
|
|
logger.info(
|
|
f"[DEBUG] Type source après normalisation SQL: {self._convertir_type_pour_sql(type_source)}"
|
|
)
|
|
|
|
type_source = self._convertir_type_pour_sql(type_source)
|
|
|
|
champ_liaison_mapping = {
|
|
0: "DL_PieceDE",
|
|
1: "DL_PieceBC",
|
|
3: "DL_PieceBL",
|
|
}
|
|
|
|
champ_liaison = champ_liaison_mapping.get(type_source)
|
|
|
|
if not champ_liaison:
|
|
logger.warning(f"[VERIF] Type source {type_source} non géré")
|
|
return {"deja_transforme": False, "documents_cibles": []}
|
|
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = f"""
|
|
SELECT DISTINCT
|
|
dc.DO_Piece,
|
|
dc.DO_Type,
|
|
dc.DO_Statut,
|
|
(SELECT COUNT(*) FROM F_DOCLIGNE
|
|
WHERE DO_Piece = dc.DO_Piece AND DO_Type = dc.DO_Type) as NbLignes
|
|
FROM F_DOCENTETE dc
|
|
INNER JOIN F_DOCLIGNE dl ON dc.DO_Piece = dl.DO_Piece AND dc.DO_Type = dl.DO_Type
|
|
WHERE dl.{champ_liaison} = ?
|
|
ORDER BY dc.DO_Type, dc.DO_Piece
|
|
"""
|
|
|
|
cursor.execute(query, (numero_source,))
|
|
resultats = cursor.fetchall()
|
|
|
|
documents_cibles = []
|
|
for row in resultats:
|
|
type_brut = int(row.DO_Type)
|
|
type_normalise = self._convertir_type_depuis_sql(type_brut)
|
|
|
|
doc = {
|
|
"numero": row.DO_Piece.strip() if row.DO_Piece else "",
|
|
"type": type_normalise, # ← TYPE NORMALISÉ
|
|
"type_brut": type_brut, # Garder aussi le type original
|
|
"type_libelle": self._get_type_libelle(type_brut),
|
|
"statut": int(row.DO_Statut) if row.DO_Statut else 0,
|
|
"nb_lignes": int(row.NbLignes) if row.NbLignes else 0,
|
|
}
|
|
documents_cibles.append(doc)
|
|
logger.info(
|
|
f"[VERIF] Trouvé: {doc['numero']} "
|
|
f"(type {type_brut}→{type_normalise} - {doc['type_libelle']}) "
|
|
f"- {doc['nb_lignes']} lignes"
|
|
)
|
|
|
|
deja_transforme = len(documents_cibles) > 0
|
|
|
|
if deja_transforme:
|
|
logger.info(
|
|
f"[VERIF] Document {numero_source} a {len(documents_cibles)} transformation(s)"
|
|
)
|
|
else:
|
|
logger.info(
|
|
f"[VERIF] Document {numero_source} pas encore transformé"
|
|
)
|
|
|
|
return {
|
|
"deja_transforme": deja_transforme,
|
|
"documents_cibles": documents_cibles,
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"[VERIF] Erreur vérification: {e}")
|
|
return {"deja_transforme": False, "documents_cibles": []}
|
|
|
|
def peut_etre_transforme(self, numero_source, type_source, type_cible):
|
|
"""Version corrigée avec normalisation"""
|
|
type_source = self._normaliser_type_document(type_source)
|
|
type_cible = self._normaliser_type_document(type_cible)
|
|
|
|
logger.info(
|
|
f"[VERIF_TRANSFO] {numero_source} "
|
|
f"(type {type_source}) → type {type_cible}"
|
|
)
|
|
|
|
verif = self.verifier_si_deja_transforme_sql(numero_source, type_source)
|
|
|
|
docs_meme_type = [
|
|
d for d in verif["documents_cibles"] if d["type"] == type_cible
|
|
]
|
|
|
|
if docs_meme_type:
|
|
nums = [d["numero"] for d in docs_meme_type]
|
|
return {
|
|
"possible": False,
|
|
"raison": f"Document déjà transformé en {self._get_type_libelle(type_cible)}",
|
|
"documents_existants": docs_meme_type,
|
|
"message_detaille": f"Document(s) existant(s): {', '.join(nums)}",
|
|
}
|
|
|
|
return {
|
|
"possible": True,
|
|
"raison": "Transformation possible",
|
|
"documents_existants": [],
|
|
}
|
|
|
|
def _get_type_libelle(self, type_doc: int) -> str:
|
|
"""
|
|
Retourne le libellé d'un type de document.
|
|
Gère les 2 formats : valeur réelle Sage (0,10,20...) ET valeur affichée (0,1,2...)
|
|
"""
|
|
types_officiels = {
|
|
0: "Devis",
|
|
10: "Bon de commande",
|
|
20: "Préparation",
|
|
30: "Bon de livraison",
|
|
40: "Bon de retour",
|
|
50: "Bon d'avoir",
|
|
60: "Facture",
|
|
}
|
|
|
|
types_alternatifs = {
|
|
1: "Bon de commande",
|
|
2: "Préparation",
|
|
3: "Bon de livraison",
|
|
4: "Bon de retour",
|
|
5: "Bon d'avoir",
|
|
6: "Facture",
|
|
}
|
|
|
|
if type_doc in types_officiels:
|
|
return types_officiels[type_doc]
|
|
|
|
if type_doc in types_alternatifs:
|
|
return types_alternatifs[type_doc]
|
|
|
|
return f"Type {type_doc}"
|
|
|
|
def _normaliser_type_document(self, type_doc: int) -> int:
|
|
"""
|
|
Normalise le type de document vers la valeur officielle Sage.
|
|
Convertit 1→10, 2→20, etc. si nécessaire
|
|
"""
|
|
|
|
logger.info(f"[INFO] TYPE RECU{type_doc}")
|
|
|
|
if type_doc in [0, 10, 20, 30, 40, 50, 60]:
|
|
return type_doc
|
|
|
|
mapping_normalisation = {
|
|
1: 10, # Commande
|
|
2: 20, # Préparation
|
|
3: 30, # BL
|
|
4: 40, # Retour
|
|
5: 50, # Avoir
|
|
6: 60, # Facture
|
|
}
|
|
|
|
return mapping_normalisation.get(type_doc, type_doc)
|
|
|
|
def transformer_document(
|
|
self,
|
|
numero_source,
|
|
type_source,
|
|
type_cible,
|
|
ignorer_controle_stock=False,
|
|
conserver_document_source=True,
|
|
verifier_doublons=True,
|
|
):
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
type_source = int(type_source)
|
|
type_cible = int(type_cible)
|
|
|
|
logger.info(
|
|
f"[TRANSFORM] Transformation: {numero_source} ({type_source}) → type {type_cible}"
|
|
)
|
|
|
|
transformations_valides = {
|
|
(0, 10): ("Vente", "CreateProcess_Commander"),
|
|
(0, 60): ("Vente", "CreateProcess_Facturer"),
|
|
(10, 30): ("Vente", "CreateProcess_Livrer"),
|
|
(10, 60): ("Vente", "CreateProcess_Facturer"),
|
|
(30, 60): ("Vente", "CreateProcess_Facturer"),
|
|
}
|
|
|
|
if (type_source, type_cible) not in transformations_valides:
|
|
raise ValueError(
|
|
f"Transformation non autorisée: "
|
|
f"{self._get_type_libelle(type_source)} → {self._get_type_libelle(type_cible)}"
|
|
)
|
|
|
|
module, methode = transformations_valides[(type_source, type_cible)]
|
|
logger.info(f"[TRANSFORM] Méthode: Transformation.{module}.{methode}()")
|
|
|
|
if verifier_doublons:
|
|
logger.info("[TRANSFORM] Vérification des doublons...")
|
|
verif = self.peut_etre_transforme(numero_source, type_source, type_cible)
|
|
|
|
if not verif["possible"]:
|
|
docs = [d["numero"] for d in verif.get("documents_existants", [])]
|
|
raise ValueError(
|
|
f"{verif['raison']}. Document(s) existant(s): {', '.join(docs)}"
|
|
)
|
|
|
|
logger.info("[TRANSFORM] Aucun doublon détecté")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
factory = self.cial.FactoryDocumentVente
|
|
|
|
logger.info(f"[TRANSFORM] Lecture de {numero_source}...")
|
|
|
|
if not factory.ExistPiece(type_source, numero_source):
|
|
raise ValueError(f"Document {numero_source} introuvable")
|
|
|
|
persist_source = factory.ReadPiece(type_source, numero_source)
|
|
if not persist_source:
|
|
raise ValueError(f"Impossible de lire {numero_source}")
|
|
|
|
doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3")
|
|
doc_source.Read()
|
|
|
|
statut_source = getattr(doc_source, "DO_Statut", 0)
|
|
nb_lignes_source = 0
|
|
|
|
try:
|
|
factory_lignes = getattr(doc_source, "FactoryDocumentLigne", None)
|
|
if factory_lignes:
|
|
lignes_list = factory_lignes.List
|
|
nb_lignes_source = lignes_list.Count if lignes_list else 0
|
|
except:
|
|
pass
|
|
|
|
logger.info(
|
|
f"[TRANSFORM] Source: statut={statut_source}, {nb_lignes_source} ligne(s)"
|
|
)
|
|
|
|
if nb_lignes_source == 0:
|
|
raise ValueError(f"Document {numero_source} vide (0 lignes)")
|
|
|
|
logger.info("[TRANSFORM] 🔧 Création du transformer...")
|
|
|
|
transformation = getattr(self.cial, "Transformation", None)
|
|
if not transformation:
|
|
raise RuntimeError("API Transformation non disponible")
|
|
|
|
module_obj = getattr(transformation, module, None)
|
|
if not module_obj:
|
|
raise RuntimeError(f"Module {module} non disponible")
|
|
|
|
methode_func = getattr(module_obj, methode, None)
|
|
if not methode_func:
|
|
raise RuntimeError(f"Méthode {methode} non disponible")
|
|
|
|
transformer = methode_func()
|
|
if not transformer:
|
|
raise RuntimeError("Échec création transformer")
|
|
|
|
logger.info("[TRANSFORM] Transformer créé")
|
|
|
|
logger.info("[TRANSFORM] Configuration...")
|
|
|
|
if hasattr(transformer, "ConserveDocuments"):
|
|
try:
|
|
transformer.ConserveDocuments = conserver_document_source
|
|
logger.info(
|
|
f"[TRANSFORM] ConserveDocuments = {conserver_document_source}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"[TRANSFORM] ConserveDocuments non modifiable: {e}"
|
|
)
|
|
|
|
logger.info("[TRANSFORM] Ajout du document...")
|
|
|
|
try:
|
|
transformer.AddDocument(doc_source)
|
|
logger.info("[TRANSFORM] Document ajouté")
|
|
except Exception as e:
|
|
raise RuntimeError(f"Impossible d'ajouter le document: {e}")
|
|
|
|
try:
|
|
can_process = getattr(transformer, "CanProcess", False)
|
|
logger.info(f"[TRANSFORM] CanProcess: {can_process}")
|
|
except:
|
|
can_process = True
|
|
|
|
if not can_process:
|
|
erreurs = self.lire_erreurs_sage(transformer, "Transformer")
|
|
if erreurs:
|
|
msgs = [f"{e['field']}: {e['description']}" for e in erreurs]
|
|
raise RuntimeError(
|
|
f"Transformation impossible: {' | '.join(msgs)}"
|
|
)
|
|
raise RuntimeError("Transformation impossible (CanProcess=False)")
|
|
|
|
transaction_active = False
|
|
try:
|
|
self.cial.CptaApplication.BeginTrans()
|
|
transaction_active = True
|
|
logger.debug("[TRANSFORM] Transaction démarrée")
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
logger.info("[TRANSFORM] Process()...")
|
|
|
|
try:
|
|
transformer.Process()
|
|
logger.info("[TRANSFORM] Process() réussi")
|
|
except Exception as e:
|
|
logger.error(f"[TRANSFORM] Erreur Process(): {e}")
|
|
|
|
erreurs = self.lire_erreurs_sage(transformer, "Transformer")
|
|
if erreurs:
|
|
msgs = [
|
|
f"{e['field']}: {e['description']}" for e in erreurs
|
|
]
|
|
raise RuntimeError(f"Échec: {' | '.join(msgs)}")
|
|
raise RuntimeError(f"Échec transformation: {e}")
|
|
|
|
logger.info("[TRANSFORM] Récupération des résultats...")
|
|
|
|
list_results = getattr(transformer, "ListDocumentsResult", None)
|
|
if not list_results:
|
|
raise RuntimeError("ListDocumentsResult non disponible")
|
|
|
|
documents_crees = []
|
|
index = 1
|
|
|
|
while index <= 100:
|
|
try:
|
|
doc_result = list_results.Item(index)
|
|
if doc_result is None:
|
|
break
|
|
|
|
doc_result = win32com.client.CastTo(
|
|
doc_result, "IBODocumentVente3"
|
|
)
|
|
doc_result.Read()
|
|
|
|
numero_cible = getattr(doc_result, "DO_Piece", "").strip()
|
|
total_ht = float(getattr(doc_result, "DO_TotalHT", 0.0))
|
|
total_ttc = float(getattr(doc_result, "DO_TotalTTC", 0.0))
|
|
|
|
nb_lignes = 0
|
|
try:
|
|
factory_lignes_result = getattr(
|
|
doc_result, "FactoryDocumentLigne", None
|
|
)
|
|
if factory_lignes_result:
|
|
lignes_list = factory_lignes_result.List
|
|
nb_lignes = lignes_list.Count if lignes_list else 0
|
|
except:
|
|
pass
|
|
|
|
documents_crees.append(
|
|
{
|
|
"numero": numero_cible,
|
|
"total_ht": total_ht,
|
|
"total_ttc": total_ttc,
|
|
"nb_lignes": nb_lignes,
|
|
}
|
|
)
|
|
|
|
logger.info(
|
|
f"[TRANSFORM] Document créé: {numero_cible} "
|
|
f"({nb_lignes} lignes, {total_ht}€ HT)"
|
|
)
|
|
|
|
index += 1
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Fin de liste à index {index}")
|
|
break
|
|
|
|
if not documents_crees:
|
|
raise RuntimeError("Aucun document créé après Process()")
|
|
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.CommitTrans()
|
|
logger.debug("[TRANSFORM] Transaction committée")
|
|
except:
|
|
pass
|
|
|
|
time.sleep(1.5)
|
|
|
|
doc_principal = documents_crees[0]
|
|
|
|
logger.info(
|
|
f"[TRANSFORM] SUCCÈS: {numero_source} → {doc_principal['numero']}"
|
|
)
|
|
logger.info(
|
|
f"[TRANSFORM] • {doc_principal['nb_lignes']} ligne(s)"
|
|
)
|
|
logger.info(
|
|
f"[TRANSFORM] • {doc_principal['total_ht']}€ HT / "
|
|
f"{doc_principal['total_ttc']}€ TTC"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"document_source": numero_source,
|
|
"document_cible": doc_principal["numero"],
|
|
"type_source": type_source,
|
|
"type_cible": type_cible,
|
|
"nb_documents_crees": len(documents_crees),
|
|
"documents": documents_crees,
|
|
"nb_lignes": doc_principal["nb_lignes"],
|
|
"total_ht": doc_principal["total_ht"],
|
|
"total_ttc": doc_principal["total_ttc"],
|
|
"methode_transformation": f"{module}.{methode}",
|
|
}
|
|
|
|
except Exception as e:
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.RollbackTrans()
|
|
logger.error("[TRANSFORM] Transaction annulée (rollback)")
|
|
except:
|
|
pass
|
|
raise
|
|
|
|
except ValueError as e:
|
|
logger.error(f"[TRANSFORM] Erreur métier: {e}")
|
|
raise
|
|
|
|
except RuntimeError as e:
|
|
logger.error(f"[TRANSFORM] Erreur technique: {e}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f"[TRANSFORM] Erreur inattendue: {e}", exc_info=True)
|
|
raise RuntimeError(f"Échec transformation: {str(e)}")
|
|
|
|
def lire_erreurs_sage(self, obj, nom_obj=""):
|
|
"""
|
|
Lit toutes les erreurs d'un objet Sage COM.
|
|
Utilisé pour diagnostiquer les échecs de Process().
|
|
"""
|
|
erreurs = []
|
|
try:
|
|
if not hasattr(obj, "Errors") or obj.Errors is None:
|
|
return erreurs
|
|
|
|
nb_erreurs = 0
|
|
try:
|
|
nb_erreurs = obj.Errors.Count
|
|
except:
|
|
return erreurs
|
|
|
|
if nb_erreurs == 0:
|
|
return erreurs
|
|
|
|
for i in range(1, nb_erreurs + 1):
|
|
try:
|
|
err = None
|
|
try:
|
|
err = obj.Errors.Item(i)
|
|
except:
|
|
try:
|
|
err = obj.Errors(i)
|
|
except:
|
|
try:
|
|
err = obj.Errors.Item(i - 1)
|
|
except:
|
|
pass
|
|
|
|
if err is not None:
|
|
description = ""
|
|
field = ""
|
|
number = ""
|
|
|
|
for attr in ["Description", "Descr", "Message", "Text"]:
|
|
try:
|
|
val = getattr(err, attr, None)
|
|
if val:
|
|
description = str(val)
|
|
break
|
|
except:
|
|
pass
|
|
|
|
for attr in ["Field", "FieldName", "Champ", "Property"]:
|
|
try:
|
|
val = getattr(err, attr, None)
|
|
if val:
|
|
field = str(val)
|
|
break
|
|
except:
|
|
pass
|
|
|
|
for attr in ["Number", "Code", "ErrorCode", "Numero"]:
|
|
try:
|
|
val = getattr(err, attr, None)
|
|
if val is not None:
|
|
number = str(val)
|
|
break
|
|
except:
|
|
pass
|
|
|
|
if description or field or number:
|
|
erreurs.append(
|
|
{
|
|
"source": nom_obj,
|
|
"index": i,
|
|
"description": description or "Erreur inconnue",
|
|
"field": field or "?",
|
|
"number": number or "?",
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Erreur lecture erreur {i}: {e}")
|
|
continue
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Erreur globale lecture erreurs {nom_obj}: {e}")
|
|
|
|
return erreurs
|
|
|
|
def _find_document_in_list(self, numero, type_doc):
|
|
"""Cherche un document dans List() si ReadPiece échoue"""
|
|
try:
|
|
factory = self.cial.FactoryDocumentVente
|
|
index = 1
|
|
|
|
while index < 10000:
|
|
try:
|
|
persist = factory.List(index)
|
|
if persist is None:
|
|
break
|
|
|
|
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
|
|
doc.Read()
|
|
|
|
if (
|
|
getattr(doc, "DO_Type", -1) == type_doc
|
|
and getattr(doc, "DO_Piece", "") == numero
|
|
):
|
|
logger.info(f"[TRANSFORM] Document trouve a l'index {index}")
|
|
return persist
|
|
|
|
index += 1
|
|
except:
|
|
index += 1
|
|
continue
|
|
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"[TRANSFORM] Erreur recherche document: {e}")
|
|
return None
|
|
|
|
def mettre_a_jour_champ_libre(self, doc_id, type_doc, nom_champ, valeur):
|
|
"""Mise à jour champ libre pour Universign ID"""
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
factory = self.cial.FactoryDocumentVente
|
|
persist = factory.ReadPiece(type_doc, doc_id)
|
|
|
|
if persist:
|
|
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
|
|
doc.Read()
|
|
|
|
try:
|
|
setattr(doc, f"DO_{nom_champ}", valeur)
|
|
doc.Write()
|
|
logger.debug(f"Champ libre {nom_champ} = {valeur} sur {doc_id}")
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f"Impossible de mettre à jour {nom_champ}: {e}")
|
|
except Exception as e:
|
|
logger.error(f"Erreur MAJ champ libre: {e}")
|
|
|
|
return False
|
|
|
|
def _lire_client_obj(self, code_client):
|
|
"""Retourne l'objet client Sage brut (pour remises)"""
|
|
if not self.cial:
|
|
return None
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
factory = self.cial.CptaApplication.FactoryClient
|
|
persist = factory.ReadNumero(code_client)
|
|
|
|
if persist:
|
|
return self._cast_client(persist)
|
|
except:
|
|
pass
|
|
|
|
return None
|
|
|
|
def lire_contact_principal_client(self, code_client):
|
|
if not self.cial:
|
|
return None
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
factory_client = self.cial.CptaApplication.FactoryClient
|
|
persist_client = factory_client.ReadNumero(code_client)
|
|
|
|
if not persist_client:
|
|
return None
|
|
|
|
client = self._cast_client(persist_client)
|
|
if not client:
|
|
return None
|
|
|
|
contact_info = {
|
|
"client_code": code_client,
|
|
"client_intitule": getattr(client, "CT_Intitule", ""),
|
|
"email": None,
|
|
"nom": None,
|
|
"telephone": None,
|
|
}
|
|
|
|
try:
|
|
telecom = getattr(client, "Telecom", None)
|
|
if telecom:
|
|
contact_info["email"] = getattr(telecom, "EMail", "")
|
|
contact_info["telephone"] = getattr(telecom, "Telephone", "")
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
contact_info["nom"] = (
|
|
getattr(client, "CT_Contact", "")
|
|
or contact_info["client_intitule"]
|
|
)
|
|
except:
|
|
contact_info["nom"] = contact_info["client_intitule"]
|
|
|
|
return contact_info
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur lecture contact client {code_client}: {e}")
|
|
return None
|
|
|
|
def mettre_a_jour_derniere_relance(self, doc_id, type_doc):
|
|
date_relance = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
return self.mettre_a_jour_champ_libre(
|
|
doc_id, type_doc, "DerniereRelance", date_relance
|
|
)
|
|
|
|
def lister_tous_prospects(self, filtre=""):
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = """
|
|
SELECT
|
|
CT_Num, CT_Intitule, CT_Adresse, CT_Ville,
|
|
CT_CodePostal, CT_Telephone, CT_EMail
|
|
FROM F_COMPTET
|
|
WHERE CT_Type = 0 AND CT_Prospect = 1
|
|
"""
|
|
|
|
params = []
|
|
|
|
if filtre:
|
|
query += " AND (CT_Num LIKE ? OR CT_Intitule LIKE ?)"
|
|
params.extend([f"%{filtre}%", f"%{filtre}%"])
|
|
|
|
query += " ORDER BY CT_Intitule"
|
|
|
|
cursor.execute(query, params)
|
|
rows = cursor.fetchall()
|
|
|
|
prospects = []
|
|
for row in rows:
|
|
prospects.append(
|
|
{
|
|
"numero": self._safe_strip(row.CT_Num),
|
|
"intitule": self._safe_strip(row.CT_Intitule),
|
|
"adresse": self._safe_strip(row.CT_Adresse),
|
|
"ville": self._safe_strip(row.CT_Ville),
|
|
"code_postal": self._safe_strip(row.CT_CodePostal),
|
|
"telephone": self._safe_strip(row.CT_Telephone),
|
|
"email": self._safe_strip(row.CT_EMail),
|
|
"type": 0,
|
|
"est_prospect": True,
|
|
}
|
|
)
|
|
|
|
logger.info(f" SQL: {len(prospects)} prospects")
|
|
return prospects
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur SQL prospects: {e}")
|
|
return []
|
|
|
|
def lire_prospect(self, code_prospect):
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute(
|
|
"""
|
|
SELECT
|
|
CT_Num, CT_Intitule, CT_Type, CT_Qualite,
|
|
CT_Adresse, CT_Complement, CT_Ville, CT_CodePostal, CT_Pays,
|
|
CT_Telephone, CT_Portable, CT_EMail, CT_Telecopie,
|
|
CT_Siret, CT_Identifiant, CT_Sommeil, CT_Prospect,
|
|
CT_Contact, CT_FormeJuridique, CT_Secteur
|
|
FROM F_COMPTET
|
|
WHERE CT_Num = ? AND CT_Type = 0 AND CT_Prospect = 1
|
|
""",
|
|
(code_prospect.upper(),),
|
|
)
|
|
|
|
row = cursor.fetchone()
|
|
|
|
if not row:
|
|
return None
|
|
|
|
return {
|
|
"numero": self._safe_strip(row.CT_Num),
|
|
"intitule": self._safe_strip(row.CT_Intitule),
|
|
"type": 0,
|
|
"qualite": self._safe_strip(row.CT_Qualite),
|
|
"est_prospect": True,
|
|
"adresse": self._safe_strip(row.CT_Adresse),
|
|
"complement": self._safe_strip(row.CT_Complement),
|
|
"ville": self._safe_strip(row.CT_Ville),
|
|
"code_postal": self._safe_strip(row.CT_CodePostal),
|
|
"pays": self._safe_strip(row.CT_Pays),
|
|
"telephone": self._safe_strip(row.CT_Telephone),
|
|
"portable": self._safe_strip(row.CT_Portable),
|
|
"email": self._safe_strip(row.CT_EMail),
|
|
"telecopie": self._safe_strip(row.CT_Telecopie),
|
|
"siret": self._safe_strip(row.CT_Siret),
|
|
"tva_intra": self._safe_strip(row.CT_Identifiant),
|
|
"est_actif": (row.CT_Sommeil == 0),
|
|
"contact": self._safe_strip(row.CT_Contact),
|
|
"forme_juridique": self._safe_strip(row.CT_FormeJuridique),
|
|
"secteur": self._safe_strip(row.CT_Secteur),
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur SQL prospect {code_prospect}: {e}")
|
|
return None
|
|
|
|
def lister_avoirs(self, limit=100, statut=None):
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = f"""
|
|
SELECT TOP ({limit})
|
|
d.DO_Piece, d.DO_Date, d.DO_Ref, d.DO_TotalHT, d.DO_TotalTTC,
|
|
d.DO_Statut, d.CT_Num, c.CT_Intitule
|
|
FROM F_DOCENTETE d
|
|
LEFT JOIN F_COMPTET c ON d.CT_Num = c.CT_Num
|
|
WHERE d.DO_Type = 50
|
|
"""
|
|
|
|
params = []
|
|
|
|
if statut is not None:
|
|
query += " AND d.DO_Statut = ?"
|
|
params.append(statut)
|
|
|
|
query += " ORDER BY d.DO_Date DESC"
|
|
|
|
cursor.execute(query, params)
|
|
rows = cursor.fetchall()
|
|
|
|
avoirs = []
|
|
for row in rows:
|
|
avoirs.append(
|
|
{
|
|
"numero": self._safe_strip(row.DO_Piece),
|
|
"reference": self._safe_strip(row.DO_Ref),
|
|
"date": str(row.DO_Date) if row.DO_Date else "",
|
|
"client_code": self._safe_strip(row.CT_Num),
|
|
"client_intitule": self._safe_strip(row.CT_Intitule),
|
|
"total_ht": (
|
|
float(row.DO_TotalHT) if row.DO_TotalHT else 0.0
|
|
),
|
|
"total_ttc": (
|
|
float(row.DO_TotalTTC) if row.DO_TotalTTC else 0.0
|
|
),
|
|
"statut": row.DO_Statut if row.DO_Statut is not None else 0,
|
|
}
|
|
)
|
|
|
|
return avoirs
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur SQL avoirs: {e}")
|
|
return []
|
|
|
|
def lire_avoir(self, numero):
|
|
return self._lire_document_sql(numero, type_doc=50)
|
|
|
|
def lister_livraisons(self, limit=100, statut=None):
|
|
""" Liste les livraisons via SQL (méthode legacy)"""
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = f"""
|
|
SELECT TOP ({limit})
|
|
d.DO_Piece, d.DO_Date, d.DO_Ref, d.DO_TotalHT, d.DO_TotalTTC,
|
|
d.DO_Statut, d.CT_Num, c.CT_Intitule
|
|
FROM F_DOCENTETE d
|
|
LEFT JOIN F_COMPTET c ON d.CT_Num = c.CT_Num
|
|
WHERE d.DO_Type = 30
|
|
"""
|
|
|
|
params = []
|
|
|
|
if statut is not None:
|
|
query += " AND d.DO_Statut = ?"
|
|
params.append(statut)
|
|
|
|
query += " ORDER BY d.DO_Date DESC"
|
|
|
|
cursor.execute(query, params)
|
|
rows = cursor.fetchall()
|
|
|
|
livraisons = []
|
|
for row in rows:
|
|
livraisons.append(
|
|
{
|
|
"numero": self._safe_strip(row.DO_Piece),
|
|
"reference": self._safe_strip(row.DO_Ref),
|
|
"date": str(row.DO_Date) if row.DO_Date else "",
|
|
"client_code": self._safe_strip(row.CT_Num),
|
|
"client_intitule": self._safe_strip(row.CT_Intitule),
|
|
"total_ht": (
|
|
float(row.DO_TotalHT) if row.DO_TotalHT else 0.0
|
|
),
|
|
"total_ttc": (
|
|
float(row.DO_TotalTTC) if row.DO_TotalTTC else 0.0
|
|
),
|
|
"statut": row.DO_Statut if row.DO_Statut is not None else 0,
|
|
}
|
|
)
|
|
|
|
return livraisons
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur SQL livraisons: {e}")
|
|
return []
|
|
|
|
def lire_livraison(self, numero):
|
|
""" Lit UNE livraison via SQL (avec lignes)"""
|
|
return self._lire_document_sql(numero, type_doc=30)
|
|
|
|
|
|
def creer_contact(self, contact_data: Dict) -> Dict:
|
|
"""
|
|
Crée un nouveau contact dans F_CONTACTT via COM
|
|
VERSION CORRIGÉE - Utilise IBOTiersContact3
|
|
"""
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info("=" * 80)
|
|
logger.info("[CREATION CONTACT F_CONTACTT]")
|
|
logger.info("=" * 80)
|
|
|
|
# Validation
|
|
if not contact_data.get("numero"):
|
|
raise ValueError("numero (code client) obligatoire")
|
|
if not contact_data.get("nom"):
|
|
raise ValueError("nom obligatoire")
|
|
|
|
numero_client = self._clean_str(contact_data["numero"], 17).upper()
|
|
nom = self._clean_str(contact_data["nom"], 35)
|
|
prenom = self._clean_str(contact_data.get("prenom", ""), 35)
|
|
|
|
logger.info(f" CLIENT: {numero_client}")
|
|
logger.info(f" CONTACT: {prenom} {nom}")
|
|
|
|
# Charger le client
|
|
logger.info(f"[1] Chargement du client: {numero_client}")
|
|
factory_client = self.cial.CptaApplication.FactoryClient
|
|
try:
|
|
persist_client = factory_client.ReadNumero(numero_client)
|
|
if not persist_client:
|
|
raise ValueError(f"Client {numero_client} non trouve")
|
|
|
|
client_obj = win32com.client.CastTo(persist_client, "IBOClient3")
|
|
client_obj.Read()
|
|
logger.info(f" OK Client charge")
|
|
except Exception as e:
|
|
raise ValueError(f"Client {numero_client} introuvable: {e}")
|
|
|
|
# Via FactoryTiersContact du client
|
|
logger.info("[2] Creation via FactoryTiersContact")
|
|
|
|
if not hasattr(client_obj, 'FactoryTiersContact'):
|
|
raise RuntimeError("FactoryTiersContact non trouvee sur le client")
|
|
|
|
factory_contact = client_obj.FactoryTiersContact
|
|
logger.info(f" OK FactoryTiersContact: {type(factory_contact).__name__}")
|
|
|
|
# Créer l'objet
|
|
persist = factory_contact.Create()
|
|
logger.info(f" Objet cree: {type(persist).__name__}")
|
|
|
|
# Cast vers IBOTiersContact3 (qui a Nom, Prenom, etc.)
|
|
contact = None
|
|
interfaces_a_tester = [
|
|
"IBOTiersContact3",
|
|
"IBOTiersContact",
|
|
"IBOContactT3",
|
|
"IBOContactT",
|
|
]
|
|
|
|
for interface_name in interfaces_a_tester:
|
|
try:
|
|
temp = win32com.client.CastTo(persist, interface_name)
|
|
|
|
# Vérifier si Nom existe (pas CT_Num !)
|
|
if hasattr(temp, '_prop_map_put_'):
|
|
props = list(temp._prop_map_put_.keys())
|
|
logger.info(f" Test {interface_name}: props={props[:15]}")
|
|
|
|
# Chercher Nom ou CT_Nom
|
|
if 'Nom' in props or 'CT_Nom' in props:
|
|
contact = temp
|
|
logger.info(f" OK Cast reussi vers {interface_name}")
|
|
break
|
|
except Exception as e:
|
|
logger.debug(f" {interface_name}: {str(e)[:50]}")
|
|
|
|
if not contact:
|
|
logger.error(" ERROR Aucun cast ne fonctionne")
|
|
raise RuntimeError("Impossible de caster vers une interface contact valide")
|
|
|
|
# Configuration du contact
|
|
logger.info("[3] Configuration du contact")
|
|
|
|
# Vérifier les propriétés disponibles
|
|
if hasattr(contact, '_prop_map_put_'):
|
|
props = list(contact._prop_map_put_.keys())
|
|
logger.info(f" Proprietes disponibles: {props}")
|
|
|
|
# Nom (obligatoire)
|
|
try:
|
|
contact.Nom = nom
|
|
logger.info(f" OK Nom = {nom}")
|
|
except Exception as e:
|
|
logger.error(f" ERROR Impossible de definir Nom: {e}")
|
|
raise RuntimeError(f"Echec definition Nom: {e}")
|
|
|
|
# Prénom
|
|
if prenom:
|
|
try:
|
|
contact.Prenom = prenom
|
|
logger.info(f" OK Prenom = {prenom}")
|
|
except Exception as e:
|
|
logger.warning(f" WARN Prenom: {e}")
|
|
|
|
# Civilité
|
|
if contact_data.get("civilite"):
|
|
civilite_map = {"M.": 0, "Mme": 1, "Mlle": 2, "Societe": 3}
|
|
civilite_code = civilite_map.get(contact_data["civilite"])
|
|
if civilite_code is not None:
|
|
try:
|
|
contact.Civilite = civilite_code
|
|
logger.info(f" OK Civilite = {civilite_code}")
|
|
except Exception as e:
|
|
logger.warning(f" WARN Civilite: {e}")
|
|
|
|
# Fonction
|
|
if contact_data.get("fonction"):
|
|
fonction = self._clean_str(contact_data["fonction"], 35)
|
|
try:
|
|
contact.Fonction = fonction
|
|
logger.info(f" OK Fonction = {fonction}")
|
|
except Exception as e:
|
|
logger.warning(f" WARN Fonction: {e}")
|
|
|
|
# Service
|
|
if contact_data.get("service_code") is not None:
|
|
try:
|
|
service = self._safe_int(contact_data["service_code"])
|
|
if service is not None and hasattr(contact, 'ServiceContact'):
|
|
contact.ServiceContact = service
|
|
logger.info(f" OK ServiceContact = {service}")
|
|
except Exception as e:
|
|
logger.warning(f" WARN ServiceContact: {e}")
|
|
|
|
# Telecom
|
|
logger.info("[4] Coordonnees (Telecom)")
|
|
|
|
if hasattr(contact, 'Telecom'):
|
|
try:
|
|
telecom = contact.Telecom
|
|
logger.info(f" Type Telecom: {type(telecom).__name__}")
|
|
|
|
if contact_data.get("telephone"):
|
|
telephone = self._clean_str(contact_data["telephone"], 21)
|
|
if self._try_set_attribute(telecom, "Telephone", telephone):
|
|
logger.info(f" Telephone = {telephone}")
|
|
|
|
if contact_data.get("portable"):
|
|
portable = self._clean_str(contact_data["portable"], 21)
|
|
if self._try_set_attribute(telecom, "Portable", portable):
|
|
logger.info(f" Portable = {portable}")
|
|
|
|
if contact_data.get("email"):
|
|
email = self._clean_str(contact_data["email"], 69)
|
|
if self._try_set_attribute(telecom, "EMail", email):
|
|
logger.info(f" EMail = {email}")
|
|
|
|
if contact_data.get("telecopie"):
|
|
fax = self._clean_str(contact_data["telecopie"], 21)
|
|
if self._try_set_attribute(telecom, "Telecopie", fax):
|
|
logger.info(f" Telecopie = {fax}")
|
|
|
|
except Exception as e:
|
|
logger.warning(f" WARN Erreur Telecom: {e}")
|
|
|
|
# Réseaux sociaux
|
|
logger.info("[5] Reseaux sociaux")
|
|
|
|
if contact_data.get("facebook"):
|
|
facebook = self._clean_str(contact_data["facebook"], 69)
|
|
try:
|
|
contact.Facebook = facebook
|
|
logger.info(f" Facebook = {facebook}")
|
|
except:
|
|
pass
|
|
|
|
if contact_data.get("linkedin"):
|
|
linkedin = self._clean_str(contact_data["linkedin"], 69)
|
|
try:
|
|
contact.LinkedIn = linkedin
|
|
logger.info(f" LinkedIn = {linkedin}")
|
|
except:
|
|
pass
|
|
|
|
if contact_data.get("skype"):
|
|
skype = self._clean_str(contact_data["skype"], 69)
|
|
try:
|
|
contact.Skype = skype
|
|
logger.info(f" Skype = {skype}")
|
|
except:
|
|
pass
|
|
|
|
# SetDefault
|
|
try:
|
|
contact.SetDefault()
|
|
logger.info(" OK SetDefault() applique")
|
|
except Exception as e:
|
|
logger.warning(f" WARN SetDefault(): {e}")
|
|
|
|
# Enregistrer
|
|
logger.info("[6] Enregistrement du contact")
|
|
try:
|
|
contact.Write()
|
|
logger.info(" OK Write() reussi")
|
|
|
|
contact.Read()
|
|
logger.info(" OK Read() reussi")
|
|
except Exception as e:
|
|
error_detail = str(e)
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
except:
|
|
pass
|
|
logger.error(f" ERROR Write: {error_detail}")
|
|
raise RuntimeError(f"Echec enregistrement: {error_detail}")
|
|
|
|
# Récupérer les IDs
|
|
contact_no = None
|
|
n_contact = None
|
|
try:
|
|
contact_no = getattr(contact, 'CT_No', None)
|
|
n_contact = getattr(contact, 'N_Contact', None)
|
|
logger.info(f" Contact CT_No={contact_no}, N_Contact={n_contact}")
|
|
except:
|
|
pass
|
|
|
|
# Contact par défaut
|
|
est_defaut = contact_data.get("est_defaut", False)
|
|
if est_defaut and (contact_no or n_contact):
|
|
logger.info("[7] Definition comme contact par defaut")
|
|
try:
|
|
nom_complet = f"{prenom} {nom}".strip() if prenom else nom
|
|
|
|
persist_client = factory_client.ReadNumero(numero_client)
|
|
client_obj = win32com.client.CastTo(persist_client, "IBOClient3")
|
|
client_obj.Read()
|
|
|
|
client_obj.CT_Contact = nom_complet
|
|
logger.info(f" CT_Contact = '{nom_complet}'")
|
|
|
|
if contact_no and hasattr(client_obj, 'CT_NoContact'):
|
|
try:
|
|
client_obj.CT_NoContact = contact_no
|
|
logger.info(f" CT_NoContact = {contact_no}")
|
|
except:
|
|
pass
|
|
|
|
client_obj.Write()
|
|
client_obj.Read()
|
|
logger.info(" OK Contact par defaut defini")
|
|
except Exception as e:
|
|
logger.warning(f" WARN Echec: {e}")
|
|
est_defaut = False
|
|
|
|
logger.info("=" * 80)
|
|
logger.info(f"[SUCCES] Contact cree: {prenom} {nom}")
|
|
logger.info(f" Lie au client {numero_client}")
|
|
if contact_no:
|
|
logger.info(f" CT_No={contact_no}")
|
|
logger.info("=" * 80)
|
|
|
|
# Retour
|
|
contact_dict = {
|
|
"numero": numero_client,
|
|
"n_contact": n_contact or contact_no,
|
|
"civilite": contact_data.get("civilite"),
|
|
"nom": nom,
|
|
"prenom": prenom,
|
|
"fonction": contact_data.get("fonction"),
|
|
"service_code": contact_data.get("service_code"),
|
|
"telephone": contact_data.get("telephone"),
|
|
"portable": contact_data.get("portable"),
|
|
"telecopie": contact_data.get("telecopie"),
|
|
"email": contact_data.get("email"),
|
|
"facebook": contact_data.get("facebook"),
|
|
"linkedin": contact_data.get("linkedin"),
|
|
"skype": contact_data.get("skype"),
|
|
"est_defaut": est_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 modifier_contact(self, numero: str, contact_numero: int, updates: Dict) -> Dict:
|
|
"""
|
|
Modifie un contact existant via COM
|
|
VERSION REFACTORISÉE - Utilise FactoryTiersContact
|
|
"""
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info("=" * 80)
|
|
logger.info(f"[MODIFICATION CONTACT] CT_No={contact_numero}")
|
|
logger.info("=" * 80)
|
|
|
|
# Charger le client
|
|
logger.info("[1] Chargement du client")
|
|
factory_client = self.cial.CptaApplication.FactoryClient
|
|
try:
|
|
persist_client = factory_client.ReadNumero(numero)
|
|
if not persist_client:
|
|
raise ValueError(f"Client {numero} non trouve")
|
|
|
|
client_obj = win32com.client.CastTo(persist_client, "IBOClient3")
|
|
client_obj.Read()
|
|
logger.info(f" OK Client charge")
|
|
except Exception as e:
|
|
raise ValueError(f"Client {numero} introuvable: {e}")
|
|
|
|
# Charger le contact via FactoryTiersContact
|
|
logger.info("[2] Chargement du contact")
|
|
|
|
if not hasattr(client_obj, 'FactoryTiersContact'):
|
|
raise RuntimeError("FactoryTiersContact non trouvee sur le client")
|
|
|
|
factory_contact = client_obj.FactoryTiersContact
|
|
|
|
try:
|
|
# Chercher le contact par son CT_No
|
|
# Il faut lister et trouver celui avec le bon CT_No
|
|
# ou utiliser une autre méthode de lecture
|
|
|
|
# STRATÉGIE : Lister tous les contacts et trouver le bon
|
|
contacts_collection = None
|
|
all_contacts = []
|
|
|
|
# Essayer de lister via SQL d'abord (plus fiable)
|
|
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}")
|
|
|
|
# Charger via ReadNomPrenom
|
|
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}")
|
|
|
|
except Exception as e:
|
|
raise ValueError(f"Contact introuvable: {e}")
|
|
|
|
# Appliquer les modifications
|
|
logger.info("[3] Application des modifications")
|
|
modifications_appliquees = []
|
|
|
|
# Identité
|
|
if "civilite" in updates:
|
|
civilite_map = {"M.": 0, "Mme": 1, "Mlle": 2, "Societe": 3}
|
|
civilite_code = civilite_map.get(updates["civilite"])
|
|
if civilite_code is not None:
|
|
try:
|
|
contact.Civilite = civilite_code
|
|
logger.info(f" Civilite = {civilite_code}")
|
|
modifications_appliquees.append("civilite")
|
|
except:
|
|
pass
|
|
|
|
if "nom" in updates:
|
|
nom = self._clean_str(updates["nom"], 35)
|
|
if nom:
|
|
try:
|
|
contact.Nom = nom
|
|
logger.info(f" Nom = {nom}")
|
|
modifications_appliquees.append("nom")
|
|
except:
|
|
pass
|
|
|
|
if "prenom" in updates:
|
|
prenom = self._clean_str(updates["prenom"], 35)
|
|
try:
|
|
contact.Prenom = prenom
|
|
logger.info(f" Prenom = {prenom}")
|
|
modifications_appliquees.append("prenom")
|
|
except:
|
|
pass
|
|
|
|
if "fonction" in updates:
|
|
fonction = self._clean_str(updates["fonction"], 35)
|
|
try:
|
|
contact.Fonction = fonction
|
|
logger.info(f" Fonction = {fonction}")
|
|
modifications_appliquees.append("fonction")
|
|
except:
|
|
pass
|
|
|
|
# Service
|
|
if "service_code" in updates:
|
|
service = self._safe_int(updates["service_code"])
|
|
if service is not None and hasattr(contact, 'ServiceContact'):
|
|
try:
|
|
contact.ServiceContact = service
|
|
logger.info(f" ServiceContact = {service}")
|
|
modifications_appliquees.append("service_code")
|
|
except:
|
|
pass
|
|
|
|
# Coordonnées via Telecom
|
|
if hasattr(contact, 'Telecom'):
|
|
try:
|
|
telecom = contact.Telecom
|
|
|
|
if "telephone" in updates:
|
|
telephone = self._clean_str(updates["telephone"], 21)
|
|
if self._try_set_attribute(telecom, "Telephone", telephone):
|
|
logger.info(f" Telephone = {telephone}")
|
|
modifications_appliquees.append("telephone")
|
|
|
|
if "portable" in updates:
|
|
portable = self._clean_str(updates["portable"], 21)
|
|
if self._try_set_attribute(telecom, "Portable", portable):
|
|
logger.info(f" Portable = {portable}")
|
|
modifications_appliquees.append("portable")
|
|
|
|
if "email" in updates:
|
|
email = self._clean_str(updates["email"], 69)
|
|
if self._try_set_attribute(telecom, "EMail", email):
|
|
logger.info(f" EMail = {email}")
|
|
modifications_appliquees.append("email")
|
|
|
|
if "telecopie" in updates:
|
|
fax = self._clean_str(updates["telecopie"], 21)
|
|
if self._try_set_attribute(telecom, "Telecopie", fax):
|
|
logger.info(f" Telecopie = {fax}")
|
|
modifications_appliquees.append("telecopie")
|
|
except:
|
|
pass
|
|
|
|
# Réseaux sociaux
|
|
if "facebook" in updates:
|
|
facebook = self._clean_str(updates["facebook"], 69)
|
|
try:
|
|
contact.Facebook = facebook
|
|
logger.info(f" Facebook = {facebook}")
|
|
modifications_appliquees.append("facebook")
|
|
except:
|
|
pass
|
|
|
|
if "linkedin" in updates:
|
|
linkedin = self._clean_str(updates["linkedin"], 69)
|
|
try:
|
|
contact.LinkedIn = linkedin
|
|
logger.info(f" LinkedIn = {linkedin}")
|
|
modifications_appliquees.append("linkedin")
|
|
except:
|
|
pass
|
|
|
|
if "skype" in updates:
|
|
skype = self._clean_str(updates["skype"], 69)
|
|
try:
|
|
contact.Skype = skype
|
|
logger.info(f" Skype = {skype}")
|
|
modifications_appliquees.append("skype")
|
|
except:
|
|
pass
|
|
|
|
# Enregistrement du contact
|
|
logger.info("[4] Enregistrement")
|
|
try:
|
|
contact.Write()
|
|
contact.Read()
|
|
logger.info(" OK Write() reussi")
|
|
except Exception as e:
|
|
error_detail = str(e)
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
except:
|
|
pass
|
|
logger.error(f" ERROR Write: {error_detail}")
|
|
raise RuntimeError(f"Echec modification contact: {error_detail}")
|
|
|
|
logger.info(f" Modifications appliquees: {', '.join(modifications_appliquees)}")
|
|
|
|
# Gestion du contact par défaut
|
|
est_defaut_demande = updates.get("est_defaut")
|
|
est_actuellement_defaut = False
|
|
|
|
if est_defaut_demande is not None and est_defaut_demande:
|
|
logger.info("[5] Gestion contact par defaut")
|
|
try:
|
|
nom_complet = f"{contact.Prenom} {contact.Nom}".strip() if contact.Prenom else contact.Nom
|
|
|
|
persist_client = factory_client.ReadNumero(numero)
|
|
client_obj = win32com.client.CastTo(persist_client, "IBOClient3")
|
|
client_obj.Read()
|
|
|
|
client_obj.CT_Contact = nom_complet
|
|
logger.info(f" CT_Contact = '{nom_complet}'")
|
|
|
|
if hasattr(client_obj, 'CT_NoContact'):
|
|
try:
|
|
client_obj.CT_NoContact = contact_numero
|
|
logger.info(f" CT_NoContact = {contact_numero}")
|
|
except:
|
|
pass
|
|
|
|
client_obj.Write()
|
|
client_obj.Read()
|
|
logger.info(" OK Contact par defaut defini")
|
|
est_actuellement_defaut = True
|
|
|
|
except Exception as e:
|
|
logger.warning(f" WARN Echec: {e}")
|
|
|
|
logger.info("=" * 80)
|
|
logger.info(f"[SUCCES] Contact modifie: CT_No={contact_numero}")
|
|
logger.info("=" * 80)
|
|
|
|
contact_dict = self._contact_to_dict(contact)
|
|
contact_dict["est_defaut"] = est_actuellement_defaut
|
|
|
|
return contact_dict
|
|
|
|
except ValueError as e:
|
|
logger.error(f"[ERREUR VALIDATION] {e}")
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"[ERREUR] {e}", exc_info=True)
|
|
raise RuntimeError(f"Erreur technique: {e}")
|
|
|
|
|
|
def definir_contact_defaut(self, numero: str, contact_numero: int) -> Dict:
|
|
"""
|
|
Définit un contact comme contact par défaut du client
|
|
VERSION REFACTORISÉE
|
|
"""
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info("=" * 80)
|
|
logger.info(f"[DEFINIR CONTACT PAR DEFAUT] Client={numero}, Contact={contact_numero}")
|
|
logger.info("=" * 80)
|
|
|
|
# Récupérer le nom du contact via SQL
|
|
logger.info("[1] Recuperation infos contact")
|
|
nom_contact = None
|
|
prenom_contact = None
|
|
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"SELECT CT_Nom, CT_Prenom FROM F_CONTACTT WHERE CT_Num = ? AND CT_No = ?",
|
|
[numero, contact_numero]
|
|
)
|
|
row = cursor.fetchone()
|
|
|
|
if not row:
|
|
raise ValueError(f"Contact CT_No={contact_numero} non trouve")
|
|
|
|
nom_contact = row.CT_Nom.strip() if row.CT_Nom else ""
|
|
prenom_contact = row.CT_Prenom.strip() if row.CT_Prenom else ""
|
|
|
|
nom_complet = f"{prenom_contact} {nom_contact}".strip() if prenom_contact else nom_contact
|
|
logger.info(f" OK Contact trouve: {nom_complet}")
|
|
|
|
except Exception as e:
|
|
raise ValueError(f"Contact introuvable: {e}")
|
|
|
|
# Charger le client
|
|
logger.info("[2] Chargement du client")
|
|
factory_client = self.cial.CptaApplication.FactoryClient
|
|
|
|
try:
|
|
persist_client = factory_client.ReadNumero(numero)
|
|
if not persist_client:
|
|
raise ValueError(f"Client {numero} non trouve")
|
|
|
|
client = win32com.client.CastTo(persist_client, "IBOClient3")
|
|
client.Read()
|
|
logger.info(f" OK Client charge: {client.CT_Intitule}")
|
|
|
|
except Exception as e:
|
|
raise ValueError(f"Client introuvable: {e}")
|
|
|
|
# Définir le contact par défaut
|
|
logger.info("[3] Definition du contact par defaut")
|
|
|
|
ancien_contact = getattr(client, "CT_Contact", "")
|
|
client.CT_Contact = nom_complet
|
|
logger.info(f" CT_Contact: '{ancien_contact}' -> '{nom_complet}'")
|
|
|
|
if hasattr(client, 'CT_NoContact'):
|
|
try:
|
|
client.CT_NoContact = contact_numero
|
|
logger.info(f" CT_NoContact = {contact_numero}")
|
|
except:
|
|
pass
|
|
|
|
# Enregistrement
|
|
logger.info("[4] Enregistrement")
|
|
try:
|
|
client.Write()
|
|
client.Read()
|
|
logger.info(" OK Client mis a jour")
|
|
except Exception as e:
|
|
error_detail = str(e)
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
except:
|
|
pass
|
|
raise RuntimeError(f"Echec mise a jour: {error_detail}")
|
|
|
|
logger.info("=" * 80)
|
|
logger.info(f"[SUCCES] Contact par defaut: {nom_complet}")
|
|
logger.info("=" * 80)
|
|
|
|
return {
|
|
"numero": numero,
|
|
"contact_numero": contact_numero,
|
|
"contact_nom": nom_complet,
|
|
"client_intitule": client.CT_Intitule,
|
|
"est_defaut": True,
|
|
"date_modification": datetime.now().isoformat()
|
|
}
|
|
|
|
except ValueError as e:
|
|
logger.error(f"[ERREUR VALIDATION] {e}")
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"[ERREUR] {e}", exc_info=True)
|
|
raise RuntimeError(f"Erreur technique: {e}")
|
|
|
|
|
|
def lister_contacts(self, numero: str) -> List[Dict]:
|
|
"""
|
|
Liste tous les contacts d'un client
|
|
"""
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
return self._get_contacts_client(numero, conn)
|
|
except Exception as e:
|
|
logger.error(f"Erreur liste contacts: {e}")
|
|
raise RuntimeError(f"Erreur lecture contacts: {str(e)}")
|
|
|
|
|
|
def obtenir_contact(self, numero: str, contact_numero: int) -> Optional[Dict]:
|
|
"""
|
|
Récupère un contact spécifique par son CT_No
|
|
"""
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = """
|
|
SELECT
|
|
CT_Num, CT_No, N_Contact,
|
|
CT_Civilite, CT_Nom, CT_Prenom, CT_Fonction,
|
|
N_Service,
|
|
CT_Telephone, CT_TelPortable, CT_Telecopie, CT_EMail,
|
|
CT_Facebook, CT_LinkedIn, CT_Skype
|
|
FROM F_CONTACTT
|
|
WHERE CT_Num = ? AND CT_No = ?
|
|
"""
|
|
|
|
cursor.execute(query, [numero, contact_numero])
|
|
row = cursor.fetchone()
|
|
|
|
if not row:
|
|
return None
|
|
|
|
return self._row_to_contact_dict(row)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur obtention contact: {e}")
|
|
raise RuntimeError(f"Erreur lecture contact: {str(e)}")
|
|
|
|
|
|
def obtenir_contact_defaut(self, numero: str) -> Optional[Dict]:
|
|
"""
|
|
Récupère le contact par défaut d'un client
|
|
|
|
Returns:
|
|
Dictionnaire avec les infos du contact par défaut, ou None si non défini
|
|
"""
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
# Charger le client
|
|
factory_client = self.cial.CptaApplication.FactoryClient
|
|
persist_client = factory_client.ReadNumero(numero)
|
|
|
|
if not persist_client:
|
|
raise ValueError(f"Client {numero} non trouvé")
|
|
|
|
client = win32com.client.CastTo(persist_client, "IBOClient3")
|
|
client.Read()
|
|
|
|
# Méthode 1: Via CT_NoContact (si disponible)
|
|
ct_no_defaut = None
|
|
try:
|
|
ct_no_defaut = getattr(client, "CT_NoContact", None)
|
|
if ct_no_defaut:
|
|
logger.info(f"Contact par défaut via CT_NoContact: {ct_no_defaut}")
|
|
except:
|
|
pass
|
|
|
|
# Méthode 2: Via CT_Contact (nom)
|
|
nom_contact_defaut = None
|
|
try:
|
|
nom_contact_defaut = getattr(client, "CT_Contact", None)
|
|
if nom_contact_defaut:
|
|
logger.info(f"Contact par défaut via CT_Contact: {nom_contact_defaut}")
|
|
except:
|
|
pass
|
|
|
|
# Si on a le CT_No, on retourne le contact complet
|
|
if ct_no_defaut:
|
|
return self.obtenir_contact(numero, ct_no_defaut)
|
|
|
|
# Sinon, chercher par nom dans la liste des contacts
|
|
if nom_contact_defaut:
|
|
contacts = self.lister_contacts(numero)
|
|
for contact in contacts:
|
|
nom_complet = f"{contact.get('prenom', '')} {contact['nom']}".strip()
|
|
if nom_complet == nom_contact_defaut or contact['nom'] == nom_contact_defaut:
|
|
return {**contact, "est_defaut": True}
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur obtention contact par défaut: {e}")
|
|
return None
|
|
|
|
|
|
def supprimer_contact(self, numero: str, contact_numero: int) -> Dict:
|
|
"""
|
|
Supprime un contact via COM
|
|
VERSION REFACTORISÉE
|
|
"""
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info("=" * 80)
|
|
logger.info(f"[SUPPRESSION CONTACT] CT_No={contact_numero}")
|
|
logger.info("=" * 80)
|
|
|
|
# Récupérer le nom du contact via SQL
|
|
logger.info("[1] Recuperation infos contact")
|
|
nom_contact = None
|
|
prenom_contact = None
|
|
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"SELECT CT_Nom, CT_Prenom FROM F_CONTACTT WHERE CT_Num = ? AND CT_No = ?",
|
|
[numero, contact_numero]
|
|
)
|
|
row = cursor.fetchone()
|
|
|
|
if not row:
|
|
raise ValueError(f"Contact CT_No={contact_numero} non trouve")
|
|
|
|
nom_contact = row.CT_Nom.strip() if row.CT_Nom else ""
|
|
prenom_contact = row.CT_Prenom.strip() if row.CT_Prenom else ""
|
|
|
|
logger.info(f" OK Contact trouve: {prenom_contact} {nom_contact}")
|
|
|
|
except Exception as e:
|
|
raise ValueError(f"Contact introuvable: {e}")
|
|
|
|
# Charger le contact via FactoryDossierContact
|
|
logger.info("[2] Chargement du contact")
|
|
factory_dossier = self.cial.CptaApplication.FactoryDossierContact
|
|
|
|
try:
|
|
persist = factory_dossier.ReadNomPrenom(nom_contact, prenom_contact)
|
|
|
|
if not persist:
|
|
raise ValueError(f"Contact non trouvable via ReadNomPrenom")
|
|
|
|
contact = win32com.client.CastTo(persist, "IBOTiersContact3")
|
|
contact.Read()
|
|
logger.info(f" OK Contact charge: {contact.Nom}")
|
|
|
|
except Exception as e:
|
|
raise ValueError(f"Contact introuvable: {e}")
|
|
|
|
# Supprimer
|
|
logger.info("[3] Suppression")
|
|
try:
|
|
contact.Remove()
|
|
logger.info(" OK Remove() reussi")
|
|
except Exception as e:
|
|
error_detail = str(e)
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
except:
|
|
pass
|
|
logger.error(f" ERROR Remove: {error_detail}")
|
|
raise RuntimeError(f"Echec suppression contact: {error_detail}")
|
|
|
|
logger.info("=" * 80)
|
|
logger.info(f"[SUCCES] Contact supprime: {prenom_contact} {nom_contact}")
|
|
logger.info("=" * 80)
|
|
|
|
return {
|
|
"numero": numero,
|
|
"contact_numero": contact_numero,
|
|
"nom": nom_contact,
|
|
"prenom": prenom_contact,
|
|
"supprime": True,
|
|
"date_suppression": datetime.now().isoformat()
|
|
}
|
|
|
|
except ValueError as e:
|
|
logger.error(f"[ERREUR VALIDATION] {e}")
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"[ERREUR] {e}", exc_info=True)
|
|
raise RuntimeError(f"Erreur technique: {e}")
|
|
|
|
|
|
def _contact_to_dict(self, contact) -> Dict:
|
|
"""
|
|
Convertit un objet COM Contact (IBOTiersContact3) en dictionnaire
|
|
VERSION REFACTORISÉE
|
|
"""
|
|
try:
|
|
# IBOTiersContact3 utilise Nom/Prenom (sans préfixe CT_)
|
|
civilite_code = getattr(contact, "Civilite", None)
|
|
civilite_map = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"}
|
|
civilite = civilite_map.get(civilite_code) if civilite_code is not None else None
|
|
|
|
# Coordonnées via Telecom
|
|
telephone = None
|
|
portable = None
|
|
telecopie = None
|
|
email = None
|
|
|
|
if hasattr(contact, 'Telecom'):
|
|
try:
|
|
telecom = contact.Telecom
|
|
telephone = self._safe_strip(getattr(telecom, "Telephone", None))
|
|
portable = self._safe_strip(getattr(telecom, "Portable", None))
|
|
telecopie = self._safe_strip(getattr(telecom, "Telecopie", None))
|
|
email = self._safe_strip(getattr(telecom, "EMail", None))
|
|
except:
|
|
pass
|
|
|
|
# Récupérer CT_No et N_Contact via SQL si possible
|
|
# Car IBOTiersContact3 du DossierContact ne les a pas
|
|
contact_numero = None
|
|
n_contact = None
|
|
numero = None
|
|
|
|
# Ces infos doivent venir de F_CONTACTT
|
|
# On les passe plutôt en paramètre ou on les récupère différemment
|
|
|
|
return {
|
|
"numero": numero, # À passer en paramètre
|
|
"contact_numero": contact_numero, # À passer en paramètre
|
|
"n_contact": n_contact, # À passer en paramètre
|
|
"civilite": civilite,
|
|
"nom": self._safe_strip(getattr(contact, "Nom", None)),
|
|
"prenom": self._safe_strip(getattr(contact, "Prenom", None)),
|
|
"fonction": self._safe_strip(getattr(contact, "Fonction", None)),
|
|
"service_code": getattr(contact, "ServiceContact", None),
|
|
"telephone": telephone,
|
|
"portable": portable,
|
|
"telecopie": telecopie,
|
|
"email": email,
|
|
"facebook": self._safe_strip(getattr(contact, "Facebook", None)),
|
|
"linkedin": self._safe_strip(getattr(contact, "LinkedIn", None)),
|
|
"skype": self._safe_strip(getattr(contact, "Skype", None)),
|
|
}
|
|
except Exception as e:
|
|
logger.warning(f"Erreur conversion contact: {e}")
|
|
return {}
|
|
|
|
def _row_to_contact_dict(self, row) -> Dict:
|
|
"""Convertit une ligne SQL en dictionnaire contact"""
|
|
civilite_code = row.CT_Civilite
|
|
civilite_map = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"}
|
|
|
|
return {
|
|
"numero": self._safe_strip(row.CT_Num),
|
|
"contact_numero": row.CT_No,
|
|
"n_contact": row.N_Contact,
|
|
"civilite": civilite_map.get(civilite_code) if civilite_code is not None else None,
|
|
"nom": self._safe_strip(row.CT_Nom),
|
|
"prenom": self._safe_strip(row.CT_Prenom),
|
|
"fonction": self._safe_strip(row.CT_Fonction),
|
|
"service_code": row.N_Service,
|
|
"telephone": self._safe_strip(row.CT_Telephone),
|
|
"portable": self._safe_strip(row.CT_TelPortable),
|
|
"telecopie": self._safe_strip(row.CT_Telecopie),
|
|
"email": self._safe_strip(row.CT_EMail),
|
|
"facebook": self._safe_strip(row.CT_Facebook),
|
|
"linkedin": self._safe_strip(row.CT_LinkedIn),
|
|
"skype": self._safe_strip(row.CT_Skype),
|
|
}
|
|
|
|
|
|
def _clean_str(self, value, max_len: int) -> str:
|
|
"""Nettoie et tronque une chaîne"""
|
|
if value is None or str(value).lower() in ('none', 'null', ''):
|
|
return ""
|
|
return str(value)[:max_len].strip()
|
|
|
|
|
|
def _safe_int(self, value, default=None):
|
|
"""Conversion sécurisée en entier"""
|
|
if value is None:
|
|
return default
|
|
try:
|
|
return int(value)
|
|
except (ValueError, TypeError):
|
|
return default
|
|
|
|
|
|
def _try_set_attribute(self, obj, attr_name, value, variants=None):
|
|
"""Essaie de définir un attribut avec plusieurs variantes"""
|
|
if variants is None:
|
|
variants = [attr_name]
|
|
else:
|
|
variants = [attr_name] + variants
|
|
|
|
for variant in variants:
|
|
try:
|
|
if hasattr(obj, variant):
|
|
setattr(obj, variant, value)
|
|
return True
|
|
except Exception as e:
|
|
logger.debug(f" {variant} échec: {str(e)[:50]}")
|
|
|
|
return False
|
|
|
|
|
|
def creer_client(self, client_data: Dict) -> Dict:
|
|
"""
|
|
Creation client Sage - Version corrigée pour erreur cohérence
|
|
"""
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non etablie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info("=" * 80)
|
|
logger.info("[CREATION CLIENT SAGE - DIAGNOSTIC COMPLET]")
|
|
logger.info("=" * 80)
|
|
|
|
def clean_str(value, max_len: int) -> str:
|
|
if value is None or str(value).lower() in ('none', 'null', ''):
|
|
return ""
|
|
return str(value)[:max_len].strip()
|
|
|
|
def safe_int(value, default=None):
|
|
if value is None:
|
|
return default
|
|
try:
|
|
return int(value)
|
|
except (ValueError, TypeError):
|
|
return default
|
|
|
|
def safe_float(value, default=None):
|
|
if value is None:
|
|
return default
|
|
try:
|
|
return float(value)
|
|
except (ValueError, TypeError):
|
|
return default
|
|
|
|
def try_set_attribute(obj, attr_name, value, variants=None):
|
|
"""Essaie de definir un attribut avec plusieurs variantes de noms"""
|
|
if variants is None:
|
|
variants = [attr_name]
|
|
else:
|
|
variants = [attr_name] + variants
|
|
|
|
for variant in variants:
|
|
try:
|
|
if hasattr(obj, variant):
|
|
setattr(obj, variant, value)
|
|
logger.debug(f" {variant} = {value} [OK]")
|
|
return True
|
|
except Exception as e:
|
|
logger.debug(f" {variant} echec: {str(e)[:50]}")
|
|
|
|
return False
|
|
|
|
if not client_data.get("intitule"):
|
|
raise ValueError("intitule obligatoire")
|
|
|
|
if not client_data.get("numero"):
|
|
raise ValueError("numero obligatoire")
|
|
|
|
intitule = clean_str(client_data["intitule"], 69)
|
|
numero = clean_str(client_data["numero"], 17).upper()
|
|
type_tiers = safe_int(client_data.get("type_tiers"), 0)
|
|
|
|
logger.info("[ETAPE 1] CREATION OBJET")
|
|
|
|
factory_map = {
|
|
0: ("FactoryClient", "IBOClient3"),
|
|
1: ("FactoryFourniss", "IBOFournisseur3"),
|
|
2: ("FactorySalarie", "IBOSalarie3"),
|
|
3: ("FactoryAutre", "IBOAutre3"),
|
|
}
|
|
|
|
factory_name, interface_name = factory_map[type_tiers]
|
|
factory = getattr(self.cial.CptaApplication, factory_name)
|
|
persist = factory.Create()
|
|
client = win32com.client.CastTo(persist, interface_name)
|
|
|
|
logger.info(f" Objet cree: {interface_name}")
|
|
|
|
logger.info("[ETAPE 2] CONFIGURATION OBLIGATOIRE")
|
|
|
|
client.CT_Intitule = intitule
|
|
client.CT_Num = numero
|
|
logger.info(f" CT_Num = {numero}")
|
|
logger.info(f" CT_Intitule = {intitule}")
|
|
|
|
qualite = clean_str(client_data.get("qualite", "CLI"), 17)
|
|
if qualite:
|
|
client.CT_Qualite = qualite
|
|
logger.info(f" CT_Qualite = {qualite}")
|
|
|
|
client.SetDefault()
|
|
logger.info(" SetDefault() applique")
|
|
|
|
if client_data.get("raccourci"):
|
|
raccourci = clean_str(client_data["raccourci"], 7).upper().strip()
|
|
|
|
try:
|
|
factory_client = self.cial.CptaApplication.FactoryClient
|
|
exist_client = factory_client.ReadRaccourci(raccourci)
|
|
|
|
if exist_client:
|
|
logger.warning(f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]")
|
|
else:
|
|
client.CT_Raccourci = raccourci
|
|
logger.info(f" CT_Raccourci = {raccourci} [OK]")
|
|
except Exception as e:
|
|
try:
|
|
client.CT_Raccourci = raccourci
|
|
logger.info(f" CT_Raccourci = {raccourci} [OK]")
|
|
except Exception as e2:
|
|
logger.warning(f" CT_Raccourci = {raccourci} [ECHEC: {e2}]")
|
|
|
|
try:
|
|
if not hasattr(client, 'CT_Type') or client.CT_Type is None:
|
|
client.CT_Type = type_tiers
|
|
logger.info(f" CT_Type force a {type_tiers}")
|
|
except:
|
|
pass
|
|
|
|
COMPTES_DEFAUT = {0: "4110000", 1: "4010000", 2: "421", 3: "471"}
|
|
compte = clean_str(
|
|
client_data.get("compte_general") or COMPTES_DEFAUT.get(type_tiers, "4110000"),
|
|
13
|
|
)
|
|
|
|
factory_compte = self.cial.CptaApplication.FactoryCompteG
|
|
compte_trouve = False
|
|
|
|
comptes_a_tester = [
|
|
compte,
|
|
COMPTES_DEFAUT.get(type_tiers, "4110000"),
|
|
"4110000", "411000", "411", # Clients
|
|
"4010000", "401000", "401", # Fournisseurs
|
|
]
|
|
|
|
for test_compte in comptes_a_tester:
|
|
try:
|
|
persist_compte = factory_compte.ReadNumero(test_compte)
|
|
if persist_compte:
|
|
compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3")
|
|
compte_obj.Read()
|
|
|
|
type_compte = getattr(compte_obj, 'CG_Type', None)
|
|
|
|
if type_compte == 0:
|
|
client.CompteGPrinc = compte_obj
|
|
compte = test_compte
|
|
compte_trouve = True
|
|
logger.info(f" CompteGPrinc = {test_compte} (Type: {type_compte}) [OK]")
|
|
break
|
|
else:
|
|
logger.debug(f" Compte {test_compte} - Type {type_compte} incompatible")
|
|
except Exception as e:
|
|
logger.debug(f" Compte {test_compte} - erreur: {e}")
|
|
|
|
if not compte_trouve:
|
|
raise RuntimeError("Aucun compte general valide trouve")
|
|
|
|
logger.info(" Configuration categories:")
|
|
|
|
try:
|
|
factory_cat_tarif = self.cial.CptaApplication.FactoryCategorieTarif
|
|
for cat_id in ["0", "1"]:
|
|
try:
|
|
persist_cat = factory_cat_tarif.ReadIntitule(cat_id)
|
|
if persist_cat:
|
|
cat_tarif_obj = win32com.client.CastTo(persist_cat, "IBOCategorieTarif3")
|
|
cat_tarif_obj.Read()
|
|
client.CatTarif = cat_tarif_obj
|
|
logger.info(f" CatTarif = {cat_id} [OK]")
|
|
break
|
|
except:
|
|
continue
|
|
except Exception as e:
|
|
logger.warning(f" CatTarif erreur: {e}")
|
|
|
|
try:
|
|
factory_cat_compta = self.cial.CptaApplication.FactoryCategorieCompta
|
|
for cat_id in ["0", "1"]:
|
|
try:
|
|
persist_cat = factory_cat_compta.ReadIntitule(cat_id)
|
|
if persist_cat:
|
|
cat_compta_obj = win32com.client.CastTo(persist_cat, "IBOCategorieCompta3")
|
|
cat_compta_obj.Read()
|
|
client.CatCompta = cat_compta_obj
|
|
logger.info(f" CatCompta = {cat_id} [OK]")
|
|
break
|
|
except:
|
|
continue
|
|
except Exception as e:
|
|
logger.warning(f" CatCompta erreur: {e}")
|
|
|
|
logger.info("[ETAPE 3] IDENTIFICATION")
|
|
|
|
if client_data.get("classement"):
|
|
try_set_attribute(client, "CT_Classement", clean_str(client_data["classement"], 17))
|
|
|
|
if client_data.get("raccourci"):
|
|
raccourci = clean_str(client_data["raccourci"], 7).upper()
|
|
try_set_attribute(client, "CT_Raccourci", raccourci)
|
|
|
|
if client_data.get("siret"):
|
|
try_set_attribute(client, "CT_Siret", clean_str(client_data["siret"], 15))
|
|
|
|
if client_data.get("tva_intra"):
|
|
try_set_attribute(client, "CT_Identifiant", clean_str(client_data["tva_intra"], 25))
|
|
|
|
if client_data.get("code_naf"):
|
|
try_set_attribute(client, "CT_Ape", clean_str(client_data["code_naf"], 7))
|
|
|
|
logger.info("[ETAPE 4] ADRESSE")
|
|
|
|
if client_data.get("contact"):
|
|
contact_nom = clean_str(client_data["contact"], 35)
|
|
|
|
try:
|
|
client.CT_Contact = contact_nom
|
|
logger.info(f" CT_Contact (client) = {contact_nom} [OK]")
|
|
except Exception as e:
|
|
logger.warning(f" CT_Contact (client) [ECHEC: {e}]")
|
|
|
|
try:
|
|
adresse_obj = client.Adresse
|
|
adresse_obj.Contact = contact_nom
|
|
logger.info(f" Contact (adresse) = {contact_nom} [OK]")
|
|
except Exception as e:
|
|
logger.warning(f" Contact (adresse) [ECHEC: {e}]")
|
|
|
|
try:
|
|
adresse_obj = client.Adresse
|
|
logger.info(" Objet Adresse OK")
|
|
|
|
if client_data.get("adresse"):
|
|
adresse_obj.Adresse = clean_str(client_data["adresse"], 35)
|
|
|
|
if client_data.get("complement"):
|
|
adresse_obj.Complement = clean_str(client_data["complement"], 35)
|
|
|
|
if client_data.get("code_postal"):
|
|
adresse_obj.CodePostal = clean_str(client_data["code_postal"], 9)
|
|
|
|
if client_data.get("ville"):
|
|
adresse_obj.Ville = clean_str(client_data["ville"], 35)
|
|
|
|
if client_data.get("region"):
|
|
adresse_obj.CodeRegion = clean_str(client_data["region"], 25)
|
|
|
|
if client_data.get("pays"):
|
|
adresse_obj.Pays = clean_str(client_data["pays"], 35)
|
|
|
|
except Exception as e:
|
|
logger.error(f" Adresse erreur: {e}")
|
|
|
|
logger.info("[ETAPE 5] TELECOM")
|
|
|
|
try:
|
|
telecom_obj = client.Telecom
|
|
logger.info(" Objet Telecom OK")
|
|
|
|
if client_data.get("telephone"):
|
|
telecom_obj.Telephone = clean_str(client_data["telephone"], 21)
|
|
|
|
if client_data.get("telecopie"):
|
|
telecom_obj.Telecopie = clean_str(client_data["telecopie"], 21)
|
|
|
|
if client_data.get("email"):
|
|
telecom_obj.EMail = clean_str(client_data["email"], 69)
|
|
|
|
if client_data.get("site_web"):
|
|
telecom_obj.Site = clean_str(client_data["site_web"], 69)
|
|
|
|
if client_data.get("portable"):
|
|
portable = clean_str(client_data["portable"], 21)
|
|
try_set_attribute(telecom_obj, "Portable", portable)
|
|
logger.info(f" Portable = {portable}")
|
|
|
|
if client_data.get("facebook"):
|
|
facebook = clean_str(client_data["facebook"], 69) # URL ou @username
|
|
if not try_set_attribute(telecom_obj, "Facebook", facebook):
|
|
try_set_attribute(client, "CT_Facebook", facebook)
|
|
logger.info(f" Facebook = {facebook}")
|
|
|
|
if client_data.get("linkedin"):
|
|
linkedin = clean_str(client_data["linkedin"], 69) # URL ou profil
|
|
if not try_set_attribute(telecom_obj, "LinkedIn", linkedin):
|
|
try_set_attribute(client, "CT_LinkedIn", linkedin)
|
|
logger.info(f" LinkedIn = {linkedin}")
|
|
|
|
except Exception as e:
|
|
logger.error(f" Telecom erreur: {e}")
|
|
|
|
logger.info("[ETAPE 6] TAUX")
|
|
|
|
for i in range(1, 5):
|
|
val = client_data.get(f"taux{i:02d}")
|
|
if val is not None:
|
|
try_set_attribute(client, f"CT_Taux{i:02d}", safe_float(val))
|
|
|
|
logger.info("[ETAPE 7] STATISTIQUES")
|
|
|
|
stat01 = client_data.get("statistique01") or client_data.get("secteur")
|
|
if stat01:
|
|
try_set_attribute(client, "CT_Statistique01", clean_str(stat01, 21))
|
|
|
|
for i in range(2, 11):
|
|
val = client_data.get(f"statistique{i:02d}")
|
|
if val:
|
|
try_set_attribute(client, f"CT_Statistique{i:02d}", clean_str(val, 21))
|
|
|
|
logger.info("[ETAPE 8] COMMERCIAL")
|
|
|
|
if client_data.get("encours_autorise"):
|
|
try_set_attribute(client, "CT_Encours", safe_float(client_data["encours_autorise"]))
|
|
|
|
if client_data.get("assurance_credit"):
|
|
try_set_attribute(client, "CT_Assurance", safe_float(client_data["assurance_credit"]))
|
|
|
|
if client_data.get("langue") is not None:
|
|
try_set_attribute(client, "CT_Langue", safe_int(client_data["langue"]))
|
|
|
|
if client_data.get("commercial_code") is not None:
|
|
co_no = safe_int(client_data["commercial_code"])
|
|
if not try_set_attribute(client, "CO_No", co_no):
|
|
try:
|
|
factory_collab = self.cial.CptaApplication.FactoryCollaborateur
|
|
persist_collab = factory_collab.ReadIntitule(str(co_no))
|
|
if persist_collab:
|
|
collab_obj = win32com.client.CastTo(persist_collab, "IBOCollaborateur3")
|
|
collab_obj.Read()
|
|
client.Collaborateur = collab_obj
|
|
logger.debug(f" Collaborateur (objet) = {co_no} [OK]")
|
|
except Exception as e:
|
|
logger.debug(f" Collaborateur echec: {e}")
|
|
|
|
logger.info("[ETAPE 9] FACTURATION")
|
|
|
|
try_set_attribute(client, "CT_Lettrage", 1 if client_data.get("lettrage_auto", True) else 0)
|
|
try_set_attribute(client, "CT_Sommeil", 0 if client_data.get("est_actif", True) else 1)
|
|
try_set_attribute(client, "CT_Facture", safe_int(client_data.get("type_facture", 1)))
|
|
|
|
if client_data.get("est_prospect") is not None:
|
|
try_set_attribute(client, "CT_Prospect", 1 if client_data["est_prospect"] else 0)
|
|
|
|
factu_map = {
|
|
"CT_BLFact": "bl_en_facture",
|
|
"CT_Saut": "saut_page",
|
|
"CT_ValidEch": "validation_echeance",
|
|
"CT_ControlEnc": "controle_encours",
|
|
"CT_NotRappel": "exclure_relance",
|
|
"CT_NotPenal": "exclure_penalites",
|
|
"CT_BonAPayer": "bon_a_payer",
|
|
}
|
|
for attr, key in factu_map.items():
|
|
if client_data.get(key) is not None:
|
|
try_set_attribute(client, attr, safe_int(client_data[key]))
|
|
|
|
logger.info("[ETAPE 10] LOGISTIQUE")
|
|
|
|
logistique_map = {
|
|
"CT_PrioriteLivr": "priorite_livraison",
|
|
"CT_LivrPartielle": "livraison_partielle",
|
|
"CT_DelaiTransport": "delai_transport",
|
|
"CT_DelaiAppro": "delai_appro",
|
|
}
|
|
for attr, key in logistique_map.items():
|
|
if client_data.get(key) is not None:
|
|
try_set_attribute(client, attr, safe_int(client_data[key]))
|
|
|
|
if client_data.get("commentaire"):
|
|
try_set_attribute(client, "CT_Commentaire", clean_str(client_data["commentaire"], 35))
|
|
|
|
logger.info("[ETAPE 12] ANALYTIQUE")
|
|
|
|
if client_data.get("section_analytique"):
|
|
try_set_attribute(client, "CA_Num", clean_str(client_data["section_analytique"], 13))
|
|
|
|
logger.info("[ETAPE 13] ORGANISATION")
|
|
|
|
if client_data.get("mode_reglement_code") is not None:
|
|
mr_no = safe_int(client_data["mode_reglement_code"])
|
|
if not try_set_attribute(client, "MR_No", mr_no):
|
|
try:
|
|
factory_mr = self.cial.CptaApplication.FactoryModeRegl
|
|
persist_mr = factory_mr.ReadIntitule(str(mr_no))
|
|
if persist_mr:
|
|
mr_obj = win32com.client.CastTo(persist_mr, "IBOModeRegl3")
|
|
mr_obj.Read()
|
|
client.ModeRegl = mr_obj
|
|
logger.debug(f" ModeRegl (objet) = {mr_no} [OK]")
|
|
except Exception as e:
|
|
logger.debug(f" ModeRegl echec: {e}")
|
|
|
|
if client_data.get("surveillance_active") is not None:
|
|
surveillance = 1 if client_data["surveillance_active"] else 0
|
|
try:
|
|
client.CT_Surveillance = surveillance
|
|
logger.info(f" CT_Surveillance = {surveillance} [OK]")
|
|
except Exception as e:
|
|
logger.warning(f" CT_Surveillance [ECHEC: {e}]")
|
|
|
|
if client_data.get("coface"):
|
|
coface = clean_str(client_data["coface"], 25)
|
|
try:
|
|
client.CT_Coface = coface
|
|
logger.info(f" CT_Coface = {coface} [OK]")
|
|
except Exception as e:
|
|
logger.warning(f" CT_Coface [ECHEC: {e}]")
|
|
|
|
if client_data.get("forme_juridique"):
|
|
try_set_attribute(client, "CT_SvFormeJuri", clean_str(client_data["forme_juridique"], 33))
|
|
|
|
if client_data.get("effectif"):
|
|
try_set_attribute(client, "CT_SvEffectif", clean_str(client_data["effectif"], 11))
|
|
|
|
if client_data.get("sv_regularite"):
|
|
try_set_attribute(client, "CT_SvRegul", clean_str(client_data["sv_regularite"], 3))
|
|
|
|
if client_data.get("sv_cotation"):
|
|
try_set_attribute(client, "CT_SvCotation", clean_str(client_data["sv_cotation"], 5))
|
|
|
|
if client_data.get("sv_objet_maj"):
|
|
try_set_attribute(client, "CT_SvObjetMaj", clean_str(client_data["sv_objet_maj"], 61))
|
|
|
|
ca = client_data.get("ca_annuel") or client_data.get("sv_chiffre_affaires")
|
|
if ca:
|
|
try_set_attribute(client, "CT_SvCA", safe_float(ca))
|
|
|
|
if client_data.get("sv_resultat"):
|
|
try_set_attribute(client, "CT_SvResultat", safe_float(client_data["sv_resultat"]))
|
|
|
|
logger.info("=" * 80)
|
|
logger.info("[DIAGNOSTIC PRE-WRITE]")
|
|
|
|
champs_diagnostic = [
|
|
'CT_Num', 'CT_Intitule', 'CT_Type', 'CT_Qualite',
|
|
'CT_Facture', 'CT_Lettrage', 'CT_Sommeil',
|
|
]
|
|
|
|
for champ in champs_diagnostic:
|
|
try:
|
|
valeur = getattr(client, champ, "ATTRIBUT_INEXISTANT")
|
|
logger.info(f" {champ}: {valeur}")
|
|
except Exception as e:
|
|
logger.info(f" {champ}: ERREUR ({str(e)[:50]})")
|
|
|
|
try:
|
|
compte_obj = client.CompteGPrinc
|
|
if compte_obj:
|
|
logger.info(f" CompteGPrinc.CG_Num: {compte_obj.CG_Num}")
|
|
logger.info(f" CompteGPrinc.CG_Type: {compte_obj.CG_Type}")
|
|
else:
|
|
logger.error(" CompteGPrinc: NULL !!!")
|
|
except Exception as e:
|
|
logger.error(f" CompteGPrinc: ERREUR - {e}")
|
|
|
|
logger.info("=" * 80)
|
|
|
|
logger.info("[WRITE]")
|
|
|
|
try:
|
|
client.Write()
|
|
client.Read()
|
|
logger.info("[OK] Write reussi")
|
|
except Exception as e:
|
|
error_detail = str(e)
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
except:
|
|
pass
|
|
|
|
logger.error(f"[ERREUR] {error_detail}")
|
|
raise RuntimeError(f"Echec Write(): {error_detail}")
|
|
|
|
num_final = getattr(client, "CT_Num", numero)
|
|
|
|
logger.info("=" * 80)
|
|
logger.info(f"[SUCCES] CLIENT CREE: {num_final}")
|
|
logger.info("=" * 80)
|
|
|
|
return {
|
|
"numero": num_final,
|
|
"intitule": intitule,
|
|
"type_tiers": type_tiers,
|
|
"qualite": qualite,
|
|
"compte_general": compte,
|
|
"date_creation": datetime.now().isoformat(),
|
|
}
|
|
|
|
except ValueError as e:
|
|
logger.error(f"[ERREUR VALIDATION] {e}")
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"[ERREUR] {e}", exc_info=True)
|
|
raise RuntimeError(f"Erreur technique: {e}")
|
|
|
|
def modifier_client(self, code: str, client_data: Dict) -> Dict:
|
|
"""
|
|
Modification client Sage - Version complète alignée sur creer_client
|
|
"""
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info("=" * 80)
|
|
logger.info(f"[MODIFICATION CLIENT SAGE - {code}]")
|
|
logger.info("=" * 80)
|
|
|
|
def clean_str(value, max_len: int) -> str:
|
|
if value is None or str(value).lower() in ('none', 'null', ''):
|
|
return ""
|
|
return str(value)[:max_len].strip()
|
|
|
|
def safe_int(value, default=None):
|
|
if value is None:
|
|
return default
|
|
try:
|
|
return int(value)
|
|
except (ValueError, TypeError):
|
|
return default
|
|
|
|
def safe_float(value, default=None):
|
|
if value is None:
|
|
return default
|
|
try:
|
|
return float(value)
|
|
except (ValueError, TypeError):
|
|
return default
|
|
|
|
def try_set_attribute(obj, attr_name, value, variants=None):
|
|
"""Essaie de définir un attribut avec plusieurs variantes de noms"""
|
|
if variants is None:
|
|
variants = [attr_name]
|
|
else:
|
|
variants = [attr_name] + variants
|
|
|
|
for variant in variants:
|
|
try:
|
|
if hasattr(obj, variant):
|
|
setattr(obj, variant, value)
|
|
logger.debug(f" {variant} = {value} [OK]")
|
|
return True
|
|
except Exception as e:
|
|
logger.debug(f" {variant} echec: {str(e)[:50]}")
|
|
|
|
return False
|
|
|
|
champs_modifies = []
|
|
|
|
logger.info("[ETAPE 1] CHARGEMENT CLIENT")
|
|
|
|
factory_client = self.cial.CptaApplication.FactoryClient
|
|
persist = factory_client.ReadNumero(code)
|
|
|
|
if not persist:
|
|
raise ValueError(f"Client {code} introuvable")
|
|
|
|
client = win32com.client.CastTo(persist, "IBOClient3")
|
|
client.Read()
|
|
|
|
logger.info(f" Client chargé: {getattr(client, 'CT_Intitule', '')}")
|
|
|
|
logger.info("[ETAPE 2] IDENTIFICATION")
|
|
|
|
if "intitule" in client_data:
|
|
intitule = clean_str(client_data["intitule"], 69)
|
|
client.CT_Intitule = intitule
|
|
champs_modifies.append("intitule")
|
|
logger.info(f" CT_Intitule = {intitule}")
|
|
|
|
if "qualite" in client_data:
|
|
qualite = clean_str(client_data["qualite"], 17)
|
|
if try_set_attribute(client, "CT_Qualite", qualite):
|
|
champs_modifies.append("qualite")
|
|
|
|
if "classement" in client_data:
|
|
if try_set_attribute(client, "CT_Classement", clean_str(client_data["classement"], 17)):
|
|
champs_modifies.append("classement")
|
|
|
|
if "raccourci" in client_data:
|
|
raccourci = clean_str(client_data["raccourci"], 7).upper()
|
|
|
|
try:
|
|
exist_client = factory_client.ReadRaccourci(raccourci)
|
|
if exist_client and exist_client.CT_Num != code:
|
|
logger.warning(f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]")
|
|
else:
|
|
if try_set_attribute(client, "CT_Raccourci", raccourci):
|
|
champs_modifies.append("raccourci")
|
|
except:
|
|
if try_set_attribute(client, "CT_Raccourci", raccourci):
|
|
champs_modifies.append("raccourci")
|
|
|
|
if "siret" in client_data:
|
|
if try_set_attribute(client, "CT_Siret", clean_str(client_data["siret"], 15)):
|
|
champs_modifies.append("siret")
|
|
|
|
if "tva_intra" in client_data:
|
|
if try_set_attribute(client, "CT_Identifiant", clean_str(client_data["tva_intra"], 25)):
|
|
champs_modifies.append("tva_intra")
|
|
|
|
if "code_naf" in client_data:
|
|
if try_set_attribute(client, "CT_Ape", clean_str(client_data["code_naf"], 7)):
|
|
champs_modifies.append("code_naf")
|
|
|
|
adresse_keys = ["contact", "adresse", "complement", "code_postal", "ville", "region", "pays"]
|
|
|
|
if any(k in client_data for k in adresse_keys):
|
|
logger.info("[ETAPE 3] ADRESSE")
|
|
|
|
try:
|
|
if "contact" in client_data:
|
|
contact_nom = clean_str(client_data["contact"], 35)
|
|
|
|
try:
|
|
client.CT_Contact = contact_nom
|
|
champs_modifies.append("contact (client)")
|
|
logger.info(f" CT_Contact (client) = {contact_nom} [OK]")
|
|
except Exception as e:
|
|
logger.warning(f" CT_Contact (client) [ECHEC: {e}]")
|
|
|
|
try:
|
|
adresse_obj = client.Adresse
|
|
adresse_obj.Contact = contact_nom
|
|
champs_modifies.append("contact (adresse)")
|
|
logger.info(f" Contact (adresse) = {contact_nom} [OK]")
|
|
except Exception as e:
|
|
logger.warning(f" Contact (adresse) [ECHEC: {e}]")
|
|
|
|
adresse_obj = client.Adresse
|
|
|
|
if "adresse" in client_data:
|
|
adresse_obj.Adresse = clean_str(client_data["adresse"], 35)
|
|
champs_modifies.append("adresse")
|
|
|
|
if "complement" in client_data:
|
|
adresse_obj.Complement = clean_str(client_data["complement"], 35)
|
|
champs_modifies.append("complement")
|
|
|
|
if "code_postal" in client_data:
|
|
adresse_obj.CodePostal = clean_str(client_data["code_postal"], 9)
|
|
champs_modifies.append("code_postal")
|
|
|
|
if "ville" in client_data:
|
|
adresse_obj.Ville = clean_str(client_data["ville"], 35)
|
|
champs_modifies.append("ville")
|
|
|
|
if "region" in client_data:
|
|
adresse_obj.CodeRegion = clean_str(client_data["region"], 25)
|
|
champs_modifies.append("region")
|
|
|
|
if "pays" in client_data:
|
|
adresse_obj.Pays = clean_str(client_data["pays"], 35)
|
|
champs_modifies.append("pays")
|
|
|
|
logger.info(f" Adresse mise à jour ({len([k for k in adresse_keys if k in client_data])} champs)")
|
|
|
|
except Exception as e:
|
|
logger.error(f" Adresse erreur: {e}")
|
|
|
|
telecom_keys = ["telephone", "telecopie", "email", "site_web", "portable", "facebook", "linkedin"]
|
|
|
|
if any(k in client_data for k in telecom_keys):
|
|
logger.info("[ETAPE 4] TELECOM")
|
|
|
|
try:
|
|
telecom_obj = client.Telecom
|
|
|
|
if "telephone" in client_data:
|
|
telecom_obj.Telephone = clean_str(client_data["telephone"], 21)
|
|
champs_modifies.append("telephone")
|
|
|
|
if "telecopie" in client_data:
|
|
telecom_obj.Telecopie = clean_str(client_data["telecopie"], 21)
|
|
champs_modifies.append("telecopie")
|
|
|
|
if "email" in client_data:
|
|
telecom_obj.EMail = clean_str(client_data["email"], 69)
|
|
champs_modifies.append("email")
|
|
|
|
if "site_web" in client_data:
|
|
telecom_obj.Site = clean_str(client_data["site_web"], 69)
|
|
champs_modifies.append("site_web")
|
|
|
|
if "portable" in client_data:
|
|
portable = clean_str(client_data["portable"], 21)
|
|
if try_set_attribute(telecom_obj, "Portable", portable):
|
|
champs_modifies.append("portable")
|
|
|
|
if "facebook" in client_data:
|
|
facebook = clean_str(client_data["facebook"], 69)
|
|
if not try_set_attribute(telecom_obj, "Facebook", facebook):
|
|
try_set_attribute(client, "CT_Facebook", facebook)
|
|
champs_modifies.append("facebook")
|
|
|
|
if "linkedin" in client_data:
|
|
linkedin = clean_str(client_data["linkedin"], 69)
|
|
if not try_set_attribute(telecom_obj, "LinkedIn", linkedin):
|
|
try_set_attribute(client, "CT_LinkedIn", linkedin)
|
|
champs_modifies.append("linkedin")
|
|
|
|
logger.info(f" Telecom mis à jour ({len([k for k in telecom_keys if k in client_data])} champs)")
|
|
|
|
except Exception as e:
|
|
logger.error(f" Telecom erreur: {e}")
|
|
|
|
if "compte_general" in client_data:
|
|
logger.info("[ETAPE 5] COMPTE GENERAL")
|
|
|
|
compte = clean_str(client_data["compte_general"], 13)
|
|
factory_compte = self.cial.CptaApplication.FactoryCompteG
|
|
|
|
try:
|
|
persist_compte = factory_compte.ReadNumero(compte)
|
|
if persist_compte:
|
|
compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3")
|
|
compte_obj.Read()
|
|
|
|
type_compte = getattr(compte_obj, 'CG_Type', None)
|
|
if type_compte == 0:
|
|
client.CompteGPrinc = compte_obj
|
|
champs_modifies.append("compte_general")
|
|
logger.info(f" CompteGPrinc = {compte} [OK]")
|
|
else:
|
|
logger.warning(f" Compte {compte} - Type {type_compte} incompatible")
|
|
except Exception as e:
|
|
logger.warning(f" CompteGPrinc erreur: {e}")
|
|
|
|
if "categorie_tarifaire" in client_data or "categorie_comptable" in client_data:
|
|
logger.info("[ETAPE 6] CATEGORIES")
|
|
|
|
if "categorie_tarifaire" in client_data:
|
|
try:
|
|
cat_id = str(client_data["categorie_tarifaire"])
|
|
factory_cat_tarif = self.cial.CptaApplication.FactoryCategorieTarif
|
|
persist_cat = factory_cat_tarif.ReadIntitule(cat_id)
|
|
|
|
if persist_cat:
|
|
cat_tarif_obj = win32com.client.CastTo(persist_cat, "IBOCategorieTarif3")
|
|
cat_tarif_obj.Read()
|
|
client.CatTarif = cat_tarif_obj
|
|
champs_modifies.append("categorie_tarifaire")
|
|
logger.info(f" CatTarif = {cat_id} [OK]")
|
|
except Exception as e:
|
|
logger.warning(f" CatTarif erreur: {e}")
|
|
|
|
if "categorie_comptable" in client_data:
|
|
try:
|
|
cat_id = str(client_data["categorie_comptable"])
|
|
factory_cat_compta = self.cial.CptaApplication.FactoryCategorieCompta
|
|
persist_cat = factory_cat_compta.ReadIntitule(cat_id)
|
|
|
|
if persist_cat:
|
|
cat_compta_obj = win32com.client.CastTo(persist_cat, "IBOCategorieCompta3")
|
|
cat_compta_obj.Read()
|
|
client.CatCompta = cat_compta_obj
|
|
champs_modifies.append("categorie_comptable")
|
|
logger.info(f" CatCompta = {cat_id} [OK]")
|
|
except Exception as e:
|
|
logger.warning(f" CatCompta erreur: {e}")
|
|
|
|
taux_modifies = False
|
|
for i in range(1, 5):
|
|
key = f"taux{i:02d}"
|
|
if key in client_data:
|
|
if not taux_modifies:
|
|
logger.info("[ETAPE 7] TAUX")
|
|
taux_modifies = True
|
|
|
|
val = safe_float(client_data[key])
|
|
if try_set_attribute(client, f"CT_Taux{i:02d}", val):
|
|
champs_modifies.append(key)
|
|
|
|
stat_keys = ["statistique01", "secteur"] + [f"statistique{i:02d}" for i in range(2, 11)]
|
|
stat_modifies = False
|
|
|
|
stat01 = client_data.get("statistique01") or client_data.get("secteur")
|
|
if stat01:
|
|
if not stat_modifies:
|
|
logger.info("[ETAPE 8] STATISTIQUES")
|
|
stat_modifies = True
|
|
|
|
if try_set_attribute(client, "CT_Statistique01", clean_str(stat01, 21)):
|
|
champs_modifies.append("statistique01")
|
|
|
|
for i in range(2, 11):
|
|
key = f"statistique{i:02d}"
|
|
if key in client_data:
|
|
if not stat_modifies:
|
|
logger.info("[ETAPE 8] STATISTIQUES")
|
|
stat_modifies = True
|
|
|
|
if try_set_attribute(client, f"CT_Statistique{i:02d}", clean_str(client_data[key], 21)):
|
|
champs_modifies.append(key)
|
|
|
|
commercial_keys = ["encours_autorise", "assurance_credit", "langue", "commercial_code"]
|
|
|
|
if any(k in client_data for k in commercial_keys):
|
|
logger.info("[ETAPE 9] COMMERCIAL")
|
|
|
|
if "encours_autorise" in client_data:
|
|
if try_set_attribute(client, "CT_Encours", safe_float(client_data["encours_autorise"])):
|
|
champs_modifies.append("encours_autorise")
|
|
|
|
if "assurance_credit" in client_data:
|
|
if try_set_attribute(client, "CT_Assurance", safe_float(client_data["assurance_credit"])):
|
|
champs_modifies.append("assurance_credit")
|
|
|
|
if "langue" in client_data:
|
|
if try_set_attribute(client, "CT_Langue", safe_int(client_data["langue"])):
|
|
champs_modifies.append("langue")
|
|
|
|
if "commercial_code" in client_data:
|
|
co_no = safe_int(client_data["commercial_code"])
|
|
if not try_set_attribute(client, "CO_No", co_no):
|
|
try:
|
|
factory_collab = self.cial.CptaApplication.FactoryCollaborateur
|
|
persist_collab = factory_collab.ReadIntitule(str(co_no))
|
|
if persist_collab:
|
|
collab_obj = win32com.client.CastTo(persist_collab, "IBOCollaborateur3")
|
|
collab_obj.Read()
|
|
client.Collaborateur = collab_obj
|
|
champs_modifies.append("commercial_code")
|
|
logger.info(f" Collaborateur = {co_no} [OK]")
|
|
except Exception as e:
|
|
logger.warning(f" Collaborateur erreur: {e}")
|
|
|
|
facturation_keys = [
|
|
"lettrage_auto", "est_actif", "type_facture", "est_prospect",
|
|
"bl_en_facture", "saut_page", "validation_echeance", "controle_encours",
|
|
"exclure_relance", "exclure_penalites", "bon_a_payer"
|
|
]
|
|
|
|
if any(k in client_data for k in facturation_keys):
|
|
logger.info("[ETAPE 10] FACTURATION")
|
|
|
|
if "lettrage_auto" in client_data:
|
|
if try_set_attribute(client, "CT_Lettrage", 1 if client_data["lettrage_auto"] else 0):
|
|
champs_modifies.append("lettrage_auto")
|
|
|
|
if "est_actif" in client_data:
|
|
if try_set_attribute(client, "CT_Sommeil", 0 if client_data["est_actif"] else 1):
|
|
champs_modifies.append("est_actif")
|
|
|
|
if "type_facture" in client_data:
|
|
if try_set_attribute(client, "CT_Facture", safe_int(client_data["type_facture"])):
|
|
champs_modifies.append("type_facture")
|
|
|
|
if "est_prospect" in client_data:
|
|
if try_set_attribute(client, "CT_Prospect", 1 if client_data["est_prospect"] else 0):
|
|
champs_modifies.append("est_prospect")
|
|
|
|
factu_map = {
|
|
"CT_BLFact": "bl_en_facture",
|
|
"CT_Saut": "saut_page",
|
|
"CT_ValidEch": "validation_echeance",
|
|
"CT_ControlEnc": "controle_encours",
|
|
"CT_NotRappel": "exclure_relance",
|
|
"CT_NotPenal": "exclure_penalites",
|
|
"CT_BonAPayer": "bon_a_payer",
|
|
}
|
|
|
|
for attr, key in factu_map.items():
|
|
if key in client_data:
|
|
if try_set_attribute(client, attr, safe_int(client_data[key])):
|
|
champs_modifies.append(key)
|
|
|
|
logistique_keys = ["priorite_livraison", "livraison_partielle", "delai_transport", "delai_appro"]
|
|
|
|
if any(k in client_data for k in logistique_keys):
|
|
logger.info("[ETAPE 11] LOGISTIQUE")
|
|
|
|
logistique_map = {
|
|
"CT_PrioriteLivr": "priorite_livraison",
|
|
"CT_LivrPartielle": "livraison_partielle",
|
|
"CT_DelaiTransport": "delai_transport",
|
|
"CT_DelaiAppro": "delai_appro",
|
|
}
|
|
|
|
for attr, key in logistique_map.items():
|
|
if key in client_data:
|
|
if try_set_attribute(client, attr, safe_int(client_data[key])):
|
|
champs_modifies.append(key)
|
|
|
|
if "commentaire" in client_data:
|
|
logger.info("[ETAPE 12] COMMENTAIRE")
|
|
if try_set_attribute(client, "CT_Commentaire", clean_str(client_data["commentaire"], 35)):
|
|
champs_modifies.append("commentaire")
|
|
|
|
if "section_analytique" in client_data:
|
|
logger.info("[ETAPE 13] ANALYTIQUE")
|
|
if try_set_attribute(client, "CA_Num", clean_str(client_data["section_analytique"], 13)):
|
|
champs_modifies.append("section_analytique")
|
|
|
|
organisation_keys = [
|
|
"mode_reglement_code", "surveillance_active", "coface",
|
|
"forme_juridique", "effectif", "sv_regularite", "sv_cotation",
|
|
"sv_objet_maj", "ca_annuel", "sv_chiffre_affaires", "sv_resultat"
|
|
]
|
|
|
|
if any(k in client_data for k in organisation_keys):
|
|
logger.info("[ETAPE 14] ORGANISATION & SURVEILLANCE")
|
|
|
|
if "mode_reglement_code" in client_data:
|
|
mr_no = safe_int(client_data["mode_reglement_code"])
|
|
if not try_set_attribute(client, "MR_No", mr_no):
|
|
try:
|
|
factory_mr = self.cial.CptaApplication.FactoryModeRegl
|
|
persist_mr = factory_mr.ReadIntitule(str(mr_no))
|
|
if persist_mr:
|
|
mr_obj = win32com.client.CastTo(persist_mr, "IBOModeRegl3")
|
|
mr_obj.Read()
|
|
client.ModeRegl = mr_obj
|
|
champs_modifies.append("mode_reglement_code")
|
|
logger.info(f" ModeRegl = {mr_no} [OK]")
|
|
except Exception as e:
|
|
logger.warning(f" ModeRegl erreur: {e}")
|
|
|
|
if "surveillance_active" in client_data:
|
|
surveillance = 1 if client_data["surveillance_active"] else 0
|
|
try:
|
|
client.CT_Surveillance = surveillance
|
|
champs_modifies.append("surveillance_active")
|
|
logger.info(f" CT_Surveillance = {surveillance} [OK]")
|
|
except Exception as e:
|
|
logger.warning(f" CT_Surveillance [ECHEC: {e}]")
|
|
|
|
if "coface" in client_data:
|
|
coface = clean_str(client_data["coface"], 25)
|
|
try:
|
|
client.CT_Coface = coface
|
|
champs_modifies.append("coface")
|
|
logger.info(f" CT_Coface = {coface} [OK]")
|
|
except Exception as e:
|
|
logger.warning(f" CT_Coface [ECHEC: {e}]")
|
|
|
|
if "forme_juridique" in client_data:
|
|
if try_set_attribute(client, "CT_SvFormeJuri", clean_str(client_data["forme_juridique"], 33)):
|
|
champs_modifies.append("forme_juridique")
|
|
|
|
if "effectif" in client_data:
|
|
if try_set_attribute(client, "CT_SvEffectif", clean_str(client_data["effectif"], 11)):
|
|
champs_modifies.append("effectif")
|
|
|
|
if "sv_regularite" in client_data:
|
|
if try_set_attribute(client, "CT_SvRegul", clean_str(client_data["sv_regularite"], 3)):
|
|
champs_modifies.append("sv_regularite")
|
|
|
|
if "sv_cotation" in client_data:
|
|
if try_set_attribute(client, "CT_SvCotation", clean_str(client_data["sv_cotation"], 5)):
|
|
champs_modifies.append("sv_cotation")
|
|
|
|
if "sv_objet_maj" in client_data:
|
|
if try_set_attribute(client, "CT_SvObjetMaj", clean_str(client_data["sv_objet_maj"], 61)):
|
|
champs_modifies.append("sv_objet_maj")
|
|
|
|
ca = client_data.get("ca_annuel") or client_data.get("sv_chiffre_affaires")
|
|
if ca:
|
|
if try_set_attribute(client, "CT_SvCA", safe_float(ca)):
|
|
champs_modifies.append("ca_annuel/sv_chiffre_affaires")
|
|
|
|
if "sv_resultat" in client_data:
|
|
if try_set_attribute(client, "CT_SvResultat", safe_float(client_data["sv_resultat"])):
|
|
champs_modifies.append("sv_resultat")
|
|
|
|
if not champs_modifies:
|
|
logger.warning("Aucun champ à modifier")
|
|
return self._extraire_client(client)
|
|
|
|
logger.info("=" * 80)
|
|
logger.info(f"[WRITE] {len(champs_modifies)} champs modifiés:")
|
|
for i, champ in enumerate(champs_modifies, 1):
|
|
logger.info(f" {i}. {champ}")
|
|
logger.info("=" * 80)
|
|
|
|
try:
|
|
client.Write()
|
|
client.Read()
|
|
logger.info("[OK] Write réussi")
|
|
except Exception as e:
|
|
error_detail = str(e)
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
except:
|
|
pass
|
|
|
|
logger.error(f"[ERREUR] {error_detail}")
|
|
raise RuntimeError(f"Echec Write(): {error_detail}")
|
|
|
|
logger.info("=" * 80)
|
|
logger.info(f"[SUCCES] CLIENT MODIFIÉ: {code} ({len(champs_modifies)} champs)")
|
|
logger.info("=" * 80)
|
|
|
|
return self._extraire_client(client)
|
|
|
|
except ValueError as e:
|
|
logger.error(f"[ERREUR VALIDATION] {e}")
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"[ERREUR] {e}", exc_info=True)
|
|
raise RuntimeError(f"Erreur technique: {e}")
|
|
|
|
def creer_commande_enrichi(self, commande_data: dict) -> Dict:
|
|
"""
|
|
Crée une commande dans Sage avec support des dates.
|
|
|
|
Args:
|
|
commande_data: dict contenant:
|
|
- client: {code: str}
|
|
- date_commande: str ou date
|
|
- date_livraison: str ou date (optionnel)
|
|
- reference: str (optionnel)
|
|
- lignes: list[dict]
|
|
"""
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
logger.info(
|
|
f" Début création commande pour client {commande_data['client']['code']}"
|
|
)
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
transaction_active = False
|
|
try:
|
|
self.cial.CptaApplication.BeginTrans()
|
|
transaction_active = True
|
|
logger.debug(" Transaction Sage démarrée")
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
process = self.cial.CreateProcess_Document(
|
|
settings.SAGE_TYPE_BON_COMMANDE
|
|
)
|
|
doc = process.Document
|
|
|
|
try:
|
|
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
|
|
except:
|
|
pass
|
|
|
|
logger.info(" Document commande créé")
|
|
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(commande_data.get("date_commande"))
|
|
)
|
|
|
|
if ("date_livraison" in commande_data and commande_data["date_livraison"]):
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(commande_data["date_livraison"])
|
|
)
|
|
logger.info(
|
|
f" Date livraison: {commande_data['date_livraison']}"
|
|
)
|
|
|
|
factory_client = self.cial.CptaApplication.FactoryClient
|
|
persist_client = factory_client.ReadNumero(
|
|
commande_data["client"]["code"]
|
|
)
|
|
|
|
if not persist_client:
|
|
raise ValueError(
|
|
f"Client {commande_data['client']['code']} introuvable"
|
|
)
|
|
|
|
client_obj = self._cast_client(persist_client)
|
|
if not client_obj:
|
|
raise ValueError(f"Impossible de charger le client")
|
|
|
|
doc.SetDefaultClient(client_obj)
|
|
doc.Write()
|
|
logger.info(f" Client {commande_data['client']['code']} associé")
|
|
|
|
if commande_data.get("reference"):
|
|
try:
|
|
doc.DO_Ref = commande_data["reference"]
|
|
logger.info(f" Référence: {commande_data['reference']}")
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
|
|
logger.info(f" Ajout de {len(commande_data['lignes'])} lignes...")
|
|
|
|
for idx, ligne_data in enumerate(commande_data["lignes"], 1):
|
|
logger.info(
|
|
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
|
|
)
|
|
|
|
persist_article = factory_article.ReadReference(
|
|
ligne_data["article_code"]
|
|
)
|
|
|
|
if not persist_article:
|
|
raise ValueError(
|
|
f" Article {ligne_data['article_code']} introuvable dans Sage"
|
|
)
|
|
|
|
article_obj = win32com.client.CastTo(
|
|
persist_article, "IBOArticle3"
|
|
)
|
|
article_obj.Read()
|
|
|
|
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
|
|
designation_sage = getattr(article_obj, "AR_Design", "")
|
|
logger.info(f" Prix Sage: {prix_sage}€")
|
|
|
|
if prix_sage == 0:
|
|
logger.warning(
|
|
f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)"
|
|
)
|
|
|
|
ligne_persist = factory_lignes.Create()
|
|
|
|
try:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentLigne3"
|
|
)
|
|
except:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentVenteLigne3"
|
|
)
|
|
|
|
quantite = float(ligne_data["quantite"])
|
|
|
|
try:
|
|
ligne_obj.SetDefaultArticleReference(
|
|
ligne_data["article_code"], quantite
|
|
)
|
|
logger.info(
|
|
f" Article associé via SetDefaultArticleReference"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"SetDefaultArticleReference échoué: {e}, tentative avec objet"
|
|
)
|
|
try:
|
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
|
logger.info(f" Article associé via SetDefaultArticle")
|
|
except Exception as e2:
|
|
logger.error(f" Toutes les méthodes ont échoué")
|
|
ligne_obj.DL_Design = (
|
|
designation_sage
|
|
or ligne_data.get("designation", "")
|
|
)
|
|
ligne_obj.DL_Qte = quantite
|
|
logger.warning("Configuration manuelle appliquée")
|
|
|
|
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
|
|
logger.info(f" Prix auto chargé: {prix_auto}€")
|
|
|
|
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
|
|
|
|
if prix_a_utiliser is not None and prix_a_utiliser > 0:
|
|
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
|
|
logger.info(f" Prix personnalisé: {prix_a_utiliser}€")
|
|
elif prix_auto == 0 and prix_sage > 0:
|
|
ligne_obj.DL_PrixUnitaire = float(prix_sage)
|
|
logger.info(f" Prix Sage forcé: {prix_sage}€")
|
|
elif prix_auto > 0:
|
|
logger.info(f" Prix auto conservé: {prix_auto}€")
|
|
|
|
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
|
|
montant_ligne = quantite * prix_final
|
|
logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€")
|
|
|
|
remise = ligne_data.get("remise_pourcentage", 0)
|
|
if remise > 0:
|
|
try:
|
|
ligne_obj.DL_Remise01REM_Valeur = float(remise)
|
|
ligne_obj.DL_Remise01REM_Type = 0
|
|
montant_apres_remise = montant_ligne * (
|
|
1 - remise / 100
|
|
)
|
|
logger.info(
|
|
f" Remise {remise}% → {montant_apres_remise}€"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Remise non appliquée: {e}")
|
|
|
|
ligne_obj.Write()
|
|
logger.info(f" Ligne {idx} écrite")
|
|
|
|
try:
|
|
ligne_obj.Read()
|
|
prix_enregistre = float(
|
|
getattr(ligne_obj, "DL_PrixUnitaire", 0.0)
|
|
)
|
|
montant_enregistre = float(
|
|
getattr(ligne_obj, "DL_MontantHT", 0.0)
|
|
)
|
|
logger.info(
|
|
f" Vérif: Prix={prix_enregistre}€, Montant HT={montant_enregistre}€"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Impossible de vérifier: {e}")
|
|
|
|
doc.Write()
|
|
process.Process()
|
|
|
|
if transaction_active:
|
|
self.cial.CptaApplication.CommitTrans()
|
|
|
|
time.sleep(2)
|
|
|
|
numero_commande = None
|
|
try:
|
|
doc_result = process.DocumentResult
|
|
if doc_result:
|
|
doc_result = win32com.client.CastTo(
|
|
doc_result, "IBODocumentVente3"
|
|
)
|
|
doc_result.Read()
|
|
numero_commande = getattr(doc_result, "DO_Piece", "")
|
|
except:
|
|
pass
|
|
|
|
if not numero_commande:
|
|
numero_commande = getattr(doc, "DO_Piece", "")
|
|
|
|
factory_doc = self.cial.FactoryDocumentVente
|
|
persist_reread = factory_doc.ReadPiece(
|
|
settings.SAGE_TYPE_BON_COMMANDE, numero_commande
|
|
)
|
|
|
|
if persist_reread:
|
|
doc_final = win32com.client.CastTo(
|
|
persist_reread, "IBODocumentVente3"
|
|
)
|
|
doc_final.Read()
|
|
|
|
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
|
|
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
|
|
reference_finale = getattr(doc_final, "DO_Ref", "")
|
|
|
|
date_livraison_final = None
|
|
|
|
try:
|
|
date_livr = getattr(doc_final, "DO_DateLivr", None)
|
|
if date_livr:
|
|
date_livraison_final = date_livr.strftime("%Y-%m-%d")
|
|
except:
|
|
pass
|
|
else:
|
|
total_ht = 0.0
|
|
total_ttc = 0.0
|
|
reference_finale = commande_data.get("reference", "")
|
|
date_livraison_final = commande_data.get("date_livraison")
|
|
|
|
logger.info(
|
|
f" COMMANDE CRÉÉE: {numero_commande} - {total_ttc}€ TTC "
|
|
)
|
|
if date_livraison_final:
|
|
logger.info(f" Date livraison: {date_livraison_final}")
|
|
|
|
return {
|
|
"numero_commande": numero_commande,
|
|
"total_ht": total_ht,
|
|
"total_ttc": total_ttc,
|
|
"nb_lignes": len(commande_data["lignes"]),
|
|
"client_code": commande_data["client"]["code"],
|
|
"date_commande": str(
|
|
self.normaliser_date(commande_data.get("date_commande"))
|
|
),
|
|
"date_livraison": date_livraison_final,
|
|
"reference": reference_finale,
|
|
}
|
|
|
|
except Exception as e:
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.RollbackTrans()
|
|
except:
|
|
pass
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur création commande: {e}", exc_info=True)
|
|
raise RuntimeError(f"Échec création commande: {str(e)}")
|
|
|
|
def modifier_commande(self, numero: str, commande_data: Dict) -> Dict:
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info(f" === MODIFICATION COMMANDE {numero} ===")
|
|
|
|
logger.info(" Chargement document...")
|
|
|
|
factory = self.cial.FactoryDocumentVente
|
|
persist = None
|
|
|
|
for type_test in [10, 3, settings.SAGE_TYPE_BON_COMMANDE]:
|
|
try:
|
|
persist_test = factory.ReadPiece(type_test, numero)
|
|
if persist_test:
|
|
persist = persist_test
|
|
logger.info(f" Document trouvé (type={type_test})")
|
|
break
|
|
except:
|
|
continue
|
|
|
|
if not persist:
|
|
raise ValueError(f" Commande {numero} INTROUVABLE")
|
|
|
|
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
|
|
doc.Read()
|
|
|
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
|
type_reel = getattr(doc, "DO_Type", -1)
|
|
|
|
logger.info(f" Type={type_reel}, Statut={statut_actuel}")
|
|
|
|
client_code_initial = ""
|
|
try:
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_code_initial = getattr(client_obj, "CT_Num", "").strip()
|
|
logger.info(f" Client initial: {client_code_initial}")
|
|
else:
|
|
logger.error(" Objet Client NULL à l'état initial !")
|
|
except Exception as e:
|
|
logger.error(f" Erreur lecture client initial: {e}")
|
|
|
|
if not client_code_initial:
|
|
raise ValueError(" Client introuvable dans le document")
|
|
|
|
nb_lignes_initial = 0
|
|
try:
|
|
factory_lignes = getattr(
|
|
doc, "FactoryDocumentLigne", None
|
|
) or getattr(doc, "FactoryDocumentVenteLigne", None)
|
|
index = 1
|
|
while index <= 100:
|
|
try:
|
|
ligne_p = factory_lignes.List(index)
|
|
if ligne_p is None:
|
|
break
|
|
nb_lignes_initial += 1
|
|
index += 1
|
|
except:
|
|
break
|
|
|
|
logger.info(f" Lignes initiales: {nb_lignes_initial}")
|
|
except Exception as e:
|
|
logger.warning(f" Erreur comptage lignes: {e}")
|
|
|
|
champs_modifies = []
|
|
|
|
modif_date = "date_commande" in commande_data
|
|
modif_date_livraison = "date_livraison" in commande_data
|
|
modif_statut = "statut" in commande_data
|
|
modif_ref = "reference" in commande_data
|
|
modif_lignes = (
|
|
"lignes" in commande_data and commande_data["lignes"] is not None
|
|
)
|
|
|
|
logger.info(f"Modifications demandées:")
|
|
logger.info(f" Date commande: {modif_date}")
|
|
logger.info(f" Date livraison: {modif_date_livraison}")
|
|
logger.info(f" Statut: {modif_statut}")
|
|
logger.info(f" Référence: {modif_ref}")
|
|
logger.info(f" Lignes: {modif_lignes}")
|
|
|
|
commande_data_temp = commande_data.copy()
|
|
reference_a_modifier = None
|
|
statut_a_modifier = None
|
|
|
|
if modif_lignes:
|
|
if modif_ref:
|
|
reference_a_modifier = commande_data_temp.pop("reference")
|
|
logger.info(
|
|
" Modification de la référence reportée après les lignes"
|
|
)
|
|
modif_ref = False
|
|
|
|
if modif_statut:
|
|
statut_a_modifier = commande_data_temp.pop("statut")
|
|
logger.info(
|
|
" Modification du statut reportée après les lignes"
|
|
)
|
|
modif_statut = False
|
|
|
|
logger.info(" Test Write() basique (sans modification)...")
|
|
|
|
try:
|
|
doc.Write()
|
|
logger.info(" Write() basique OK")
|
|
doc.Read()
|
|
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_apres = getattr(client_obj, "CT_Num", "")
|
|
if client_apres == client_code_initial:
|
|
logger.info(f" Client préservé: {client_apres}")
|
|
else:
|
|
logger.error(
|
|
f" Client a changé: {client_code_initial} → {client_apres}"
|
|
)
|
|
else:
|
|
logger.error(" Client devenu NULL après Write() basique")
|
|
|
|
except Exception as e:
|
|
logger.error(f" Write() basique ÉCHOUE: {e}")
|
|
logger.error(f" ABANDON: Le document est VERROUILLÉ")
|
|
raise ValueError(
|
|
f"Document verrouillé, impossible de modifier: {e}"
|
|
)
|
|
|
|
if not modif_lignes and (
|
|
modif_date
|
|
or modif_date_livraison
|
|
or modif_statut
|
|
or modif_ref
|
|
):
|
|
logger.info(" Modifications simples (sans lignes)...")
|
|
|
|
if modif_date:
|
|
logger.info(" Modification date commande...")
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(
|
|
commande_data_temp.get("date_commande")
|
|
)
|
|
)
|
|
champs_modifies.append("date")
|
|
|
|
if modif_date_livraison:
|
|
logger.info(" Modification date livraison...")
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(commande_data_temp["date_livraison"])
|
|
)
|
|
logger.info(
|
|
f" Date livraison: {commande_data_temp['date_livraison']}"
|
|
)
|
|
champs_modifies.append("date_livraison")
|
|
|
|
if modif_statut:
|
|
logger.info(" Modification statut...")
|
|
nouveau_statut = commande_data_temp["statut"]
|
|
doc.DO_Statut = nouveau_statut
|
|
champs_modifies.append("statut")
|
|
logger.info(f" Statut défini: {nouveau_statut}")
|
|
|
|
if modif_ref:
|
|
logger.info(" Modification référence...")
|
|
try:
|
|
doc.DO_Ref = commande_data_temp["reference"]
|
|
champs_modifies.append("reference")
|
|
logger.info(
|
|
f" Référence définie: {commande_data_temp['reference']}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f" Référence non définie: {e}")
|
|
|
|
logger.info(" Write() sans réassociation client...")
|
|
try:
|
|
doc.Write()
|
|
logger.info(" Write() réussi")
|
|
|
|
doc.Read()
|
|
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_apres = getattr(client_obj, "CT_Num", "")
|
|
if client_apres == client_code_initial:
|
|
logger.info(f" Client préservé: {client_apres}")
|
|
else:
|
|
logger.error(
|
|
f" Client perdu: {client_code_initial} → {client_apres}"
|
|
)
|
|
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_msg = f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
except:
|
|
pass
|
|
|
|
logger.error(f" Write() échoue: {error_msg}")
|
|
raise ValueError(f"Sage refuse: {error_msg}")
|
|
|
|
elif modif_lignes:
|
|
logger.info(" REMPLACEMENT COMPLET DES LIGNES...")
|
|
|
|
if modif_date:
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(
|
|
commande_data_temp.get("date_commande")
|
|
)
|
|
)
|
|
champs_modifies.append("date")
|
|
logger.info(" Date commande modifiée")
|
|
|
|
if modif_date_livraison:
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(commande_data_temp["date_livraison"])
|
|
)
|
|
logger.info(" Date livraison modifiée")
|
|
champs_modifies.append("date_livraison")
|
|
|
|
nouvelles_lignes = commande_data["lignes"]
|
|
nb_nouvelles = len(nouvelles_lignes)
|
|
|
|
logger.info(
|
|
f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes"
|
|
)
|
|
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
|
|
if nb_lignes_initial > 0:
|
|
logger.info(
|
|
f" Suppression de {nb_lignes_initial} lignes existantes..."
|
|
)
|
|
|
|
for idx in range(nb_lignes_initial, 0, -1):
|
|
try:
|
|
ligne_p = factory_lignes.List(idx)
|
|
if ligne_p:
|
|
ligne = win32com.client.CastTo(
|
|
ligne_p, "IBODocumentLigne3"
|
|
)
|
|
ligne.Read()
|
|
|
|
ligne.Remove()
|
|
logger.debug(f" Ligne {idx} supprimée")
|
|
except Exception as e:
|
|
logger.warning(
|
|
f" Impossible de supprimer ligne {idx}: {e}"
|
|
)
|
|
|
|
logger.info(" Toutes les lignes existantes supprimées")
|
|
|
|
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
|
|
|
|
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
|
|
logger.info(
|
|
f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---"
|
|
)
|
|
|
|
persist_article = factory_article.ReadReference(
|
|
ligne_data["article_code"]
|
|
)
|
|
if not persist_article:
|
|
raise ValueError(
|
|
f"Article {ligne_data['article_code']} introuvable"
|
|
)
|
|
|
|
article_obj = win32com.client.CastTo(
|
|
persist_article, "IBOArticle3"
|
|
)
|
|
article_obj.Read()
|
|
|
|
ligne_persist = factory_lignes.Create()
|
|
|
|
try:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentLigne3"
|
|
)
|
|
except:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentVenteLigne3"
|
|
)
|
|
|
|
quantite = float(ligne_data["quantite"])
|
|
|
|
try:
|
|
ligne_obj.SetDefaultArticleReference(
|
|
ligne_data["article_code"], quantite
|
|
)
|
|
except:
|
|
try:
|
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
|
except:
|
|
ligne_obj.DL_Design = ligne_data.get("designation", "")
|
|
ligne_obj.DL_Qte = quantite
|
|
|
|
if ligne_data.get("prix_unitaire_ht"):
|
|
ligne_obj.DL_PrixUnitaire = float(
|
|
ligne_data["prix_unitaire_ht"]
|
|
)
|
|
|
|
if ligne_data.get("remise_pourcentage", 0) > 0:
|
|
try:
|
|
ligne_obj.DL_Remise01REM_Valeur = float(
|
|
ligne_data["remise_pourcentage"]
|
|
)
|
|
ligne_obj.DL_Remise01REM_Type = 0
|
|
except:
|
|
pass
|
|
|
|
ligne_obj.Write()
|
|
logger.info(f" Ligne {idx} ajoutée")
|
|
|
|
logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées")
|
|
|
|
logger.info(" Write() document après remplacement lignes...")
|
|
doc.Write()
|
|
logger.info(" Document écrit")
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_apres = getattr(client_obj, "CT_Num", "")
|
|
logger.info(f" Client après remplacement: {client_apres}")
|
|
else:
|
|
logger.error(" Client NULL après remplacement")
|
|
|
|
champs_modifies.append("lignes")
|
|
|
|
if reference_a_modifier is not None:
|
|
try:
|
|
ancienne_reference = getattr(doc, "DO_Ref", "")
|
|
nouvelle_reference = (
|
|
str(reference_a_modifier) if reference_a_modifier else ""
|
|
)
|
|
|
|
doc.DO_Ref = nouvelle_reference
|
|
doc.Write()
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
champs_modifies.append("reference")
|
|
logger.info(
|
|
f" Référence modifiée: '{ancienne_reference}' → '{nouvelle_reference}'"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Impossible de modifier la référence: {e}")
|
|
|
|
if statut_a_modifier is not None:
|
|
try:
|
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
|
nouveau_statut = int(statut_a_modifier)
|
|
|
|
if nouveau_statut != statut_actuel:
|
|
doc.DO_Statut = nouveau_statut
|
|
doc.Write()
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
champs_modifies.append("statut")
|
|
logger.info(
|
|
f" Statut modifié: {statut_actuel} → {nouveau_statut}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Impossible de modifier le statut: {e}")
|
|
|
|
logger.info(" Relecture finale...")
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
client_obj_final = getattr(doc, "Client", None)
|
|
if client_obj_final:
|
|
client_obj_final.Read()
|
|
client_final = getattr(client_obj_final, "CT_Num", "")
|
|
else:
|
|
client_final = ""
|
|
|
|
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
|
|
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
|
|
reference_finale = getattr(doc, "DO_Ref", "")
|
|
|
|
date_livraison_final = None
|
|
|
|
try:
|
|
date_livr = getattr(doc, "DO_DateLivr", None)
|
|
if date_livr:
|
|
date_livraison_final = date_livr.strftime("%Y-%m-%d")
|
|
except:
|
|
pass
|
|
|
|
logger.info(f" SUCCÈS: {numero} modifiée ")
|
|
logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
|
|
logger.info(f" Client final: {client_final}")
|
|
logger.info(f" Référence: {reference_finale}")
|
|
if date_livraison_final:
|
|
logger.info(f" Date livraison: {date_livraison_final}")
|
|
logger.info(f" Champs modifiés: {champs_modifies}")
|
|
|
|
return {
|
|
"numero": numero,
|
|
"total_ht": total_ht,
|
|
"total_ttc": total_ttc,
|
|
"reference": reference_finale,
|
|
"date_livraison": date_livraison_final,
|
|
"champs_modifies": champs_modifies,
|
|
"statut": getattr(doc, "DO_Statut", 0),
|
|
"client_code": client_final,
|
|
}
|
|
|
|
except ValueError as e:
|
|
logger.error(f" ERREUR MÉTIER: {e}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True)
|
|
|
|
error_message = str(e)
|
|
if self.cial:
|
|
try:
|
|
err = self.cial.CptaApplication.LastError
|
|
if err:
|
|
error_message = (
|
|
f"Erreur Sage: {err.Description} (Code: {err.Number})"
|
|
)
|
|
except:
|
|
pass
|
|
|
|
raise RuntimeError(f"Erreur Sage: {error_message}")
|
|
|
|
def creer_livraison_enrichi(self, livraison_data: dict) -> Dict:
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
logger.info(
|
|
f" Début création livraison pour client {livraison_data['client']['code']}"
|
|
)
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
transaction_active = False
|
|
try:
|
|
self.cial.CptaApplication.BeginTrans()
|
|
transaction_active = True
|
|
logger.debug(" Transaction Sage démarrée")
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
process = self.cial.CreateProcess_Document(
|
|
settings.SAGE_TYPE_BON_LIVRAISON
|
|
)
|
|
doc = process.Document
|
|
|
|
try:
|
|
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
|
|
except:
|
|
pass
|
|
|
|
logger.info(" Document livraison créé")
|
|
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(livraison_data.get("date_livraison"))
|
|
)
|
|
|
|
if (
|
|
"date_livraison_prevue" in livraison_data
|
|
and livraison_data["date_livraison_prevue"]
|
|
):
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(
|
|
livraison_data["date_livraison_prevue"]
|
|
)
|
|
)
|
|
logger.info(
|
|
f" Date livraison prévue: {livraison_data['date_livraison_prevue']}"
|
|
)
|
|
|
|
factory_client = self.cial.CptaApplication.FactoryClient
|
|
persist_client = factory_client.ReadNumero(
|
|
livraison_data["client"]["code"]
|
|
)
|
|
|
|
if not persist_client:
|
|
raise ValueError(
|
|
f"Client {livraison_data['client']['code']} introuvable"
|
|
)
|
|
|
|
client_obj = self._cast_client(persist_client)
|
|
if not client_obj:
|
|
raise ValueError(f"Impossible de charger le client")
|
|
|
|
doc.SetDefaultClient(client_obj)
|
|
doc.Write()
|
|
logger.info(f" Client {livraison_data['client']['code']} associé")
|
|
|
|
if livraison_data.get("reference"):
|
|
try:
|
|
doc.DO_Ref = livraison_data["reference"]
|
|
logger.info(f" Référence: {livraison_data['reference']}")
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
|
|
logger.info(
|
|
f" Ajout de {len(livraison_data['lignes'])} lignes..."
|
|
)
|
|
|
|
for idx, ligne_data in enumerate(livraison_data["lignes"], 1):
|
|
logger.info(
|
|
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
|
|
)
|
|
|
|
persist_article = factory_article.ReadReference(
|
|
ligne_data["article_code"]
|
|
)
|
|
|
|
if not persist_article:
|
|
raise ValueError(
|
|
f" Article {ligne_data['article_code']} introuvable dans Sage"
|
|
)
|
|
|
|
article_obj = win32com.client.CastTo(
|
|
persist_article, "IBOArticle3"
|
|
)
|
|
article_obj.Read()
|
|
|
|
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
|
|
designation_sage = getattr(article_obj, "AR_Design", "")
|
|
logger.info(f" Prix Sage: {prix_sage}€")
|
|
|
|
if prix_sage == 0:
|
|
logger.warning(
|
|
f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)"
|
|
)
|
|
|
|
ligne_persist = factory_lignes.Create()
|
|
|
|
try:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentLigne3"
|
|
)
|
|
except:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentVenteLigne3"
|
|
)
|
|
|
|
quantite = float(ligne_data["quantite"])
|
|
|
|
try:
|
|
ligne_obj.SetDefaultArticleReference(
|
|
ligne_data["article_code"], quantite
|
|
)
|
|
logger.info(
|
|
f" Article associé via SetDefaultArticleReference"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"SetDefaultArticleReference échoué: {e}, tentative avec objet"
|
|
)
|
|
try:
|
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
|
logger.info(f" Article associé via SetDefaultArticle")
|
|
except Exception as e2:
|
|
logger.error(f" Toutes les méthodes ont échoué")
|
|
ligne_obj.DL_Design = (
|
|
designation_sage
|
|
or ligne_data.get("designation", "")
|
|
)
|
|
ligne_obj.DL_Qte = quantite
|
|
logger.warning("Configuration manuelle appliquée")
|
|
|
|
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
|
|
logger.info(f" Prix auto chargé: {prix_auto}€")
|
|
|
|
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
|
|
|
|
if prix_a_utiliser is not None and prix_a_utiliser > 0:
|
|
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
|
|
logger.info(f" Prix personnalisé: {prix_a_utiliser}€")
|
|
elif prix_auto == 0 and prix_sage > 0:
|
|
ligne_obj.DL_PrixUnitaire = float(prix_sage)
|
|
logger.info(f" Prix Sage forcé: {prix_sage}€")
|
|
elif prix_auto > 0:
|
|
logger.info(f" Prix auto conservé: {prix_auto}€")
|
|
|
|
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
|
|
montant_ligne = quantite * prix_final
|
|
logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€")
|
|
|
|
remise = ligne_data.get("remise_pourcentage", 0)
|
|
if remise > 0:
|
|
try:
|
|
ligne_obj.DL_Remise01REM_Valeur = float(remise)
|
|
ligne_obj.DL_Remise01REM_Type = 0
|
|
montant_apres_remise = montant_ligne * (
|
|
1 - remise / 100
|
|
)
|
|
logger.info(
|
|
f" Remise {remise}% → {montant_apres_remise}€"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Remise non appliquée: {e}")
|
|
|
|
ligne_obj.Write()
|
|
logger.info(f" Ligne {idx} écrite")
|
|
|
|
doc.Write()
|
|
process.Process()
|
|
|
|
if transaction_active:
|
|
self.cial.CptaApplication.CommitTrans()
|
|
|
|
time.sleep(2)
|
|
|
|
numero_livraison = None
|
|
try:
|
|
doc_result = process.DocumentResult
|
|
if doc_result:
|
|
doc_result = win32com.client.CastTo(
|
|
doc_result, "IBODocumentVente3"
|
|
)
|
|
doc_result.Read()
|
|
numero_livraison = getattr(doc_result, "DO_Piece", "")
|
|
except:
|
|
pass
|
|
|
|
if not numero_livraison:
|
|
numero_livraison = getattr(doc, "DO_Piece", "")
|
|
|
|
factory_doc = self.cial.FactoryDocumentVente
|
|
persist_reread = factory_doc.ReadPiece(
|
|
settings.SAGE_TYPE_BON_LIVRAISON, numero_livraison
|
|
)
|
|
|
|
if persist_reread:
|
|
doc_final = win32com.client.CastTo(
|
|
persist_reread, "IBODocumentVente3"
|
|
)
|
|
doc_final.Read()
|
|
|
|
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
|
|
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
|
|
reference_finale = getattr(doc_final, "DO_Ref", "")
|
|
|
|
date_livraison_prevue_final = None
|
|
|
|
try:
|
|
date_livr = getattr(doc_final, "DO_DateLivr", None)
|
|
if date_livr:
|
|
date_livraison_prevue_final = date_livr.strftime(
|
|
"%Y-%m-%d"
|
|
)
|
|
except:
|
|
pass
|
|
else:
|
|
total_ht = 0.0
|
|
total_ttc = 0.0
|
|
reference_finale = livraison_data.get("reference", "")
|
|
date_livraison_prevue_final = livraison_data.get(
|
|
"date_livraison_prevue"
|
|
)
|
|
|
|
logger.info(
|
|
f" LIVRAISON CRÉÉE: {numero_livraison} - {total_ttc}€ TTC "
|
|
)
|
|
if date_livraison_prevue_final:
|
|
logger.info(
|
|
f" Date livraison prévue: {date_livraison_prevue_final}"
|
|
)
|
|
|
|
return {
|
|
"numero_livraison": numero_livraison,
|
|
"total_ht": total_ht,
|
|
"total_ttc": total_ttc,
|
|
"nb_lignes": len(livraison_data["lignes"]),
|
|
"client_code": livraison_data["client"]["code"],
|
|
"date_livraison": str(
|
|
self.normaliser_date(livraison_data.get("date_livraison"))
|
|
),
|
|
"date_livraison_prevue": date_livraison_prevue_final,
|
|
"reference": reference_finale,
|
|
}
|
|
|
|
except Exception as e:
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.RollbackTrans()
|
|
except:
|
|
pass
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur création livraison: {e}", exc_info=True)
|
|
raise RuntimeError(f"Échec création livraison: {str(e)}")
|
|
|
|
def modifier_livraison(self, numero: str, livraison_data: Dict) -> Dict:
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info(f" === MODIFICATION LIVRAISON {numero} ===")
|
|
|
|
logger.info(" Chargement document...")
|
|
|
|
factory = self.cial.FactoryDocumentVente
|
|
persist = None
|
|
|
|
for type_test in [30, settings.SAGE_TYPE_BON_LIVRAISON]:
|
|
try:
|
|
persist_test = factory.ReadPiece(type_test, numero)
|
|
if persist_test:
|
|
persist = persist_test
|
|
logger.info(f" Document trouvé (type={type_test})")
|
|
break
|
|
except:
|
|
continue
|
|
|
|
if not persist:
|
|
raise ValueError(f" Livraison {numero} INTROUVABLE")
|
|
|
|
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
|
|
doc.Read()
|
|
|
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
|
|
|
logger.info(f" Statut={statut_actuel}")
|
|
|
|
if statut_actuel == 5:
|
|
raise ValueError(f"La livraison {numero} a déjà été transformée")
|
|
|
|
if statut_actuel == 6:
|
|
raise ValueError(f"La livraison {numero} est annulée")
|
|
|
|
nb_lignes_initial = 0
|
|
try:
|
|
factory_lignes = getattr(
|
|
doc, "FactoryDocumentLigne", None
|
|
) or getattr(doc, "FactoryDocumentVenteLigne", None)
|
|
index = 1
|
|
while index <= 100:
|
|
try:
|
|
ligne_p = factory_lignes.List(index)
|
|
if ligne_p is None:
|
|
break
|
|
nb_lignes_initial += 1
|
|
index += 1
|
|
except:
|
|
break
|
|
|
|
logger.info(f" Lignes initiales: {nb_lignes_initial}")
|
|
except Exception as e:
|
|
logger.warning(f" Erreur comptage lignes: {e}")
|
|
|
|
champs_modifies = []
|
|
|
|
modif_date = "date_livraison" in livraison_data
|
|
modif_date_livraison_prevue = "date_livraison_prevue" in livraison_data
|
|
modif_statut = "statut" in livraison_data
|
|
modif_ref = "reference" in livraison_data
|
|
modif_lignes = (
|
|
"lignes" in livraison_data and livraison_data["lignes"] is not None
|
|
)
|
|
|
|
logger.info(f"Modifications demandées:")
|
|
logger.info(f" Date livraison: {modif_date}")
|
|
logger.info(f" Date livraison prévue: {modif_date_livraison_prevue}")
|
|
logger.info(f" Statut: {modif_statut}")
|
|
logger.info(f" Référence: {modif_ref}")
|
|
logger.info(f" Lignes: {modif_lignes}")
|
|
|
|
livraison_data_temp = livraison_data.copy()
|
|
reference_a_modifier = None
|
|
statut_a_modifier = None
|
|
|
|
if modif_lignes:
|
|
if modif_ref:
|
|
reference_a_modifier = livraison_data_temp.pop("reference")
|
|
logger.info(
|
|
" Modification de la référence reportée après les lignes"
|
|
)
|
|
modif_ref = False
|
|
|
|
if modif_statut:
|
|
statut_a_modifier = livraison_data_temp.pop("statut")
|
|
logger.info(
|
|
" Modification du statut reportée après les lignes"
|
|
)
|
|
modif_statut = False
|
|
|
|
if not modif_lignes and (
|
|
modif_date
|
|
or modif_date_livraison_prevue
|
|
or modif_statut
|
|
or modif_ref
|
|
):
|
|
logger.info(" Modifications simples (sans lignes)...")
|
|
|
|
if modif_date:
|
|
logger.info(" Modification date livraison...")
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(
|
|
livraison_data_temp.get("date_livraison")
|
|
)
|
|
)
|
|
champs_modifies.append("date")
|
|
|
|
if modif_date_livraison_prevue:
|
|
logger.info(" Modification date livraison prévue...")
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(
|
|
livraison_data_temp["date_livraison_prevue"]
|
|
)
|
|
)
|
|
logger.info(
|
|
f" Date livraison prévue: {livraison_data_temp['date_livraison_prevue']}"
|
|
)
|
|
champs_modifies.append("date_livraison_prevue")
|
|
|
|
if modif_statut:
|
|
logger.info(" Modification statut...")
|
|
nouveau_statut = livraison_data_temp["statut"]
|
|
doc.DO_Statut = nouveau_statut
|
|
champs_modifies.append("statut")
|
|
logger.info(f" Statut défini: {nouveau_statut}")
|
|
|
|
if modif_ref:
|
|
logger.info(" Modification référence...")
|
|
try:
|
|
doc.DO_Ref = livraison_data_temp["reference"]
|
|
champs_modifies.append("reference")
|
|
logger.info(
|
|
f" Référence définie: {livraison_data_temp['reference']}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f" Référence non définie: {e}")
|
|
|
|
logger.info(" Write()...")
|
|
doc.Write()
|
|
logger.info(" Write() réussi")
|
|
|
|
elif modif_lignes:
|
|
logger.info(" REMPLACEMENT COMPLET DES LIGNES...")
|
|
|
|
if modif_date:
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(
|
|
livraison_data_temp.get("date_livraison")
|
|
)
|
|
)
|
|
champs_modifies.append("date")
|
|
logger.info(" Date livraison modifiée")
|
|
|
|
if modif_date_livraison_prevue:
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(
|
|
livraison_data_temp["date_livraison_prevue"]
|
|
)
|
|
)
|
|
logger.info(" Date livraison prévue modifiée")
|
|
champs_modifies.append("date_livraison_prevue")
|
|
|
|
nouvelles_lignes = livraison_data["lignes"]
|
|
nb_nouvelles = len(nouvelles_lignes)
|
|
|
|
logger.info(
|
|
f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles"
|
|
)
|
|
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
|
|
if nb_lignes_initial > 0:
|
|
logger.info(f" Suppression de {nb_lignes_initial} lignes...")
|
|
|
|
for idx in range(nb_lignes_initial, 0, -1):
|
|
try:
|
|
ligne_p = factory_lignes.List(idx)
|
|
if ligne_p:
|
|
ligne = win32com.client.CastTo(
|
|
ligne_p, "IBODocumentLigne3"
|
|
)
|
|
ligne.Read()
|
|
ligne.Remove()
|
|
logger.debug(f" Ligne {idx} supprimée")
|
|
except Exception as e:
|
|
logger.warning(
|
|
f" Erreur suppression ligne {idx}: {e}"
|
|
)
|
|
|
|
logger.info(" Toutes les lignes supprimées")
|
|
|
|
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
|
|
|
|
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
|
|
logger.info(
|
|
f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---"
|
|
)
|
|
|
|
persist_article = factory_article.ReadReference(
|
|
ligne_data["article_code"]
|
|
)
|
|
if not persist_article:
|
|
raise ValueError(
|
|
f"Article {ligne_data['article_code']} introuvable"
|
|
)
|
|
|
|
article_obj = win32com.client.CastTo(
|
|
persist_article, "IBOArticle3"
|
|
)
|
|
article_obj.Read()
|
|
|
|
ligne_persist = factory_lignes.Create()
|
|
|
|
try:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentLigne3"
|
|
)
|
|
except:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentVenteLigne3"
|
|
)
|
|
|
|
quantite = float(ligne_data["quantite"])
|
|
|
|
try:
|
|
ligne_obj.SetDefaultArticleReference(
|
|
ligne_data["article_code"], quantite
|
|
)
|
|
except:
|
|
try:
|
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
|
except:
|
|
ligne_obj.DL_Design = ligne_data.get("designation", "")
|
|
ligne_obj.DL_Qte = quantite
|
|
|
|
if ligne_data.get("prix_unitaire_ht"):
|
|
ligne_obj.DL_PrixUnitaire = float(
|
|
ligne_data["prix_unitaire_ht"]
|
|
)
|
|
|
|
if ligne_data.get("remise_pourcentage", 0) > 0:
|
|
try:
|
|
ligne_obj.DL_Remise01REM_Valeur = float(
|
|
ligne_data["remise_pourcentage"]
|
|
)
|
|
ligne_obj.DL_Remise01REM_Type = 0
|
|
except:
|
|
pass
|
|
|
|
ligne_obj.Write()
|
|
logger.info(f" Ligne {idx} ajoutée")
|
|
|
|
logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées")
|
|
|
|
logger.info(" Write() document après remplacement lignes...")
|
|
doc.Write()
|
|
logger.info(" Document écrit")
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
champs_modifies.append("lignes")
|
|
|
|
if reference_a_modifier is not None:
|
|
try:
|
|
ancienne_reference = getattr(doc, "DO_Ref", "")
|
|
nouvelle_reference = (
|
|
str(reference_a_modifier) if reference_a_modifier else ""
|
|
)
|
|
|
|
logger.info(
|
|
f" Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'"
|
|
)
|
|
|
|
doc.DO_Ref = nouvelle_reference
|
|
doc.Write()
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
champs_modifies.append("reference")
|
|
logger.info(f" Référence modifiée avec succès")
|
|
except Exception as e:
|
|
logger.warning(f"Impossible de modifier la référence: {e}")
|
|
|
|
if statut_a_modifier is not None:
|
|
try:
|
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
|
nouveau_statut = int(statut_a_modifier)
|
|
|
|
if nouveau_statut != statut_actuel:
|
|
logger.info(
|
|
f" Modification statut: {statut_actuel} → {nouveau_statut}"
|
|
)
|
|
|
|
doc.DO_Statut = nouveau_statut
|
|
doc.Write()
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
champs_modifies.append("statut")
|
|
logger.info(f" Statut modifié avec succès")
|
|
except Exception as e:
|
|
logger.warning(f"Impossible de modifier le statut: {e}")
|
|
|
|
logger.info(" Relecture finale...")
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
|
|
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
|
|
reference_finale = getattr(doc, "DO_Ref", "")
|
|
statut_final = getattr(doc, "DO_Statut", 0)
|
|
|
|
date_livraison_prevue_final = None
|
|
|
|
try:
|
|
date_livr = getattr(doc, "DO_DateLivr", None)
|
|
if date_livr:
|
|
date_livraison_prevue_final = date_livr.strftime("%Y-%m-%d")
|
|
except:
|
|
pass
|
|
|
|
logger.info(f" SUCCÈS: {numero} modifiée ")
|
|
logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
|
|
logger.info(f" Référence: {reference_finale}")
|
|
logger.info(f" Statut: {statut_final}")
|
|
|
|
if date_livraison_prevue_final:
|
|
logger.info(
|
|
f" Date livraison prévue: {date_livraison_prevue_final}"
|
|
)
|
|
logger.info(f" Champs modifiés: {champs_modifies}")
|
|
|
|
return {
|
|
"numero": numero,
|
|
"total_ht": total_ht,
|
|
"total_ttc": total_ttc,
|
|
"reference": reference_finale,
|
|
"date_livraison_prevue": date_livraison_prevue_final,
|
|
"champs_modifies": champs_modifies,
|
|
"statut": statut_final,
|
|
}
|
|
|
|
except ValueError as e:
|
|
logger.error(f" ERREUR MÉTIER: {e}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True)
|
|
|
|
error_message = str(e)
|
|
if self.cial:
|
|
try:
|
|
err = self.cial.CptaApplication.LastError
|
|
if err:
|
|
error_message = (
|
|
f"Erreur Sage: {err.Description} (Code: {err.Number})"
|
|
)
|
|
except:
|
|
pass
|
|
|
|
raise RuntimeError(f"Erreur Sage: {error_message}")
|
|
|
|
def creer_avoir_enrichi(self, avoir_data: dict) -> Dict:
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
logger.info(
|
|
f" Début création avoir pour client {avoir_data['client']['code']}"
|
|
)
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
transaction_active = False
|
|
try:
|
|
self.cial.CptaApplication.BeginTrans()
|
|
transaction_active = True
|
|
logger.debug(" Transaction Sage démarrée")
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
process = self.cial.CreateProcess_Document(
|
|
settings.SAGE_TYPE_BON_AVOIR
|
|
)
|
|
doc = process.Document
|
|
|
|
try:
|
|
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
|
|
except:
|
|
pass
|
|
|
|
logger.info(" Document avoir créé")
|
|
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(avoir_data.get("date_avoir"))
|
|
)
|
|
|
|
if "date_livraison" in avoir_data and avoir_data["date_livraison"]:
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(avoir_data["date_livraison"])
|
|
)
|
|
logger.info(
|
|
f" Date livraison: {avoir_data['date_livraison']}"
|
|
)
|
|
|
|
factory_client = self.cial.CptaApplication.FactoryClient
|
|
persist_client = factory_client.ReadNumero(
|
|
avoir_data["client"]["code"]
|
|
)
|
|
|
|
if not persist_client:
|
|
raise ValueError(
|
|
f"Client {avoir_data['client']['code']} introuvable"
|
|
)
|
|
|
|
client_obj = self._cast_client(persist_client)
|
|
if not client_obj:
|
|
raise ValueError(f"Impossible de charger le client")
|
|
|
|
doc.SetDefaultClient(client_obj)
|
|
doc.Write()
|
|
logger.info(f" Client {avoir_data['client']['code']} associé")
|
|
|
|
if avoir_data.get("reference"):
|
|
try:
|
|
doc.DO_Ref = avoir_data["reference"]
|
|
logger.info(f" Référence: {avoir_data['reference']}")
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
|
|
logger.info(f" Ajout de {len(avoir_data['lignes'])} lignes...")
|
|
|
|
for idx, ligne_data in enumerate(avoir_data["lignes"], 1):
|
|
logger.info(
|
|
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
|
|
)
|
|
|
|
persist_article = factory_article.ReadReference(
|
|
ligne_data["article_code"]
|
|
)
|
|
|
|
if not persist_article:
|
|
raise ValueError(
|
|
f" Article {ligne_data['article_code']} introuvable dans Sage"
|
|
)
|
|
|
|
article_obj = win32com.client.CastTo(
|
|
persist_article, "IBOArticle3"
|
|
)
|
|
article_obj.Read()
|
|
|
|
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
|
|
designation_sage = getattr(article_obj, "AR_Design", "")
|
|
logger.info(f" Prix Sage: {prix_sage}€")
|
|
|
|
if prix_sage == 0:
|
|
logger.warning(
|
|
f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)"
|
|
)
|
|
|
|
ligne_persist = factory_lignes.Create()
|
|
|
|
try:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentLigne3"
|
|
)
|
|
except:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentVenteLigne3"
|
|
)
|
|
|
|
quantite = float(ligne_data["quantite"])
|
|
|
|
try:
|
|
ligne_obj.SetDefaultArticleReference(
|
|
ligne_data["article_code"], quantite
|
|
)
|
|
logger.info(
|
|
f" Article associé via SetDefaultArticleReference"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"SetDefaultArticleReference échoué: {e}, tentative avec objet"
|
|
)
|
|
try:
|
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
|
logger.info(f" Article associé via SetDefaultArticle")
|
|
except Exception as e2:
|
|
logger.error(f" Toutes les méthodes ont échoué")
|
|
ligne_obj.DL_Design = (
|
|
designation_sage
|
|
or ligne_data.get("designation", "")
|
|
)
|
|
ligne_obj.DL_Qte = quantite
|
|
logger.warning("Configuration manuelle appliquée")
|
|
|
|
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
|
|
logger.info(f" Prix auto chargé: {prix_auto}€")
|
|
|
|
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
|
|
|
|
if prix_a_utiliser is not None and prix_a_utiliser > 0:
|
|
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
|
|
logger.info(f" Prix personnalisé: {prix_a_utiliser}€")
|
|
elif prix_auto == 0 and prix_sage > 0:
|
|
ligne_obj.DL_PrixUnitaire = float(prix_sage)
|
|
logger.info(f" Prix Sage forcé: {prix_sage}€")
|
|
elif prix_auto > 0:
|
|
logger.info(f" Prix auto conservé: {prix_auto}€")
|
|
|
|
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
|
|
montant_ligne = quantite * prix_final
|
|
logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€")
|
|
|
|
remise = ligne_data.get("remise_pourcentage", 0)
|
|
if remise > 0:
|
|
try:
|
|
ligne_obj.DL_Remise01REM_Valeur = float(remise)
|
|
ligne_obj.DL_Remise01REM_Type = 0
|
|
montant_apres_remise = montant_ligne * (
|
|
1 - remise / 100
|
|
)
|
|
logger.info(
|
|
f" Remise {remise}% → {montant_apres_remise}€"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Remise non appliquée: {e}")
|
|
|
|
ligne_obj.Write()
|
|
logger.info(f" Ligne {idx} écrite")
|
|
|
|
doc.Write()
|
|
process.Process()
|
|
|
|
if transaction_active:
|
|
self.cial.CptaApplication.CommitTrans()
|
|
|
|
time.sleep(2)
|
|
|
|
numero_avoir = None
|
|
try:
|
|
doc_result = process.DocumentResult
|
|
if doc_result:
|
|
doc_result = win32com.client.CastTo(
|
|
doc_result, "IBODocumentVente3"
|
|
)
|
|
doc_result.Read()
|
|
numero_avoir = getattr(doc_result, "DO_Piece", "")
|
|
except:
|
|
pass
|
|
|
|
if not numero_avoir:
|
|
numero_avoir = getattr(doc, "DO_Piece", "")
|
|
|
|
factory_doc = self.cial.FactoryDocumentVente
|
|
persist_reread = factory_doc.ReadPiece(
|
|
settings.SAGE_TYPE_BON_AVOIR, numero_avoir
|
|
)
|
|
|
|
if persist_reread:
|
|
doc_final = win32com.client.CastTo(
|
|
persist_reread, "IBODocumentVente3"
|
|
)
|
|
doc_final.Read()
|
|
|
|
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
|
|
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
|
|
reference_finale = getattr(doc_final, "DO_Ref", "")
|
|
|
|
date_livraison_final = None
|
|
|
|
try:
|
|
date_livr = getattr(doc_final, "DO_DateLivr", None)
|
|
if date_livr:
|
|
date_livraison_final = date_livr.strftime("%Y-%m-%d")
|
|
except:
|
|
pass
|
|
else:
|
|
total_ht = 0.0
|
|
total_ttc = 0.0
|
|
reference_finale = avoir_data.get("reference", "")
|
|
date_livraison_final = avoir_data.get("date_livraison")
|
|
|
|
logger.info(
|
|
f" AVOIR CRÉÉ: {numero_avoir} - {total_ttc}€ TTC "
|
|
)
|
|
|
|
if date_livraison_final:
|
|
logger.info(f" Date livraison: {date_livraison_final}")
|
|
|
|
return {
|
|
"numero_avoir": numero_avoir,
|
|
"total_ht": total_ht,
|
|
"total_ttc": total_ttc,
|
|
"nb_lignes": len(avoir_data["lignes"]),
|
|
"client_code": avoir_data["client"]["code"],
|
|
"date_avoir": str(
|
|
self.normaliser_date(avoir_data.get("date_avoir"))
|
|
),
|
|
"date_livraison": date_livraison_final,
|
|
"reference": reference_finale,
|
|
}
|
|
|
|
except Exception as e:
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.RollbackTrans()
|
|
except:
|
|
pass
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur création avoir: {e}", exc_info=True)
|
|
raise RuntimeError(f"Échec création avoir: {str(e)}")
|
|
|
|
def modifier_avoir(self, numero: str, avoir_data: Dict) -> Dict:
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info(f" === MODIFICATION AVOIR {numero} ===")
|
|
|
|
logger.info(" Chargement document...")
|
|
|
|
factory = self.cial.FactoryDocumentVente
|
|
persist = None
|
|
|
|
for type_test in [50, settings.SAGE_TYPE_BON_AVOIR]:
|
|
try:
|
|
persist_test = factory.ReadPiece(type_test, numero)
|
|
if persist_test:
|
|
persist = persist_test
|
|
logger.info(f" Document trouvé (type={type_test})")
|
|
break
|
|
except:
|
|
continue
|
|
|
|
if not persist:
|
|
raise ValueError(f" Avoir {numero} INTROUVABLE")
|
|
|
|
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
|
|
doc.Read()
|
|
|
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
|
type_reel = getattr(doc, "DO_Type", -1)
|
|
|
|
logger.info(f" Type={type_reel}, Statut={statut_actuel}")
|
|
|
|
if statut_actuel == 5:
|
|
raise ValueError(f"L'avoir {numero} a déjà été transformé")
|
|
|
|
if statut_actuel == 6:
|
|
raise ValueError(f"L'avoir {numero} est annulé")
|
|
|
|
client_code_initial = ""
|
|
try:
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_code_initial = getattr(client_obj, "CT_Num", "").strip()
|
|
logger.info(f" Client initial: {client_code_initial}")
|
|
else:
|
|
logger.error(" Objet Client NULL à l'état initial !")
|
|
except Exception as e:
|
|
logger.error(f" Erreur lecture client initial: {e}")
|
|
|
|
if not client_code_initial:
|
|
raise ValueError(" Client introuvable dans le document")
|
|
|
|
nb_lignes_initial = 0
|
|
try:
|
|
factory_lignes = getattr(
|
|
doc, "FactoryDocumentLigne", None
|
|
) or getattr(doc, "FactoryDocumentVenteLigne", None)
|
|
index = 1
|
|
while index <= 100:
|
|
try:
|
|
ligne_p = factory_lignes.List(index)
|
|
if ligne_p is None:
|
|
break
|
|
nb_lignes_initial += 1
|
|
index += 1
|
|
except:
|
|
break
|
|
|
|
logger.info(f" Lignes initiales: {nb_lignes_initial}")
|
|
except Exception as e:
|
|
logger.warning(f" Erreur comptage lignes: {e}")
|
|
|
|
champs_modifies = []
|
|
|
|
modif_date = "date_avoir" in avoir_data
|
|
modif_date_livraison = "date_livraison" in avoir_data
|
|
modif_statut = "statut" in avoir_data
|
|
modif_ref = "reference" in avoir_data
|
|
modif_lignes = (
|
|
"lignes" in avoir_data and avoir_data["lignes"] is not None
|
|
)
|
|
|
|
logger.info(f"Modifications demandées:")
|
|
logger.info(f" Date avoir: {modif_date}")
|
|
logger.info(f" Date livraison: {modif_date_livraison}")
|
|
logger.info(f" Statut: {modif_statut}")
|
|
logger.info(f" Référence: {modif_ref}")
|
|
logger.info(f" Lignes: {modif_lignes}")
|
|
|
|
avoir_data_temp = avoir_data.copy()
|
|
reference_a_modifier = None
|
|
statut_a_modifier = None
|
|
|
|
if modif_lignes:
|
|
if modif_ref:
|
|
reference_a_modifier = avoir_data_temp.pop("reference")
|
|
logger.info(
|
|
" Modification de la référence reportée après les lignes"
|
|
)
|
|
modif_ref = False
|
|
|
|
if modif_statut:
|
|
statut_a_modifier = avoir_data_temp.pop("statut")
|
|
logger.info(
|
|
" Modification du statut reportée après les lignes"
|
|
)
|
|
modif_statut = False
|
|
|
|
logger.info(" Test Write() basique (sans modification)...")
|
|
|
|
try:
|
|
doc.Write()
|
|
logger.info(" Write() basique OK")
|
|
doc.Read()
|
|
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_apres = getattr(client_obj, "CT_Num", "")
|
|
if client_apres == client_code_initial:
|
|
logger.info(f" Client préservé: {client_apres}")
|
|
else:
|
|
logger.error(
|
|
f" Client a changé: {client_code_initial} → {client_apres}"
|
|
)
|
|
else:
|
|
logger.error(" Client devenu NULL après Write() basique")
|
|
|
|
except Exception as e:
|
|
logger.error(f" Write() basique ÉCHOUE: {e}")
|
|
logger.error(f" ABANDON: Le document est VERROUILLÉ")
|
|
raise ValueError(
|
|
f"Document verrouillé, impossible de modifier: {e}"
|
|
)
|
|
|
|
if not modif_lignes and (
|
|
modif_date
|
|
or modif_date_livraison
|
|
or modif_statut
|
|
or modif_ref
|
|
):
|
|
logger.info(" Modifications simples (sans lignes)...")
|
|
|
|
if modif_date:
|
|
logger.info(" Modification date avoir...")
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(avoir_data_temp.get("date_avoir"))
|
|
)
|
|
champs_modifies.append("date")
|
|
|
|
if modif_date_livraison:
|
|
logger.info(" Modification date livraison...")
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(avoir_data_temp["date_livraison"])
|
|
)
|
|
logger.info(
|
|
f" Date livraison: {avoir_data_temp['date_livraison']}"
|
|
)
|
|
champs_modifies.append("date_livraison")
|
|
|
|
if modif_statut:
|
|
logger.info(" Modification statut...")
|
|
nouveau_statut = avoir_data_temp["statut"]
|
|
doc.DO_Statut = nouveau_statut
|
|
champs_modifies.append("statut")
|
|
logger.info(f" Statut défini: {nouveau_statut}")
|
|
|
|
if modif_ref:
|
|
logger.info(" Modification référence...")
|
|
try:
|
|
doc.DO_Ref = avoir_data_temp["reference"]
|
|
champs_modifies.append("reference")
|
|
logger.info(
|
|
f" Référence définie: {avoir_data_temp['reference']}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f" Référence non définie: {e}")
|
|
|
|
logger.info(" Write() sans réassociation client...")
|
|
try:
|
|
doc.Write()
|
|
logger.info(" Write() réussi")
|
|
|
|
doc.Read()
|
|
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_apres = getattr(client_obj, "CT_Num", "")
|
|
if client_apres == client_code_initial:
|
|
logger.info(f" Client préservé: {client_apres}")
|
|
else:
|
|
logger.error(
|
|
f" Client perdu: {client_code_initial} → {client_apres}"
|
|
)
|
|
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_msg = f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
except:
|
|
pass
|
|
|
|
logger.error(f" Write() échoue: {error_msg}")
|
|
raise ValueError(f"Sage refuse: {error_msg}")
|
|
|
|
elif modif_lignes:
|
|
logger.info(" REMPLACEMENT COMPLET DES LIGNES...")
|
|
|
|
if modif_date:
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(avoir_data_temp.get("date_avoir"))
|
|
)
|
|
champs_modifies.append("date")
|
|
logger.info(" Date avoir modifiée")
|
|
|
|
if modif_date_livraison:
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(avoir_data_temp["date_livraison"])
|
|
)
|
|
logger.info(" Date livraison modifiée")
|
|
champs_modifies.append("date_livraison")
|
|
|
|
nouvelles_lignes = avoir_data["lignes"]
|
|
nb_nouvelles = len(nouvelles_lignes)
|
|
|
|
logger.info(
|
|
f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes"
|
|
)
|
|
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
|
|
if nb_lignes_initial > 0:
|
|
logger.info(
|
|
f" Suppression de {nb_lignes_initial} lignes existantes..."
|
|
)
|
|
|
|
for idx in range(nb_lignes_initial, 0, -1):
|
|
try:
|
|
ligne_p = factory_lignes.List(idx)
|
|
if ligne_p:
|
|
ligne = win32com.client.CastTo(
|
|
ligne_p, "IBODocumentLigne3"
|
|
)
|
|
ligne.Read()
|
|
|
|
ligne.Remove()
|
|
logger.debug(f" Ligne {idx} supprimée")
|
|
except Exception as e:
|
|
logger.warning(
|
|
f" Impossible de supprimer ligne {idx}: {e}"
|
|
)
|
|
|
|
logger.info(" Toutes les lignes existantes supprimées")
|
|
|
|
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
|
|
|
|
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
|
|
logger.info(
|
|
f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---"
|
|
)
|
|
|
|
persist_article = factory_article.ReadReference(
|
|
ligne_data["article_code"]
|
|
)
|
|
if not persist_article:
|
|
raise ValueError(
|
|
f"Article {ligne_data['article_code']} introuvable"
|
|
)
|
|
|
|
article_obj = win32com.client.CastTo(
|
|
persist_article, "IBOArticle3"
|
|
)
|
|
article_obj.Read()
|
|
|
|
ligne_persist = factory_lignes.Create()
|
|
|
|
try:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentLigne3"
|
|
)
|
|
except:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentVenteLigne3"
|
|
)
|
|
|
|
quantite = float(ligne_data["quantite"])
|
|
|
|
try:
|
|
ligne_obj.SetDefaultArticleReference(
|
|
ligne_data["article_code"], quantite
|
|
)
|
|
except:
|
|
try:
|
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
|
except:
|
|
ligne_obj.DL_Design = ligne_data.get("designation", "")
|
|
ligne_obj.DL_Qte = quantite
|
|
|
|
if ligne_data.get("prix_unitaire_ht"):
|
|
ligne_obj.DL_PrixUnitaire = float(
|
|
ligne_data["prix_unitaire_ht"]
|
|
)
|
|
|
|
if ligne_data.get("remise_pourcentage", 0) > 0:
|
|
try:
|
|
ligne_obj.DL_Remise01REM_Valeur = float(
|
|
ligne_data["remise_pourcentage"]
|
|
)
|
|
ligne_obj.DL_Remise01REM_Type = 0
|
|
except:
|
|
pass
|
|
|
|
ligne_obj.Write()
|
|
logger.info(f" Ligne {idx} ajoutée")
|
|
|
|
logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées")
|
|
|
|
logger.info(" Write() document après remplacement lignes...")
|
|
doc.Write()
|
|
logger.info(" Document écrit")
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_apres = getattr(client_obj, "CT_Num", "")
|
|
logger.info(f" Client après remplacement: {client_apres}")
|
|
else:
|
|
logger.error(" Client NULL après remplacement")
|
|
|
|
champs_modifies.append("lignes")
|
|
|
|
if reference_a_modifier is not None:
|
|
try:
|
|
ancienne_reference = getattr(doc, "DO_Ref", "")
|
|
nouvelle_reference = (
|
|
str(reference_a_modifier) if reference_a_modifier else ""
|
|
)
|
|
|
|
logger.info(
|
|
f" Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'"
|
|
)
|
|
|
|
doc.DO_Ref = nouvelle_reference
|
|
doc.Write()
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
champs_modifies.append("reference")
|
|
logger.info(f" Référence modifiée avec succès")
|
|
except Exception as e:
|
|
logger.warning(f"Impossible de modifier la référence: {e}")
|
|
|
|
if statut_a_modifier is not None:
|
|
try:
|
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
|
nouveau_statut = int(statut_a_modifier)
|
|
|
|
if nouveau_statut != statut_actuel:
|
|
logger.info(
|
|
f" Modification statut: {statut_actuel} → {nouveau_statut}"
|
|
)
|
|
|
|
doc.DO_Statut = nouveau_statut
|
|
doc.Write()
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
champs_modifies.append("statut")
|
|
logger.info(f" Statut modifié avec succès")
|
|
except Exception as e:
|
|
logger.warning(f"Impossible de modifier le statut: {e}")
|
|
|
|
logger.info(" Relecture finale...")
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
client_obj_final = getattr(doc, "Client", None)
|
|
if client_obj_final:
|
|
client_obj_final.Read()
|
|
client_final = getattr(client_obj_final, "CT_Num", "")
|
|
else:
|
|
client_final = ""
|
|
|
|
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
|
|
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
|
|
reference_finale = getattr(doc, "DO_Ref", "")
|
|
statut_final = getattr(doc, "DO_Statut", 0)
|
|
|
|
date_livraison_final = None
|
|
|
|
try:
|
|
date_livr = getattr(doc, "DO_DateLivr", None)
|
|
if date_livr:
|
|
date_livraison_final = date_livr.strftime("%Y-%m-%d")
|
|
except:
|
|
pass
|
|
|
|
logger.info(f" SUCCÈS: {numero} modifié ")
|
|
logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
|
|
logger.info(f" Client final: {client_final}")
|
|
logger.info(f" Référence: {reference_finale}")
|
|
logger.info(f" Statut: {statut_final}")
|
|
|
|
if date_livraison_final:
|
|
logger.info(f" Date livraison: {date_livraison_final}")
|
|
logger.info(f" Champs modifiés: {champs_modifies}")
|
|
|
|
return {
|
|
"numero": numero,
|
|
"total_ht": total_ht,
|
|
"total_ttc": total_ttc,
|
|
"reference": reference_finale,
|
|
"date_livraison": date_livraison_final,
|
|
"champs_modifies": champs_modifies,
|
|
"statut": statut_final,
|
|
"client_code": client_final,
|
|
}
|
|
|
|
except ValueError as e:
|
|
logger.error(f" ERREUR MÉTIER: {e}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True)
|
|
|
|
error_message = str(e)
|
|
if self.cial:
|
|
try:
|
|
err = self.cial.CptaApplication.LastError
|
|
if err:
|
|
error_message = (
|
|
f"Erreur Sage: {err.Description} (Code: {err.Number})"
|
|
)
|
|
except:
|
|
pass
|
|
|
|
raise RuntimeError(f"Erreur Sage: {error_message}")
|
|
|
|
def creer_facture_enrichi(self, facture_data: dict) -> Dict:
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
logger.info(
|
|
f" Début création facture pour client {facture_data['client']['code']}"
|
|
)
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
transaction_active = False
|
|
try:
|
|
self.cial.CptaApplication.BeginTrans()
|
|
transaction_active = True
|
|
logger.debug(" Transaction Sage démarrée")
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
process = self.cial.CreateProcess_Document(
|
|
settings.SAGE_TYPE_FACTURE
|
|
)
|
|
doc = process.Document
|
|
|
|
try:
|
|
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
|
|
except:
|
|
pass
|
|
|
|
logger.info(" Document facture créé")
|
|
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(facture_data.get("date_facture"))
|
|
)
|
|
|
|
if (
|
|
"date_livraison" in facture_data
|
|
and facture_data["date_livraison"]
|
|
):
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(facture_data["date_livraison"])
|
|
)
|
|
logger.info(
|
|
f" Date livraison: {facture_data['date_livraison']}"
|
|
)
|
|
|
|
factory_client = self.cial.CptaApplication.FactoryClient
|
|
persist_client = factory_client.ReadNumero(
|
|
facture_data["client"]["code"]
|
|
)
|
|
|
|
if not persist_client:
|
|
raise ValueError(
|
|
f"Client {facture_data['client']['code']} introuvable"
|
|
)
|
|
|
|
client_obj = self._cast_client(persist_client)
|
|
if not client_obj:
|
|
raise ValueError(f"Impossible de charger le client")
|
|
|
|
doc.SetDefaultClient(client_obj)
|
|
doc.Write()
|
|
logger.info(f" Client {facture_data['client']['code']} associé")
|
|
|
|
if facture_data.get("reference"):
|
|
try:
|
|
doc.DO_Ref = facture_data["reference"]
|
|
logger.info(f" Référence: {facture_data['reference']}")
|
|
except:
|
|
pass
|
|
|
|
logger.info(" Configuration champs spécifiques factures...")
|
|
|
|
try:
|
|
if hasattr(doc, "DO_CodeJournal"):
|
|
try:
|
|
param_societe = (
|
|
self.cial.CptaApplication.ParametreSociete
|
|
)
|
|
journal_defaut = getattr(
|
|
param_societe, "P_CodeJournalVte", "VTE"
|
|
)
|
|
doc.DO_CodeJournal = journal_defaut
|
|
logger.info(f" Code journal: {journal_defaut}")
|
|
except:
|
|
doc.DO_CodeJournal = "VTE"
|
|
logger.info(" Code journal: VTE (défaut)")
|
|
except Exception as e:
|
|
logger.debug(f" Code journal: {e}")
|
|
|
|
try:
|
|
if hasattr(doc, "DO_Souche"):
|
|
doc.DO_Souche = 0
|
|
logger.debug(" Souche: 0 (défaut)")
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
if hasattr(doc, "DO_Regime"):
|
|
doc.DO_Regime = 0
|
|
logger.debug(" Régime: 0 (défaut)")
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
|
|
logger.info(f" Ajout de {len(facture_data['lignes'])} lignes...")
|
|
|
|
for idx, ligne_data in enumerate(facture_data["lignes"], 1):
|
|
logger.info(
|
|
f"--- Ligne {idx}: {ligne_data['article_code']} ---"
|
|
)
|
|
|
|
persist_article = factory_article.ReadReference(
|
|
ligne_data["article_code"]
|
|
)
|
|
|
|
if not persist_article:
|
|
raise ValueError(
|
|
f" Article {ligne_data['article_code']} introuvable dans Sage"
|
|
)
|
|
|
|
article_obj = win32com.client.CastTo(
|
|
persist_article, "IBOArticle3"
|
|
)
|
|
article_obj.Read()
|
|
|
|
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
|
|
designation_sage = getattr(article_obj, "AR_Design", "")
|
|
logger.info(f" Prix Sage: {prix_sage}€")
|
|
|
|
if prix_sage == 0:
|
|
logger.warning(
|
|
f"Article {ligne_data['article_code']} a un prix = 0€ (toléré)"
|
|
)
|
|
|
|
ligne_persist = factory_lignes.Create()
|
|
|
|
try:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentLigne3"
|
|
)
|
|
except:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentVenteLigne3"
|
|
)
|
|
|
|
quantite = float(ligne_data["quantite"])
|
|
|
|
try:
|
|
ligne_obj.SetDefaultArticleReference(
|
|
ligne_data["article_code"], quantite
|
|
)
|
|
logger.info(
|
|
f" Article associé via SetDefaultArticleReference"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"SetDefaultArticleReference échoué: {e}, tentative avec objet"
|
|
)
|
|
try:
|
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
|
logger.info(f" Article associé via SetDefaultArticle")
|
|
except Exception as e2:
|
|
logger.error(f" Toutes les méthodes ont échoué")
|
|
ligne_obj.DL_Design = (
|
|
designation_sage
|
|
or ligne_data.get("designation", "")
|
|
)
|
|
ligne_obj.DL_Qte = quantite
|
|
logger.warning("Configuration manuelle appliquée")
|
|
|
|
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
|
|
logger.info(f" Prix auto chargé: {prix_auto}€")
|
|
|
|
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
|
|
|
|
if prix_a_utiliser is not None and prix_a_utiliser > 0:
|
|
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
|
|
logger.info(f" Prix personnalisé: {prix_a_utiliser}€")
|
|
elif prix_auto == 0 and prix_sage > 0:
|
|
ligne_obj.DL_PrixUnitaire = float(prix_sage)
|
|
logger.info(f" Prix Sage forcé: {prix_sage}€")
|
|
elif prix_auto > 0:
|
|
logger.info(f" Prix auto conservé: {prix_auto}€")
|
|
|
|
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
|
|
montant_ligne = quantite * prix_final
|
|
logger.info(f" {quantite} x {prix_final}€ = {montant_ligne}€")
|
|
|
|
remise = ligne_data.get("remise_pourcentage", 0)
|
|
if remise > 0:
|
|
try:
|
|
ligne_obj.DL_Remise01REM_Valeur = float(remise)
|
|
ligne_obj.DL_Remise01REM_Type = 0
|
|
montant_apres_remise = montant_ligne * (
|
|
1 - remise / 100
|
|
)
|
|
logger.info(
|
|
f" Remise {remise}% → {montant_apres_remise}€"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Remise non appliquée: {e}")
|
|
|
|
ligne_obj.Write()
|
|
logger.info(f" Ligne {idx} écrite")
|
|
|
|
logger.info(" Validation facture...")
|
|
|
|
try:
|
|
doc.SetClient(client_obj)
|
|
logger.debug(" Client réassocié avant validation")
|
|
except:
|
|
try:
|
|
doc.SetDefaultClient(client_obj)
|
|
except:
|
|
pass
|
|
|
|
doc.Write()
|
|
|
|
logger.info(" Process()...")
|
|
process.Process()
|
|
|
|
if transaction_active:
|
|
self.cial.CptaApplication.CommitTrans()
|
|
logger.info(" Transaction committée")
|
|
|
|
time.sleep(2)
|
|
|
|
numero_facture = None
|
|
try:
|
|
doc_result = process.DocumentResult
|
|
if doc_result:
|
|
doc_result = win32com.client.CastTo(
|
|
doc_result, "IBODocumentVente3"
|
|
)
|
|
doc_result.Read()
|
|
numero_facture = getattr(doc_result, "DO_Piece", "")
|
|
except:
|
|
pass
|
|
|
|
if not numero_facture:
|
|
numero_facture = getattr(doc, "DO_Piece", "")
|
|
|
|
if not numero_facture:
|
|
raise RuntimeError("Numéro facture vide après création")
|
|
|
|
logger.info(f" Numéro facture: {numero_facture}")
|
|
|
|
factory_doc = self.cial.FactoryDocumentVente
|
|
persist_reread = factory_doc.ReadPiece(
|
|
settings.SAGE_TYPE_FACTURE, numero_facture
|
|
)
|
|
|
|
if persist_reread:
|
|
doc_final = win32com.client.CastTo(
|
|
persist_reread, "IBODocumentVente3"
|
|
)
|
|
doc_final.Read()
|
|
|
|
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
|
|
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
|
|
reference_finale = getattr(doc_final, "DO_Ref", "")
|
|
|
|
date_livraison_final = None
|
|
|
|
try:
|
|
date_livr = getattr(doc_final, "DO_DateLivr", None)
|
|
if date_livr:
|
|
date_livraison_final = date_livr.strftime("%Y-%m-%d")
|
|
except:
|
|
pass
|
|
else:
|
|
total_ht = 0.0
|
|
total_ttc = 0.0
|
|
reference_finale = facture_data.get("reference", "")
|
|
date_livraison_final = facture_data.get("date_livraison")
|
|
|
|
logger.info(
|
|
f" FACTURE CRÉÉE: {numero_facture} - {total_ttc}€ TTC "
|
|
)
|
|
|
|
if date_livraison_final:
|
|
logger.info(f" Date livraison: {date_livraison_final}")
|
|
|
|
return {
|
|
"numero_facture": numero_facture,
|
|
"total_ht": total_ht,
|
|
"total_ttc": total_ttc,
|
|
"nb_lignes": len(facture_data["lignes"]),
|
|
"client_code": facture_data["client"]["code"],
|
|
"date_facture": str(
|
|
self.normaliser_date(facture_data.get("date_facture"))
|
|
),
|
|
"date_livraison": date_livraison_final,
|
|
"reference": reference_finale,
|
|
}
|
|
|
|
except Exception as e:
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.RollbackTrans()
|
|
logger.error(" Transaction annulée (rollback)")
|
|
except:
|
|
pass
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f" Erreur création facture: {e}", exc_info=True)
|
|
raise RuntimeError(f"Échec création facture: {str(e)}")
|
|
|
|
def modifier_facture(self, numero: str, facture_data: Dict) -> Dict:
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info(f" === MODIFICATION FACTURE {numero} ===")
|
|
|
|
logger.info(" Chargement document...")
|
|
|
|
factory = self.cial.FactoryDocumentVente
|
|
persist = None
|
|
|
|
for type_test in [60, 5, settings.SAGE_TYPE_FACTURE]:
|
|
try:
|
|
persist_test = factory.ReadPiece(type_test, numero)
|
|
if persist_test:
|
|
persist = persist_test
|
|
logger.info(f" Document trouvé (type={type_test})")
|
|
break
|
|
except:
|
|
continue
|
|
|
|
if not persist:
|
|
raise ValueError(f" Facture {numero} INTROUVABLE")
|
|
|
|
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
|
|
doc.Read()
|
|
|
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
|
type_reel = getattr(doc, "DO_Type", -1)
|
|
|
|
logger.info(f" Type={type_reel}, Statut={statut_actuel}")
|
|
|
|
if statut_actuel == 5:
|
|
raise ValueError(f"La facture {numero} a déjà été transformée")
|
|
|
|
if statut_actuel == 6:
|
|
raise ValueError(f"La facture {numero} est annulée")
|
|
|
|
client_code_initial = ""
|
|
try:
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_code_initial = getattr(client_obj, "CT_Num", "").strip()
|
|
logger.info(f" Client initial: {client_code_initial}")
|
|
else:
|
|
logger.error(" Objet Client NULL à l'état initial !")
|
|
except Exception as e:
|
|
logger.error(f" Erreur lecture client initial: {e}")
|
|
|
|
if not client_code_initial:
|
|
raise ValueError(" Client introuvable dans le document")
|
|
|
|
nb_lignes_initial = 0
|
|
try:
|
|
factory_lignes = getattr(
|
|
doc, "FactoryDocumentLigne", None
|
|
) or getattr(doc, "FactoryDocumentVenteLigne", None)
|
|
index = 1
|
|
while index <= 100:
|
|
try:
|
|
ligne_p = factory_lignes.List(index)
|
|
if ligne_p is None:
|
|
break
|
|
nb_lignes_initial += 1
|
|
index += 1
|
|
except:
|
|
break
|
|
|
|
logger.info(f" Lignes initiales: {nb_lignes_initial}")
|
|
except Exception as e:
|
|
logger.warning(f" Erreur comptage lignes: {e}")
|
|
|
|
champs_modifies = []
|
|
|
|
modif_date = "date_facture" in facture_data
|
|
modif_date_livraison = "date_livraison" in facture_data
|
|
modif_statut = "statut" in facture_data
|
|
modif_ref = "reference" in facture_data
|
|
modif_lignes = (
|
|
"lignes" in facture_data and facture_data["lignes"] is not None
|
|
)
|
|
|
|
logger.info(f"Modifications demandées:")
|
|
logger.info(f" Date facture: {modif_date}")
|
|
logger.info(f" Date livraison: {modif_date_livraison}")
|
|
logger.info(f" Statut: {modif_statut}")
|
|
logger.info(f" Référence: {modif_ref}")
|
|
logger.info(f" Lignes: {modif_lignes}")
|
|
|
|
facture_data_temp = facture_data.copy()
|
|
reference_a_modifier = None
|
|
statut_a_modifier = None
|
|
|
|
if modif_lignes:
|
|
if modif_ref:
|
|
reference_a_modifier = facture_data_temp.pop("reference")
|
|
logger.info(
|
|
" Modification de la référence reportée après les lignes"
|
|
)
|
|
modif_ref = False
|
|
|
|
if modif_statut:
|
|
statut_a_modifier = facture_data_temp.pop("statut")
|
|
logger.info(
|
|
" Modification du statut reportée après les lignes"
|
|
)
|
|
modif_statut = False
|
|
|
|
logger.info(" Test Write() basique (sans modification)...")
|
|
|
|
try:
|
|
doc.Write()
|
|
logger.info(" Write() basique OK")
|
|
doc.Read()
|
|
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_apres = getattr(client_obj, "CT_Num", "")
|
|
if client_apres == client_code_initial:
|
|
logger.info(f" Client préservé: {client_apres}")
|
|
else:
|
|
logger.error(
|
|
f" Client a changé: {client_code_initial} → {client_apres}"
|
|
)
|
|
else:
|
|
logger.error(" Client devenu NULL après Write() basique")
|
|
|
|
except Exception as e:
|
|
logger.error(f" Write() basique ÉCHOUE: {e}")
|
|
logger.error(f" ABANDON: Le document est VERROUILLÉ")
|
|
raise ValueError(
|
|
f"Document verrouillé, impossible de modifier: {e}"
|
|
)
|
|
|
|
if not modif_lignes and (
|
|
modif_date
|
|
or modif_date_livraison
|
|
or modif_statut
|
|
or modif_ref
|
|
):
|
|
logger.info(" Modifications simples (sans lignes)...")
|
|
|
|
if modif_date:
|
|
logger.info(" Modification date facture...")
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(facture_data_temp.get("date_facture"))
|
|
)
|
|
champs_modifies.append("date")
|
|
|
|
if modif_date_livraison:
|
|
logger.info(" Modification date livraison...")
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(facture_data_temp["date_livraison"])
|
|
)
|
|
logger.info(
|
|
f" Date livraison: {facture_data_temp['date_livraison']}"
|
|
)
|
|
champs_modifies.append("date_livraison")
|
|
|
|
if modif_statut:
|
|
logger.info(" Modification statut...")
|
|
nouveau_statut = facture_data_temp["statut"]
|
|
doc.DO_Statut = nouveau_statut
|
|
champs_modifies.append("statut")
|
|
logger.info(f" Statut défini: {nouveau_statut}")
|
|
|
|
if modif_ref:
|
|
logger.info(" Modification référence...")
|
|
try:
|
|
doc.DO_Ref = facture_data_temp["reference"]
|
|
champs_modifies.append("reference")
|
|
logger.info(
|
|
f" Référence définie: {facture_data_temp['reference']}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f" Référence non définie: {e}")
|
|
|
|
logger.info(" Write() sans réassociation client...")
|
|
try:
|
|
doc.Write()
|
|
logger.info(" Write() réussi")
|
|
|
|
doc.Read()
|
|
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_apres = getattr(client_obj, "CT_Num", "")
|
|
if client_apres == client_code_initial:
|
|
logger.info(f" Client préservé: {client_apres}")
|
|
else:
|
|
logger.error(
|
|
f" Client perdu: {client_code_initial} → {client_apres}"
|
|
)
|
|
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_msg = f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
except:
|
|
pass
|
|
|
|
logger.error(f" Write() échoue: {error_msg}")
|
|
raise ValueError(f"Sage refuse: {error_msg}")
|
|
|
|
elif modif_lignes:
|
|
logger.info(" REMPLACEMENT COMPLET DES LIGNES...")
|
|
|
|
if modif_date:
|
|
doc.DO_Date = pywintypes.Time(
|
|
self.normaliser_date(facture_data_temp.get("date_facture"))
|
|
)
|
|
champs_modifies.append("date")
|
|
logger.info(" Date facture modifiée")
|
|
|
|
if modif_date_livraison:
|
|
doc.DO_DateLivr = pywintypes.Time(
|
|
self.normaliser_date(facture_data_temp["date_livraison"])
|
|
)
|
|
logger.info(" Date livraison modifiée")
|
|
champs_modifies.append("date_livraison")
|
|
|
|
nouvelles_lignes = facture_data["lignes"]
|
|
nb_nouvelles = len(nouvelles_lignes)
|
|
|
|
logger.info(
|
|
f" {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles lignes"
|
|
)
|
|
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
|
|
if nb_lignes_initial > 0:
|
|
logger.info(
|
|
f" Suppression de {nb_lignes_initial} lignes existantes..."
|
|
)
|
|
|
|
for idx in range(nb_lignes_initial, 0, -1):
|
|
try:
|
|
ligne_p = factory_lignes.List(idx)
|
|
if ligne_p:
|
|
ligne = win32com.client.CastTo(
|
|
ligne_p, "IBODocumentLigne3"
|
|
)
|
|
ligne.Read()
|
|
|
|
ligne.Remove()
|
|
logger.debug(f" Ligne {idx} supprimée")
|
|
except Exception as e:
|
|
logger.warning(
|
|
f" Impossible de supprimer ligne {idx}: {e}"
|
|
)
|
|
|
|
logger.info(" Toutes les lignes existantes supprimées")
|
|
|
|
logger.info(f" Ajout de {nb_nouvelles} nouvelles lignes...")
|
|
|
|
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
|
|
logger.info(
|
|
f" --- Ligne {idx}/{nb_nouvelles}: {ligne_data['article_code']} ---"
|
|
)
|
|
|
|
persist_article = factory_article.ReadReference(
|
|
ligne_data["article_code"]
|
|
)
|
|
if not persist_article:
|
|
raise ValueError(
|
|
f"Article {ligne_data['article_code']} introuvable"
|
|
)
|
|
|
|
article_obj = win32com.client.CastTo(
|
|
persist_article, "IBOArticle3"
|
|
)
|
|
article_obj.Read()
|
|
|
|
ligne_persist = factory_lignes.Create()
|
|
|
|
try:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentLigne3"
|
|
)
|
|
except:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentVenteLigne3"
|
|
)
|
|
|
|
quantite = float(ligne_data["quantite"])
|
|
|
|
try:
|
|
ligne_obj.SetDefaultArticleReference(
|
|
ligne_data["article_code"], quantite
|
|
)
|
|
except:
|
|
try:
|
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
|
except:
|
|
ligne_obj.DL_Design = ligne_data.get("designation", "")
|
|
ligne_obj.DL_Qte = quantite
|
|
|
|
if ligne_data.get("prix_unitaire_ht"):
|
|
ligne_obj.DL_PrixUnitaire = float(
|
|
ligne_data["prix_unitaire_ht"]
|
|
)
|
|
|
|
if ligne_data.get("remise_pourcentage", 0) > 0:
|
|
try:
|
|
ligne_obj.DL_Remise01REM_Valeur = float(
|
|
ligne_data["remise_pourcentage"]
|
|
)
|
|
ligne_obj.DL_Remise01REM_Type = 0
|
|
except:
|
|
pass
|
|
|
|
ligne_obj.Write()
|
|
logger.info(f" Ligne {idx} ajoutée")
|
|
|
|
logger.info(f" {nb_nouvelles} nouvelles lignes ajoutées")
|
|
|
|
logger.info(" Write() document après remplacement lignes...")
|
|
doc.Write()
|
|
logger.info(" Document écrit")
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
client_obj = getattr(doc, "Client", None)
|
|
if client_obj:
|
|
client_obj.Read()
|
|
client_apres = getattr(client_obj, "CT_Num", "")
|
|
logger.info(f" Client après remplacement: {client_apres}")
|
|
else:
|
|
logger.error(" Client NULL après remplacement")
|
|
|
|
champs_modifies.append("lignes")
|
|
|
|
if reference_a_modifier is not None:
|
|
try:
|
|
ancienne_reference = getattr(doc, "DO_Ref", "")
|
|
nouvelle_reference = (
|
|
str(reference_a_modifier) if reference_a_modifier else ""
|
|
)
|
|
|
|
logger.info(
|
|
f" Modification référence: '{ancienne_reference}' → '{nouvelle_reference}'"
|
|
)
|
|
|
|
doc.DO_Ref = nouvelle_reference
|
|
doc.Write()
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
champs_modifies.append("reference")
|
|
logger.info(f" Référence modifiée avec succès")
|
|
except Exception as e:
|
|
logger.warning(f"Impossible de modifier la référence: {e}")
|
|
|
|
if statut_a_modifier is not None:
|
|
try:
|
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
|
nouveau_statut = int(statut_a_modifier)
|
|
|
|
if nouveau_statut != statut_actuel:
|
|
logger.info(
|
|
f" Modification statut: {statut_actuel} → {nouveau_statut}"
|
|
)
|
|
|
|
doc.DO_Statut = nouveau_statut
|
|
doc.Write()
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
champs_modifies.append("statut")
|
|
logger.info(f" Statut modifié avec succès")
|
|
except Exception as e:
|
|
logger.warning(f"Impossible de modifier le statut: {e}")
|
|
|
|
logger.info(" Relecture finale...")
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
doc.Read()
|
|
|
|
client_obj_final = getattr(doc, "Client", None)
|
|
if client_obj_final:
|
|
client_obj_final.Read()
|
|
client_final = getattr(client_obj_final, "CT_Num", "")
|
|
else:
|
|
client_final = ""
|
|
|
|
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
|
|
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
|
|
reference_finale = getattr(doc, "DO_Ref", "")
|
|
statut_final = getattr(doc, "DO_Statut", 0)
|
|
|
|
date_livraison_final = None
|
|
|
|
try:
|
|
date_livr = getattr(doc, "DO_DateLivr", None)
|
|
if date_livr:
|
|
date_livraison_final = date_livr.strftime("%Y-%m-%d")
|
|
except:
|
|
pass
|
|
|
|
logger.info(f" SUCCÈS: {numero} modifiée ")
|
|
logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
|
|
logger.info(f" Client final: {client_final}")
|
|
logger.info(f" Référence: {reference_finale}")
|
|
logger.info(f" Statut: {statut_final}")
|
|
|
|
if date_livraison_final:
|
|
logger.info(f" Date livraison: {date_livraison_final}")
|
|
logger.info(f" Champs modifiés: {champs_modifies}")
|
|
|
|
return {
|
|
"numero": numero,
|
|
"total_ht": total_ht,
|
|
"total_ttc": total_ttc,
|
|
"reference": reference_finale,
|
|
"date_livraison": date_livraison_final,
|
|
"champs_modifies": champs_modifies,
|
|
"statut": statut_final,
|
|
"client_code": client_final,
|
|
}
|
|
|
|
except ValueError as e:
|
|
logger.error(f" ERREUR MÉTIER: {e}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True)
|
|
|
|
error_message = str(e)
|
|
if self.cial:
|
|
try:
|
|
err = self.cial.CptaApplication.LastError
|
|
if err:
|
|
error_message = (
|
|
f"Erreur Sage: {err.Description} (Code: {err.Number})"
|
|
)
|
|
except:
|
|
pass
|
|
|
|
raise RuntimeError(f"Erreur Sage: {error_message}")
|
|
|
|
def creer_article(self, article_data: dict) -> dict:
|
|
with self._com_context(), self._lock_com:
|
|
try:
|
|
logger.info("[ARTICLE] === CREATION ARTICLE ===")
|
|
|
|
transaction_active = False
|
|
try:
|
|
self.cial.CptaApplication.BeginTrans()
|
|
transaction_active = True
|
|
logger.debug("Transaction Sage démarrée")
|
|
except Exception as e:
|
|
logger.debug(f"BeginTrans non disponible : {e}")
|
|
|
|
try:
|
|
depots_disponibles = []
|
|
depot_a_utiliser = None
|
|
depot_code_demande = article_data.get("depot_code")
|
|
|
|
try:
|
|
factory_depot = self.cial.FactoryDepot
|
|
index = 1
|
|
|
|
while index <= 100:
|
|
try:
|
|
persist = factory_depot.List(index)
|
|
if persist is None:
|
|
break
|
|
|
|
depot_obj = win32com.client.CastTo(persist, "IBODepot3")
|
|
depot_obj.Read()
|
|
|
|
code = getattr(depot_obj, "DE_Code", "").strip()
|
|
if not code:
|
|
index += 1
|
|
continue
|
|
|
|
numero = int(getattr(depot_obj, "Compteur", 0))
|
|
intitule = getattr(
|
|
depot_obj, "DE_Intitule", f"Depot {code}"
|
|
)
|
|
|
|
depot_info = {
|
|
"code": code,
|
|
"numero": numero,
|
|
"intitule": intitule,
|
|
"objet": depot_obj,
|
|
}
|
|
|
|
depots_disponibles.append(depot_info)
|
|
|
|
if depot_code_demande and code == depot_code_demande:
|
|
depot_a_utiliser = depot_info
|
|
elif not depot_code_demande and not depot_a_utiliser:
|
|
depot_a_utiliser = depot_info
|
|
|
|
index += 1
|
|
|
|
except Exception as e:
|
|
if "Acces refuse" in str(e):
|
|
break
|
|
index += 1
|
|
|
|
except Exception as e:
|
|
logger.warning(f"[DEPOT] Erreur découverte dépôts : {e}")
|
|
|
|
if not depots_disponibles:
|
|
raise ValueError(
|
|
"Aucun dépôt trouvé dans Sage. Créez d'abord un dépôt."
|
|
)
|
|
|
|
if not depot_a_utiliser:
|
|
depot_a_utiliser = depots_disponibles[0]
|
|
|
|
logger.info(
|
|
f"[DEPOT] Utilisation : '{depot_a_utiliser['code']}' ({depot_a_utiliser['intitule']})"
|
|
)
|
|
|
|
reference = article_data.get("reference", "").upper().strip()
|
|
if not reference:
|
|
raise ValueError("La référence est obligatoire")
|
|
|
|
if len(reference) > 18:
|
|
raise ValueError(
|
|
"La référence ne peut pas dépasser 18 caractères"
|
|
)
|
|
|
|
designation = article_data.get("designation", "").strip()
|
|
if not designation:
|
|
raise ValueError("La désignation est obligatoire")
|
|
|
|
if len(designation) > 69:
|
|
designation = designation[:69]
|
|
|
|
stock_reel = article_data.get("stock_reel", 0.0)
|
|
stock_mini = article_data.get("stock_mini", 0.0)
|
|
stock_maxi = article_data.get("stock_maxi", 0.0)
|
|
|
|
logger.info(f"[ARTICLE] Référence : {reference}")
|
|
logger.info(f"[ARTICLE] Désignation : {designation}")
|
|
logger.info(f"[ARTICLE] Stock réel demandé : {stock_reel}")
|
|
logger.info(f"[ARTICLE] Stock mini demandé : {stock_mini}")
|
|
logger.info(f"[ARTICLE] Stock maxi demandé : {stock_maxi}")
|
|
|
|
factory = self.cial.FactoryArticle
|
|
try:
|
|
article_existant = factory.ReadReference(reference)
|
|
if article_existant:
|
|
raise ValueError(f"L'article {reference} existe déjà")
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
if (
|
|
"Enregistrement non trouve" in error_msg
|
|
or "non trouve" in error_msg
|
|
or "-2607" in error_msg
|
|
):
|
|
logger.debug(
|
|
f"[ARTICLE] {reference} n'existe pas encore, création possible"
|
|
)
|
|
else:
|
|
logger.error(f"[ARTICLE] Erreur vérification : {e}")
|
|
raise
|
|
|
|
persist = factory.Create()
|
|
article = win32com.client.CastTo(persist, "IBOArticle3")
|
|
article.SetDefault()
|
|
|
|
article.AR_Ref = reference
|
|
article.AR_Design = designation
|
|
|
|
logger.info("[MODELE] Recherche article modèle via SQL...")
|
|
|
|
article_modele_ref = None
|
|
article_modele = None
|
|
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute(
|
|
"""
|
|
SELECT TOP 1 AR_Ref
|
|
FROM F_ARTICLE
|
|
WHERE AR_Sommeil = 0
|
|
ORDER BY AR_Ref
|
|
"""
|
|
)
|
|
|
|
row = cursor.fetchone()
|
|
|
|
if row:
|
|
article_modele_ref = self._safe_strip(row.AR_Ref)
|
|
logger.info(
|
|
f" [SQL] Article modèle trouvé : {article_modele_ref}"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.warning(f" [SQL] Erreur recherche article : {e}")
|
|
|
|
if article_modele_ref:
|
|
try:
|
|
persist_modele = factory.ReadReference(article_modele_ref)
|
|
|
|
if persist_modele:
|
|
article_modele = win32com.client.CastTo(
|
|
persist_modele, "IBOArticle3"
|
|
)
|
|
article_modele.Read()
|
|
logger.info(
|
|
f" [OK] Article modèle chargé : {article_modele_ref}"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.warning(f" [WARN] Erreur chargement modèle : {e}")
|
|
article_modele = None
|
|
|
|
if not article_modele:
|
|
raise ValueError(
|
|
"Aucun article modèle trouvé dans Sage.\n"
|
|
"Créez au moins un article manuellement dans Sage pour servir de modèle."
|
|
)
|
|
|
|
logger.info("[OBJETS] Copie Unite + Famille depuis modèle...")
|
|
|
|
unite_trouvee = False
|
|
try:
|
|
unite_obj = getattr(article_modele, "Unite", None)
|
|
if unite_obj:
|
|
article.Unite = unite_obj
|
|
logger.info(
|
|
f" [OK] Objet Unite copié depuis {article_modele_ref}"
|
|
)
|
|
unite_trouvee = True
|
|
except Exception as e:
|
|
logger.debug(f" Unite non copiable : {str(e)[:80]}")
|
|
|
|
if not unite_trouvee:
|
|
raise ValueError(
|
|
"Impossible de copier l'unité de vente depuis le modèle"
|
|
)
|
|
|
|
famille_trouvee = False
|
|
famille_code_personnalise = article_data.get("famille")
|
|
famille_obj = None
|
|
|
|
if famille_code_personnalise:
|
|
logger.info(
|
|
f" [FAMILLE] Code personnalisé demandé : {famille_code_personnalise}"
|
|
)
|
|
|
|
try:
|
|
famille_existe_sql = False
|
|
famille_code_exact = None
|
|
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute(
|
|
"""
|
|
SELECT FA_CodeFamille, FA_Type
|
|
FROM F_FAMILLE
|
|
WHERE UPPER(FA_CodeFamille) = ?
|
|
""",
|
|
(famille_code_personnalise.upper(),),
|
|
)
|
|
|
|
row = cursor.fetchone()
|
|
|
|
if row:
|
|
famille_code_exact = self._safe_strip(
|
|
row.FA_CodeFamille
|
|
)
|
|
famille_existe_sql = True
|
|
logger.info(
|
|
f" [SQL] Famille trouvée : {famille_code_exact}"
|
|
)
|
|
else:
|
|
raise ValueError(
|
|
f"Famille '{famille_code_personnalise}' introuvable"
|
|
)
|
|
|
|
except ValueError:
|
|
raise
|
|
except Exception as e_sql:
|
|
logger.warning(f" [SQL] Erreur : {e_sql}")
|
|
|
|
if famille_existe_sql and famille_code_exact:
|
|
factory_famille = self.cial.FactoryFamille
|
|
try:
|
|
index = 1
|
|
max_scan = 1000
|
|
|
|
while index <= max_scan:
|
|
try:
|
|
persist_test = factory_famille.List(index)
|
|
if persist_test is None:
|
|
break
|
|
|
|
fam_test = win32com.client.CastTo(
|
|
persist_test, "IBOFamille3"
|
|
)
|
|
fam_test.Read()
|
|
|
|
code_test = (
|
|
getattr(fam_test, "FA_CodeFamille", "")
|
|
.strip()
|
|
.upper()
|
|
)
|
|
|
|
if code_test == famille_code_exact.upper():
|
|
famille_obj = fam_test
|
|
famille_trouvee = True
|
|
logger.info(
|
|
f" [OK] Famille trouvée à l'index {index}"
|
|
)
|
|
break
|
|
|
|
index += 1
|
|
|
|
except Exception as e:
|
|
if "Accès refusé" in str(e):
|
|
break
|
|
index += 1
|
|
|
|
if famille_obj:
|
|
famille_obj.Read()
|
|
article.Famille = famille_obj
|
|
logger.info(
|
|
f" [OK] Famille '{famille_code_personnalise}' assignée"
|
|
)
|
|
else:
|
|
raise ValueError(
|
|
f"Famille '{famille_code_personnalise}' inaccessible via COM"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.warning(f" [COM] Erreur scanner : {e}")
|
|
raise
|
|
|
|
except ValueError:
|
|
raise
|
|
except Exception as e:
|
|
logger.warning(
|
|
f" [WARN] Famille personnalisée non chargée : {str(e)[:100]}"
|
|
)
|
|
|
|
if not famille_trouvee:
|
|
try:
|
|
famille_obj = getattr(article_modele, "Famille", None)
|
|
if famille_obj:
|
|
article.Famille = famille_obj
|
|
logger.info(
|
|
f" [OK] Objet Famille copié depuis {article_modele_ref}"
|
|
)
|
|
famille_trouvee = True
|
|
except Exception as e:
|
|
logger.debug(f" Famille non copiable : {str(e)[:80]}")
|
|
|
|
logger.info("[CHAMPS] Copie champs obligatoires depuis modèle...")
|
|
|
|
article.AR_Type = int(getattr(article_modele, "AR_Type", 0))
|
|
article.AR_Nature = int(getattr(article_modele, "AR_Nature", 0))
|
|
article.AR_Nomencl = int(getattr(article_modele, "AR_Nomencl", 0))
|
|
|
|
article.AR_SuiviStock = 2
|
|
logger.info(f" [OK] AR_SuiviStock=2 (FIFO/LIFO)")
|
|
|
|
prix_vente = article_data.get("prix_vente")
|
|
if prix_vente is not None:
|
|
try:
|
|
article.AR_PrixVen = float(prix_vente)
|
|
logger.info(f" Prix vente : {prix_vente} EUR")
|
|
except Exception as e:
|
|
logger.warning(f" Prix vente erreur : {str(e)[:100]}")
|
|
|
|
prix_achat = article_data.get("prix_achat")
|
|
if prix_achat is not None:
|
|
try:
|
|
try:
|
|
article.AR_PrixAch = float(prix_achat)
|
|
logger.info(
|
|
f" Prix achat (AR_PrixAch) : {prix_achat} EUR"
|
|
)
|
|
except:
|
|
article.AR_PrixAchat = float(prix_achat)
|
|
logger.info(
|
|
f" Prix achat (AR_PrixAchat) : {prix_achat} EUR"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f" Prix achat erreur : {str(e)[:100]}")
|
|
|
|
code_ean = article_data.get("code_ean")
|
|
if code_ean:
|
|
article.AR_CodeBarre = str(code_ean)
|
|
logger.info(f" Code EAN/Barre : {code_ean}")
|
|
|
|
description = article_data.get("description")
|
|
if description:
|
|
try:
|
|
article.AR_Commentaire = description
|
|
logger.info(f" Description définie")
|
|
except:
|
|
pass
|
|
|
|
logger.info("[ARTICLE] Écriture dans Sage...")
|
|
|
|
try:
|
|
article.Write()
|
|
logger.info(" [OK] Write() réussi")
|
|
except Exception as e:
|
|
error_detail = str(e)
|
|
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
except:
|
|
pass
|
|
|
|
logger.error(f" [ERREUR] Write() échoué : {error_detail}")
|
|
raise RuntimeError(f"Échec création article : {error_detail}")
|
|
|
|
stock_defini = False
|
|
stock_erreur = None
|
|
|
|
has_stock_values = stock_reel or stock_mini or stock_maxi
|
|
|
|
if has_stock_values:
|
|
logger.info(
|
|
f"[STOCK] Définition stock dans F_ARTSTOCK (dépôt '{depot_a_utiliser['code']}')..."
|
|
)
|
|
|
|
try:
|
|
depot_obj = depot_a_utiliser["objet"]
|
|
|
|
factory_stock = None
|
|
for factory_name in [
|
|
"FactoryArticleStock",
|
|
"FactoryDepotStock",
|
|
]:
|
|
try:
|
|
factory_stock = getattr(
|
|
depot_obj, factory_name, None
|
|
)
|
|
if factory_stock:
|
|
logger.info(
|
|
f" Factory trouvée : {factory_name}"
|
|
)
|
|
break
|
|
except:
|
|
continue
|
|
|
|
if not factory_stock:
|
|
raise RuntimeError(
|
|
"Factory de stock introuvable sur le dépôt"
|
|
)
|
|
|
|
stock_persist = factory_stock.Create()
|
|
stock_obj = win32com.client.CastTo(
|
|
stock_persist, "IBODepotStock3"
|
|
)
|
|
stock_obj.SetDefault()
|
|
|
|
stock_obj.AR_Ref = reference
|
|
|
|
if stock_reel:
|
|
stock_obj.AS_QteSto = float(stock_reel)
|
|
logger.info(f" AS_QteSto = {stock_reel}")
|
|
|
|
if stock_mini:
|
|
try:
|
|
stock_obj.AS_QteMini = float(stock_mini)
|
|
logger.info(f" AS_QteMini = {stock_mini}")
|
|
except Exception as e:
|
|
logger.warning(f" AS_QteMini non défini : {e}")
|
|
|
|
if stock_maxi:
|
|
try:
|
|
stock_obj.AS_QteMaxi = float(stock_maxi)
|
|
logger.info(f" AS_QteMaxi = {stock_maxi}")
|
|
except Exception as e:
|
|
logger.warning(f" AS_QteMaxi non défini : {e}")
|
|
|
|
stock_obj.Write()
|
|
|
|
stock_defini = True
|
|
logger.info(
|
|
f" [OK] Stock défini dans F_ARTSTOCK : réel={stock_reel}, min={stock_mini}, max={stock_maxi}"
|
|
)
|
|
|
|
except Exception as e:
|
|
stock_erreur = str(e)
|
|
logger.error(
|
|
f" [ERREUR] Stock non défini dans F_ARTSTOCK : {e}",
|
|
exc_info=True,
|
|
)
|
|
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.CommitTrans()
|
|
logger.info(
|
|
"[COMMIT] Transaction committée - Article persiste dans Sage"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"[COMMIT] Erreur commit : {e}")
|
|
|
|
logger.info("[VERIF] Relecture article créé...")
|
|
|
|
article_cree_persist = factory.ReadReference(reference)
|
|
if not article_cree_persist:
|
|
raise RuntimeError(
|
|
"Article créé mais introuvable à la relecture"
|
|
)
|
|
|
|
article_cree = win32com.client.CastTo(
|
|
article_cree_persist, "IBOArticle3"
|
|
)
|
|
article_cree.Read()
|
|
|
|
stocks_par_depot = []
|
|
stock_total = 0.0
|
|
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute(
|
|
"""
|
|
SELECT
|
|
d.DE_Code,
|
|
s.AS_QteSto,
|
|
s.AS_QteMini,
|
|
s.AS_QteMaxi
|
|
FROM F_ARTSTOCK s
|
|
LEFT JOIN F_DEPOT d ON s.DE_No = d.DE_No
|
|
WHERE s.AR_Ref = ?
|
|
""",
|
|
(reference.upper(),),
|
|
)
|
|
|
|
depot_rows = cursor.fetchall()
|
|
|
|
for depot_row in depot_rows:
|
|
if len(depot_row) >= 4:
|
|
qte = float(depot_row[1]) if depot_row[1] else 0.0
|
|
stock_total += qte
|
|
|
|
stocks_par_depot.append(
|
|
{
|
|
"depot_code": self._safe_strip(
|
|
depot_row[0]
|
|
),
|
|
"quantite": qte,
|
|
"qte_mini": (
|
|
float(depot_row[2])
|
|
if depot_row[2]
|
|
else 0.0
|
|
),
|
|
"qte_maxi": (
|
|
float(depot_row[3])
|
|
if depot_row[3]
|
|
else 0.0
|
|
),
|
|
}
|
|
)
|
|
|
|
logger.info(
|
|
f"[VERIF] Stock dans F_ARTSTOCK : {stock_total} unités dans {len(stocks_par_depot)} dépôt(s)"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"[VERIF] Impossible de vérifier le stock dans F_ARTSTOCK : {e}"
|
|
)
|
|
|
|
logger.info(
|
|
f"[OK] ARTICLE CREE : {reference} - Stock total : {stock_total}"
|
|
)
|
|
|
|
logger.info("[EXTRACTION] Extraction complète de l'article créé...")
|
|
|
|
resultat = self._extraire_article(article_cree)
|
|
|
|
if not resultat:
|
|
resultat = {
|
|
"reference": reference,
|
|
"designation": designation,
|
|
}
|
|
|
|
resultat["stock_reel"] = stock_total
|
|
|
|
if stock_mini:
|
|
resultat["stock_mini"] = float(stock_mini)
|
|
|
|
if stock_maxi:
|
|
resultat["stock_maxi"] = float(stock_maxi)
|
|
|
|
resultat["stock_disponible"] = stock_total
|
|
resultat["stock_reserve"] = 0.0
|
|
resultat["stock_commande"] = 0.0
|
|
|
|
if prix_vente is not None:
|
|
resultat["prix_vente"] = float(prix_vente)
|
|
|
|
if prix_achat is not None:
|
|
resultat["prix_achat"] = float(prix_achat)
|
|
|
|
if description:
|
|
resultat["description"] = description
|
|
|
|
if code_ean:
|
|
resultat["code_ean"] = str(code_ean)
|
|
resultat["code_barre"] = str(code_ean)
|
|
|
|
if famille_code_personnalise and famille_trouvee:
|
|
resultat["famille_code"] = famille_code_personnalise
|
|
try:
|
|
if famille_obj:
|
|
famille_obj.Read()
|
|
resultat["famille_libelle"] = getattr(
|
|
famille_obj, "FA_Intitule", ""
|
|
)
|
|
except:
|
|
pass
|
|
|
|
if stocks_par_depot:
|
|
resultat["stocks_par_depot"] = stocks_par_depot
|
|
resultat["depot_principal"] = {
|
|
"code": depot_a_utiliser["code"],
|
|
"intitule": depot_a_utiliser["intitule"],
|
|
}
|
|
|
|
resultat["suivi_stock_active"] = stock_defini
|
|
|
|
if has_stock_values and not stock_defini and stock_erreur:
|
|
resultat["avertissement"] = (
|
|
f"Stock demandé mais non défini dans F_ARTSTOCK : {stock_erreur}"
|
|
)
|
|
|
|
logger.info(
|
|
f"[EXTRACTION] Article extrait et enrichi avec {len(resultat)} champs"
|
|
)
|
|
|
|
return resultat
|
|
|
|
except ValueError:
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.RollbackTrans()
|
|
except:
|
|
pass
|
|
raise
|
|
|
|
except Exception as e:
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.RollbackTrans()
|
|
except:
|
|
pass
|
|
|
|
logger.error(f"Erreur creation article : {e}", exc_info=True)
|
|
raise RuntimeError(f"Erreur creation article : {str(e)}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur globale : {e}", exc_info=True)
|
|
raise
|
|
|
|
def modifier_article(self, reference: str, article_data: Dict) -> Dict:
|
|
if not self.cial:
|
|
raise RuntimeError("Connexion Sage non établie")
|
|
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info(f"[ARTICLE] === MODIFICATION {reference} ===")
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
persist = factory_article.ReadReference(reference.upper())
|
|
|
|
if not persist:
|
|
raise ValueError(f"Article {reference} introuvable")
|
|
|
|
article = win32com.client.CastTo(persist, "IBOArticle3")
|
|
article.Read()
|
|
|
|
designation_actuelle = getattr(article, "AR_Design", "")
|
|
logger.info(f"[ARTICLE] Trouvé : {reference} - {designation_actuelle}")
|
|
|
|
logger.info("[ARTICLE] Mise à jour des champs...")
|
|
|
|
champs_modifies = []
|
|
|
|
if "famille" in article_data and article_data["famille"]:
|
|
famille_code_demande = article_data["famille"].upper().strip()
|
|
logger.info(
|
|
f"[FAMILLE] Changement demandé : {famille_code_demande}"
|
|
)
|
|
|
|
try:
|
|
famille_existe_sql = False
|
|
famille_code_exact = None
|
|
famille_type = None
|
|
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute(
|
|
"""
|
|
SELECT FA_CodeFamille, FA_Type
|
|
FROM F_FAMILLE
|
|
WHERE UPPER(FA_CodeFamille) = ?
|
|
""",
|
|
(famille_code_demande,),
|
|
)
|
|
|
|
row = cursor.fetchone()
|
|
|
|
if row:
|
|
famille_code_exact = self._safe_strip(
|
|
row.FA_CodeFamille
|
|
)
|
|
famille_type = row.FA_Type if len(row) > 1 else 0
|
|
famille_existe_sql = True
|
|
|
|
if famille_type == 1:
|
|
raise ValueError(
|
|
f"La famille '{famille_code_demande}' est de type 'Total' "
|
|
f"et ne peut pas contenir d'articles. "
|
|
f"Utilisez une famille de type Détail."
|
|
)
|
|
|
|
logger.info(
|
|
f" [SQL] Famille trouvée : {famille_code_exact} (type={famille_type})"
|
|
)
|
|
else:
|
|
raise ValueError(
|
|
f"Famille '{famille_code_demande}' introuvable dans Sage"
|
|
)
|
|
|
|
except ValueError:
|
|
raise
|
|
except Exception as e:
|
|
logger.warning(f" [SQL] Erreur : {e}")
|
|
raise ValueError(f"Impossible de vérifier la famille : {e}")
|
|
|
|
if famille_existe_sql and famille_code_exact:
|
|
logger.info(f" [COM] Recherche via scanner...")
|
|
|
|
factory_famille = self.cial.FactoryFamille
|
|
famille_obj = None
|
|
|
|
try:
|
|
index = 1
|
|
max_scan = 1000
|
|
|
|
while index <= max_scan:
|
|
try:
|
|
persist_test = factory_famille.List(index)
|
|
if persist_test is None:
|
|
break
|
|
|
|
fam_test = win32com.client.CastTo(
|
|
persist_test, "IBOFamille3"
|
|
)
|
|
fam_test.Read()
|
|
|
|
code_test = (
|
|
getattr(fam_test, "FA_CodeFamille", "")
|
|
.strip()
|
|
.upper()
|
|
)
|
|
|
|
if code_test == famille_code_exact.upper():
|
|
famille_obj = fam_test
|
|
logger.info(
|
|
f" [OK] Famille trouvée à l'index {index}"
|
|
)
|
|
break
|
|
|
|
index += 1
|
|
|
|
except Exception as e:
|
|
if "Accès refusé" in str(e) or "Access" in str(
|
|
e
|
|
):
|
|
break
|
|
index += 1
|
|
|
|
except Exception as e:
|
|
logger.warning(
|
|
f" [COM] Scanner échoué : {str(e)[:200]}"
|
|
)
|
|
|
|
if famille_obj:
|
|
famille_obj.Read()
|
|
article.Famille = famille_obj
|
|
champs_modifies.append(f"famille={famille_code_exact}")
|
|
logger.info(
|
|
f" [OK] Famille changée : {famille_code_exact}"
|
|
)
|
|
else:
|
|
raise ValueError(
|
|
f"Famille '{famille_code_demande}' trouvée en SQL mais inaccessible via COM. "
|
|
f"Essayez avec une autre famille."
|
|
)
|
|
|
|
except ValueError:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f" [ERREUR] Changement famille : {e}")
|
|
raise ValueError(f"Impossible de changer la famille : {str(e)}")
|
|
|
|
if "designation" in article_data:
|
|
designation = str(article_data["designation"])[:69].strip()
|
|
article.AR_Design = designation
|
|
champs_modifies.append(f"designation")
|
|
logger.info(f" [OK] Désignation : {designation}")
|
|
|
|
if "prix_vente" in article_data:
|
|
try:
|
|
prix_vente = float(article_data["prix_vente"])
|
|
article.AR_PrixVen = prix_vente
|
|
champs_modifies.append("prix_vente")
|
|
logger.info(f" [OK] Prix vente : {prix_vente} EUR")
|
|
except Exception as e:
|
|
logger.warning(f" [WARN] Prix vente : {e}")
|
|
|
|
if "prix_achat" in article_data:
|
|
try:
|
|
prix_achat = float(article_data["prix_achat"])
|
|
|
|
try:
|
|
article.AR_PrixAch = prix_achat
|
|
champs_modifies.append("prix_achat")
|
|
logger.info(
|
|
f" [OK] Prix achat (AR_PrixAch) : {prix_achat} EUR"
|
|
)
|
|
except:
|
|
article.AR_PrixAchat = prix_achat
|
|
champs_modifies.append("prix_achat")
|
|
logger.info(
|
|
f" [OK] Prix achat (AR_PrixAchat) : {prix_achat} EUR"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.warning(f" [WARN] Prix achat : {e}")
|
|
|
|
if "stock_reel" in article_data:
|
|
try:
|
|
stock_reel = float(article_data["stock_reel"])
|
|
ancien_stock = float(getattr(article, "AR_Stock", 0.0))
|
|
|
|
article.AR_Stock = stock_reel
|
|
champs_modifies.append("stock_reel")
|
|
|
|
logger.info(f" [OK] Stock : {ancien_stock} -> {stock_reel}")
|
|
|
|
if stock_reel > ancien_stock:
|
|
logger.info(
|
|
f" [+] Stock augmenté de {stock_reel - ancien_stock}"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f" [ERREUR] Stock : {e}")
|
|
raise ValueError(f"Impossible de modifier le stock: {e}")
|
|
|
|
if "stock_mini" in article_data:
|
|
try:
|
|
stock_mini = float(article_data["stock_mini"])
|
|
article.AR_StockMini = stock_mini
|
|
champs_modifies.append("stock_mini")
|
|
logger.info(f" [OK] Stock mini : {stock_mini}")
|
|
except Exception as e:
|
|
logger.warning(f" [WARN] Stock mini : {e}")
|
|
|
|
if "stock_maxi" in article_data:
|
|
try:
|
|
stock_maxi = float(article_data["stock_maxi"])
|
|
article.AR_StockMaxi = stock_maxi
|
|
champs_modifies.append("stock_maxi")
|
|
logger.info(f" [OK] Stock maxi : {stock_maxi}")
|
|
except Exception as e:
|
|
logger.warning(f" [WARN] Stock maxi : {e}")
|
|
|
|
if "code_ean" in article_data:
|
|
try:
|
|
code_ean = str(article_data["code_ean"])[:13].strip()
|
|
article.AR_CodeBarre = code_ean
|
|
champs_modifies.append("code_ean")
|
|
logger.info(f" [OK] Code EAN : {code_ean}")
|
|
except Exception as e:
|
|
logger.warning(f" [WARN] Code EAN : {e}")
|
|
|
|
if "description" in article_data:
|
|
try:
|
|
description = str(article_data["description"])[:255].strip()
|
|
article.AR_Commentaire = description
|
|
champs_modifies.append("description")
|
|
logger.info(f" [OK] Description définie")
|
|
except Exception as e:
|
|
logger.warning(f" [WARN] Description : {e}")
|
|
|
|
if not champs_modifies:
|
|
logger.warning("[ARTICLE] Aucun champ à modifier")
|
|
return self._extraire_article(article)
|
|
|
|
logger.info(
|
|
f"[ARTICLE] Champs à modifier : {', '.join(champs_modifies)}"
|
|
)
|
|
|
|
logger.info("[ARTICLE] Écriture des modifications...")
|
|
|
|
try:
|
|
article.Write()
|
|
logger.info("[ARTICLE] Write() réussi")
|
|
|
|
except Exception as e:
|
|
error_detail = str(e)
|
|
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_detail = (
|
|
f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
)
|
|
except:
|
|
pass
|
|
|
|
logger.error(f"[ARTICLE] Erreur Write() : {error_detail}")
|
|
raise RuntimeError(f"Échec modification : {error_detail}")
|
|
|
|
article.Read()
|
|
|
|
logger.info(
|
|
f"[ARTICLE] MODIFIÉ : {reference} ({len(champs_modifies)} champs)"
|
|
)
|
|
|
|
resultat = self._extraire_article(article)
|
|
|
|
if not resultat:
|
|
resultat = {
|
|
"reference": reference,
|
|
"designation": getattr(article, "AR_Design", ""),
|
|
}
|
|
|
|
if "prix_vente" in article_data:
|
|
resultat["prix_vente"] = float(article_data["prix_vente"])
|
|
|
|
if "prix_achat" in article_data:
|
|
resultat["prix_achat"] = float(article_data["prix_achat"])
|
|
|
|
if "stock_reel" in article_data:
|
|
resultat["stock_reel"] = float(article_data["stock_reel"])
|
|
|
|
if "stock_mini" in article_data:
|
|
resultat["stock_mini"] = float(article_data["stock_mini"])
|
|
|
|
if "stock_maxi" in article_data:
|
|
resultat["stock_maxi"] = float(article_data["stock_maxi"])
|
|
|
|
if "code_ean" in article_data:
|
|
resultat["code_ean"] = str(article_data["code_ean"])
|
|
resultat["code_barre"] = str(article_data["code_ean"])
|
|
|
|
if "description" in article_data:
|
|
resultat["description"] = str(article_data["description"])
|
|
|
|
if "famille" in article_data:
|
|
resultat["famille_code"] = (
|
|
famille_code_exact if "famille_code_exact" in locals() else ""
|
|
)
|
|
|
|
return resultat
|
|
|
|
except ValueError as e:
|
|
logger.error(f"[ARTICLE] Erreur métier : {e}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f"[ARTICLE] Erreur technique : {e}", exc_info=True)
|
|
|
|
error_message = str(e)
|
|
if self.cial:
|
|
try:
|
|
err = self.cial.CptaApplication.LastError
|
|
if err:
|
|
error_message = f"Erreur Sage: {err.Description}"
|
|
except:
|
|
pass
|
|
|
|
raise RuntimeError(f"Erreur technique Sage : {error_message}")
|
|
|
|
def creer_famille(self, famille_data: dict) -> dict:
|
|
with self._com_context(), self._lock_com:
|
|
try:
|
|
logger.info("[FAMILLE] === CRÉATION FAMILLE (TYPE DÉTAIL) ===")
|
|
|
|
code = famille_data.get("code", "").upper().strip()
|
|
if not code:
|
|
raise ValueError("Le code famille est obligatoire")
|
|
|
|
if len(code) > 18:
|
|
raise ValueError(
|
|
"Le code famille ne peut pas dépasser 18 caractères"
|
|
)
|
|
|
|
intitule = famille_data.get("intitule", "").strip()
|
|
if not intitule:
|
|
raise ValueError("L'intitulé est obligatoire")
|
|
|
|
if len(intitule) > 69:
|
|
intitule = intitule[:69]
|
|
|
|
logger.info(f"[FAMILLE] Code : {code}")
|
|
logger.info(f"[FAMILLE] Intitulé : {intitule}")
|
|
|
|
type_demande = famille_data.get("type", 0)
|
|
if type_demande == 1:
|
|
logger.warning(
|
|
"[FAMILLE] Type 'Total' demandé mais IGNORÉ - Création en type Détail uniquement"
|
|
)
|
|
|
|
factory_famille = self.cial.FactoryFamille
|
|
|
|
try:
|
|
index = 1
|
|
while index <= 1000:
|
|
try:
|
|
persist_test = factory_famille.List(index)
|
|
if persist_test is None:
|
|
break
|
|
|
|
fam_test = win32com.client.CastTo(
|
|
persist_test, "IBOFamille3"
|
|
)
|
|
fam_test.Read()
|
|
|
|
code_existant = (
|
|
getattr(fam_test, "FA_CodeFamille", "").strip().upper()
|
|
)
|
|
|
|
if code_existant == code:
|
|
raise ValueError(f"La famille {code} existe déjà")
|
|
|
|
index += 1
|
|
except ValueError:
|
|
raise # Re-raise si c'est notre erreur
|
|
except:
|
|
index += 1
|
|
except ValueError:
|
|
raise
|
|
|
|
persist = factory_famille.Create()
|
|
famille = win32com.client.CastTo(persist, "IBOFamille3")
|
|
famille.SetDefault()
|
|
|
|
famille.FA_CodeFamille = code
|
|
famille.FA_Intitule = intitule
|
|
|
|
try:
|
|
famille.FA_Type = 0 # Toujours Détail
|
|
logger.info(f"[FAMILLE] Type : 0 (Détail)")
|
|
except Exception as e:
|
|
logger.warning(f"[FAMILLE] FA_Type non défini : {e}")
|
|
|
|
compte_achat = famille_data.get("compte_achat")
|
|
if compte_achat:
|
|
try:
|
|
factory_compte = self.cial.CptaApplication.FactoryCompteG
|
|
persist_compte = factory_compte.ReadNumero(compte_achat)
|
|
|
|
if persist_compte:
|
|
compte_obj = win32com.client.CastTo(
|
|
persist_compte, "IBOCompteG3"
|
|
)
|
|
compte_obj.Read()
|
|
|
|
famille.CompteGAchat = compte_obj
|
|
logger.info(f"[FAMILLE] Compte achat : {compte_achat}")
|
|
except Exception as e:
|
|
logger.warning(f"[FAMILLE] Compte achat non défini : {e}")
|
|
|
|
compte_vente = famille_data.get("compte_vente")
|
|
if compte_vente:
|
|
try:
|
|
factory_compte = self.cial.CptaApplication.FactoryCompteG
|
|
persist_compte = factory_compte.ReadNumero(compte_vente)
|
|
|
|
if persist_compte:
|
|
compte_obj = win32com.client.CastTo(
|
|
persist_compte, "IBOCompteG3"
|
|
)
|
|
compte_obj.Read()
|
|
|
|
famille.CompteGVente = compte_obj
|
|
logger.info(f"[FAMILLE] Compte vente : {compte_vente}")
|
|
except Exception as e:
|
|
logger.warning(f"[FAMILLE] Compte vente non défini : {e}")
|
|
|
|
logger.info("[FAMILLE] Écriture dans Sage...")
|
|
|
|
try:
|
|
famille.Write()
|
|
logger.info("[FAMILLE] Write() réussi")
|
|
except Exception as e:
|
|
error_detail = str(e)
|
|
|
|
try:
|
|
sage_error = self.cial.CptaApplication.LastError
|
|
if sage_error:
|
|
error_detail = (
|
|
f"{sage_error.Description} (Code: {sage_error.Number})"
|
|
)
|
|
except:
|
|
pass
|
|
|
|
logger.error(f"[FAMILLE] Erreur Write() : {error_detail}")
|
|
raise RuntimeError(f"Échec création famille : {error_detail}")
|
|
|
|
famille.Read()
|
|
|
|
resultat = {
|
|
"code": getattr(famille, "FA_CodeFamille", "").strip(),
|
|
"intitule": getattr(famille, "FA_Intitule", "").strip(),
|
|
"type": 0, # Toujours Détail
|
|
"type_libelle": "Détail",
|
|
}
|
|
|
|
logger.info(f"[FAMILLE] FAMILLE CRÉÉE : {code} - {intitule} (Détail)")
|
|
|
|
return resultat
|
|
|
|
except ValueError as e:
|
|
logger.error(f"[FAMILLE] Erreur métier : {e}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f"[FAMILLE] Erreur technique : {e}", exc_info=True)
|
|
|
|
error_message = str(e)
|
|
if self.cial:
|
|
try:
|
|
err = self.cial.CptaApplication.LastError
|
|
if err:
|
|
error_message = f"Erreur Sage: {err.Description}"
|
|
except:
|
|
pass
|
|
|
|
raise RuntimeError(f"Erreur technique Sage : {error_message}")
|
|
|
|
def lister_toutes_familles(
|
|
self, filtre: str = "", inclure_totaux: bool = True
|
|
) -> List[Dict]:
|
|
"""Liste toutes les familles avec leurs comptes comptables et fournisseur principal"""
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
logger.info("[SQL] Chargement des familles avec F_FAMCOMPTA et F_FAMFOURNISS...")
|
|
|
|
query = """
|
|
SELECT
|
|
-- F_FAMILLE - Identification
|
|
f.FA_CodeFamille,
|
|
f.FA_Type,
|
|
f.FA_Intitule,
|
|
f.FA_UniteVen,
|
|
f.FA_Coef,
|
|
f.FA_SuiviStock,
|
|
f.FA_Garantie,
|
|
f.FA_Central,
|
|
|
|
-- F_FAMILLE - Statistiques
|
|
f.FA_Stat01,
|
|
f.FA_Stat02,
|
|
f.FA_Stat03,
|
|
f.FA_Stat04,
|
|
f.FA_Stat05,
|
|
|
|
-- F_FAMILLE - Fiscal et gestion
|
|
f.FA_CodeFiscal,
|
|
f.FA_Pays,
|
|
f.FA_UnitePoids,
|
|
f.FA_Escompte,
|
|
f.FA_Delai,
|
|
f.FA_HorsStat,
|
|
f.FA_VteDebit,
|
|
f.FA_NotImp,
|
|
|
|
-- F_FAMILLE - Frais annexes (3 frais avec 3 remises chacun)
|
|
f.FA_Frais01FR_Denomination,
|
|
f.FA_Frais01FR_Rem01REM_Valeur,
|
|
f.FA_Frais01FR_Rem01REM_Type,
|
|
f.FA_Frais01FR_Rem02REM_Valeur,
|
|
f.FA_Frais01FR_Rem02REM_Type,
|
|
f.FA_Frais01FR_Rem03REM_Valeur,
|
|
f.FA_Frais01FR_Rem03REM_Type,
|
|
f.FA_Frais02FR_Denomination,
|
|
f.FA_Frais02FR_Rem01REM_Valeur,
|
|
f.FA_Frais02FR_Rem01REM_Type,
|
|
f.FA_Frais02FR_Rem02REM_Valeur,
|
|
f.FA_Frais02FR_Rem02REM_Type,
|
|
f.FA_Frais02FR_Rem03REM_Valeur,
|
|
f.FA_Frais02FR_Rem03REM_Type,
|
|
f.FA_Frais03FR_Denomination,
|
|
f.FA_Frais03FR_Rem01REM_Valeur,
|
|
f.FA_Frais03FR_Rem01REM_Type,
|
|
f.FA_Frais03FR_Rem02REM_Valeur,
|
|
f.FA_Frais03FR_Rem02REM_Type,
|
|
f.FA_Frais03FR_Rem03REM_Valeur,
|
|
f.FA_Frais03FR_Rem03REM_Type,
|
|
|
|
-- F_FAMILLE - Options diverses
|
|
f.FA_Contremarque,
|
|
f.FA_FactPoids,
|
|
f.FA_FactForfait,
|
|
f.FA_Publie,
|
|
f.FA_RacineRef,
|
|
f.FA_RacineCB,
|
|
|
|
-- F_FAMILLE - Catégories
|
|
f.CL_No1,
|
|
f.CL_No2,
|
|
f.CL_No3,
|
|
f.CL_No4,
|
|
|
|
-- F_FAMILLE - Gestion avancée
|
|
f.FA_Nature,
|
|
f.FA_NbColis,
|
|
f.FA_SousTraitance,
|
|
f.FA_Fictif,
|
|
f.FA_Criticite,
|
|
|
|
-- F_FAMILLE - Métadonnées système
|
|
f.cbMarq,
|
|
f.cbCreateur,
|
|
f.cbModification,
|
|
f.cbCreation,
|
|
f.cbCreationUser,
|
|
|
|
-- F_FAMCOMPTA Vente (FCP_Type = 0)
|
|
vte.FCP_ComptaCPT_CompteG,
|
|
vte.FCP_ComptaCPT_CompteA,
|
|
vte.FCP_ComptaCPT_Taxe1,
|
|
vte.FCP_ComptaCPT_Taxe2,
|
|
vte.FCP_ComptaCPT_Taxe3,
|
|
vte.FCP_ComptaCPT_Date1,
|
|
vte.FCP_ComptaCPT_Date2,
|
|
vte.FCP_ComptaCPT_Date3,
|
|
vte.FCP_TypeFacture,
|
|
|
|
-- F_FAMCOMPTA Achat (FCP_Type = 1)
|
|
ach.FCP_ComptaCPT_CompteG,
|
|
ach.FCP_ComptaCPT_CompteA,
|
|
ach.FCP_ComptaCPT_Taxe1,
|
|
ach.FCP_ComptaCPT_Taxe2,
|
|
ach.FCP_ComptaCPT_Taxe3,
|
|
ach.FCP_ComptaCPT_Date1,
|
|
ach.FCP_ComptaCPT_Date2,
|
|
ach.FCP_ComptaCPT_Date3,
|
|
ach.FCP_TypeFacture,
|
|
|
|
-- F_FAMCOMPTA Stock (FCP_Type = 2)
|
|
sto.FCP_ComptaCPT_CompteG,
|
|
sto.FCP_ComptaCPT_CompteA,
|
|
|
|
-- F_FAMFOURNISS (fournisseur principal FF_Principal=1)
|
|
ff.CT_Num,
|
|
ff.FF_Unite,
|
|
ff.FF_Conversion,
|
|
ff.FF_DelaiAppro,
|
|
ff.FF_Garantie,
|
|
ff.FF_Colisage,
|
|
ff.FF_QteMini,
|
|
ff.FF_QteMont,
|
|
ff.EG_Champ,
|
|
ff.FF_Devise,
|
|
ff.FF_Remise,
|
|
ff.FF_ConvDiv,
|
|
ff.FF_TypeRem,
|
|
|
|
-- Nombre d'articles
|
|
ISNULL(COUNT(DISTINCT a.AR_Ref), 0) as nb_articles
|
|
|
|
FROM F_FAMILLE f
|
|
|
|
-- Jointures comptables
|
|
LEFT JOIN F_FAMCOMPTA vte ON f.FA_CodeFamille = vte.FA_CodeFamille
|
|
AND vte.FCP_Type = 0 -- Vente
|
|
AND vte.FCP_Champ = 1 -- Compte principal
|
|
|
|
LEFT JOIN F_FAMCOMPTA ach ON f.FA_CodeFamille = ach.FA_CodeFamille
|
|
AND ach.FCP_Type = 1 -- Achat
|
|
AND ach.FCP_Champ = 1
|
|
|
|
LEFT JOIN F_FAMCOMPTA sto ON f.FA_CodeFamille = sto.FA_CodeFamille
|
|
AND sto.FCP_Type = 2 -- Stock
|
|
AND sto.FCP_Champ = 1
|
|
|
|
-- Fournisseur principal
|
|
LEFT JOIN F_FAMFOURNISS ff ON f.FA_CodeFamille = ff.FA_CodeFamille
|
|
AND ff.FF_Principal = 1
|
|
|
|
-- Nombre d'articles
|
|
LEFT JOIN F_ARTICLE a ON f.FA_CodeFamille = a.FA_CodeFamille
|
|
|
|
WHERE 1=1
|
|
"""
|
|
|
|
params = []
|
|
|
|
if not inclure_totaux:
|
|
query += " AND f.FA_Type = 0"
|
|
logger.info("[SQL] Filtre : FA_Type = 0 (Détail uniquement)")
|
|
|
|
if filtre:
|
|
query += """
|
|
AND (
|
|
f.FA_CodeFamille LIKE ?
|
|
OR f.FA_Intitule LIKE ?
|
|
)
|
|
"""
|
|
params.extend([f"%{filtre}%", f"%{filtre}%"])
|
|
|
|
query += """
|
|
GROUP BY
|
|
f.FA_CodeFamille, f.FA_Type, f.FA_Intitule, f.FA_UniteVen, f.FA_Coef,
|
|
f.FA_SuiviStock, f.FA_Garantie, f.FA_Central,
|
|
f.FA_Stat01, f.FA_Stat02, f.FA_Stat03, f.FA_Stat04, f.FA_Stat05,
|
|
f.FA_CodeFiscal, f.FA_Pays, f.FA_UnitePoids, f.FA_Escompte, f.FA_Delai,
|
|
f.FA_HorsStat, f.FA_VteDebit, f.FA_NotImp,
|
|
f.FA_Frais01FR_Denomination, f.FA_Frais01FR_Rem01REM_Valeur, f.FA_Frais01FR_Rem01REM_Type,
|
|
f.FA_Frais01FR_Rem02REM_Valeur, f.FA_Frais01FR_Rem02REM_Type,
|
|
f.FA_Frais01FR_Rem03REM_Valeur, f.FA_Frais01FR_Rem03REM_Type,
|
|
f.FA_Frais02FR_Denomination, f.FA_Frais02FR_Rem01REM_Valeur, f.FA_Frais02FR_Rem01REM_Type,
|
|
f.FA_Frais02FR_Rem02REM_Valeur, f.FA_Frais02FR_Rem02REM_Type,
|
|
f.FA_Frais02FR_Rem03REM_Valeur, f.FA_Frais02FR_Rem03REM_Type,
|
|
f.FA_Frais03FR_Denomination, f.FA_Frais03FR_Rem01REM_Valeur, f.FA_Frais03FR_Rem01REM_Type,
|
|
f.FA_Frais03FR_Rem02REM_Valeur, f.FA_Frais03FR_Rem02REM_Type,
|
|
f.FA_Frais03FR_Rem03REM_Valeur, f.FA_Frais03FR_Rem03REM_Type,
|
|
f.FA_Contremarque, f.FA_FactPoids, f.FA_FactForfait, f.FA_Publie,
|
|
f.FA_RacineRef, f.FA_RacineCB,
|
|
f.CL_No1, f.CL_No2, f.CL_No3, f.CL_No4,
|
|
f.FA_Nature, f.FA_NbColis, f.FA_SousTraitance, f.FA_Fictif, f.FA_Criticite,
|
|
f.cbMarq, f.cbCreateur, f.cbModification, f.cbCreation, f.cbCreationUser,
|
|
vte.FCP_ComptaCPT_CompteG, vte.FCP_ComptaCPT_CompteA,
|
|
vte.FCP_ComptaCPT_Taxe1, vte.FCP_ComptaCPT_Taxe2, vte.FCP_ComptaCPT_Taxe3,
|
|
vte.FCP_ComptaCPT_Date1, vte.FCP_ComptaCPT_Date2, vte.FCP_ComptaCPT_Date3,
|
|
vte.FCP_TypeFacture,
|
|
ach.FCP_ComptaCPT_CompteG, ach.FCP_ComptaCPT_CompteA,
|
|
ach.FCP_ComptaCPT_Taxe1, ach.FCP_ComptaCPT_Taxe2, ach.FCP_ComptaCPT_Taxe3,
|
|
ach.FCP_ComptaCPT_Date1, ach.FCP_ComptaCPT_Date2, ach.FCP_ComptaCPT_Date3,
|
|
ach.FCP_TypeFacture,
|
|
sto.FCP_ComptaCPT_CompteG, sto.FCP_ComptaCPT_CompteA,
|
|
ff.CT_Num, ff.FF_Unite, ff.FF_Conversion, ff.FF_DelaiAppro,
|
|
ff.FF_Garantie, ff.FF_Colisage, ff.FF_QteMini, ff.FF_QteMont,
|
|
ff.EG_Champ, ff.FF_Devise, ff.FF_Remise, ff.FF_ConvDiv, ff.FF_TypeRem
|
|
ORDER BY f.FA_Intitule
|
|
"""
|
|
|
|
cursor.execute(query, params)
|
|
rows = cursor.fetchall()
|
|
|
|
def to_str(val):
|
|
"""Convertit en string, gère None et int"""
|
|
if val is None:
|
|
return ""
|
|
return str(val).strip() if isinstance(val, str) else str(val)
|
|
|
|
def to_float(val):
|
|
"""Convertit en float, gère None"""
|
|
if val is None or val == "":
|
|
return 0.0
|
|
try:
|
|
return float(val)
|
|
except (ValueError, TypeError):
|
|
return 0.0
|
|
|
|
def to_int(val):
|
|
"""Convertit en int, gère None"""
|
|
if val is None or val == "":
|
|
return 0
|
|
try:
|
|
return int(val)
|
|
except (ValueError, TypeError):
|
|
return 0
|
|
|
|
def to_bool(val):
|
|
"""Convertit en bool"""
|
|
if val is None:
|
|
return False
|
|
if isinstance(val, bool):
|
|
return val
|
|
if isinstance(val, int):
|
|
return val != 0
|
|
return bool(val)
|
|
|
|
familles = []
|
|
|
|
for row in rows:
|
|
idx = 0
|
|
|
|
famille = {
|
|
"code": to_str(row[idx]),
|
|
"type": to_int(row[idx+1]),
|
|
"intitule": to_str(row[idx+2]),
|
|
"unite_vente": to_str(row[idx+3]),
|
|
"coef": to_float(row[idx+4]),
|
|
"suivi_stock": to_bool(row[idx+5]),
|
|
"garantie": to_int(row[idx+6]),
|
|
"est_centrale": to_bool(row[idx+7]),
|
|
}
|
|
idx += 8
|
|
|
|
famille.update({
|
|
"stat_01": to_str(row[idx]),
|
|
"stat_02": to_str(row[idx+1]),
|
|
"stat_03": to_str(row[idx+2]),
|
|
"stat_04": to_str(row[idx+3]),
|
|
"stat_05": to_str(row[idx+4]),
|
|
})
|
|
idx += 5
|
|
|
|
famille.update({
|
|
"code_fiscal": to_str(row[idx]),
|
|
"pays": to_str(row[idx+1]),
|
|
"unite_poids": to_str(row[idx+2]),
|
|
"escompte": to_bool(row[idx+3]),
|
|
"delai": to_int(row[idx+4]),
|
|
"hors_statistique": to_bool(row[idx+5]),
|
|
"vente_debit": to_bool(row[idx+6]),
|
|
"non_imprimable": to_bool(row[idx+7]),
|
|
})
|
|
idx += 8
|
|
|
|
famille.update({
|
|
"frais_01_libelle": to_str(row[idx]),
|
|
"frais_01_remise_1_valeur": to_float(row[idx+1]),
|
|
"frais_01_remise_1_type": to_int(row[idx+2]),
|
|
"frais_01_remise_2_valeur": to_float(row[idx+3]),
|
|
"frais_01_remise_2_type": to_int(row[idx+4]),
|
|
"frais_01_remise_3_valeur": to_float(row[idx+5]),
|
|
"frais_01_remise_3_type": to_int(row[idx+6]),
|
|
"frais_02_libelle": to_str(row[idx+7]),
|
|
"frais_02_remise_1_valeur": to_float(row[idx+8]),
|
|
"frais_02_remise_1_type": to_int(row[idx+9]),
|
|
"frais_02_remise_2_valeur": to_float(row[idx+10]),
|
|
"frais_02_remise_2_type": to_int(row[idx+11]),
|
|
"frais_02_remise_3_valeur": to_float(row[idx+12]),
|
|
"frais_02_remise_3_type": to_int(row[idx+13]),
|
|
"frais_03_libelle": to_str(row[idx+14]),
|
|
"frais_03_remise_1_valeur": to_float(row[idx+15]),
|
|
"frais_03_remise_1_type": to_int(row[idx+16]),
|
|
"frais_03_remise_2_valeur": to_float(row[idx+17]),
|
|
"frais_03_remise_2_type": to_int(row[idx+18]),
|
|
"frais_03_remise_3_valeur": to_float(row[idx+19]),
|
|
"frais_03_remise_3_type": to_int(row[idx+20]),
|
|
})
|
|
idx += 21
|
|
|
|
famille.update({
|
|
"contremarque": to_bool(row[idx]),
|
|
"fact_poids": to_bool(row[idx+1]),
|
|
"fact_forfait": to_bool(row[idx+2]),
|
|
"publie": to_bool(row[idx+3]),
|
|
"racine_reference": to_str(row[idx+4]),
|
|
"racine_code_barre": to_str(row[idx+5]),
|
|
})
|
|
idx += 6
|
|
|
|
famille.update({
|
|
"categorie_1": to_int(row[idx]),
|
|
"categorie_2": to_int(row[idx+1]),
|
|
"categorie_3": to_int(row[idx+2]),
|
|
"categorie_4": to_int(row[idx+3]),
|
|
})
|
|
idx += 4
|
|
|
|
famille.update({
|
|
"nature": to_int(row[idx]),
|
|
"nb_colis": to_int(row[idx+1]),
|
|
"sous_traitance": to_bool(row[idx+2]),
|
|
"fictif": to_bool(row[idx+3]),
|
|
"criticite": to_int(row[idx+4]),
|
|
})
|
|
idx += 5
|
|
|
|
famille.update({
|
|
"cb_marq": to_int(row[idx]),
|
|
"cb_createur": to_str(row[idx+1]),
|
|
"cb_modification": row[idx+2], # datetime - garder tel quel
|
|
"cb_creation": row[idx+3], # datetime - garder tel quel
|
|
"cb_creation_user": to_str(row[idx+4]),
|
|
})
|
|
idx += 5
|
|
|
|
famille.update({
|
|
"compte_vente": to_str(row[idx]),
|
|
"compte_auxiliaire_vente": to_str(row[idx+1]),
|
|
"tva_vente_1": to_str(row[idx+2]),
|
|
"tva_vente_2": to_str(row[idx+3]),
|
|
"tva_vente_3": to_str(row[idx+4]),
|
|
"tva_vente_date_1": row[idx+5], # datetime
|
|
"tva_vente_date_2": row[idx+6],
|
|
"tva_vente_date_3": row[idx+7],
|
|
"type_facture_vente": to_int(row[idx+8]),
|
|
})
|
|
idx += 9
|
|
|
|
famille.update({
|
|
"compte_achat": to_str(row[idx]),
|
|
"compte_auxiliaire_achat": to_str(row[idx+1]),
|
|
"tva_achat_1": to_str(row[idx+2]),
|
|
"tva_achat_2": to_str(row[idx+3]),
|
|
"tva_achat_3": to_str(row[idx+4]),
|
|
"tva_achat_date_1": row[idx+5],
|
|
"tva_achat_date_2": row[idx+6],
|
|
"tva_achat_date_3": row[idx+7],
|
|
"type_facture_achat": to_int(row[idx+8]),
|
|
})
|
|
idx += 9
|
|
|
|
famille.update({
|
|
"compte_stock": to_str(row[idx]),
|
|
"compte_auxiliaire_stock": to_str(row[idx+1]),
|
|
})
|
|
idx += 2
|
|
|
|
famille.update({
|
|
"fournisseur_principal": to_str(row[idx]),
|
|
"fournisseur_unite": to_str(row[idx+1]),
|
|
"fournisseur_conversion": to_float(row[idx+2]),
|
|
"fournisseur_delai_appro": to_int(row[idx+3]),
|
|
"fournisseur_garantie": to_int(row[idx+4]),
|
|
"fournisseur_colisage": to_int(row[idx+5]),
|
|
"fournisseur_qte_mini": to_float(row[idx+6]),
|
|
"fournisseur_qte_mont": to_float(row[idx+7]),
|
|
"fournisseur_enumere_gamme": to_int(row[idx+8]),
|
|
"fournisseur_devise": to_int(row[idx+9]),
|
|
"fournisseur_remise": to_float(row[idx+10]),
|
|
"fournisseur_conv_div": to_float(row[idx+11]),
|
|
"fournisseur_type_remise": to_int(row[idx+12]),
|
|
})
|
|
idx += 13
|
|
|
|
famille["nb_articles"] = to_int(row[idx])
|
|
|
|
famille["type_libelle"] = "Total" if famille["type"] == 1 else "Détail"
|
|
famille["est_total"] = famille["type"] == 1
|
|
famille["est_detail"] = famille["type"] == 0
|
|
|
|
famille["FA_CodeFamille"] = famille["code"]
|
|
famille["FA_Intitule"] = famille["intitule"]
|
|
famille["FA_Type"] = famille["type"]
|
|
famille["CG_NumVte"] = famille["compte_vente"]
|
|
famille["CG_NumAch"] = famille["compte_achat"]
|
|
|
|
familles.append(famille)
|
|
|
|
type_msg = "DÉTAIL uniquement" if not inclure_totaux else "TOUS types"
|
|
logger.info(f"✓ {len(familles)} familles chargées ({type_msg})")
|
|
|
|
return familles
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur SQL familles: {e}", exc_info=True)
|
|
raise RuntimeError(f"Erreur lecture familles: {str(e)}")
|
|
|
|
|
|
def lire_famille(self, code: str) -> Dict:
|
|
"""
|
|
Lit une seule famille - même structure que lister_toutes_familles
|
|
|
|
Args:
|
|
code: Code de la famille à lire
|
|
|
|
Returns:
|
|
Dict avec la structure identique à lister_toutes_familles
|
|
"""
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
logger.info(f"[SQL] Lecture famille : {code}")
|
|
|
|
cursor.execute("SELECT TOP 1 * FROM F_FAMILLE")
|
|
colonnes_disponibles = [column[0] for column in cursor.description]
|
|
|
|
logger.info(f"[SQL] Colonnes trouvées : {len(colonnes_disponibles)}")
|
|
|
|
colonnes_souhaitees = [
|
|
"FA_CodeFamille",
|
|
"FA_Intitule",
|
|
"FA_Type",
|
|
|
|
"FA_UniteVen",
|
|
"FA_Coef",
|
|
"FA_SuiviStock",
|
|
"FA_Garantie",
|
|
"FA_UnitePoids",
|
|
"FA_Delai",
|
|
"FA_NbColis",
|
|
|
|
"CG_NumAch",
|
|
"CG_NumVte",
|
|
"FA_CodeFiscal",
|
|
"FA_Escompte",
|
|
|
|
"FA_Central",
|
|
"FA_Nature",
|
|
"CL_No1",
|
|
"CL_No2",
|
|
"CL_No3",
|
|
"CL_No4",
|
|
|
|
"FA_Stat01",
|
|
"FA_Stat02",
|
|
"FA_Stat03",
|
|
"FA_Stat04",
|
|
"FA_Stat05",
|
|
"FA_HorsStat",
|
|
|
|
"FA_Pays",
|
|
"FA_VteDebit",
|
|
"FA_NotImp",
|
|
"FA_Contremarque",
|
|
"FA_FactPoids",
|
|
"FA_FactForfait",
|
|
"FA_Publie",
|
|
|
|
"FA_RacineRef",
|
|
"FA_RacineCB",
|
|
"FA_Raccourci",
|
|
|
|
"FA_SousTraitance",
|
|
"FA_Fictif",
|
|
"FA_Criticite"
|
|
]
|
|
|
|
colonnes_a_lire = [
|
|
col for col in colonnes_souhaitees if col in colonnes_disponibles
|
|
]
|
|
|
|
if not colonnes_a_lire:
|
|
colonnes_a_lire = colonnes_disponibles
|
|
|
|
logger.info(f"[SQL] Colonnes sélectionnées : {len(colonnes_a_lire)}")
|
|
|
|
colonnes_str = ", ".join([f"f.{col}" for col in colonnes_a_lire])
|
|
|
|
query = f"""
|
|
SELECT {colonnes_str},
|
|
ISNULL(COUNT(a.AR_Ref), 0) as nb_articles
|
|
FROM F_FAMILLE f
|
|
LEFT JOIN F_ARTICLE a ON f.FA_CodeFamille = a.FA_CodeFamille
|
|
WHERE UPPER(f.FA_CodeFamille) = ?
|
|
GROUP BY {colonnes_str}
|
|
"""
|
|
|
|
cursor.execute(query, (code.upper().strip(),))
|
|
row = cursor.fetchone()
|
|
|
|
if not row:
|
|
raise ValueError(f"Famille '{code}' introuvable dans Sage")
|
|
|
|
famille = {}
|
|
|
|
for idx, colonne in enumerate(colonnes_a_lire):
|
|
valeur = row[idx]
|
|
|
|
if isinstance(valeur, str):
|
|
valeur = valeur.strip()
|
|
|
|
famille[colonne] = valeur
|
|
|
|
famille["nb_articles"] = row[-1]
|
|
|
|
if "FA_CodeFamille" in famille:
|
|
famille["code"] = famille["FA_CodeFamille"]
|
|
|
|
if "FA_Intitule" in famille:
|
|
famille["intitule"] = famille["FA_Intitule"]
|
|
|
|
if "FA_Type" in famille:
|
|
type_val = famille["FA_Type"]
|
|
famille["type"] = type_val
|
|
famille["type_libelle"] = "Total" if type_val == 1 else "Détail"
|
|
famille["est_total"] = type_val == 1
|
|
else:
|
|
famille["type"] = 0
|
|
famille["type_libelle"] = "Détail"
|
|
famille["est_total"] = False
|
|
|
|
famille["unite_vente"] = str(famille.get("FA_UniteVen", ""))
|
|
famille["unite_poids"] = str(famille.get("FA_UnitePoids", ""))
|
|
famille["coef"] = (
|
|
float(famille.get("FA_Coef", 0.0))
|
|
if famille.get("FA_Coef") is not None
|
|
else 0.0
|
|
)
|
|
|
|
famille["suivi_stock"] = bool(famille.get("FA_SuiviStock", 0))
|
|
famille["garantie"] = int(famille.get("FA_Garantie", 0))
|
|
famille["delai"] = int(famille.get("FA_Delai", 0))
|
|
famille["nb_colis"] = int(famille.get("FA_NbColis", 0))
|
|
|
|
famille["compte_achat"] = famille.get("CG_NumAch", "")
|
|
famille["compte_vente"] = famille.get("CG_NumVte", "")
|
|
famille["code_fiscal"] = famille.get("FA_CodeFiscal", "")
|
|
famille["escompte"] = bool(famille.get("FA_Escompte", 0))
|
|
|
|
famille["est_centrale"] = bool(famille.get("FA_Central", 0))
|
|
famille["nature"] = famille.get("FA_Nature", 0)
|
|
famille["pays"] = famille.get("FA_Pays", "")
|
|
|
|
famille["categorie_1"] = famille.get("CL_No1", 0)
|
|
famille["categorie_2"] = famille.get("CL_No2", 0)
|
|
famille["categorie_3"] = famille.get("CL_No3", 0)
|
|
famille["categorie_4"] = famille.get("CL_No4", 0)
|
|
|
|
famille["stat_01"] = famille.get("FA_Stat01", "")
|
|
famille["stat_02"] = famille.get("FA_Stat02", "")
|
|
famille["stat_03"] = famille.get("FA_Stat03", "")
|
|
famille["stat_04"] = famille.get("FA_Stat04", "")
|
|
famille["stat_05"] = famille.get("FA_Stat05", "")
|
|
famille["hors_statistique"] = bool(famille.get("FA_HorsStat", 0))
|
|
|
|
famille["vente_debit"] = bool(famille.get("FA_VteDebit", 0))
|
|
famille["non_imprimable"] = bool(famille.get("FA_NotImp", 0))
|
|
famille["contremarque"] = bool(famille.get("FA_Contremarque", 0))
|
|
famille["fact_poids"] = bool(famille.get("FA_FactPoids", 0))
|
|
famille["fact_forfait"] = bool(famille.get("FA_FactForfait", 0))
|
|
famille["publie"] = bool(famille.get("FA_Publie", 0))
|
|
|
|
famille["racine_reference"] = famille.get("FA_RacineRef", "")
|
|
famille["racine_code_barre"] = famille.get("FA_RacineCB", "")
|
|
famille["raccourci"] = famille.get("FA_Raccourci", "")
|
|
|
|
famille["sous_traitance"] = bool(famille.get("FA_SousTraitance", 0))
|
|
famille["fictif"] = bool(famille.get("FA_Fictif", 0))
|
|
famille["criticite"] = int(famille.get("FA_Criticite", 0))
|
|
|
|
logger.info(f"SQL: Famille '{famille['code']}' chargée ({famille['nb_articles']} articles)")
|
|
|
|
return famille
|
|
|
|
except ValueError as e:
|
|
logger.error(f"Erreur famille: {e}")
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur SQL famille: {e}", exc_info=True)
|
|
raise RuntimeError(f"Erreur lecture famille: {str(e)}")
|
|
|
|
|
|
def creer_entree_stock(self, entree_data: Dict) -> Dict:
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info(f"[STOCK] === CRÉATION ENTRÉE STOCK (COM pur) ===")
|
|
|
|
transaction_active = False
|
|
try:
|
|
self.cial.CptaApplication.BeginTrans()
|
|
transaction_active = True
|
|
logger.debug("Transaction démarrée")
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
factory_doc = self.cial.FactoryDocumentStock
|
|
persist_doc = factory_doc.CreateType(180) # 180 = Entrée
|
|
doc = win32com.client.CastTo(persist_doc, "IBODocumentStock3")
|
|
doc.SetDefault()
|
|
|
|
date_mouv = entree_data.get("date_mouvement")
|
|
if isinstance(date_mouv, date):
|
|
doc.DO_Date = pywintypes.Time(
|
|
datetime.combine(date_mouv, datetime.min.time())
|
|
)
|
|
else:
|
|
doc.DO_Date = pywintypes.Time(datetime.now())
|
|
|
|
if entree_data.get("reference"):
|
|
doc.DO_Ref = entree_data["reference"]
|
|
|
|
doc.Write()
|
|
logger.info(f"[STOCK] Document créé")
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
factory_depot = self.cial.FactoryDepot
|
|
|
|
stocks_mis_a_jour = []
|
|
depot_principal = None
|
|
|
|
try:
|
|
persist_depot = factory_depot.List(1)
|
|
if persist_depot:
|
|
depot_principal = win32com.client.CastTo(
|
|
persist_depot, "IBODepot3"
|
|
)
|
|
depot_principal.Read()
|
|
logger.info(
|
|
f"Dépôt principal: {getattr(depot_principal, 'DE_Code', '?')}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Erreur chargement dépôt: {e}")
|
|
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentStockLigne
|
|
|
|
for idx, ligne_data in enumerate(entree_data["lignes"], 1):
|
|
article_ref = ligne_data["article_ref"].upper()
|
|
quantite = ligne_data["quantite"]
|
|
stock_mini = ligne_data.get("stock_mini")
|
|
stock_maxi = ligne_data.get("stock_maxi")
|
|
|
|
logger.info(f"[STOCK] Ligne {idx}: {article_ref} x {quantite}")
|
|
|
|
persist_article = factory_article.ReadReference(article_ref)
|
|
if not persist_article:
|
|
raise ValueError(f"Article {article_ref} introuvable")
|
|
|
|
article_obj = win32com.client.CastTo(
|
|
persist_article, "IBOArticle3"
|
|
)
|
|
article_obj.Read()
|
|
|
|
ligne_persist = factory_lignes.Create()
|
|
try:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentLigne3"
|
|
)
|
|
except:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentStockLigne3"
|
|
)
|
|
|
|
ligne_obj.SetDefault()
|
|
|
|
try:
|
|
ligne_obj.SetDefaultArticleReference(
|
|
article_ref, float(quantite)
|
|
)
|
|
except:
|
|
try:
|
|
ligne_obj.SetDefaultArticle(
|
|
article_obj, float(quantite)
|
|
)
|
|
except:
|
|
raise ValueError(
|
|
f"Impossible de lier l'article {article_ref}"
|
|
)
|
|
|
|
prix = ligne_data.get("prix_unitaire")
|
|
if prix:
|
|
try:
|
|
ligne_obj.DL_PrixUnitaire = float(prix)
|
|
except:
|
|
pass
|
|
|
|
ligne_obj.Write()
|
|
|
|
if stock_mini is not None or stock_maxi is not None:
|
|
logger.info(
|
|
f"[STOCK] Ajustement stock pour {article_ref}..."
|
|
)
|
|
|
|
try:
|
|
logger.info(
|
|
f" [COM] Méthode A : Article.FactoryArticleStock"
|
|
)
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
persist_article_full = factory_article.ReadReference(
|
|
article_ref
|
|
)
|
|
article_full = win32com.client.CastTo(
|
|
persist_article_full, "IBOArticle3"
|
|
)
|
|
article_full.Read()
|
|
|
|
factory_article_stock = None
|
|
try:
|
|
factory_article_stock = (
|
|
article_full.FactoryArticleStock
|
|
)
|
|
logger.info(" FactoryArticleStock trouvée")
|
|
except AttributeError:
|
|
logger.warning(
|
|
" FactoryArticleStock non disponible"
|
|
)
|
|
|
|
if factory_article_stock:
|
|
stock_trouve = None
|
|
index_stock = 1
|
|
|
|
while index_stock <= 100:
|
|
try:
|
|
stock_persist = factory_article_stock.List(
|
|
index_stock
|
|
)
|
|
if stock_persist is None:
|
|
break
|
|
|
|
stock_obj = win32com.client.CastTo(
|
|
stock_persist, "IBOArticleStock3"
|
|
)
|
|
stock_obj.Read()
|
|
|
|
depot_stock = None
|
|
try:
|
|
depot_stock = getattr(
|
|
stock_obj, "Depot", None
|
|
)
|
|
if depot_stock:
|
|
depot_stock.Read()
|
|
depot_code = getattr(
|
|
depot_stock, "DE_Code", ""
|
|
).strip()
|
|
logger.debug(
|
|
f" Dépôt {index_stock}: {depot_code}"
|
|
)
|
|
|
|
if (
|
|
not stock_trouve
|
|
or depot_code
|
|
== getattr(
|
|
depot_principal,
|
|
"DE_Code",
|
|
"",
|
|
)
|
|
):
|
|
stock_trouve = stock_obj
|
|
logger.info(
|
|
f" Stock trouvé pour dépôt {depot_code}"
|
|
)
|
|
except:
|
|
pass
|
|
|
|
index_stock += 1
|
|
except Exception as e:
|
|
logger.debug(
|
|
f" Erreur stock {index_stock}: {e}"
|
|
)
|
|
index_stock += 1
|
|
|
|
if not stock_trouve:
|
|
try:
|
|
stock_persist = (
|
|
factory_article_stock.Create()
|
|
)
|
|
stock_trouve = win32com.client.CastTo(
|
|
stock_persist, "IBOArticleStock3"
|
|
)
|
|
stock_trouve.SetDefault()
|
|
|
|
if depot_principal:
|
|
try:
|
|
stock_trouve.Depot = depot_principal
|
|
logger.info(
|
|
" Dépôt principal lié"
|
|
)
|
|
except:
|
|
pass
|
|
|
|
logger.info(" Nouvel ArticleStock créé")
|
|
except Exception as e:
|
|
logger.error(
|
|
f" Impossible de créer ArticleStock: {e}"
|
|
)
|
|
raise
|
|
|
|
if stock_trouve:
|
|
try:
|
|
stock_trouve.Read()
|
|
except:
|
|
pass
|
|
|
|
if stock_mini is not None:
|
|
try:
|
|
for prop_name in [
|
|
"AS_QteMini",
|
|
"AS_Mini",
|
|
"AR_StockMini",
|
|
"StockMini",
|
|
]:
|
|
try:
|
|
setattr(
|
|
stock_trouve,
|
|
prop_name,
|
|
float(stock_mini),
|
|
)
|
|
logger.info(
|
|
f" Stock mini défini via {prop_name}: {stock_mini}"
|
|
)
|
|
break
|
|
except AttributeError:
|
|
continue
|
|
except Exception as e:
|
|
logger.debug(
|
|
f" {prop_name} échoué: {e}"
|
|
)
|
|
continue
|
|
except Exception as e:
|
|
logger.warning(
|
|
f" Stock mini non défini: {e}"
|
|
)
|
|
|
|
if stock_maxi is not None:
|
|
try:
|
|
for prop_name in [
|
|
"AS_QteMaxi",
|
|
"AS_Maxi",
|
|
"AR_StockMaxi",
|
|
"StockMaxi",
|
|
]:
|
|
try:
|
|
setattr(
|
|
stock_trouve,
|
|
prop_name,
|
|
float(stock_maxi),
|
|
)
|
|
logger.info(
|
|
f" Stock maxi défini via {prop_name}: {stock_maxi}"
|
|
)
|
|
break
|
|
except AttributeError:
|
|
continue
|
|
except Exception as e:
|
|
logger.debug(
|
|
f" {prop_name} échoué: {e}"
|
|
)
|
|
continue
|
|
except Exception as e:
|
|
logger.warning(
|
|
f" Stock maxi non défini: {e}"
|
|
)
|
|
|
|
try:
|
|
stock_trouve.Write()
|
|
logger.info(
|
|
f" ArticleStock sauvegardé"
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
f" Erreur Write() ArticleStock: {e}"
|
|
)
|
|
raise
|
|
|
|
if depot_principal and (
|
|
stock_mini is not None or stock_maxi is not None
|
|
):
|
|
logger.info(
|
|
f" [COM] Méthode B : Depot.FactoryDepotStock (alternative)"
|
|
)
|
|
|
|
try:
|
|
factory_depot_stock = None
|
|
for factory_name in [
|
|
"FactoryDepotStock",
|
|
"FactoryArticleStock",
|
|
]:
|
|
try:
|
|
factory_depot_stock = getattr(
|
|
depot_principal, factory_name, None
|
|
)
|
|
if factory_depot_stock:
|
|
logger.info(
|
|
f" Factory trouvée: {factory_name}"
|
|
)
|
|
break
|
|
except:
|
|
continue
|
|
|
|
if factory_depot_stock:
|
|
stock_depot_trouve = None
|
|
index_ds = 1
|
|
|
|
while index_ds <= 100:
|
|
try:
|
|
stock_ds_persist = (
|
|
factory_depot_stock.List(
|
|
index_ds
|
|
)
|
|
)
|
|
if stock_ds_persist is None:
|
|
break
|
|
|
|
stock_ds = win32com.client.CastTo(
|
|
stock_ds_persist,
|
|
"IBODepotStock3",
|
|
)
|
|
stock_ds.Read()
|
|
|
|
ar_ref_ds = (
|
|
getattr(stock_ds, "AR_Ref", "")
|
|
.strip()
|
|
.upper()
|
|
)
|
|
if ar_ref_ds == article_ref:
|
|
stock_depot_trouve = stock_ds
|
|
break
|
|
|
|
index_ds += 1
|
|
except:
|
|
index_ds += 1
|
|
|
|
if not stock_depot_trouve:
|
|
try:
|
|
stock_ds_persist = (
|
|
factory_depot_stock.Create()
|
|
)
|
|
stock_depot_trouve = (
|
|
win32com.client.CastTo(
|
|
stock_ds_persist,
|
|
"IBODepotStock3",
|
|
)
|
|
)
|
|
stock_depot_trouve.SetDefault()
|
|
stock_depot_trouve.AR_Ref = (
|
|
article_ref
|
|
)
|
|
logger.info(
|
|
" Nouveau DepotStock créé"
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
f" Impossible de créer DepotStock: {e}"
|
|
)
|
|
|
|
if stock_depot_trouve:
|
|
if stock_mini is not None:
|
|
try:
|
|
stock_depot_trouve.AS_QteMini = float(
|
|
stock_mini
|
|
)
|
|
logger.info(
|
|
f" DepotStock.AS_QteMini = {stock_mini}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f" DepotStock mini échoué: {e}"
|
|
)
|
|
|
|
if stock_maxi is not None:
|
|
try:
|
|
stock_depot_trouve.AS_QteMaxi = float(
|
|
stock_maxi
|
|
)
|
|
logger.info(
|
|
f" DepotStock.AS_QteMaxi = {stock_maxi}"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f" DepotStock maxi échoué: {e}"
|
|
)
|
|
|
|
try:
|
|
stock_depot_trouve.Write()
|
|
logger.info(
|
|
" DepotStock sauvegardé"
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
f" DepotStock Write() échoué: {e}"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.warning(f" Méthode B échouée: {e}")
|
|
|
|
except Exception as e:
|
|
logger.error(
|
|
f"[STOCK] Erreur ajustement stock: {e}",
|
|
exc_info=True,
|
|
)
|
|
|
|
stocks_mis_a_jour.append(
|
|
{
|
|
"article_ref": article_ref,
|
|
"quantite_ajoutee": quantite,
|
|
"stock_mini_defini": stock_mini,
|
|
"stock_maxi_defini": stock_maxi,
|
|
}
|
|
)
|
|
|
|
doc.Write()
|
|
doc.Read()
|
|
|
|
numero = getattr(doc, "DO_Piece", "")
|
|
logger.info(f"[STOCK] Document finalisé: {numero}")
|
|
|
|
logger.info(f"[STOCK] Vérification finale via COM...")
|
|
|
|
for stock_info in stocks_mis_a_jour:
|
|
article_ref = stock_info["article_ref"]
|
|
|
|
try:
|
|
persist_article = factory_article.ReadReference(article_ref)
|
|
article_verif = win32com.client.CastTo(
|
|
persist_article, "IBOArticle3"
|
|
)
|
|
article_verif.Read()
|
|
|
|
stock_total = 0.0
|
|
stock_mini_lu = 0.0
|
|
stock_maxi_lu = 0.0
|
|
|
|
for attr in ["AR_Stock", "AS_QteSto", "Stock"]:
|
|
try:
|
|
val = getattr(article_verif, attr, None)
|
|
if val is not None:
|
|
stock_total = float(val)
|
|
break
|
|
except:
|
|
pass
|
|
|
|
for attr in ["AR_StockMini", "AS_QteMini", "StockMini"]:
|
|
try:
|
|
val = getattr(article_verif, attr, None)
|
|
if val is not None:
|
|
stock_mini_lu = float(val)
|
|
break
|
|
except:
|
|
pass
|
|
|
|
for attr in ["AR_StockMaxi", "AS_QteMaxi", "StockMaxi"]:
|
|
try:
|
|
val = getattr(article_verif, attr, None)
|
|
if val is not None:
|
|
stock_maxi_lu = float(val)
|
|
break
|
|
except:
|
|
pass
|
|
|
|
logger.info(
|
|
f"[VERIF] {article_ref}: "
|
|
f"Total={stock_total}, "
|
|
f"Mini={stock_mini_lu}, "
|
|
f"Maxi={stock_maxi_lu}"
|
|
)
|
|
|
|
stock_info["stock_total_verifie"] = stock_total
|
|
stock_info["stock_mini_verifie"] = stock_mini_lu
|
|
stock_info["stock_maxi_verifie"] = stock_maxi_lu
|
|
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"[VERIF] Erreur vérification {article_ref}: {e}"
|
|
)
|
|
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.CommitTrans()
|
|
logger.info(f"[STOCK] Transaction committée")
|
|
except:
|
|
logger.info(f"[STOCK] Changements sauvegardés")
|
|
|
|
return {
|
|
"article_ref": article_ref,
|
|
"numero": numero,
|
|
"type": 180,
|
|
"type_libelle": "Entrée en stock",
|
|
"date": str(getattr(doc, "DO_Date", "")),
|
|
"nb_lignes": len(stocks_mis_a_jour),
|
|
"stocks_mis_a_jour": stocks_mis_a_jour,
|
|
}
|
|
|
|
except Exception as e:
|
|
if transaction_active:
|
|
try:
|
|
self.cial.CptaApplication.RollbackTrans()
|
|
logger.info(f"[STOCK] Transaction annulée")
|
|
except:
|
|
pass
|
|
|
|
logger.error(f"[STOCK] ERREUR : {e}", exc_info=True)
|
|
raise ValueError(f"Erreur création entrée stock : {str(e)}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"[STOCK] ERREUR GLOBALE : {e}", exc_info=True)
|
|
raise ValueError(f"Erreur création entrée stock : {str(e)}")
|
|
|
|
def lire_stock_article(self, reference: str, calcul_complet: bool = False) -> Dict:
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info(f"[STOCK] Lecture stock : {reference}")
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
persist_article = factory_article.ReadReference(reference.upper())
|
|
|
|
if not persist_article:
|
|
raise ValueError(f"Article {reference} introuvable")
|
|
|
|
article = win32com.client.CastTo(persist_article, "IBOArticle3")
|
|
article.Read()
|
|
|
|
ar_suivi = getattr(article, "AR_SuiviStock", 0)
|
|
ar_design = getattr(article, "AR_Design", reference)
|
|
|
|
stock_info = {
|
|
"article": reference.upper(),
|
|
"designation": ar_design,
|
|
"stock_total": 0.0,
|
|
"suivi_stock": ar_suivi,
|
|
"suivi_libelle": {
|
|
0: "Aucun suivi",
|
|
1: "CMUP (sans lot)",
|
|
2: "FIFO/LIFO (avec lot)",
|
|
}.get(ar_suivi, f"Code {ar_suivi}"),
|
|
"depots": [],
|
|
"methode_lecture": None,
|
|
}
|
|
|
|
logger.info("[STOCK] Tentative Méthode 1 : Depot.FactoryDepotStock...")
|
|
|
|
try:
|
|
factory_depot = self.cial.FactoryDepot
|
|
index_depot = 1
|
|
stocks_trouves = []
|
|
|
|
while index_depot <= 20:
|
|
try:
|
|
persist_depot = factory_depot.List(index_depot)
|
|
if persist_depot is None:
|
|
break
|
|
|
|
depot = win32com.client.CastTo(persist_depot, "IBODepot3")
|
|
depot.Read()
|
|
|
|
depot_code = ""
|
|
depot_intitule = ""
|
|
|
|
try:
|
|
depot_code = getattr(depot, "DE_Code", "").strip()
|
|
depot_intitule = getattr(
|
|
depot, "DE_Intitule", f"Dépôt {depot_code}"
|
|
)
|
|
except:
|
|
pass
|
|
|
|
if not depot_code:
|
|
index_depot += 1
|
|
continue
|
|
|
|
factory_depot_stock = None
|
|
|
|
for factory_name in [
|
|
"FactoryDepotStock",
|
|
"FactoryArticleStock",
|
|
]:
|
|
try:
|
|
factory_depot_stock = getattr(
|
|
depot, factory_name, None
|
|
)
|
|
if factory_depot_stock:
|
|
break
|
|
except:
|
|
pass
|
|
|
|
if factory_depot_stock:
|
|
index_stock = 1
|
|
|
|
while index_stock <= 1000:
|
|
try:
|
|
stock_persist = factory_depot_stock.List(
|
|
index_stock
|
|
)
|
|
if stock_persist is None:
|
|
break
|
|
|
|
stock = win32com.client.CastTo(
|
|
stock_persist, "IBODepotStock3"
|
|
)
|
|
stock.Read()
|
|
|
|
article_ref_stock = ""
|
|
|
|
for attr_ref in [
|
|
"AR_Ref",
|
|
"AS_Article",
|
|
"Article_Ref",
|
|
]:
|
|
try:
|
|
val = getattr(stock, attr_ref, None)
|
|
if val:
|
|
article_ref_stock = (
|
|
str(val).strip().upper()
|
|
)
|
|
break
|
|
except:
|
|
pass
|
|
|
|
if not article_ref_stock:
|
|
try:
|
|
article_obj = getattr(
|
|
stock, "Article", None
|
|
)
|
|
if article_obj:
|
|
article_obj.Read()
|
|
article_ref_stock = (
|
|
getattr(
|
|
article_obj, "AR_Ref", ""
|
|
)
|
|
.strip()
|
|
.upper()
|
|
)
|
|
except:
|
|
pass
|
|
|
|
if article_ref_stock == reference.upper():
|
|
quantite = 0.0
|
|
qte_mini = 0.0
|
|
qte_maxi = 0.0
|
|
|
|
for attr_qte in [
|
|
"AS_QteSto",
|
|
"AS_Qte",
|
|
"QteSto",
|
|
"Quantite",
|
|
]:
|
|
try:
|
|
val = getattr(stock, attr_qte, None)
|
|
if val is not None:
|
|
quantite = float(val)
|
|
break
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
qte_mini = float(
|
|
getattr(stock, "AS_QteMini", 0.0)
|
|
)
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
qte_maxi = float(
|
|
getattr(stock, "AS_QteMaxi", 0.0)
|
|
)
|
|
except:
|
|
pass
|
|
|
|
stocks_trouves.append(
|
|
{
|
|
"code": depot_code,
|
|
"intitule": depot_intitule,
|
|
"quantite": quantite,
|
|
"qte_mini": qte_mini,
|
|
"qte_maxi": qte_maxi,
|
|
}
|
|
)
|
|
|
|
logger.info(
|
|
f"[STOCK] Trouvé dépôt {depot_code} : {quantite} unités"
|
|
)
|
|
break
|
|
|
|
index_stock += 1
|
|
|
|
except Exception as e:
|
|
if "Accès refusé" in str(e):
|
|
break
|
|
index_stock += 1
|
|
|
|
index_depot += 1
|
|
|
|
except Exception as e:
|
|
if "Accès refusé" in str(e):
|
|
break
|
|
index_depot += 1
|
|
|
|
if stocks_trouves:
|
|
stock_info["depots"] = stocks_trouves
|
|
stock_info["stock_total"] = sum(
|
|
d["quantite"] for d in stocks_trouves
|
|
)
|
|
stock_info["methode_lecture"] = (
|
|
"Depot.FactoryDepotStock (RAPIDE)"
|
|
)
|
|
|
|
logger.info(
|
|
f"[STOCK] Méthode 1 réussie : {stock_info['stock_total']} unités"
|
|
)
|
|
return stock_info
|
|
|
|
except Exception as e:
|
|
logger.warning(f"[STOCK] Méthode 1 échouée : {e}")
|
|
|
|
logger.info("[STOCK] Tentative Méthode 2 : Attributs Article...")
|
|
|
|
try:
|
|
stock_trouve = False
|
|
|
|
for attr_stock in ["AR_Stock", "AR_QteSto", "AR_QteStock"]:
|
|
try:
|
|
val = getattr(article, attr_stock, None)
|
|
if val is not None:
|
|
stock_info["stock_total"] = float(val)
|
|
stock_info["methode_lecture"] = (
|
|
f"Article.{attr_stock} (RAPIDE)"
|
|
)
|
|
stock_trouve = True
|
|
logger.info(
|
|
f"[STOCK] Méthode 2 réussie via {attr_stock}"
|
|
)
|
|
break
|
|
except:
|
|
pass
|
|
|
|
if stock_trouve:
|
|
return stock_info
|
|
|
|
except Exception as e:
|
|
logger.warning(f"[STOCK] Méthode 2 échouée : {e}")
|
|
|
|
|
|
if not calcul_complet:
|
|
logger.warning(
|
|
f"[STOCK] Méthodes rapides échouées pour {reference}"
|
|
)
|
|
|
|
stock_info["methode_lecture"] = "ÉCHEC - Méthodes rapides échouées"
|
|
stock_info["stock_total"] = 0.0
|
|
stock_info["note"] = (
|
|
"Les méthodes rapides de lecture de stock ont échoué. "
|
|
"Options disponibles :\n"
|
|
"1. Utiliser GET /sage/diagnostic/stock-rapide-test/{reference} pour un diagnostic détaillé\n"
|
|
"2. Appeler lire_stock_article(reference, calcul_complet=True) pour un calcul depuis mouvements (LENT - 20+ min)\n"
|
|
"3. Vérifier que l'article a bien un suivi de stock activé (AR_SuiviStock)"
|
|
)
|
|
|
|
return stock_info
|
|
|
|
logger.warning(
|
|
"[STOCK] CALCUL DEPUIS MOUVEMENTS ACTIVÉ - PEUT PRENDRE 20+ MINUTES"
|
|
)
|
|
|
|
|
|
except ValueError:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"[STOCK] Erreur lecture : {e}", exc_info=True)
|
|
raise ValueError(f"Erreur lecture stock : {str(e)}")
|
|
|
|
def creer_sortie_stock(self, sortie_data: Dict) -> Dict:
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
logger.info(f"[STOCK] === CRÉATION SORTIE STOCK ===")
|
|
logger.info(f"[STOCK] {len(sortie_data.get('lignes', []))} ligne(s)")
|
|
|
|
try:
|
|
self.cial.CptaApplication.BeginTrans()
|
|
except:
|
|
pass
|
|
|
|
try:
|
|
factory = self.cial.FactoryDocumentStock
|
|
persist = factory.CreateType(181) # 181 = Sortie
|
|
doc = win32com.client.CastTo(persist, "IBODocumentStock3")
|
|
doc.SetDefault()
|
|
|
|
|
|
date_mouv = sortie_data.get("date_mouvement")
|
|
if isinstance(date_mouv, date):
|
|
doc.DO_Date = pywintypes.Time(
|
|
datetime.combine(date_mouv, datetime.min.time())
|
|
)
|
|
else:
|
|
doc.DO_Date = pywintypes.Time(datetime.now())
|
|
|
|
if sortie_data.get("reference"):
|
|
doc.DO_Ref = sortie_data["reference"]
|
|
|
|
doc.Write()
|
|
logger.info(
|
|
f"[STOCK] Document créé : {getattr(doc, 'DO_Piece', '?')}"
|
|
)
|
|
|
|
try:
|
|
factory_lignes = doc.FactoryDocumentLigne
|
|
except:
|
|
factory_lignes = doc.FactoryDocumentStockLigne
|
|
|
|
factory_article = self.cial.FactoryArticle
|
|
stocks_mis_a_jour = []
|
|
|
|
for idx, ligne_data in enumerate(sortie_data["lignes"], 1):
|
|
article_ref = ligne_data["article_ref"].upper()
|
|
quantite = ligne_data["quantite"]
|
|
|
|
logger.info(
|
|
f"[STOCK] ======== LIGNE {idx} : {article_ref} x {quantite} ========"
|
|
)
|
|
|
|
persist_article = factory_article.ReadReference(article_ref)
|
|
if not persist_article:
|
|
raise ValueError(f"Article {article_ref} introuvable")
|
|
|
|
article_obj = win32com.client.CastTo(
|
|
persist_article, "IBOArticle3"
|
|
)
|
|
article_obj.Read()
|
|
|
|
ar_suivi = getattr(article_obj, "AR_SuiviStock", 0)
|
|
ar_design = getattr(article_obj, "AR_Design", article_ref)
|
|
|
|
logger.info(f"[STOCK] Article : {ar_design}")
|
|
logger.info(f"[STOCK] AR_SuiviStock : {ar_suivi}")
|
|
|
|
stock_dispo = self.verifier_stock_suffisant(
|
|
article_ref, quantite, None
|
|
)
|
|
if not stock_dispo["suffisant"]:
|
|
raise ValueError(
|
|
f"Stock insuffisant pour {article_ref} : "
|
|
f"disponible={stock_dispo['stock_disponible']}, "
|
|
f"demandé={quantite}"
|
|
)
|
|
|
|
logger.info(
|
|
f"[STOCK] Stock disponible : {stock_dispo['stock_disponible']}"
|
|
)
|
|
|
|
numero_lot = ligne_data.get("numero_lot")
|
|
|
|
if ar_suivi == 1: # CMUP
|
|
if numero_lot:
|
|
logger.warning(f"[STOCK] CMUP : Suppression du lot")
|
|
numero_lot = None
|
|
|
|
elif ar_suivi == 2: # FIFO/LIFO
|
|
if not numero_lot:
|
|
import uuid
|
|
|
|
numero_lot = f"AUTO-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6].upper()}"
|
|
logger.info(
|
|
f"[STOCK] FIFO/LIFO : Lot auto-généré '{numero_lot}'"
|
|
)
|
|
|
|
ligne_persist = factory_lignes.Create()
|
|
|
|
try:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentLigne3"
|
|
)
|
|
except:
|
|
ligne_obj = win32com.client.CastTo(
|
|
ligne_persist, "IBODocumentStockLigne3"
|
|
)
|
|
|
|
ligne_obj.SetDefault()
|
|
|
|
article_lie = False
|
|
|
|
try:
|
|
ligne_obj.SetDefaultArticleReference(
|
|
article_ref, float(quantite)
|
|
)
|
|
article_lie = True
|
|
logger.info(f"[STOCK] SetDefaultArticleReference()")
|
|
except:
|
|
try:
|
|
ligne_obj.SetDefaultArticle(
|
|
article_obj, float(quantite)
|
|
)
|
|
article_lie = True
|
|
logger.info(f"[STOCK] SetDefaultArticle()")
|
|
except:
|
|
pass
|
|
|
|
if not article_lie:
|
|
raise ValueError(
|
|
f"Impossible de lier l'article {article_ref}"
|
|
)
|
|
|
|
if numero_lot and ar_suivi == 2:
|
|
try:
|
|
ligne_obj.SetDefaultLot(numero_lot)
|
|
logger.info(f"[STOCK] Lot défini")
|
|
except:
|
|
try:
|
|
ligne_obj.LS_NoSerie = numero_lot
|
|
logger.info(f"[STOCK] Lot via LS_NoSerie")
|
|
except:
|
|
pass
|
|
|
|
prix = ligne_data.get("prix_unitaire")
|
|
if prix:
|
|
try:
|
|
ligne_obj.DL_PrixUnitaire = float(prix)
|
|
except:
|
|
pass
|
|
|
|
ligne_obj.Write()
|
|
logger.info(f"[STOCK] Write() réussi")
|
|
|
|
ligne_obj.Read()
|
|
ref_verifiee = article_ref # Supposer OK si Write() réussi
|
|
|
|
try:
|
|
article_lie_obj = getattr(ligne_obj, "Article", None)
|
|
if article_lie_obj:
|
|
article_lie_obj.Read()
|
|
ref_verifiee = (
|
|
getattr(article_lie_obj, "AR_Ref", "").strip()
|
|
or article_ref
|
|
)
|
|
except:
|
|
pass
|
|
|
|
logger.info(f"[STOCK] LIGNE {idx} COMPLÈTE")
|
|
|
|
stocks_mis_a_jour.append(
|
|
{
|
|
"article_ref": article_ref,
|
|
"quantite_retiree": quantite,
|
|
"reference_verifiee": ref_verifiee,
|
|
"stock_avant": stock_dispo["stock_disponible"],
|
|
"stock_apres": stock_dispo["stock_apres"],
|
|
"numero_lot": numero_lot if ar_suivi == 2 else None,
|
|
}
|
|
)
|
|
|
|
doc.Write()
|
|
doc.Read()
|
|
|
|
numero = getattr(doc, "DO_Piece", "")
|
|
logger.info(f"[STOCK] Document finalisé : {numero}")
|
|
|
|
try:
|
|
self.cial.CptaApplication.CommitTrans()
|
|
logger.info(f"[STOCK] Transaction committée")
|
|
except:
|
|
pass
|
|
|
|
return {
|
|
"numero": numero,
|
|
"type": 1,
|
|
"date": str(getattr(doc, "DO_Date", "")),
|
|
"nb_lignes": len(stocks_mis_a_jour),
|
|
"reference": sortie_data.get("reference"),
|
|
"stocks_mis_a_jour": stocks_mis_a_jour,
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"[STOCK] ERREUR : {e}", exc_info=True)
|
|
try:
|
|
self.cial.CptaApplication.RollbackTrans()
|
|
except:
|
|
pass
|
|
raise ValueError(f"Erreur création sortie stock : {str(e)}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"[STOCK] ERREUR GLOBALE : {e}", exc_info=True)
|
|
raise ValueError(f"Erreur création sortie stock : {str(e)}")
|
|
|
|
def lire_mouvement_stock(self, numero: str) -> Dict:
|
|
try:
|
|
with self._com_context(), self._lock_com:
|
|
factory = self.cial.FactoryDocumentStock
|
|
|
|
persist = None
|
|
index = 1
|
|
|
|
logger.info(f"[MOUVEMENT] Recherche de {numero}...")
|
|
|
|
while index < 10000:
|
|
try:
|
|
persist_test = factory.List(index)
|
|
if persist_test is None:
|
|
break
|
|
|
|
doc_test = win32com.client.CastTo(
|
|
persist_test, "IBODocumentStock3"
|
|
)
|
|
doc_test.Read()
|
|
|
|
if getattr(doc_test, "DO_Piece", "") == numero:
|
|
persist = persist_test
|
|
logger.info(f"[MOUVEMENT] Trouvé à l'index {index}")
|
|
break
|
|
|
|
index += 1
|
|
except:
|
|
index += 1
|
|
|
|
if not persist:
|
|
raise ValueError(f"Mouvement {numero} introuvable")
|
|
|
|
doc = win32com.client.CastTo(persist, "IBODocumentStock3")
|
|
doc.Read()
|
|
|
|
do_type = getattr(doc, "DO_Type", -1)
|
|
|
|
types_mouvements = {
|
|
180: "Entrée",
|
|
181: "Sortie",
|
|
182: "Transfert",
|
|
183: "Inventaire",
|
|
}
|
|
|
|
mouvement = {
|
|
"numero": numero,
|
|
"type": do_type,
|
|
"type_libelle": types_mouvements.get(do_type, f"Type {do_type}"),
|
|
"date": str(getattr(doc, "DO_Date", "")),
|
|
"reference": getattr(doc, "DO_Ref", ""),
|
|
"lignes": [],
|
|
}
|
|
|
|
try:
|
|
factory_lignes = getattr(
|
|
doc, "FactoryDocumentLigne", None
|
|
) or getattr(doc, "FactoryDocumentStockLigne", None)
|
|
|
|
if factory_lignes:
|
|
idx = 1
|
|
while idx <= 100:
|
|
try:
|
|
ligne_p = factory_lignes.List(idx)
|
|
if ligne_p is None:
|
|
break
|
|
|
|
try:
|
|
ligne = win32com.client.CastTo(
|
|
ligne_p, "IBODocumentLigne3"
|
|
)
|
|
except:
|
|
ligne = win32com.client.CastTo(
|
|
ligne_p, "IBODocumentStockLigne3"
|
|
)
|
|
|
|
ligne.Read()
|
|
|
|
article_ref = ""
|
|
try:
|
|
article_obj = getattr(ligne, "Article", None)
|
|
if article_obj:
|
|
article_obj.Read()
|
|
article_ref = getattr(
|
|
article_obj, "AR_Ref", ""
|
|
).strip()
|
|
except:
|
|
pass
|
|
|
|
ligne_info = {
|
|
"article_ref": article_ref,
|
|
"designation": getattr(ligne, "DL_Design", ""),
|
|
"quantite": float(getattr(ligne, "DL_Qte", 0.0)),
|
|
"prix_unitaire": float(
|
|
getattr(ligne, "DL_PrixUnitaire", 0.0)
|
|
),
|
|
"montant_ht": float(
|
|
getattr(ligne, "DL_MontantHT", 0.0)
|
|
),
|
|
"numero_lot": getattr(ligne, "LS_NoSerie", ""),
|
|
}
|
|
|
|
mouvement["lignes"].append(ligne_info)
|
|
|
|
idx += 1
|
|
except:
|
|
break
|
|
except Exception as e:
|
|
logger.warning(f"[MOUVEMENT] Erreur lecture lignes : {e}")
|
|
|
|
mouvement["nb_lignes"] = len(mouvement["lignes"])
|
|
|
|
logger.info(
|
|
f"[MOUVEMENT] Lu : {numero} - {mouvement['nb_lignes']} ligne(s)"
|
|
)
|
|
|
|
return mouvement
|
|
|
|
except ValueError:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"[MOUVEMENT] Erreur : {e}", exc_info=True)
|
|
raise ValueError(f"Erreur lecture mouvement : {str(e)}")
|
|
|
|
def verifier_stock_suffisant(self, article_ref, quantite, depot=None):
|
|
"""Version thread-safe avec lock SQL"""
|
|
try:
|
|
with self._get_sql_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
|
|
cursor.execute("BEGIN TRANSACTION")
|
|
|
|
try:
|
|
cursor.execute(
|
|
"""
|
|
SELECT SUM(AS_QteSto)
|
|
FROM F_ARTSTOCK WITH (UPDLOCK, ROWLOCK)
|
|
WHERE AR_Ref = ?
|
|
""",
|
|
(article_ref.upper(),),
|
|
)
|
|
|
|
row = cursor.fetchone()
|
|
stock_dispo = float(row[0]) if row and row[0] else 0.0
|
|
|
|
suffisant = stock_dispo >= quantite
|
|
|
|
cursor.execute("COMMIT")
|
|
|
|
return {
|
|
"suffisant": suffisant,
|
|
"stock_disponible": stock_dispo,
|
|
"quantite_demandee": quantite,
|
|
}
|
|
|
|
except:
|
|
cursor.execute("ROLLBACK")
|
|
raise
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur vérification stock: {e}")
|
|
raise
|
|
|
|
|