feat: Add API endpoints and Sage connector methods for managing prospects, suppliers, credit notes, and delivery notes.

This commit is contained in:
Fanilo-Nantenaina 2025-12-04 13:55:38 +03:00
parent 53ecccd712
commit ae5fa9e0be
2 changed files with 657 additions and 0 deletions

117
main.py
View file

@ -37,6 +37,7 @@ class TypeDocument(int, Enum):
# MODÈLES # MODÈLES
# ===================================================== # =====================================================
class DocumentGetRequest(BaseModel): class DocumentGetRequest(BaseModel):
numero: str numero: str
type_doc: int type_doc: int
@ -2450,6 +2451,122 @@ def diagnostiquer_erreur_transformation(
raise HTTPException(500, str(e)) 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 # LANCEMENT
# ===================================================== # =====================================================

View file

@ -1805,3 +1805,543 @@ class SageConnector:
return self.mettre_a_jour_champ_libre( return self.mettre_a_jour_champ_libre(
doc_id, type_doc, "DerniereRelance", date_relance 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