Creating new fournisseur

This commit is contained in:
Fanilo-Nantenaina 2025-12-06 15:03:13 +03:00
parent 3aadc67abf
commit c66280b305
2 changed files with 354 additions and 3 deletions

43
main.py
View file

@ -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):
""" """

View file

@ -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):
""" """