Creating new fournisseur
This commit is contained in:
parent
3aadc67abf
commit
c66280b305
2 changed files with 354 additions and 3 deletions
43
main.py
43
main.py
|
|
@ -94,7 +94,20 @@ class ClientUpdateGatewayRequest(BaseModel):
|
||||||
code: str
|
code: str
|
||||||
client_data: Dict
|
client_data: Dict
|
||||||
|
|
||||||
|
class FournisseurCreateRequest(BaseModel):
|
||||||
|
intitule: str = Field(..., description="Raison sociale du fournisseur")
|
||||||
|
compte_collectif: str = Field("401000", description="Compte général rattaché")
|
||||||
|
num: Optional[str] = Field(None, description="Code fournisseur (auto si vide)")
|
||||||
|
adresse: Optional[str] = None
|
||||||
|
code_postal: Optional[str] = None
|
||||||
|
ville: Optional[str] = None
|
||||||
|
pays: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
telephone: Optional[str] = None
|
||||||
|
siret: Optional[str] = None
|
||||||
|
tva_intra: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
# =====================================================
|
# =====================================================
|
||||||
# SÉCURITÉ
|
# SÉCURITÉ
|
||||||
# =====================================================
|
# =====================================================
|
||||||
|
|
@ -2595,6 +2608,34 @@ def fournisseurs_list(req: FiltreRequest):
|
||||||
logger.error(f"❌ Erreur liste fournisseurs: {e}", exc_info=True)
|
logger.error(f"❌ Erreur liste fournisseurs: {e}", exc_info=True)
|
||||||
raise HTTPException(500, str(e))
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
@app.post("/sage/fournisseurs/create", dependencies=[Depends(verify_token)])
|
||||||
|
def create_fournisseur_endpoint(req: FournisseurCreateRequest):
|
||||||
|
"""
|
||||||
|
➕ Création d'un fournisseur dans Sage
|
||||||
|
|
||||||
|
✅ Utilise FactoryFournisseur.Create() directement
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Appel au connecteur Sage
|
||||||
|
resultat = sage.creer_fournisseur(req.dict())
|
||||||
|
|
||||||
|
logger.info(f"✅ Fournisseur créé: {resultat.get('numero')}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": resultat
|
||||||
|
}
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
# Erreur métier (ex: doublon)
|
||||||
|
logger.warning(f"⚠️ Erreur métier création fournisseur: {e}")
|
||||||
|
raise HTTPException(400, str(e))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Erreur technique (ex: COM)
|
||||||
|
logger.error(f"❌ Erreur technique création fournisseur: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
@app.post("/sage/fournisseurs/get", dependencies=[Depends(verify_token)])
|
@app.post("/sage/fournisseurs/get", dependencies=[Depends(verify_token)])
|
||||||
def fournisseur_get(req: CodeRequest):
|
def fournisseur_get(req: CodeRequest):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ class SageConnector:
|
||||||
|
|
||||||
def _refresh_cache_clients(self):
|
def _refresh_cache_clients(self):
|
||||||
"""
|
"""
|
||||||
Actualise le cache des clients ET fournisseurs
|
Actualise le cache des clients
|
||||||
Charge TOUS les tiers (CT_Type=0 ET CT_Type=1)
|
Charge TOUS les tiers (CT_Type=0 ET CT_Type=1)
|
||||||
"""
|
"""
|
||||||
if not self.cial:
|
if not self.cial:
|
||||||
|
|
@ -191,7 +191,7 @@ class SageConnector:
|
||||||
erreurs_consecutives = 0
|
erreurs_consecutives = 0
|
||||||
max_erreurs = 50
|
max_erreurs = 50
|
||||||
|
|
||||||
logger.info("🔄 Actualisation cache clients/fournisseurs/prospects...")
|
logger.info("🔄 Actualisation cache clients et prospects...")
|
||||||
|
|
||||||
while index < 10000 and erreurs_consecutives < max_erreurs:
|
while index < 10000 and erreurs_consecutives < max_erreurs:
|
||||||
try:
|
try:
|
||||||
|
|
@ -399,6 +399,316 @@ class SageConnector:
|
||||||
logger.error(f"❌ Erreur liste fournisseurs: {e}", exc_info=True)
|
logger.error(f"❌ Erreur liste fournisseurs: {e}", exc_info=True)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def creer_fournisseur(self, fournisseur_data: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
✅ Crée un nouveau fournisseur dans Sage 100c via FactoryFournisseur
|
||||||
|
|
||||||
|
IMPORTANT: Utilise FactoryFournisseur.Create() et NON FactoryClient.Create()
|
||||||
|
car les fournisseurs sont gérés séparément dans Sage.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fournisseur_data: Dictionnaire contenant:
|
||||||
|
- intitule (obligatoire): Raison sociale
|
||||||
|
- compte_collectif (défaut: "401000"): Compte général
|
||||||
|
- num (optionnel): Code fournisseur personnalisé
|
||||||
|
- adresse, code_postal, ville, pays
|
||||||
|
- email, telephone
|
||||||
|
- siret, tva_intra
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict contenant le fournisseur créé avec son numéro définitif
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Si le fournisseur existe déjà ou données invalides
|
||||||
|
RuntimeError: Si erreur technique Sage
|
||||||
|
"""
|
||||||
|
if not self.cial:
|
||||||
|
raise RuntimeError("Connexion Sage non établie")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with self._com_context(), self._lock_com:
|
||||||
|
# ========================================
|
||||||
|
# ÉTAPE 0 : VALIDATION & NETTOYAGE
|
||||||
|
# ========================================
|
||||||
|
logger.info("🔍 === VALIDATION DONNÉES FOURNISSEUR ===")
|
||||||
|
|
||||||
|
if not fournisseur_data.get("intitule"):
|
||||||
|
raise ValueError("Le champ 'intitule' est obligatoire")
|
||||||
|
|
||||||
|
# Nettoyage et troncature (longueurs max Sage)
|
||||||
|
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", "401000"))[: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)})")
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# ÉTAPE 1 : CRÉATION OBJET FOURNISSEUR
|
||||||
|
# ========================================
|
||||||
|
# 🔑 CRITIQUE: Utiliser FactoryFournisseur, PAS FactoryClient !
|
||||||
|
factory_fournisseur = self.cial.CptaApplication.FactoryFournisseur
|
||||||
|
|
||||||
|
persist = factory_fournisseur.Create()
|
||||||
|
fournisseur = win32com.client.CastTo(persist, "IBOFournisseur3")
|
||||||
|
|
||||||
|
# 🔑 CRITIQUE : Initialiser l'objet
|
||||||
|
fournisseur.SetDefault()
|
||||||
|
|
||||||
|
logger.info("✅ Objet fournisseur créé et initialisé")
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# ÉTAPE 2 : CHAMPS OBLIGATOIRES
|
||||||
|
# ========================================
|
||||||
|
logger.info("📝 Définition des champs obligatoires...")
|
||||||
|
|
||||||
|
# 1. Intitulé (OBLIGATOIRE)
|
||||||
|
fournisseur.CT_Intitule = intitule
|
||||||
|
logger.debug(f" ✅ CT_Intitule: '{intitule}'")
|
||||||
|
|
||||||
|
# 2. Type = Fournisseur (1)
|
||||||
|
# ⚠️ NOTE: Sur certaines versions Sage, CT_Type n'existe pas
|
||||||
|
# et le type est automatiquement défini par la factory utilisée
|
||||||
|
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)")
|
||||||
|
|
||||||
|
# 3. Qualité (pour versions récentes Sage)
|
||||||
|
try:
|
||||||
|
fournisseur.CT_Qualite = "FOU"
|
||||||
|
logger.debug(" ✅ CT_Qualite: 'FOU'")
|
||||||
|
except:
|
||||||
|
logger.debug(" ⚠️ CT_Qualite non défini (pas critique)")
|
||||||
|
|
||||||
|
# 4. Compte général principal (OBLIGATOIRE)
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Assigner l'objet CompteG
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# 5. Numéro fournisseur (OBLIGATOIRE - générer si vide)
|
||||||
|
if num_prop:
|
||||||
|
fournisseur.CT_Num = num_prop
|
||||||
|
logger.debug(f" ✅ CT_Num fourni: '{num_prop}'")
|
||||||
|
else:
|
||||||
|
# 🔑 CRITIQUE : Générer le numéro automatiquement
|
||||||
|
try:
|
||||||
|
# Méthode 1 : SetDefaultNumPiece (si disponible)
|
||||||
|
if hasattr(fournisseur, 'SetDefaultNumPiece'):
|
||||||
|
fournisseur.SetDefaultNumPiece()
|
||||||
|
num_genere = getattr(fournisseur, "CT_Num", "")
|
||||||
|
logger.debug(f" ✅ CT_Num auto-généré: '{num_genere}'")
|
||||||
|
else:
|
||||||
|
# Méthode 2 : GetNextNumero depuis la factory
|
||||||
|
num_genere = factory_fournisseur.GetNextNumero()
|
||||||
|
if num_genere:
|
||||||
|
fournisseur.CT_Num = num_genere
|
||||||
|
logger.debug(f" ✅ CT_Num auto (GetNextNumero): '{num_genere}'")
|
||||||
|
else:
|
||||||
|
# Méthode 3 : Fallback - timestamp
|
||||||
|
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")
|
||||||
|
|
||||||
|
# 6. Catégories (valeurs par défaut)
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# ÉTAPE 3 : CHAMPS OPTIONNELS
|
||||||
|
# ========================================
|
||||||
|
logger.info("📝 Définition champs optionnels...")
|
||||||
|
|
||||||
|
# Adresse (objet IAdresse)
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# Télécom (objet ITelecom)
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# Identifiants fiscaux
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# Options par défaut
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# ÉTAPE 4 : VÉRIFICATION PRÉ-WRITE
|
||||||
|
# ========================================
|
||||||
|
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}'")
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# ÉTAPE 5 : ÉCRITURE EN BASE
|
||||||
|
# ========================================
|
||||||
|
logger.info("💾 Écriture du fournisseur dans Sage...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
fournisseur.Write()
|
||||||
|
logger.info("✅ Write() réussi !")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_detail = str(e)
|
||||||
|
|
||||||
|
# Récupérer l'erreur Sage détaillé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
|
||||||
|
|
||||||
|
# Analyser l'erreur
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# ÉTAPE 6 : RELECTURE & FINALISATION
|
||||||
|
# ========================================
|
||||||
|
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} ✅✅✅")
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# ÉTAPE 7 : CONSTRUCTION RÉPONSE
|
||||||
|
# ========================================
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
# ⚠️ PAS DE REFRESH CACHE ICI
|
||||||
|
# Car lister_tous_fournisseurs() utilise FactoryFournisseur.List()
|
||||||
|
# qui lit directement depuis Sage (pas de cache)
|
||||||
|
|
||||||
|
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 lire_fournisseur(self, code):
|
def lire_fournisseur(self, code):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue