From df6e09af07542c4eba61c48e7d404468f62e30a7 Mon Sep 17 00:00:00 2001 From: Fanilo-Nantenaina Date: Thu, 27 Nov 2025 13:08:44 +0300 Subject: [PATCH] feat: Add `BaremeRemiseResponse` model, expand `SageGatewayClient` with methods for document listing, status updates, discount retrieval, and PDF generation, and ignore `.db` files. --- .gitignore | 2 + api.py | 8 ++++ sage_client.py | 103 ++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 95 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 5407a98..023d3fc 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ htmlcov/ *~ .build/ dist/ + +*.db diff --git a/api.py b/api.py index 0377673..9cc55fd 100644 --- a/api.py +++ b/api.py @@ -131,6 +131,14 @@ class RelanceDevisRequest(BaseModel): message_personnalise: Optional[str] = None +class BaremeRemiseResponse(BaseModel): + client_id: str + remise_max_autorisee: float + remise_demandee: float + autorisee: bool + message: str + + # ===================================================== # SERVICES EXTERNES (Universign) # ===================================================== diff --git a/sage_client.py b/sage_client.py index 0b5cf95..a0f4342 100644 --- a/sage_client.py +++ b/sage_client.py @@ -9,6 +9,7 @@ logger = logging.getLogger(__name__) class SageGatewayClient: """ Client HTTP pour communiquer avec la gateway Sage Windows + ✅ VERSION COMPLÈTE avec toutes les routes nécessaires """ def __init__(self): @@ -63,46 +64,59 @@ class SageGatewayClient: raise time.sleep(2**attempt) - # === CLIENTS === + # ===================================================== + # CLIENTS + # ===================================================== def lister_clients(self, filtre: str = "") -> List[Dict]: + """Liste tous les clients avec filtre optionnel""" return self._post("/sage/clients/list", {"filtre": filtre}).get("data", []) def lire_client(self, code: str) -> Optional[Dict]: + """Lecture d'un client par code""" return self._post("/sage/clients/get", {"code": code}).get("data") - # === ARTICLES === + # ===================================================== + # ARTICLES + # ===================================================== def lister_articles(self, filtre: str = "") -> List[Dict]: + """Liste tous les articles avec filtre optionnel""" return self._post("/sage/articles/list", {"filtre": filtre}).get("data", []) def lire_article(self, ref: str) -> Optional[Dict]: + """Lecture d'un article par référence""" return self._post("/sage/articles/get", {"code": ref}).get("data") - # === DEVIS === + # ===================================================== + # DEVIS (US-A1) + # ===================================================== def creer_devis(self, devis_data: Dict) -> Dict: + """Création d'un devis""" return self._post("/sage/devis/create", devis_data).get("data", {}) def lire_devis(self, numero: str) -> Optional[Dict]: + """Lecture d'un devis""" return self._post("/sage/devis/get", {"code": numero}).get("data") - # 🆕 US-A1: Lister devis def lister_devis( self, limit: int = 100, statut: Optional[int] = None ) -> List[Dict]: - """Liste tous les devis avec filtres""" + """✅ NOUVEAU: Liste tous les devis avec filtres""" payload = {"limit": limit} if statut is not None: payload["statut"] = statut return self._post("/sage/devis/list", payload).get("data", []) - # 🆕 US-A1: Changer statut devis def changer_statut_devis(self, numero: str, nouveau_statut: int) -> Dict: - """Change le statut d'un devis""" + """✅ NOUVEAU: Change le statut d'un devis""" return self._post( "/sage/devis/statut", {"numero": numero, "nouveau_statut": nouveau_statut} ).get("data", {}) - # === DOCUMENTS === + # ===================================================== + # DOCUMENTS GÉNÉRIQUES + # ===================================================== def lire_document(self, numero: str, type_doc: int) -> Optional[Dict]: + """Lecture d'un document générique""" return self._post( "/sage/documents/get", {"numero": numero, "type_doc": type_doc} ).get("data") @@ -110,6 +124,7 @@ class SageGatewayClient: def transformer_document( self, numero_source: str, type_source: int, type_cible: int ) -> Dict: + """Transformation de document (devis → commande → facture)""" return self._post( "/sage/documents/transform", { @@ -122,6 +137,7 @@ class SageGatewayClient: def mettre_a_jour_champ_libre( self, doc_id: str, type_doc: int, nom_champ: str, valeur: str ) -> bool: + """Mise à jour d'un champ libre""" resp = self._post( "/sage/documents/champ-libre", { @@ -133,46 +149,97 @@ class SageGatewayClient: ) return resp.get("success", False) - # 🆕 US-A2: Lister commandes + # ===================================================== + # COMMANDES (US-A2) + # ===================================================== def lister_commandes( self, limit: int = 100, statut: Optional[int] = None ) -> List[Dict]: - """Liste toutes les commandes""" + """✅ NOUVEAU: Liste toutes les commandes""" payload = {"limit": limit} if statut is not None: payload["statut"] = statut return self._post("/sage/commandes/list", payload).get("data", []) - # 🆕 US-A7: Lister factures + # ===================================================== + # FACTURES (US-A7) + # ===================================================== def lister_factures( self, limit: int = 100, statut: Optional[int] = None ) -> List[Dict]: - """Liste toutes les factures""" + """✅ NOUVEAU: Liste toutes les factures""" payload = {"limit": limit} if statut is not None: payload["statut"] = statut return self._post("/sage/factures/list", payload).get("data", []) - # === CONTACTS === + def mettre_a_jour_derniere_relance(self, doc_id: str, type_doc: int) -> bool: + """✅ NOUVEAU: Met à jour le champ 'Dernière relance' d'une facture""" + resp = self._post( + "/sage/documents/derniere-relance", {"doc_id": doc_id, "type_doc": type_doc} + ) + return resp.get("success", False) + + # ===================================================== + # CONTACTS (US-A6) + # ===================================================== def lire_contact_client(self, code_client: str) -> Optional[Dict]: + """Lecture du contact principal d'un client""" return self._post("/sage/contact/read", {"code": code_client}).get("data") - # 🆕 US-A5: Lire remise max client + # ===================================================== + # REMISES (US-A5) + # ===================================================== def lire_remise_max_client(self, code_client: str) -> float: - """Récupère la remise max autorisée pour un client""" + """✅ NOUVEAU: Récupère la remise max autorisée pour un client""" result = self._post("/sage/client/remise-max", {"code": code_client}) return result.get("data", {}).get("remise_max", 10.0) - # === CACHE === + # ===================================================== + # GÉNÉRATION PDF (pour email_queue) + # ===================================================== + def generer_pdf_document(self, doc_id: str, type_doc: int) -> bytes: + """✅ NOUVEAU: Génère le PDF d'un document via la gateway Windows""" + try: + r = requests.post( + f"{self.url}/sage/documents/generate-pdf", + json={"doc_id": doc_id, "type_doc": type_doc}, + headers=self.headers, + timeout=60, # Timeout plus long pour génération PDF + ) + r.raise_for_status() + + # Le PDF est retourné en base64 dans la réponse JSON + import base64 + + response_data = r.json() + pdf_base64 = response_data.get("data", {}).get("pdf_base64", "") + + if not pdf_base64: + raise ValueError("PDF vide retourné par la gateway") + + return base64.b64decode(pdf_base64) + + except Exception as e: + logger.error(f"Erreur génération PDF: {e}") + raise + + # ===================================================== + # CACHE (ADMIN) + # ===================================================== def refresh_cache(self) -> Dict: + """Force le rafraîchissement du cache Windows""" return self._post("/sage/cache/refresh") def get_cache_info(self) -> Dict: - """Récupère les infos du cache""" + """Récupère les infos du cache Windows""" return self._get("/sage/cache/info").get("data", {}) - # === HEALTH === + # ===================================================== + # HEALTH + # ===================================================== def health(self) -> dict: + """Health check de la gateway Windows""" try: r = requests.get(f"{self.url}/health", timeout=5) return r.json()