From ae5fa9e0be281bccb86d23d24152e6e8b8067c37 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 4 Dec 2025 13:55:38 +0300 Subject: [PATCH] feat: Add API endpoints and Sage connector methods for managing prospects, suppliers, credit notes, and delivery notes. --- main.py | 117 ++++++++++ sage_connector.py | 540 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 657 insertions(+) diff --git a/main.py b/main.py index 0788e60..426ed98 100644 --- a/main.py +++ b/main.py @@ -37,6 +37,7 @@ class TypeDocument(int, Enum): # MODÈLES # ===================================================== + class DocumentGetRequest(BaseModel): numero: str type_doc: int @@ -2450,6 +2451,122 @@ def diagnostiquer_erreur_transformation( raise HTTPException(500, str(e)) +# ===================================================== +# ENDPOINTS - PROSPECTS +# ===================================================== +@app.post("/sage/prospects/list", dependencies=[Depends(verify_token)]) +def prospects_list(req: FiltreRequest): + """📋 Liste tous les prospects (CT_Type=0 AND CT_Prospect=1)""" + try: + prospects = sage.lister_tous_prospects(req.filtre) + return {"success": True, "data": prospects} + except Exception as e: + logger.error(f"Erreur liste prospects: {e}") + raise HTTPException(500, str(e)) + + +@app.post("/sage/prospects/get", dependencies=[Depends(verify_token)]) +def prospect_get(req: CodeRequest): + """📄 Lecture d'un prospect par code""" + try: + prospect = sage.lire_prospect(req.code) + if not prospect: + raise HTTPException(404, f"Prospect {req.code} non trouvé") + return {"success": True, "data": prospect} + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture prospect: {e}") + raise HTTPException(500, str(e)) + + +# ===================================================== +# ENDPOINTS - FOURNISSEURS +# ===================================================== +@app.post("/sage/fournisseurs/list", dependencies=[Depends(verify_token)]) +def fournisseurs_list(req: FiltreRequest): + """📋 Liste tous les fournisseurs (CT_Type=1)""" + try: + fournisseurs = sage.lister_tous_fournisseurs(req.filtre) + return {"success": True, "data": fournisseurs} + except Exception as e: + logger.error(f"Erreur liste fournisseurs: {e}") + raise HTTPException(500, str(e)) + + +@app.post("/sage/fournisseurs/get", dependencies=[Depends(verify_token)]) +def fournisseur_get(req: CodeRequest): + """📄 Lecture d'un fournisseur par code""" + try: + fournisseur = sage.lire_fournisseur(req.code) + if not fournisseur: + raise HTTPException(404, f"Fournisseur {req.code} non trouvé") + return {"success": True, "data": fournisseur} + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture fournisseur: {e}") + raise HTTPException(500, str(e)) + + +# ===================================================== +# ENDPOINTS - AVOIRS +# ===================================================== +@app.post("/sage/avoirs/list", dependencies=[Depends(verify_token)]) +def avoirs_list(limit: int = 100, statut: Optional[int] = None): + """📋 Liste tous les avoirs (DO_Domaine=0 AND DO_Type=5)""" + try: + avoirs = sage.lister_avoirs(limit=limit, statut=statut) + return {"success": True, "data": avoirs} + except Exception as e: + logger.error(f"Erreur liste avoirs: {e}") + raise HTTPException(500, str(e)) + + +@app.post("/sage/avoirs/get", dependencies=[Depends(verify_token)]) +def avoir_get(req: CodeRequest): + """📄 Lecture d'un avoir avec ses lignes""" + try: + avoir = sage.lire_avoir(req.code) + if not avoir: + raise HTTPException(404, f"Avoir {req.code} non trouvé") + return {"success": True, "data": avoir} + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture avoir: {e}") + raise HTTPException(500, str(e)) + + +# ===================================================== +# ENDPOINTS - LIVRAISONS +# ===================================================== +@app.post("/sage/livraisons/list", dependencies=[Depends(verify_token)]) +def livraisons_list(limit: int = 100, statut: Optional[int] = None): + """📋 Liste tous les bons de livraison (DO_Domaine=0 AND DO_Type=30)""" + try: + livraisons = sage.lister_livraisons(limit=limit, statut=statut) + return {"success": True, "data": livraisons} + except Exception as e: + logger.error(f"Erreur liste livraisons: {e}") + raise HTTPException(500, str(e)) + + +@app.post("/sage/livraisons/get", dependencies=[Depends(verify_token)]) +def livraison_get(req: CodeRequest): + """📄 Lecture d'une livraison avec ses lignes""" + try: + livraison = sage.lire_livraison(req.code) + if not livraison: + raise HTTPException(404, f"Livraison {req.code} non trouvée") + return {"success": True, "data": livraison} + except HTTPException: + raise + except Exception as e: + logger.error(f"Erreur lecture livraison: {e}") + raise HTTPException(500, str(e)) + + # ===================================================== # LANCEMENT # ===================================================== diff --git a/sage_connector.py b/sage_connector.py index efff8fc..433df7b 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -1805,3 +1805,543 @@ class SageConnector: return self.mettre_a_jour_champ_libre( doc_id, type_doc, "DerniereRelance", date_relance ) + + # ========================================================================= + # PROSPECTS (CT_Type = 0 AND CT_Prospect = 1) + # ========================================================================= + def lister_tous_prospects(self, filtre=""): + """Liste tous les prospects depuis le cache""" + with self._lock_clients: + if not filtre: + return [ + c + for c in self._cache_clients + if c.get("type") == 0 and c.get("est_prospect") + ] + + filtre_lower = filtre.lower() + return [ + c + for c in self._cache_clients + if c.get("type") == 0 + and c.get("est_prospect") + and ( + filtre_lower in c["numero"].lower() + or filtre_lower in c["intitule"].lower() + ) + ] + + def lire_prospect(self, code_prospect): + """Retourne un prospect depuis le cache""" + with self._lock_clients: + prospect = self._cache_clients_dict.get(code_prospect) + if prospect and prospect.get("type") == 0 and prospect.get("est_prospect"): + return prospect + return None + + # ========================================================================= + # FOURNISSEURS (CT_Type = 1) + # ========================================================================= + def lister_tous_fournisseurs(self, filtre=""): + """Liste tous les fournisseurs depuis le cache""" + with self._lock_clients: + if not filtre: + return [c for c in self._cache_clients if c.get("type") == 1] + + filtre_lower = filtre.lower() + return [ + c + for c in self._cache_clients + if c.get("type") == 1 + and ( + filtre_lower in c["numero"].lower() + or filtre_lower in c["intitule"].lower() + ) + ] + + def lire_fournisseur(self, code_fournisseur): + """Retourne un fournisseur depuis le cache""" + with self._lock_clients: + fournisseur = self._cache_clients_dict.get(code_fournisseur) + if fournisseur and fournisseur.get("type") == 1: + return fournisseur + return None + + # ========================================================================= + # EXTRACTION CLIENTS (Mise à jour pour inclure prospects) + # ========================================================================= + def _extraire_client(self, client_obj): + """MISE À JOUR : Extraction avec détection prospect""" + data = { + "numero": getattr(client_obj, "CT_Num", ""), + "intitule": getattr(client_obj, "CT_Intitule", ""), + "type": getattr( + client_obj, "CT_Type", 0 + ), # 0=Client/Prospect, 1=Fournisseur + "est_prospect": getattr(client_obj, "CT_Prospect", 0) == 1, # ✅ NOUVEAU + } + + try: + adresse = getattr(client_obj, "Adresse", None) + if adresse: + data["adresse"] = getattr(adresse, "Adresse", "") + data["code_postal"] = getattr(adresse, "CodePostal", "") + data["ville"] = getattr(adresse, "Ville", "") + except: + pass + + try: + telecom = getattr(client_obj, "Telecom", None) + if telecom: + data["telephone"] = getattr(telecom, "Telephone", "") + data["email"] = getattr(telecom, "EMail", "") + except: + pass + + return data + + # ========================================================================= + # AVOIRS (DO_Domaine = 0 AND DO_Type = 5) + # ========================================================================= + def lister_avoirs(self, limit=100, statut=None): + """Liste tous les avoirs de vente""" + if not self.cial: + return [] + + avoirs = [] + + try: + with self._com_context(), self._lock_com: + factory = self.cial.FactoryDocumentVente + index = 1 + max_iterations = limit * 3 + erreurs_consecutives = 0 + max_erreurs = 50 + + while ( + len(avoirs) < limit + and index < max_iterations + and erreurs_consecutives < max_erreurs + ): + try: + persist = factory.List(index) + if persist is None: + break + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + # ✅ Filtrer : DO_Domaine = 0 (Vente) AND DO_Type = 5 (Avoir) + doc_type = getattr(doc, "DO_Type", -1) + doc_domaine = getattr(doc, "DO_Domaine", -1) + + if doc_domaine != 0 or doc_type != settings.SAGE_TYPE_BON_AVOIR: + index += 1 + continue + + doc_statut = getattr(doc, "DO_Statut", 0) + + # Filtre statut optionnel + if statut is not None and doc_statut != statut: + index += 1 + continue + + # Charger client + client_code = "" + client_intitule = "" + + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_code = getattr(client_obj, "CT_Num", "").strip() + client_intitule = getattr( + client_obj, "CT_Intitule", "" + ).strip() + except: + pass + + avoirs.append( + { + "numero": getattr(doc, "DO_Piece", ""), + "reference": getattr(doc, "DO_Ref", ""), + "date": str(getattr(doc, "DO_Date", "")), + "client_code": client_code, + "client_intitule": client_intitule, + "total_ht": float(getattr(doc, "DO_TotalHT", 0.0)), + "total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)), + "statut": doc_statut, + } + ) + + erreurs_consecutives = 0 + index += 1 + + except: + erreurs_consecutives += 1 + index += 1 + + if erreurs_consecutives >= max_erreurs: + break + + logger.info(f"✅ {len(avoirs)} avoirs retournés") + return avoirs + + except Exception as e: + logger.error(f"❌ Erreur liste avoirs: {e}", exc_info=True) + return [] + + def lire_avoir(self, numero): + """Lecture d'un avoir avec ses lignes""" + if not self.cial: + return None + + try: + with self._com_context(), self._lock_com: + factory = self.cial.FactoryDocumentVente + + # Essayer ReadPiece + persist = factory.ReadPiece(settings.SAGE_TYPE_BON_AVOIR, numero) + + if not persist: + # Chercher dans List() + 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) + == settings.SAGE_TYPE_BON_AVOIR + and getattr(doc_test, "DO_Piece", "") == numero + ): + persist = persist_test + break + + index += 1 + except: + index += 1 + + if not persist: + return None + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + # Charger client + client_code = "" + client_intitule = "" + + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_code = getattr(client_obj, "CT_Num", "").strip() + client_intitule = getattr(client_obj, "CT_Intitule", "").strip() + except: + pass + + avoir = { + "numero": getattr(doc, "DO_Piece", ""), + "reference": getattr(doc, "DO_Ref", ""), + "date": str(getattr(doc, "DO_Date", "")), + "client_code": client_code, + "client_intitule": client_intitule, + "total_ht": float(getattr(doc, "DO_TotalHT", 0.0)), + "total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)), + "statut": getattr(doc, "DO_Statut", 0), + "lignes": [], + } + + # Charger lignes + try: + factory_lignes = getattr( + doc, "FactoryDocumentLigne", None + ) or getattr(doc, "FactoryDocumentVenteLigne", None) + + if factory_lignes: + index = 1 + while index <= 100: + try: + ligne_persist = factory_lignes.List(index) + if ligne_persist is None: + break + + ligne = win32com.client.CastTo( + ligne_persist, "IBODocumentLigne3" + ) + ligne.Read() + + article_ref = "" + try: + article_ref = getattr(ligne, "AR_Ref", "").strip() + if not article_ref: + article_obj = getattr(ligne, "Article", None) + if article_obj: + article_obj.Read() + article_ref = getattr( + article_obj, "AR_Ref", "" + ).strip() + except: + pass + + avoir["lignes"].append( + { + "article": 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) + ), + } + ) + + index += 1 + except: + break + except: + pass + + logger.info(f"✅ Avoir {numero} lu: {len(avoir['lignes'])} lignes") + return avoir + + except Exception as e: + logger.error(f"❌ Erreur lecture avoir {numero}: {e}") + return None + + # ========================================================================= + # LIVRAISONS (DO_Domaine = 0 AND DO_Type = 3) + # ========================================================================= + def lister_livraisons(self, limit=100, statut=None): + """Liste tous les bons de livraison""" + if not self.cial: + return [] + + livraisons = [] + + try: + with self._com_context(), self._lock_com: + factory = self.cial.FactoryDocumentVente + index = 1 + max_iterations = limit * 3 + erreurs_consecutives = 0 + max_erreurs = 50 + + while ( + len(livraisons) < limit + and index < max_iterations + and erreurs_consecutives < max_erreurs + ): + try: + persist = factory.List(index) + if persist is None: + break + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + # ✅ Filtrer : DO_Domaine = 0 (Vente) AND DO_Type = 30 (Livraison) + doc_type = getattr(doc, "DO_Type", -1) + doc_domaine = getattr(doc, "DO_Domaine", -1) + + if ( + doc_domaine != 0 + or doc_type != settings.SAGE_TYPE_BON_LIVRAISON + ): + index += 1 + continue + + doc_statut = getattr(doc, "DO_Statut", 0) + + if statut is not None and doc_statut != statut: + index += 1 + continue + + # Charger client + client_code = "" + client_intitule = "" + + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_code = getattr(client_obj, "CT_Num", "").strip() + client_intitule = getattr( + client_obj, "CT_Intitule", "" + ).strip() + except: + pass + + livraisons.append( + { + "numero": getattr(doc, "DO_Piece", ""), + "reference": getattr(doc, "DO_Ref", ""), + "date": str(getattr(doc, "DO_Date", "")), + "client_code": client_code, + "client_intitule": client_intitule, + "total_ht": float(getattr(doc, "DO_TotalHT", 0.0)), + "total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)), + "statut": doc_statut, + } + ) + + erreurs_consecutives = 0 + index += 1 + + except: + erreurs_consecutives += 1 + index += 1 + + if erreurs_consecutives >= max_erreurs: + break + + logger.info(f"✅ {len(livraisons)} livraisons retournées") + return livraisons + + except Exception as e: + logger.error(f"❌ Erreur liste livraisons: {e}", exc_info=True) + return [] + + def lire_livraison(self, numero): + """Lecture d'une livraison avec ses lignes""" + if not self.cial: + return None + + try: + with self._com_context(), self._lock_com: + factory = self.cial.FactoryDocumentVente + + # Essayer ReadPiece + persist = factory.ReadPiece(settings.SAGE_TYPE_BON_LIVRAISON, numero) + + if not persist: + # Chercher dans List() + 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) + == settings.SAGE_TYPE_BON_LIVRAISON + and getattr(doc_test, "DO_Piece", "") == numero + ): + persist = persist_test + break + + index += 1 + except: + index += 1 + + if not persist: + return None + + doc = win32com.client.CastTo(persist, "IBODocumentVente3") + doc.Read() + + # Charger client + client_code = "" + client_intitule = "" + + try: + client_obj = getattr(doc, "Client", None) + if client_obj: + client_obj.Read() + client_code = getattr(client_obj, "CT_Num", "").strip() + client_intitule = getattr(client_obj, "CT_Intitule", "").strip() + except: + pass + + livraison = { + "numero": getattr(doc, "DO_Piece", ""), + "reference": getattr(doc, "DO_Ref", ""), + "date": str(getattr(doc, "DO_Date", "")), + "client_code": client_code, + "client_intitule": client_intitule, + "total_ht": float(getattr(doc, "DO_TotalHT", 0.0)), + "total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)), + "statut": getattr(doc, "DO_Statut", 0), + "lignes": [], + } + + # Charger lignes + try: + factory_lignes = getattr( + doc, "FactoryDocumentLigne", None + ) or getattr(doc, "FactoryDocumentVenteLigne", None) + + if factory_lignes: + index = 1 + while index <= 100: + try: + ligne_persist = factory_lignes.List(index) + if ligne_persist is None: + break + + ligne = win32com.client.CastTo( + ligne_persist, "IBODocumentLigne3" + ) + ligne.Read() + + article_ref = "" + try: + article_ref = getattr(ligne, "AR_Ref", "").strip() + if not article_ref: + article_obj = getattr(ligne, "Article", None) + if article_obj: + article_obj.Read() + article_ref = getattr( + article_obj, "AR_Ref", "" + ).strip() + except: + pass + + livraison["lignes"].append( + { + "article": 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) + ), + } + ) + + index += 1 + except: + break + except: + pass + + logger.info( + f"✅ Livraison {numero} lue: {len(livraison['lignes'])} lignes" + ) + return livraison + + except Exception as e: + logger.error(f"❌ Erreur lecture livraison {numero}: {e}") + return None