From c66280b30583913d208d69ef63939a9dfd2bab5e Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Sat, 6 Dec 2025 15:03:13 +0300 Subject: [PATCH] Creating new fournisseur --- main.py | 43 ++++++- sage_connector.py | 314 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 354 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 9025c11..b378533 100644 --- a/main.py +++ b/main.py @@ -94,7 +94,20 @@ class ClientUpdateGatewayRequest(BaseModel): code: str 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É # ===================================================== @@ -2595,6 +2608,34 @@ def fournisseurs_list(req: FiltreRequest): logger.error(f"❌ Erreur liste fournisseurs: {e}", exc_info=True) 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)]) def fournisseur_get(req: CodeRequest): """ diff --git a/sage_connector.py b/sage_connector.py index 776ab36..d65682a 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -175,7 +175,7 @@ class SageConnector: 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) """ if not self.cial: @@ -191,7 +191,7 @@ class SageConnector: erreurs_consecutives = 0 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: try: @@ -399,6 +399,316 @@ class SageConnector: logger.error(f"❌ Erreur liste fournisseurs: {e}", exc_info=True) 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): """