diff --git a/config.py b/config.py index 769d506..c2678fd 100644 --- a/config.py +++ b/config.py @@ -15,26 +15,21 @@ class Settings(BaseSettings): SAGE_TYPE_BON_AVOIR: int = 50 SAGE_TYPE_FACTURE: int = 60 - # === SAGE 100c (Windows uniquement) === chemin_base: str utilisateur: str = "Administrateur" mot_de_passe: str - # === Sécurité Gateway === - sage_gateway_token: str # Token partagé avec le VPS Linux + sage_gateway_token: str - # === SMTP (optionnel sur Windows) === smtp_host: Optional[str] = None smtp_port: int = 587 smtp_user: Optional[str] = None smtp_password: Optional[str] = None smtp_from: Optional[str] = None - # === API Windows === api_host: str = "0.0.0.0" api_port: int = 8000 - # === CORS === cors_origins: List[str] = ["*"] diff --git a/diagnostic_crystal.txt b/diagnostic_crystal.txt deleted file mode 100644 index 77bacdb..0000000 --- a/diagnostic_crystal.txt +++ /dev/null @@ -1,12 +0,0 @@ -====================================================================== -DIAGNOSTIC CRYSTAL REPORTS -====================================================================== - -Installation détectée: True -DLL trouvées: -ProgID valides: -Architecture OK: False - -PROBLÈMES: - -SOLUTIONS: diff --git a/main.py b/main.py index 2e5606f..469ec99 100644 --- a/main.py +++ b/main.py @@ -48,8 +48,8 @@ from schemas import ( SortieStockRequest, FamilleCreate, PDFGenerationRequest, - DevisUpdateGatewayRequest - ) + DevisUpdateGatewayRequest, +) logging.basicConfig( level=logging.INFO, @@ -58,6 +58,7 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) + def verify_token(x_sage_token: str = Header(...)): """Vérification du token d'authentification""" if x_sage_token != settings.sage_gateway_token: @@ -278,7 +279,9 @@ def changer_statut_document(numero: str, type_doc: int, nouveau_statut: int): doc.DO_Statut = nouveau_statut doc.Write() - logger.info(f" Statut document {numero}: {statut_actuel} → {nouveau_statut}") + logger.info( + f" Statut document {numero}: {statut_actuel} → {nouveau_statut}" + ) return { "success": True, @@ -323,11 +326,11 @@ def transformer_document( ) transformations_valides = { - (0, 10), # Devis → Commande - (10, 30), # Commande → Bon de livraison - (10, 60), # Commande → Facture - (30, 60), # Bon de livraison → Facture - (0, 60), # Devis → Facture (si autorisé) + (0, 10), + (10, 30), + (10, 60), + (30, 60), + (0, 60), } if (type_source, type_cible) not in transformations_valides: @@ -443,11 +446,11 @@ def lire_remise_max_client(code: str): if not client_obj: raise HTTPException(404, f"Client {code} introuvable") - remise_max = 10.0 # Défaut + remise_max = 10.0 try: remise_max = float(getattr(client_obj, "CT_RemiseMax", 10.0)) - except: + except Exception: pass logger.info(f" Remise max client {code}: {remise_max}%") @@ -633,7 +636,7 @@ def livraisons_list( livraisons = sage.lister_toutes_livraisons_cache(filtre) if statut is not None: - livraisons = [l for l in livraisons if l.get("statut") == statut] + livraisons = [ligne for ligne in livraisons if ligne.get("statut") == statut] livraisons = livraisons[:limit] @@ -1026,7 +1029,7 @@ def lister_depots(): factory_depot = sage.cial.FactoryDepot index = 1 - while index <= 100: # Max 100 dépôts + while index <= 100: try: persist = factory_depot.List(index) @@ -1045,30 +1048,30 @@ def lister_depots(): try: code = getattr(depot, "DE_Code", "").strip() - except: + except Exception: pass try: numero = int(getattr(depot, "Compteur", 0)) - except: + except Exception: try: numero = int(code) - except: + except Exception: numero = 0 try: intitule = getattr(depot, "DE_Intitule", "") - except: + except Exception: pass try: contact = getattr(depot, "DE_Contact", "") - except: + except Exception: pass try: exclu = getattr(depot, "DE_Exclure", False) - except: + except Exception: pass if not code: @@ -1085,9 +1088,9 @@ def lister_depots(): cp = getattr(adresse_obj, "CodePostal", "") ville = getattr(adresse_obj, "Ville", "") adresse_complete = f"{adresse} {cp} {ville}".strip() - except: + except Exception: pass - except: + except Exception: pass principal = False @@ -1095,8 +1098,8 @@ def lister_depots(): principal = True depot_info = { - "code": code, # ⭐ "01", "02" - "numero": numero, # ⭐ 1, 2 (depuis Compteur) + "code": code, + "numero": numero, "intitule": intitule, "adresse": adresse_complete, "contact": contact, @@ -1341,6 +1344,6 @@ if __name__ == "__main__": "main:app", host=settings.api_host, port=settings.api_port, - reload=False, # Pas de reload en production + reload=False, log_level="info", ) diff --git a/main.py.bak b/main.py.bak deleted file mode 100644 index 1ff993e..0000000 --- a/main.py.bak +++ /dev/null @@ -1,4514 +0,0 @@ -from fastapi import FastAPI, HTTPException, Header, Depends, Query -from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel, Field -from typing import Optional, List, Dict -from datetime import datetime, date -from enum import Enum -import uvicorn -import logging -import win32com.client -import time -from config import settings, validate_settings -from sage_connector import SageConnector - -# ===================================================== -# LOGGING -# ===================================================== -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - handlers=[logging.FileHandler("sage_gateway.log"), logging.StreamHandler()], -) -logger = logging.getLogger(__name__) - - -# ===================================================== -# ENUMS -# ===================================================== -class TypeDocument(int, Enum): - DEVIS = 0 - BON_LIVRAISON = 1 - BON_RETOUR = 2 - COMMANDE = 3 - PREPARATION = 4 - FACTURE = 5 - - -# ===================================================== -# MODÈLES -# ===================================================== - - -class DocumentGetRequest(BaseModel): - numero: str - type_doc: int - - -class FiltreRequest(BaseModel): - filtre: Optional[str] = "" - - -class CodeRequest(BaseModel): - code: str - - -class ChampLibreRequest(BaseModel): - doc_id: str - type_doc: int - nom_champ: str - valeur: str - - -class DevisRequest(BaseModel): - client_id: str - date_devis: Optional[date] = None - lignes: List[ - Dict - ] # Format: {article_code, quantite, prix_unitaire_ht, remise_pourcentage} - - -class TransformationRequest(BaseModel): - numero_source: str - type_source: int - type_cible: int - - -class StatutRequest(BaseModel): - nouveau_statut: int - - -class ClientCreateRequest(BaseModel): - intitule: str = Field(..., description="Nom du client (CT_Intitule)") - compte_collectif: str = Field("411000", description="Compte général rattaché") - num: Optional[str] = Field(None, description="Laisser vide pour numérotation auto") - 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 - - -class ClientUpdateGatewayRequest(BaseModel): - """Modèle pour modification client côté gateway""" - - 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 - - -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 - - -class FournisseurUpdateGatewayRequest(BaseModel): - """Modèle pour modification fournisseur côté gateway""" - - code: str - fournisseur_data: Dict - - -class DevisUpdateGatewayRequest(BaseModel): - """Modèle pour modification devis côté gateway""" - - numero: str - devis_data: Dict - - -class CommandeCreateRequest(BaseModel): - """Création d'une commande""" - - client_id: str - date_commande: Optional[date] = None - reference: Optional[str] = None - lignes: List[Dict] - - -class CommandeUpdateGatewayRequest(BaseModel): - """Modèle pour modification commande côté gateway""" - - numero: str - commande_data: Dict - - -class LivraisonCreateGatewayRequest(BaseModel): - """Création d'une livraison côté gateway""" - - client_id: str - date_livraison: Optional[date] = None - lignes: List[Dict] - reference: Optional[str] = None - - -class LivraisonUpdateGatewayRequest(BaseModel): - """Modèle pour modification livraison côté gateway""" - - numero: str - livraison_data: Dict - - -class AvoirCreateGatewayRequest(BaseModel): - """Création d'un avoir côté gateway""" - - client_id: str - date_avoir: Optional[date] = None - lignes: List[Dict] - reference: Optional[str] = None - - -class AvoirUpdateGatewayRequest(BaseModel): - """Modèle pour modification avoir côté gateway""" - - numero: str - avoir_data: Dict - - -class FactureCreateGatewayRequest(BaseModel): - """Création d'une facture côté gateway""" - - client_id: str - date_facture: Optional[date] = None - lignes: List[Dict] - reference: Optional[str] = None - - -class FactureUpdateGatewayRequest(BaseModel): - """Modèle pour modification facture côté gateway""" - - numero: str - facture_data: Dict - - -class PDFGenerationRequest(BaseModel): - """Modèle pour génération PDF""" - - doc_id: str = Field(..., description="Numéro du document") - type_doc: int = Field(..., ge=0, le=60, description="Type de document Sage") - - -class ArticleCreateRequest(BaseModel): - reference: str = Field(..., description="Référence article (max 18 car)") - designation: str = Field(..., description="Désignation (max 69 car)") - famille: Optional[str] = Field(None, description="Code famille") - prix_vente: Optional[float] = Field(None, ge=0, description="Prix vente HT") - prix_achat: Optional[float] = Field(None, ge=0, description="Prix achat HT") - stock_reel: Optional[float] = Field(None, ge=0, description="Stock initial") - stock_mini: Optional[float] = Field(None, ge=0, description="Stock minimum") - code_ean: Optional[str] = Field(None, description="Code-barres EAN") - unite_vente: Optional[str] = Field("UN", description="Unité de vente") - tva_code: Optional[str] = Field(None, description="Code TVA") - description: Optional[str] = Field(None, description="Description/Commentaire") - - -class ArticleUpdateGatewayRequest(BaseModel): - """Modèle pour modification article côté gateway""" - reference: str - article_data: Dict - - -# ===================================================== -# SÉCURITÉ -# ===================================================== -def verify_token(x_sage_token: str = Header(...)): - """Vérification du token d'authentification""" - if x_sage_token != settings.sage_gateway_token: - logger.warning(f" Token invalide reçu: {x_sage_token[:20]}...") - raise HTTPException(401, "Token invalide") - return True - - -# ===================================================== -# APPLICATION -# ===================================================== -app = FastAPI( - title="Sage Gateway - Windows Server", - version="1.0.0", - description="Passerelle d'accès à Sage 100c pour VPS Linux", -) - -app.add_middleware( - CORSMiddleware, - allow_origins=settings.cors_origins, - allow_methods=["*"], - allow_headers=["*"], - allow_credentials=True, -) - -sage: Optional[SageConnector] = None - - -# ===================================================== -# LIFECYCLE -# ===================================================== -@app.on_event("startup") -def startup(): - global sage - - logger.info("🚀 Démarrage Sage Gateway Windows...") - - # Validation config - try: - validate_settings() - logger.info(" Configuration validée") - except ValueError as e: - logger.error(f" Configuration invalide: {e}") - raise - - # Connexion Sage - sage = SageConnector( - settings.chemin_base, settings.utilisateur, settings.mot_de_passe - ) - - if not sage.connecter(): - raise RuntimeError(" Impossible de se connecter à Sage 100c") - - logger.info(" Sage Gateway démarré et connecté") - - -@app.on_event("shutdown") -def shutdown(): - if sage: - sage.deconnecter() - logger.info("👋 Sage Gateway arrêté") - - -# ===================================================== -# ENDPOINTS - SYSTÈME -# ===================================================== -@app.get("/health") -def health(): - """Health check""" - return { - "status": "ok", - "sage_connected": sage is not None and sage.cial is not None, - "cache_info": sage.get_cache_info() if sage else None, - "timestamp": datetime.now().isoformat(), - } - - -# ===================================================== -# ENDPOINTS - CLIENTS -# ===================================================== -@app.post("/sage/clients/list", dependencies=[Depends(verify_token)]) -def clients_list(req: FiltreRequest): - """Liste des clients avec filtre optionnel""" - try: - clients = sage.lister_tous_clients(req.filtre) - return {"success": True, "data": clients} - except Exception as e: - logger.error(f"Erreur liste clients: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/clients/update", dependencies=[Depends(verify_token)]) -def modifier_client_endpoint(req: ClientUpdateGatewayRequest): - """ - ✏️ Modification d'un client dans Sage - """ - try: - resultat = sage.modifier_client(req.code, req.client_data) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier modification client: {e}") - raise HTTPException(404, str(e)) - except Exception as e: - logger.error(f"Erreur technique modification client: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/clients/get", dependencies=[Depends(verify_token)]) -def client_get(req: CodeRequest): - """Lecture d'un client par code""" - try: - client = sage.lire_client(req.code) - if not client: - raise HTTPException(404, f"Client {req.code} non trouvé") - return {"success": True, "data": client} - except HTTPException: - raise - except Exception as e: - logger.error(f"Erreur lecture client: {e}") - raise HTTPException(500, str(e)) - - -# DANS main.py -@app.post("/sage/clients/create", dependencies=[Depends(verify_token)]) -def create_client_endpoint(req: ClientCreateRequest): - """Création d'un client dans Sage""" - try: - # L'appel au connecteur est fait ici - resultat = sage.creer_client(req.dict()) - return {"success": True, "data": resultat} - except ValueError as e: - logger.warning(f"Erreur métier création client: {e}") - # Erreur métier (ex: doublon) -> 400 Bad Request - raise HTTPException(400, str(e)) - except Exception as e: - logger.error(f"Erreur technique création client: {e}") - # Erreur technique (ex: COM) -> 500 Internal Server Error - raise HTTPException(500, str(e)) - - -# ===================================================== -# ENDPOINTS - ARTICLES -# ===================================================== -@app.post("/sage/articles/list", dependencies=[Depends(verify_token)]) -def articles_list(req: FiltreRequest): - """Liste des articles avec filtre optionnel""" - try: - articles = sage.lister_tous_articles(req.filtre) - return {"success": True, "data": articles} - except Exception as e: - logger.error(f"Erreur liste articles: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/articles/get", dependencies=[Depends(verify_token)]) -def article_get(req: CodeRequest): - """Lecture d'un article par référence""" - try: - article = sage.lire_article(req.code) - if not article: - raise HTTPException(404, f"Article {req.code} non trouvé") - return {"success": True, "data": article} - except HTTPException: - raise - except Exception as e: - logger.error(f"Erreur lecture article: {e}") - raise HTTPException(500, str(e)) - - -# ===================================================== -# ENDPOINTS - DEVIS -# ===================================================== -@app.post("/sage/devis/create", dependencies=[Depends(verify_token)]) -def creer_devis(req: DevisRequest): - """Création d'un devis""" - try: - # Transformer en format attendu par sage_connector - devis_data = { - "client": {"code": req.client_id, "intitule": ""}, - "date_devis": req.date_devis or date.today(), - "lignes": req.lignes, - } - - resultat = sage.creer_devis_enrichi(devis_data) - return {"success": True, "data": resultat} - except Exception as e: - logger.error(f"Erreur création devis: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/devis/get", dependencies=[Depends(verify_token)]) -def lire_devis(req: CodeRequest): - """ - 📄 Lecture d'un devis AVEC ses lignes (lecture Sage directe) - - Plus lent que /list car charge les lignes depuis Sage - 💡 Utiliser /list pour afficher une table rapide - """ - try: - # Lecture complète depuis Sage (avec lignes) - devis = sage.lire_devis(req.code) - if not devis: - raise HTTPException(404, f"Devis {req.code} non trouvé") - return {"success": True, "data": devis} - except HTTPException: - raise - except Exception as e: - logger.error(f"Erreur lecture devis: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/devis/list", dependencies=[Depends(verify_token)]) -def devis_list( - limit: int = Query(100, description="Nombre max de devis"), - statut: Optional[int] = Query(None, description="Filtrer par statut"), - filtre: str = Query("", description="Filtre texte (numero, client)"), -): - """ - 📋 Liste rapide des devis depuis le CACHE (sans lignes) - - ⚡ ULTRA-RAPIDE: Utilise le cache mémoire au lieu de scanner Sage - 💡 Pour les détails avec lignes, utiliser GET /sage/devis/get - """ - try: - # Récupération depuis le cache (instantané) - devis_list = sage.lister_tous_devis_cache(filtre) - - # Filtrer par statut si demandé - if statut is not None: - devis_list = [d for d in devis_list if d.get("statut") == statut] - - # Limiter le nombre de résultats - devis_list = devis_list[:limit] - - logger.info(f" {len(devis_list)} devis retournés depuis le cache") - - return {"success": True, "data": devis_list} - - except Exception as e: - logger.error(f" Erreur liste devis: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.post("/sage/devis/statut", dependencies=[Depends(verify_token)]) -def changer_statut_devis_endpoint(numero: str, nouveau_statut: int): - """Change le statut d'un devis""" - try: - with sage._com_context(), sage._lock_com: - factory = sage.cial.FactoryDocumentVente - persist = factory.ReadPiece(0, numero) - - if not persist: - raise HTTPException(404, f"Devis {numero} introuvable") - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - statut_actuel = getattr(doc, "DO_Statut", 0) - doc.DO_Statut = nouveau_statut - doc.Write() - - logger.info(f" Statut devis {numero}: {statut_actuel} → {nouveau_statut}") - - return { - "success": True, - "data": { - "numero": numero, - "statut_ancien": statut_actuel, - "statut_nouveau": nouveau_statut, - }, - } - except HTTPException: - raise - except Exception as e: - logger.error(f"Erreur changement statut: {e}") - raise HTTPException(500, str(e)) - - -# ===================================================== -# ENDPOINTS - DOCUMENTS -# ===================================================== -@app.post("/sage/documents/get", dependencies=[Depends(verify_token)]) -def lire_document(req: DocumentGetRequest): - """Lecture d'un document (commande, facture, etc.)""" - try: - doc = sage.lire_document(req.numero, req.type_doc) - if not doc: - raise HTTPException(404, f"Document {req.numero} non trouvé") - return {"success": True, "data": doc} - except HTTPException: - raise - except Exception as e: - logger.error(f"Erreur lecture document: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/documents/transform", dependencies=[Depends(verify_token)]) -def transformer_document( - numero_source: str = Query(..., description="Numéro du document source"), - type_source: int = Query(..., description="Type document source"), - type_cible: int = Query(..., description="Type document cible"), -): - """ - 🔧 Transformation de document - - CORRECTION : Utilise les VRAIS types Sage Dataven - - Types valides : - - 0: Devis - - 10: Bon de commande - - 20: Préparation - - 30: Bon de livraison - - 40: Bon de retour - - 50: Bon d'avoir - - 60: Facture - - Transformations autorisées : - - Devis (0) → Commande (10) - - Commande (10) → Bon livraison (30) - - Commande (10) → Facture (60) - - Bon livraison (30) → Facture (60) - """ - try: - logger.info( - f"🔄 Transformation demandée: {numero_source} " - f"(type {type_source}) → type {type_cible}" - ) - - # Matrice des transformations valides pour VOTRE Sage - transformations_valides = { - (0, 10), # Devis → Commande - (10, 30), # Commande → Bon de livraison - (10, 60), # Commande → Facture - (30, 60), # Bon de livraison → Facture - (0, 60), # Devis → Facture (si autorisé) - } - - if (type_source, type_cible) not in transformations_valides: - logger.error( - f" Transformation non autorisée: {type_source} → {type_cible}" - ) - raise HTTPException( - 400, - f"Transformation non autorisée: type {type_source} → type {type_cible}. " - f"Transformations valides: {transformations_valides}", - ) - - # Appel au connecteur Sage - resultat = sage.transformer_document(numero_source, type_source, type_cible) - - logger.info( - f" Transformation réussie: {numero_source} → " - f"{resultat.get('document_cible', '?')} " - f"({resultat.get('nb_lignes', 0)} lignes)" - ) - - return {"success": True, "data": resultat} - - except HTTPException: - raise - except ValueError as e: - logger.error(f" Erreur métier transformation: {e}") - raise HTTPException(400, str(e)) - except Exception as e: - logger.error(f" Erreur technique transformation: {e}", exc_info=True) - raise HTTPException(500, f"Erreur transformation: {str(e)}") - - -@app.post("/sage/documents/champ-libre", dependencies=[Depends(verify_token)]) -def maj_champ_libre(req: ChampLibreRequest): - """Mise à jour d'un champ libre""" - try: - success = sage.mettre_a_jour_champ_libre( - req.doc_id, req.type_doc, req.nom_champ, req.valeur - ) - return {"success": success} - except Exception as e: - logger.error(f"Erreur MAJ champ libre: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/documents/derniere-relance", dependencies=[Depends(verify_token)]) -def maj_derniere_relance(doc_id: str, type_doc: int): - """📅 Met à jour le champ 'Dernière relance' d'un document""" - try: - success = sage.mettre_a_jour_derniere_relance(doc_id, type_doc) - return {"success": success} - except Exception as e: - logger.error(f"Erreur MAJ dernière relance: {e}") - raise HTTPException(500, str(e)) - - -# ===================================================== -# ENDPOINTS - CONTACTS -# ===================================================== -@app.post("/sage/contact/read", dependencies=[Depends(verify_token)]) -def contact_read(req: CodeRequest): - """Lecture du contact principal d'un client""" - try: - contact = sage.lire_contact_principal_client(req.code) - if not contact: - raise HTTPException(404, f"Contact non trouvé pour client {req.code}") - return {"success": True, "data": contact} - except HTTPException: - raise - except Exception as e: - logger.error(f"Erreur lecture contact: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/commandes/list", dependencies=[Depends(verify_token)]) -def commandes_list( - limit: int = Query(100, description="Nombre max de commandes"), - statut: Optional[int] = Query(None, description="Filtrer par statut"), - filtre: str = Query("", description="Filtre texte"), -): - """ - 📋 Liste rapide des commandes depuis le CACHE (sans lignes) - - ⚡ ULTRA-RAPIDE: Utilise le cache mémoire - """ - try: - commandes = sage.lister_toutes_commandes_cache(filtre) - - if statut is not None: - commandes = [c for c in commandes if c.get("statut") == statut] - - commandes = commandes[:limit] - - logger.info(f" {len(commandes)} commandes retournées depuis le cache") - - return {"success": True, "data": commandes} - - except Exception as e: - logger.error(f" Erreur liste commandes: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.post("/sage/factures/list", dependencies=[Depends(verify_token)]) -def factures_list( - limit: int = Query(100, description="Nombre max de factures"), - statut: Optional[int] = Query(None, description="Filtrer par statut"), - filtre: str = Query("", description="Filtre texte"), -): - """ - 📋 Liste rapide des factures depuis le CACHE (sans lignes) - - ⚡ ULTRA-RAPIDE: Utilise le cache mémoire - 💡 Pour les détails avec lignes, utiliser /sage/documents/get - """ - try: - factures = sage.lister_toutes_factures_cache(filtre) - - if statut is not None: - factures = [f for f in factures if f.get("statut") == statut] - - factures = factures[:limit] - - logger.info(f" {len(factures)} factures retournées depuis le cache") - - return {"success": True, "data": factures} - - except Exception as e: - logger.error(f" Erreur liste factures: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.post("/sage/client/remise-max", dependencies=[Depends(verify_token)]) -def lire_remise_max_client(code: str): - """Récupère la remise max autorisée pour un client""" - try: - client_obj = sage._lire_client_obj(code) - - if not client_obj: - raise HTTPException(404, f"Client {code} introuvable") - - remise_max = 10.0 # Défaut - - try: - remise_max = float(getattr(client_obj, "CT_RemiseMax", 10.0)) - except: - pass - - logger.info(f" Remise max client {code}: {remise_max}%") - - return { - "success": True, - "data": {"client_code": code, "remise_max": remise_max}, - } - - except HTTPException: - raise - except Exception as e: - logger.error(f"Erreur lecture remise: {e}") - raise HTTPException(500, str(e)) - - -# ===================================================== -# ENDPOINTS - ADMIN -# ===================================================== -@app.post("/sage/cache/refresh", dependencies=[Depends(verify_token)]) -def refresh_cache(): - """Force le rafraîchissement du cache""" - try: - sage.forcer_actualisation_cache() - return { - "success": True, - "message": "Cache actualisé", - "info": sage.get_cache_info(), - } - except Exception as e: - logger.error(f"Erreur refresh cache: {e}") - raise HTTPException(500, str(e)) - - -@app.get("/sage/cache/info", dependencies=[Depends(verify_token)]) -def cache_info_get(): - """Informations sur le cache (endpoint GET)""" - try: - return {"success": True, "data": sage.get_cache_info()} - except Exception as e: - logger.error(f"Erreur info cache: {e}") - raise HTTPException(500, str(e)) - - -# Script à ajouter temporairement dans main.py pour diagnostiquer - - -@app.get("/sage/devis/{numero}/diagnostic", dependencies=[Depends(verify_token)]) -def diagnostiquer_devis(numero: str): - """ - ENDPOINT DE DIAGNOSTIC: Affiche TOUTES les infos d'un devis - - Permet de comprendre pourquoi un devis ne peut pas être transformé - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - factory = sage.cial.FactoryDocumentVente - - # Essayer ReadPiece - persist = factory.ReadPiece(0, numero) - - # Si échec, chercher dans List() - if not persist: - logger.info(f"[DIAG] ReadPiece echoue, recherche 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) == 0 - and getattr(doc_test, "DO_Piece", "") == numero - ): - persist = persist_test - logger.info(f"[DIAG] Trouve a l'index {index}") - break - - index += 1 - except: - index += 1 - - if not persist: - raise HTTPException(404, f"Devis {numero} introuvable") - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - # EXTRACTION COMPLÈTE - diagnostic = { - "numero": getattr(doc, "DO_Piece", ""), - "type": getattr(doc, "DO_Type", -1), - "statut": getattr(doc, "DO_Statut", -1), - "statut_libelle": { - 0: "Brouillon", - 1: "Soumis", - 2: "Accepte", - 3: "Realise partiellement", - 4: "Realise totalement", - 5: "Transforme", - 6: "Annule", - }.get(getattr(doc, "DO_Statut", -1), "Inconnu"), - "date": str(getattr(doc, "DO_Date", "")), - "total_ht": float(getattr(doc, "DO_TotalHT", 0.0)), - "total_ttc": float(getattr(doc, "DO_TotalTTC", 0.0)), - "est_transformable": False, - "raison_blocage": None, - } - - # Client - try: - client_obj = getattr(doc, "Client", None) - if client_obj: - client_obj.Read() - diagnostic["client_code"] = getattr( - client_obj, "CT_Num", "" - ).strip() - diagnostic["client_intitule"] = getattr( - client_obj, "CT_Intitule", "" - ).strip() - except Exception as e: - diagnostic["erreur_client"] = str(e) - - # Lignes - try: - factory_lignes = doc.FactoryDocumentLigne - except: - factory_lignes = doc.FactoryDocumentVenteLigne - - lignes = [] - index = 1 - while index <= 100: - try: - ligne_p = factory_lignes.List(index) - if ligne_p is None: - break - - ligne = win32com.client.CastTo(ligne_p, "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 - - lignes.append( - { - "index": index, - "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 - - diagnostic["nb_lignes"] = len(lignes) - diagnostic["lignes"] = lignes - - # ANALYSE TRANSFORMABILITÉ - statut = diagnostic["statut"] - - if statut == 5: - diagnostic["raison_blocage"] = "Document deja transforme (statut=5)" - elif statut == 6: - diagnostic["raison_blocage"] = "Document annule (statut=6)" - elif statut in [3, 4]: - diagnostic["raison_blocage"] = ( - f"Document deja realise partiellement ou totalement (statut={statut}). " - f"Une commande/BL/facture existe probablement deja." - ) - diagnostic["suggestion"] = ( - "Cherchez les documents lies a ce devis dans Sage. " - "Il a peut-etre deja ete transforme manuellement." - ) - elif statut == 0: - diagnostic["est_transformable"] = True - diagnostic["action_requise"] = ( - "Statut 'Brouillon'. Le systeme le passera automatiquement a 'Accepte' " - "avant transformation." - ) - elif statut == 2: - diagnostic["est_transformable"] = True - diagnostic["action_requise"] = ( - "Statut 'Accepte'. Transformation possible." - ) - else: - diagnostic["raison_blocage"] = f"Statut inconnu ou non gere: {statut}" - - # Champs libres (pour Universign, etc.) - champs_libres = {} - try: - for champ in ["UniversignID", "DerniereRelance", "DO_Ref"]: - try: - valeur = getattr(doc, f"DO_{champ}", None) - if valeur: - champs_libres[champ] = str(valeur) - except: - pass - except: - pass - - if champs_libres: - diagnostic["champs_libres"] = champs_libres - - logger.info( - f"[DIAG] Devis {numero}: statut={statut}, transformable={diagnostic['est_transformable']}" - ) - - return {"success": True, "diagnostic": diagnostic} - - except HTTPException: - raise - except Exception as e: - logger.error(f"[DIAG] Erreur diagnostic: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.get("/sage/diagnostic/configuration", dependencies=[Depends(verify_token)]) -def diagnostic_configuration(): - """ - DIAGNOSTIC COMPLET de la configuration Sage - - Teste: - - Quelles méthodes COM sont disponibles - - Quels types de documents sont autorisés - - Quelles permissions l'utilisateur a - - Version de Sage - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - diagnostic = { - "connexion": "OK", - "chemin_base": sage.chemin_base, - "utilisateur": sage.utilisateur, - } - - # Version Sage - try: - version = getattr(sage.cial, "Version", "Inconnue") - diagnostic["version_sage"] = str(version) - except: - diagnostic["version_sage"] = "Non disponible" - - # Test des méthodes disponibles sur IBSCIALApplication3 - methodes_disponibles = [] - methodes_a_tester = [ - "CreateProcess_Document", - "FactoryDocumentVente", - "FactoryArticle", - "CptaApplication", - "BeginTrans", - "CommitTrans", - "RollbackTrans", - ] - - for methode in methodes_a_tester: - try: - if hasattr(sage.cial, methode): - methodes_disponibles.append(methode) - except: - pass - - diagnostic["methodes_cial_disponibles"] = methodes_disponibles - - # Test des types de documents autorisés - types_autorises = [] - types_bloques = [] - - for type_doc in range(6): # 0-5 - try: - # Essayer de créer un process (sans le valider) - process = sage.cial.CreateProcess_Document(type_doc) - if process: - types_autorises.append( - { - "type": type_doc, - "libelle": { - 0: "Devis", - 1: "Bon de livraison", - 2: "Bon de retour", - 3: "Commande", - 4: "Preparation", - 5: "Facture", - }[type_doc], - } - ) - # Ne pas valider, juste tester - del process - except Exception as e: - types_bloques.append( - { - "type": type_doc, - "libelle": { - 0: "Devis", - 1: "Bon de livraison", - 2: "Bon de retour", - 3: "Commande", - 4: "Preparation", - 5: "Facture", - }[type_doc], - "erreur": str(e)[:200], - } - ) - - diagnostic["types_documents_autorises"] = types_autorises - diagnostic["types_documents_bloques"] = types_bloques - - # Test TransformInto() sur un devis test - try: - factory = sage.cial.FactoryDocumentVente - - # Chercher n'importe quel devis - index = 1 - devis_test = None - - while index < 100: - try: - persist = factory.List(index) - if persist is None: - break - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - if getattr(doc, "DO_Type", -1) == 0: # Devis - devis_test = doc - break - - index += 1 - except: - index += 1 - - if devis_test: - # Tester si TransformInto existe - if hasattr(devis_test, "TransformInto"): - diagnostic["transforminto_disponible"] = True - diagnostic["transforminto_test"] = "Methode existe (non testee)" - else: - diagnostic["transforminto_disponible"] = False - diagnostic["transforminto_test"] = ( - "Methode TransformInto() inexistante" - ) - else: - diagnostic["transforminto_disponible"] = ( - "Impossible de tester (aucun devis trouve)" - ) - - except Exception as e: - diagnostic["transforminto_disponible"] = False - diagnostic["transforminto_erreur"] = str(e) - - # Modules Sage actifs - try: - # Tester l'accès aux différentes factories - modules = {} - - try: - sage.cial.FactoryDocumentVente - modules["Ventes"] = "OK" - except: - modules["Ventes"] = "INACCESSIBLE" - - try: - sage.cial.CptaApplication.FactoryClient - modules["Clients"] = "OK" - except: - modules["Clients"] = "INACCESSIBLE" - - try: - sage.cial.FactoryArticle - modules["Articles"] = "OK" - except: - modules["Articles"] = "INACCESSIBLE" - - diagnostic["modules_actifs"] = modules - except Exception as e: - diagnostic["modules_actifs_erreur"] = str(e) - - # Compter documents existants - try: - counts = {} - factory = sage.cial.FactoryDocumentVente - - for type_doc in range(6): - count = 0 - index = 1 - - while index < 1000: - try: - persist = factory.List(index) - if persist is None: - break - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - if getattr(doc, "DO_Type", -1) == type_doc: - count += 1 - - index += 1 - except: - index += 1 - break - - counts[ - { - 0: "Devis", - 1: "Bons_livraison", - 2: "Bons_retour", - 3: "Commandes", - 4: "Preparations", - 5: "Factures", - }[type_doc] - ] = count - - diagnostic["documents_existants"] = counts - except Exception as e: - diagnostic["documents_existants_erreur"] = str(e) - - logger.info("[DIAG] Configuration Sage analysee") - - return {"success": True, "diagnostic": diagnostic} - - except HTTPException: - raise - except Exception as e: - logger.error(f"[DIAG] Erreur diagnostic config: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.get("/sage/diagnostic/types-reels", dependencies=[Depends(verify_token)]) -def decouvrir_types_reels(): - """ - DIAGNOSTIC CRITIQUE: Découvre les VRAIS types de documents Sage - - Au lieu de deviner les types (0-5), on va: - 1. Créer manuellement un document de chaque type dans Sage - 2. Les lister ici pour voir leurs vrais numéros de type - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - factory = sage.cial.FactoryDocumentVente - - # Parcourir TOUS les documents - documents_par_type = {} - index = 1 - max_docs = 500 # Limiter pour ne pas bloquer - - logger.info("[DIAG] Scan de tous les documents...") - - while index < max_docs: - try: - persist = factory.List(index) - if persist is None: - break - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - # Récupérer le type ET le sous-type - type_doc = getattr(doc, "DO_Type", -1) - piece = getattr(doc, "DO_Piece", "") - statut = getattr(doc, "DO_Statut", -1) - - # Essayer de récupérer le domaine (vente/achat) - domaine = "Inconnu" - try: - domaine_val = getattr(doc, "DO_Domaine", -1) - domaine = {0: "Vente", 1: "Achat"}.get( - domaine_val, f"Code {domaine_val}" - ) - except: - pass - - # Récupérer la catégorie - categorie = "Inconnue" - try: - cat_val = getattr(doc, "DO_Categorie", -1) - categorie = str(cat_val) - except: - pass - - # Grouper par type - if type_doc not in documents_par_type: - documents_par_type[type_doc] = { - "count": 0, - "exemples": [], - "domaine": domaine, - "categorie": categorie, - } - - documents_par_type[type_doc]["count"] += 1 - - # Garder quelques exemples - if len(documents_par_type[type_doc]["exemples"]) < 3: - documents_par_type[type_doc]["exemples"].append( - { - "numero": piece, - "statut": statut, - "domaine": domaine, - "categorie": categorie, - } - ) - - index += 1 - - except Exception as e: - logger.debug(f"Erreur index {index}: {e}") - index += 1 - - # Formater le résultat - types_trouves = [] - - for type_num, infos in sorted(documents_par_type.items()): - types_trouves.append( - { - "type_code": type_num, - "nombre_documents": infos["count"], - "domaine": infos["domaine"], - "categorie": infos["categorie"], - "exemples": infos["exemples"], - "suggestion_libelle": _deviner_libelle_type( - type_num, infos["exemples"] - ), - } - ) - - logger.info( - f"[DIAG] {len(types_trouves)} types de documents distincts trouves" - ) - - return { - "success": True, - "types_documents_reels": types_trouves, - "instructions": ( - "Pour identifier les types corrects:\n" - "1. Creez manuellement dans Sage: 1 Bon de commande, 1 BL, 1 Facture\n" - "2. Appelez de nouveau cet endpoint\n" - "3. Les nouveaux types apparaitront avec leurs numeros corrects" - ), - "total_documents_scannes": index - 1, - } - - except Exception as e: - logger.error(f"[DIAG] Erreur decouverte types: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -def _deviner_libelle_type(type_num, exemples): - """Devine le libellé d'un type basé sur les numéros de pièce""" - if not exemples: - return "Type inconnu" - - # Analyser les préfixes des numéros - prefixes = [ex["numero"][:2] for ex in exemples if ex["numero"]] - prefix_commun = max(set(prefixes), key=prefixes.count) if prefixes else "" - - # Deviner selon le type_num et les préfixes - suggestions = { - 0: "Devis (DE)", - 1: "Bon de livraison (BL)", - 2: "Bon de retour (BR)", - 3: "Bon de commande (BC)", - 4: "Preparation de livraison (PL)", - 5: "Facture (FA)", - 6: "Facture d'avoir (AV)", - 7: "Bon d'avoir financier (BA)", - } - - libelle_base = suggestions.get(type_num, f"Type {type_num}") - - if prefix_commun: - libelle_base += f" - Detecte: prefix '{prefix_commun}'" - - return libelle_base - - -@app.post("/sage/test-creation-par-type", dependencies=[Depends(verify_token)]) -def tester_creation_par_type(type_doc: int = Query(..., ge=0, le=20)): - """ - TEST: Essaie de créer un document d'un type spécifique - - Permet de tester tous les types possibles (0-20) pour trouver - lesquels fonctionnent sur votre installation - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - logger.info(f"[TEST] Tentative creation type {type_doc}...") - - try: - # Essayer de créer un process - process = sage.cial.CreateProcess_Document(type_doc) - - if not process: - return { - "success": False, - "type": type_doc, - "resultat": "Process NULL retourne", - } - - # Si on arrive ici, le type est valide ! - doc = process.Document - - try: - doc = win32com.client.CastTo(doc, "IBODocumentVente3") - except: - pass - - # Récupérer les infos du document créé - type_reel = getattr(doc, "DO_Type", -1) - domaine = getattr(doc, "DO_Domaine", -1) - - # NE PAS VALIDER le document (pas de Write/Process) - # On veut juste savoir si la création est possible - - del process - del doc - - return { - "success": True, - "type_demande": type_doc, - "type_reel_doc": type_reel, - "domaine": {0: "Vente", 1: "Achat"}.get(domaine, domaine), - "resultat": "CREATION POSSIBLE", - "note": "Document non valide (test uniquement)", - } - - except Exception as e: - return { - "success": False, - "type": type_doc, - "erreur": str(e), - "resultat": "CREATION IMPOSSIBLE", - } - - except Exception as e: - logger.error(f"[TEST] Erreur test type {type_doc}: {e}") - raise HTTPException(500, str(e)) - - -@app.get("/sage/diagnostic/facture-requirements", dependencies=[Depends(verify_token)]) -def diagnostiquer_exigences_facture(): - """ - DIAGNOSTIC: Découvre les champs obligatoires pour créer une facture - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - # Créer un process facture de test - process = sage.cial.CreateProcess_Document(60) - doc_test = process.Document - - try: - doc_test = win32com.client.CastTo(doc_test, "IBODocumentVente3") - except: - pass - - # Tester tous les champs potentiellement obligatoires - champs_a_tester = [ - "DO_ModeRegl", - "DO_CondRegl", - "DO_CodeJournal", - "DO_Souche", - "DO_TypeCalcul", - "DO_CodeTaxe1", - "CT_Num", - "DO_Date", - "DO_Statut", - ] - - resultats = {} - - for champ in champs_a_tester: - try: - valeur = getattr(doc_test, champ, None) - resultats[champ] = { - "valeur_defaut": str(valeur) if valeur is not None else "None", - "accessible": True, - } - except Exception as e: - resultats[champ] = { - "valeur_defaut": "N/A", - "accessible": False, - "erreur": str(e)[:100], - } - - # Ne pas valider le document de test - del process - del doc_test - - return { - "success": True, - "champs_facture": resultats, - "conseil": "Les champs avec valeur_defaut=None ou 0 sont souvent obligatoires", - } - - except Exception as e: - logger.error(f"[DIAG] Erreur: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.post("/sage/test/creer-facture-vide", dependencies=[Depends(verify_token)]) -def tester_creation_facture_vide(): - """ - 🧪 TEST: Crée une facture vide pour identifier les champs obligatoires - - Ce test permet de découvrir EXACTEMENT quels champs Sage exige - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - logger.info("[TEST] Creation facture test...") - - # 1. Créer le process - process = sage.cial.CreateProcess_Document(60) - doc = process.Document - - try: - doc = win32com.client.CastTo(doc, "IBODocumentVente3") - except: - pass - - # 2. Définir UNIQUEMENT les champs absolument critiques - import pywintypes - - # Date (obligatoire) - doc.DO_Date = pywintypes.Time(datetime.now()) - - # Client (obligatoire) - Utiliser le premier client disponible - factory_client = sage.cial.CptaApplication.FactoryClient - persist_client = factory_client.List(1) - - if persist_client: - client = sage._cast_client(persist_client) - if client: - doc.SetDefaultClient(client) - client_code = getattr(client, "CT_Num", "?") - logger.info(f"[TEST] Client test: {client_code}") - - # 3. Écrire sans Process() pour voir les valeurs par défaut - doc.Write() - doc.Read() - - # 4. Analyser tous les champs - champs_analyse = {} - - for attr in dir(doc): - if attr.startswith("DO_") or attr.startswith("CT_"): - try: - valeur = getattr(doc, attr, None) - if valeur is not None: - champs_analyse[attr] = { - "valeur": str(valeur), - "type": type(valeur).__name__, - } - except: - pass - - logger.info(f"[TEST] {len(champs_analyse)} champs analyses") - - # 5. Tester Process() pour voir l'erreur exacte - erreur_process = None - try: - process.Process() - logger.info("[TEST] Process() reussi (inattendu!)") - except Exception as e: - erreur_process = str(e) - logger.info(f"[TEST] Process() echoue comme prevu: {e}") - - # Ne pas commit - c'est juste un test - try: - sage.cial.CptaApplication.RollbackTrans() - except: - pass - - return { - "success": True, - "champs_definis": champs_analyse, - "erreur_process": erreur_process, - "conseil": "Les champs manquants dans l'erreur sont probablement obligatoires", - } - - except Exception as e: - logger.error(f"[TEST] Erreur: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.get("/sage/config/parametres-facture", dependencies=[Depends(verify_token)]) -def verifier_parametres_facture(): - """ - 🔍 Vérifie les paramètres Sage pour la création de factures - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - parametres = {} - - # Paramètres société - try: - param_societe = sage.cial.CptaApplication.ParametreSociete - - parametres["societe"] = { - "journal_vente_defaut": getattr( - param_societe, "P_CodeJournalVte", "N/A" - ), - "mode_reglement_defaut": getattr( - param_societe, "P_ModeRegl", "N/A" - ), - "souche_facture": getattr(param_societe, "P_SoucheFacture", "N/A"), - } - except Exception as e: - parametres["erreur_societe"] = str(e) - - # Tester un client existant - try: - factory_client = sage.cial.CptaApplication.FactoryClient - persist = factory_client.List(1) - - if persist: - client = sage._cast_client(persist) - if client: - parametres["exemple_client"] = { - "code": getattr(client, "CT_Num", "?"), - "mode_reglement": getattr(client, "CT_ModeRegl", "N/A"), - "conditions_reglement": getattr( - client, "CT_CondRegl", "N/A" - ), - } - except Exception as e: - parametres["erreur_client"] = str(e) - - # Journaux disponibles - try: - factory_journal = sage.cial.CptaApplication.FactoryJournal - journaux = [] - - index = 1 - while index <= 20: # Max 20 journaux - try: - persist_journal = factory_journal.List(index) - if persist_journal is None: - break - - # Cast en journal - journal = win32com.client.CastTo(persist_journal, "IBOJournal3") - journal.Read() - - journaux.append( - { - "code": getattr(journal, "JO_Num", "?"), - "intitule": getattr(journal, "JO_Intitule", "?"), - "type": getattr(journal, "JO_Type", "?"), - } - ) - - index += 1 - except: - index += 1 - break - - parametres["journaux_disponibles"] = journaux - - except Exception as e: - parametres["erreur_journaux"] = str(e) - - return { - "success": True, - "parametres": parametres, - "conseil": "Utilisez ces valeurs pour remplir les champs obligatoires des factures", - } - - except Exception as e: - logger.error(f"Erreur verification config: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.get("/sage/diagnostic/statuts-globaux", dependencies=[Depends(verify_token)]) -def diagnostiquer_statuts_globaux(): - """ - 📊 MATRICE COMPLÈTE DES STATUTS SAGE - - Retourne pour CHAQUE type de document : - - Tous les statuts possibles avec leurs descriptions - - Les statuts requis pour transformation - - Les changements de statuts après transformation - - Les restrictions de changement de statut - - Cette route analyse la base Sage pour découvrir les règles réelles - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - factory = sage.cial.FactoryDocumentVente - - # Définition des types de documents - types_documents = { - 0: "Devis", - 10: "Bon de commande", - 20: "Préparation", - 30: "Bon de livraison", - 40: "Bon de retour", - 50: "Bon d'avoir", - 60: "Facture", - } - - # Descriptions standard des statuts Sage - descriptions_statuts = { - 0: "Brouillon", - 1: "Soumis/En attente", - 2: "Accepté/Validé", - 3: "Réalisé partiellement", - 4: "Réalisé totalement", - 5: "Transformé", - 6: "Annulé", - } - - matrice_complete = {} - - logger.info( - "[DIAG] 🔍 Analyse des statuts pour tous les types de documents..." - ) - - # Pour chaque type de document - for type_doc, libelle_type in types_documents.items(): - logger.info(f"[DIAG] Analyse type {type_doc} ({libelle_type})...") - - analyse_type = { - "type": type_doc, - "libelle": libelle_type, - "statuts_observes": {}, - "exemples_par_statut": {}, - "nb_documents_total": 0, - } - - # Scanner tous les documents de ce type - index = 1 - max_scan = 1000 - - while index < max_scan: - try: - persist = factory.List(index) - if persist is None: - break - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - doc_type = getattr(doc, "DO_Type", -1) - - # Filtrer sur le type qu'on analyse - if doc_type != type_doc: - index += 1 - continue - - analyse_type["nb_documents_total"] += 1 - - # Récupérer le statut - statut = getattr(doc, "DO_Statut", -1) - - # Compter les statuts observés - if statut not in analyse_type["statuts_observes"]: - analyse_type["statuts_observes"][statut] = { - "count": 0, - "description": descriptions_statuts.get( - statut, f"Statut {statut}" - ), - "exemples": [], - } - - analyse_type["statuts_observes"][statut]["count"] += 1 - - # Garder quelques exemples - if ( - len(analyse_type["statuts_observes"][statut]["exemples"]) - < 3 - ): - numero = getattr(doc, "DO_Piece", "") - date = str(getattr(doc, "DO_Date", "")) - - analyse_type["statuts_observes"][statut]["exemples"].append( - { - "numero": numero, - "date": date, - "total_ttc": float( - getattr(doc, "DO_TotalTTC", 0.0) - ), - } - ) - - index += 1 - - except Exception as e: - index += 1 - continue - - # Trier les statuts par nombre d'occurrences - analyse_type["statuts_par_frequence"] = sorted( - [ - { - "statut": s, - "description": info["description"], - "count": info["count"], - "pourcentage": ( - round( - info["count"] - / analyse_type["nb_documents_total"] - * 100, - 1, - ) - if analyse_type["nb_documents_total"] > 0 - else 0 - ), - } - for s, info in analyse_type["statuts_observes"].items() - ], - key=lambda x: x["count"], - reverse=True, - ) - - matrice_complete[type_doc] = analyse_type - - logger.info( - f"[DIAG] Type {type_doc}: {analyse_type['nb_documents_total']} docs, " - f"{len(analyse_type['statuts_observes'])} statuts différents" - ) - - # RÈGLES DE TRANSFORMATION - regles_transformation = { - "transformations_valides": [ - { - "source_type": 0, - "source_libelle": "Devis", - "cible_type": 10, - "cible_libelle": "Bon de commande", - "statut_source_requis": [2], - "statut_source_requis_description": ["Accepté/Validé"], - "statut_source_apres": 5, - "statut_source_apres_description": "Transformé", - "statut_cible_initial": 2, - "statut_cible_initial_description": "Accepté/Validé", - }, - { - "source_type": 10, - "source_libelle": "Bon de commande", - "cible_type": 30, - "cible_libelle": "Bon de livraison", - "statut_source_requis": [2], - "statut_source_requis_description": ["Accepté/Validé"], - "statut_source_apres": 5, - "statut_source_apres_description": "Transformé", - "statut_cible_initial": 2, - "statut_cible_initial_description": "Accepté/Validé", - }, - { - "source_type": 10, - "source_libelle": "Bon de commande", - "cible_type": 60, - "cible_libelle": "Facture", - "statut_source_requis": [2], - "statut_source_requis_description": ["Accepté/Validé"], - "statut_source_apres": 5, - "statut_source_apres_description": "Transformé", - "statut_cible_initial": 2, - "statut_cible_initial_description": "Accepté/Validé", - }, - { - "source_type": 30, - "source_libelle": "Bon de livraison", - "cible_type": 60, - "cible_libelle": "Facture", - "statut_source_requis": [2], - "statut_source_requis_description": ["Accepté/Validé"], - "statut_source_apres": 5, - "statut_source_apres_description": "Transformé", - "statut_cible_initial": 2, - "statut_cible_initial_description": "Accepté/Validé", - }, - { - "source_type": 0, - "source_libelle": "Devis", - "cible_type": 60, - "cible_libelle": "Facture", - "statut_source_requis": [2], - "statut_source_requis_description": ["Accepté/Validé"], - "statut_source_apres": 5, - "statut_source_apres_description": "Transformé", - "statut_cible_initial": 2, - "statut_cible_initial_description": "Accepté/Validé", - }, - ], - "statuts_bloquants_pour_transformation": [ - { - "statut": 5, - "description": "Transformé", - "raison": "Le document a déjà été transformé", - }, - { - "statut": 6, - "description": "Annulé", - "raison": "Le document est annulé", - }, - { - "statut": 3, - "description": "Réalisé partiellement", - "raison": "Un document cible existe probablement déjà (transformation partielle effectuée)", - }, - { - "statut": 4, - "description": "Réalisé totalement", - "raison": "Le document a été entièrement réalisé (transformation déjà effectuée)", - }, - ], - "changements_statut_autorises": { - "0_Brouillon": { - "vers": [2, 6], - "descriptions": ["Accepté/Validé", "Annulé"], - "note": "Un brouillon peut être accepté ou annulé", - }, - "2_Accepte": { - "vers": [5, 6], - "descriptions": ["Transformé", "Annulé"], - "note": "Un document accepté peut être transformé ou annulé", - }, - "5_Transforme": { - "vers": [], - "descriptions": [], - "note": "Un document transformé ne peut plus changer de statut", - }, - "6_Annule": { - "vers": [], - "descriptions": [], - "note": "Un document annulé ne peut plus changer de statut", - }, - }, - } - - return { - "success": True, - "matrice_statuts_par_type": matrice_complete, - "regles_transformation": regles_transformation, - "legende_statuts": descriptions_statuts, - "types_documents": types_documents, - "date_analyse": datetime.now().isoformat(), - "note": "Cette matrice est construite à partir des documents réels dans votre base Sage", - } - - except Exception as e: - logger.error(f"[DIAG] Erreur diagnostic global: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.get( - "/sage/diagnostic/statuts-permis/{numero}", dependencies=[Depends(verify_token)] -) -def diagnostiquer_statuts_permis(numero: str): - """ - 🔍 DIAGNOSTIC CRITIQUE: Découvre TOUS les statuts possibles pour un document - - Teste tous les statuts de 0 à 10 pour identifier lesquels sont valides - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - factory = sage.cial.FactoryDocumentVente - - # Chercher le document (tous types confondus) - persist = None - type_doc_trouve = None - - # Essayer ReadPiece pour différents types - for type_test in range(7): # 0-6 - try: - persist_test = factory.ReadPiece(type_test, numero) - if persist_test: - persist = persist_test - type_doc_trouve = type_test - logger.info( - f"[DIAG] Document {numero} trouvé avec ReadPiece(type={type_test})" - ) - break - except: - continue - - # Si pas trouvé, chercher dans List() - if not persist: - 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_Piece", "") == numero: - persist = persist_test - type_doc_trouve = getattr(doc_test, "DO_Type", -1) - logger.info( - f"[DIAG] Document {numero} trouvé dans List() à l'index {index}" - ) - break - - index += 1 - except: - index += 1 - - if not persist: - raise HTTPException(404, f"Document {numero} introuvable") - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - # Infos du document - statut_actuel = getattr(doc, "DO_Statut", -1) - type_actuel = getattr(doc, "DO_Type", -1) - - diagnostic = { - "numero": numero, - "type_document": type_actuel, - "type_libelle": { - 0: "Devis", - 10: "Bon de commande", - 20: "Préparation", - 30: "Bon de livraison", - 40: "Bon de retour", - 50: "Bon d'avoir", - 60: "Facture", - }.get(type_actuel, f"Type {type_actuel}"), - "statut_actuel": statut_actuel, - "statut_actuel_libelle": { - 0: "Brouillon", - 1: "Soumis/En attente", - 2: "Accepté/Validé", - 3: "Réalisé partiellement", - 4: "Réalisé totalement", - 5: "Transformé", - 6: "Annulé", - }.get(statut_actuel, f"Statut {statut_actuel}"), - "tests_statuts": [], - } - - # Tester tous les statuts de 0 à 10 - logger.info(f"[DIAG] Test des statuts pour {numero}...") - - for statut_test in range(11): - resultat_test = { - "statut": statut_test, - "libelle": { - 0: "Brouillon", - 1: "Soumis/En attente", - 2: "Accepté/Validé", - 3: "Réalisé partiellement", - 4: "Réalisé totalement", - 5: "Transformé", - 6: "Annulé", - 7: "Statut 7", - 8: "Statut 8", - 9: "Statut 9", - 10: "Statut 10", - }.get(statut_test, f"Statut {statut_test}"), - "autorise": False, - "erreur": None, - "est_statut_actuel": (statut_test == statut_actuel), - } - - # Si c'est le statut actuel, on sait qu'il est valide - if statut_test == statut_actuel: - resultat_test["autorise"] = True - resultat_test["note"] = "Statut actuel du document" - else: - # Tester le changement de statut - try: - # Relire le document - doc.Read() - - # Essayer de changer le statut - doc.DO_Statut = statut_test - - # Essayer d'écrire - doc.Write() - - # Si on arrive ici, le statut est valide ! - resultat_test["autorise"] = True - resultat_test["note"] = "Changement de statut réussi" - - logger.info(f"[DIAG] Statut {statut_test} AUTORISÉ") - - # Restaurer le statut d'origine immédiatement - doc.Read() - doc.DO_Statut = statut_actuel - doc.Write() - - except Exception as e: - erreur_str = str(e) - resultat_test["autorise"] = False - resultat_test["erreur"] = erreur_str - - logger.debug( - f"[DIAG] Statut {statut_test} REFUSÉ: {erreur_str[:100]}" - ) - - # Restaurer en cas d'erreur - try: - doc.Read() - except: - pass - - diagnostic["tests_statuts"].append(resultat_test) - - # Résumé - statuts_autorises = [ - t["statut"] for t in diagnostic["tests_statuts"] if t["autorise"] - ] - statuts_refuses = [ - t["statut"] for t in diagnostic["tests_statuts"] if not t["autorise"] - ] - - diagnostic["resume"] = { - "nb_statuts_autorises": len(statuts_autorises), - "statuts_autorises": statuts_autorises, - "statuts_autorises_libelles": [ - t["libelle"] for t in diagnostic["tests_statuts"] if t["autorise"] - ], - "nb_statuts_refuses": len(statuts_refuses), - "statuts_refuses": statuts_refuses, - } - - # Recommandations - recommendations = [] - - if 2 in statuts_autorises and statut_actuel == 0: - recommendations.append( - " Vous pouvez passer ce document de 'Brouillon' (0) à 'Accepté' (2)" - ) - - if 5 in statuts_autorises: - recommendations.append( - " Le statut 'Transformé' (5) est disponible - utilisé après transformation" - ) - - if 6 in statuts_autorises: - recommendations.append(" Vous pouvez annuler ce document (statut 6)") - - if not any(s in statuts_autorises for s in [2, 3, 4]): - recommendations.append( - " Aucun statut de validation (2/3/4) n'est disponible - " - "le document a peut-être déjà été traité" - ) - - diagnostic["recommendations"] = recommendations - - logger.info( - f"[DIAG] Statuts autorisés pour {numero}: " - f"{statuts_autorises} / Refusés: {statuts_refuses}" - ) - - return {"success": True, "diagnostic": diagnostic} - - except HTTPException: - raise - except Exception as e: - logger.error(f"[DIAG] Erreur diagnostic statuts: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.get( - "/sage/diagnostic/erreur-transformation/{numero}", - dependencies=[Depends(verify_token)], -) -def diagnostiquer_erreur_transformation( - numero: str, type_source: int = Query(...), type_cible: int = Query(...) -): - """ - 🔍 DIAGNOSTIC AVANCÉ: Analyse pourquoi une transformation échoue - - Vérifie: - - Statut du document source - - Statuts autorisés - - Lignes du document - - Client associé - - Champs obligatoires manquants - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - factory = sage.cial.FactoryDocumentVente - - # Lire le document source - persist = factory.ReadPiece(type_source, numero) - - if not persist: - persist = sage._find_document_in_list(numero, type_source) - - if not persist: - raise HTTPException( - 404, f"Document {numero} (type {type_source}) introuvable" - ) - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - diagnostic = { - "numero": numero, - "type_source": type_source, - "type_cible": type_cible, - "problemes_detectes": [], - "avertissements": [], - "suggestions": [], - } - - # 1. Vérifier le statut - statut_actuel = getattr(doc, "DO_Statut", -1) - diagnostic["statut_actuel"] = statut_actuel - - if statut_actuel == 5: - diagnostic["problemes_detectes"].append( - { - "severite": "BLOQUANT", - "champ": "DO_Statut", - "valeur": 5, - "message": "Document déjà transformé (statut=5)", - } - ) - - elif statut_actuel == 6: - diagnostic["problemes_detectes"].append( - { - "severite": "BLOQUANT", - "champ": "DO_Statut", - "valeur": 6, - "message": "Document annulé (statut=6)", - } - ) - - elif statut_actuel in [3, 4]: - diagnostic["avertissements"].append( - { - "severite": "ATTENTION", - "champ": "DO_Statut", - "valeur": statut_actuel, - "message": f"Document déjà réalisé (statut={statut_actuel}). " - f"Un document cible existe peut-être déjà.", - } - ) - - elif statut_actuel == 0: - diagnostic["suggestions"].append( - "Le document est en 'Brouillon' (statut=0). " - "Le système le passera automatiquement à 'Accepté' (statut=2) avant transformation." - ) - - # 2. Vérifier le client - client_code = "" - try: - client_obj = getattr(doc, "Client", None) - if client_obj: - client_obj.Read() - client_code = getattr(client_obj, "CT_Num", "").strip() - except: - pass - - if not client_code: - diagnostic["problemes_detectes"].append( - { - "severite": "BLOQUANT", - "champ": "CT_Num", - "valeur": None, - "message": "Aucun client associé au document", - } - ) - else: - diagnostic["client_code"] = client_code - - # 3. Vérifier les lignes - try: - factory_lignes = getattr(doc, "FactoryDocumentLigne", None) or getattr( - doc, "FactoryDocumentVenteLigne", None - ) - - nb_lignes = 0 - lignes_problemes = [] - - if factory_lignes: - index = 1 - while index <= 100: - try: - ligne_p = factory_lignes.List(index) - if ligne_p is None: - break - - ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") - ligne.Read() - - nb_lignes += 1 - - # Vérifier article - 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 - - if not article_ref: - lignes_problemes.append( - { - "ligne": index, - "probleme": "Aucune référence article", - } - ) - - # Vérifier prix - prix = float(getattr(ligne, "DL_PrixUnitaire", 0.0)) - if prix == 0: - lignes_problemes.append( - {"ligne": index, "probleme": "Prix unitaire = 0"} - ) - - index += 1 - except: - break - - diagnostic["nb_lignes"] = nb_lignes - - if nb_lignes == 0: - diagnostic["problemes_detectes"].append( - { - "severite": "BLOQUANT", - "champ": "Lignes", - "valeur": 0, - "message": "Document vide (aucune ligne)", - } - ) - - if lignes_problemes: - diagnostic["avertissements"].append( - { - "severite": "ATTENTION", - "champ": "Lignes", - "message": f"{len(lignes_problemes)} ligne(s) avec des problèmes", - "details": lignes_problemes, - } - ) - - except Exception as e: - diagnostic["avertissements"].append( - { - "severite": "ERREUR", - "champ": "Lignes", - "message": f"Impossible de lire les lignes: {e}", - } - ) - - # 4. Vérifier les totaux - total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) - total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0)) - - diagnostic["totaux"] = {"total_ht": total_ht, "total_ttc": total_ttc} - - if total_ht == 0 and total_ttc == 0: - diagnostic["avertissements"].append( - { - "severite": "ATTENTION", - "champ": "Totaux", - "message": "Tous les totaux sont à 0", - } - ) - - # 5. Vérifier si la transformation est autorisée - transformations_valides = {(0, 10), (10, 30), (10, 60), (30, 60), (0, 60)} - - if (type_source, type_cible) not in transformations_valides: - diagnostic["problemes_detectes"].append( - { - "severite": "BLOQUANT", - "champ": "Transformation", - "message": f"Transformation {type_source} → {type_cible} non autorisée. " - f"Transformations valides: {transformations_valides}", - } - ) - - # Résumé - nb_bloquants = sum( - 1 - for p in diagnostic["problemes_detectes"] - if p.get("severite") == "BLOQUANT" - ) - nb_avertissements = len(diagnostic["avertissements"]) - - diagnostic["resume"] = { - "peut_transformer": nb_bloquants == 0, - "nb_problemes_bloquants": nb_bloquants, - "nb_avertissements": nb_avertissements, - } - - if nb_bloquants == 0: - diagnostic["suggestions"].append( - " Aucun problème bloquant détecté. La transformation devrait fonctionner." - ) - else: - diagnostic["suggestions"].append( - f" {nb_bloquants} problème(s) bloquant(s) doivent être résolus avant la transformation." - ) - - return {"success": True, "diagnostic": diagnostic} - - except HTTPException: - raise - except Exception as e: - logger.error(f"[DIAG] Erreur diagnostic transformation: {e}", exc_info=True) - 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 rapide des fournisseurs depuis le CACHE - - Utilise le cache mémoire pour une réponse instantanée - 🔄 Cache actualisé automatiquement toutes les 15 minutes - """ - try: - # Utiliser le cache au lieu de la lecture directe - fournisseurs = sage.lister_tous_fournisseurs_cache(req.filtre) - - logger.info(f" {len(fournisseurs)} fournisseurs retournés depuis le cache") - - return {"success": True, "data": fournisseurs} - - except Exception as e: - 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/update", dependencies=[Depends(verify_token)]) -def modifier_fournisseur_endpoint(req: FournisseurUpdateGatewayRequest): - """ - ✏️ Modification d'un fournisseur dans Sage - """ - try: - resultat = sage.modifier_fournisseur(req.code, req.fournisseur_data) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier modification fournisseur: {e}") - raise HTTPException(404, str(e)) - except Exception as e: - logger.error(f"Erreur technique modification fournisseur: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/fournisseurs/get", dependencies=[Depends(verify_token)]) -def fournisseur_get(req: CodeRequest): - """ - NOUVEAU : 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 = Query(100, description="Nombre max d'avoirs"), - statut: Optional[int] = Query(None, description="Filtrer par statut"), - filtre: str = Query("", description="Filtre texte"), -): - """ - 📋 Liste rapide des avoirs depuis le CACHE (avec lignes) - - ⚡ ULTRA-RAPIDE: Utilise le cache mémoire - LIGNES INCLUSES: Contrairement aux anciennes méthodes - 💡 Pour forcer une relecture depuis Sage, utiliser /sage/avoirs/get - """ - try: - # Récupération depuis le cache (instantané) - avoirs = sage.lister_tous_avoirs_cache(filtre) - - # Filtrer par statut si demandé - if statut is not None: - avoirs = [a for a in avoirs if a.get("statut") == statut] - - # Limiter le nombre de résultats - avoirs = avoirs[:limit] - - logger.info(f" {len(avoirs)} avoirs retournés depuis le cache") - - return {"success": True, "data": avoirs} - - except Exception as e: - logger.error(f" Erreur liste avoirs: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.post("/sage/avoirs/get", dependencies=[Depends(verify_token)]) -def avoir_get(req: CodeRequest): - """ - 📄 Lecture d'un avoir (depuis cache en priorité) - - ⚡ Essaie d'abord le cache (instantané) - 🔄 Si introuvable, force une relecture depuis Sage - """ - try: - # Essayer le cache d'abord - avoir = sage.lire_avoir_cache(req.code) - - if avoir: - logger.info(f" Avoir {req.code} retourné depuis le cache") - return {"success": True, "data": avoir, "source": "cache"} - - # Pas dans le cache → Lecture directe depuis Sage - logger.info(f" Avoir {req.code} absent du cache, lecture depuis Sage...") - avoir = sage.lire_avoir(req.code) - - if not avoir: - raise HTTPException(404, f"Avoir {req.code} non trouvé") - - return {"success": True, "data": avoir, "source": "sage"} - - 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 = Query(100, description="Nombre max de livraisons"), - statut: Optional[int] = Query(None, description="Filtrer par statut"), - filtre: str = Query("", description="Filtre texte"), -): - """ - 📋 Liste rapide des livraisons depuis le CACHE (avec lignes) - - ⚡ ULTRA-RAPIDE: Utilise le cache mémoire - LIGNES INCLUSES: Contrairement aux anciennes méthodes - 💡 Pour forcer une relecture depuis Sage, utiliser /sage/livraisons/get - """ - try: - # Récupération depuis le cache (instantané) - livraisons = sage.lister_toutes_livraisons_cache(filtre) - - # Filtrer par statut si demandé - if statut is not None: - livraisons = [l for l in livraisons if l.get("statut") == statut] - - # Limiter le nombre de résultats - livraisons = livraisons[:limit] - - logger.info(f" {len(livraisons)} livraisons retournées depuis le cache") - - return {"success": True, "data": livraisons} - - except Exception as e: - logger.error(f" Erreur liste livraisons: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.post("/sage/livraisons/get", dependencies=[Depends(verify_token)]) -def livraison_get(req: CodeRequest): - """ - 📄 Lecture d'une livraison (depuis cache en priorité) - - ⚡ Essaie d'abord le cache (instantané) - 🔄 Si introuvable, force une relecture depuis Sage - """ - try: - # Essayer le cache d'abord - livraison = sage.lire_livraison_cache(req.code) - - if livraison: - logger.info(f" Livraison {req.code} retournée depuis le cache") - return {"success": True, "data": livraison, "source": "cache"} - - # Pas dans le cache → Lecture directe depuis Sage - logger.info(f" Livraison {req.code} absente du cache, lecture depuis Sage...") - livraison = sage.lire_livraison(req.code) - - if not livraison: - raise HTTPException(404, f"Livraison {req.code} non trouvée") - - return {"success": True, "data": livraison, "source": "sage"} - - except HTTPException: - raise - except Exception as e: - logger.error(f"Erreur lecture livraison: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/devis/update", dependencies=[Depends(verify_token)]) -def modifier_devis_endpoint(req: DevisUpdateGatewayRequest): - """ - ✏️ Modification d'un devis dans Sage - - Permet de modifier: - - La date du devis - - Les lignes (remplace toutes les lignes) - - Le statut - """ - try: - resultat = sage.modifier_devis(req.numero, req.devis_data) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier modification devis: {e}") - raise HTTPException(404, str(e)) - except Exception as e: - logger.error(f"Erreur technique modification devis: {e}") - raise HTTPException(500, str(e)) - - -# ===================================================== -# ENDPOINTS - CRÉATION ET MODIFICATION COMMANDES -# ===================================================== - - -@app.post("/sage/commandes/create", dependencies=[Depends(verify_token)]) -def creer_commande_endpoint(req: CommandeCreateRequest): - """ - ➕ Création d'une commande (Bon de commande) dans Sage - """ - try: - # Transformer en format attendu par sage_connector - commande_data = { - "client": {"code": req.client_id, "intitule": ""}, - "date_commande": req.date_commande or date.today(), - "reference": req.reference, - "lignes": req.lignes, - } - - resultat = sage.creer_commande_enrichi(commande_data) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier création commande: {e}") - raise HTTPException(400, str(e)) - except Exception as e: - logger.error(f"Erreur technique création commande: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/commandes/update", dependencies=[Depends(verify_token)]) -def modifier_commande_endpoint(req: CommandeUpdateGatewayRequest): - """ - ✏️ Modification d'une commande dans Sage - - Permet de modifier: - - La date de la commande - - Les lignes (remplace toutes les lignes) - - Le statut - - La référence externe - """ - try: - resultat = sage.modifier_commande(req.numero, req.commande_data) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier modification commande: {e}") - raise HTTPException(404, str(e)) - except Exception as e: - logger.error(f"Erreur technique modification commande: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/livraisons/create", dependencies=[Depends(verify_token)]) -def creer_livraison_endpoint(req: LivraisonCreateGatewayRequest): - """ - ➕ Création d'une livraison (Bon de livraison) dans Sage - """ - try: - # Vérifier que le client existe - client = sage.lire_client(req.client_id) - if not client: - raise HTTPException(404, f"Client {req.client_id} introuvable") - - # Préparer les données pour le connecteur - livraison_data = { - "client": {"code": req.client_id, "intitule": ""}, - "date_livraison": req.date_livraison or date.today(), - "reference": req.reference, - "lignes": req.lignes, - } - - resultat = sage.creer_livraison_enrichi(livraison_data) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier création livraison: {e}") - raise HTTPException(400, str(e)) - except Exception as e: - logger.error(f"Erreur technique création livraison: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/livraisons/update", dependencies=[Depends(verify_token)]) -def modifier_livraison_endpoint(req: LivraisonUpdateGatewayRequest): - """ - ✏️ Modification d'une livraison dans Sage - """ - try: - resultat = sage.modifier_livraison(req.numero, req.livraison_data) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier modification livraison: {e}") - raise HTTPException(404, str(e)) - except Exception as e: - logger.error(f"Erreur technique modification livraison: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/avoirs/create", dependencies=[Depends(verify_token)]) -def creer_avoir_endpoint(req: AvoirCreateGatewayRequest): - """ - ➕ Création d'un avoir (Bon d'avoir) dans Sage - """ - try: - # Vérifier que le client existe - client = sage.lire_client(req.client_id) - if not client: - raise HTTPException(404, f"Client {req.client_id} introuvable") - - # Préparer les données pour le connecteur - avoir_data = { - "client": {"code": req.client_id, "intitule": ""}, - "date_avoir": req.date_avoir or date.today(), - "reference": req.reference, - "lignes": req.lignes, - } - - resultat = sage.creer_avoir_enrichi(avoir_data) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier création avoir: {e}") - raise HTTPException(400, str(e)) - except Exception as e: - logger.error(f"Erreur technique création avoir: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/avoirs/update", dependencies=[Depends(verify_token)]) -def modifier_avoir_endpoint(req: AvoirUpdateGatewayRequest): - """ - ✏️ Modification d'un avoir dans Sage - """ - try: - resultat = sage.modifier_avoir(req.numero, req.avoir_data) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier modification avoir: {e}") - raise HTTPException(404, str(e)) - except Exception as e: - logger.error(f"Erreur technique modification avoir: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/factures/create", dependencies=[Depends(verify_token)]) -def creer_facture_endpoint(req: FactureCreateGatewayRequest): - """ - ➕ Création d'une facture dans Sage - - NOTE: Les factures peuvent avoir des champs obligatoires supplémentaires - selon la configuration Sage (DO_CodeJournal, DO_Souche, etc.) - """ - try: - # Vérifier que le client existe - client = sage.lire_client(req.client_id) - if not client: - raise HTTPException(404, f"Client {req.client_id} introuvable") - - # Préparer les données pour le connecteur - facture_data = { - "client": {"code": req.client_id, "intitule": ""}, - "date_facture": req.date_facture or date.today(), - "reference": req.reference, - "lignes": req.lignes, - } - - resultat = sage.creer_facture_enrichi(facture_data) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier création facture: {e}") - raise HTTPException(400, str(e)) - except Exception as e: - logger.error(f"Erreur technique création facture: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/factures/update", dependencies=[Depends(verify_token)]) -def modifier_facture_endpoint(req: FactureUpdateGatewayRequest): - """ - ✏️ Modification d'une facture dans Sage - - ATTENTION: Les factures comptabilisées peuvent être verrouillées - """ - try: - resultat = sage.modifier_facture(req.numero, req.facture_data) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier modification facture: {e}") - raise HTTPException(404, str(e)) - except Exception as e: - logger.error(f"Erreur technique modification facture: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/articles/create", dependencies=[Depends(verify_token)]) -def create_article_endpoint(req: ArticleCreateRequest): - """ - ➕ Création d'un article dans Sage - - **Usage typique**: Créer un article avec stock pour éviter l'erreur 2881 - """ - try: - resultat = sage.creer_article(req.dict()) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier création article: {e}") - raise HTTPException(400, str(e)) - - except Exception as e: - logger.error(f"Erreur technique création article: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/articles/update", dependencies=[Depends(verify_token)]) -def modifier_article_endpoint(req: ArticleUpdateGatewayRequest): - """ - ✏️ Modification d'un article dans Sage - - **Usage critique**: Augmenter le stock pour résoudre l'erreur 2881 - - Example: -```json - { - "reference": "ART001", - "article_data": { - "stock_reel": 100.0 - } - } -``` - """ - try: - resultat = sage.modifier_article(req.reference, req.article_data) - return {"success": True, "data": resultat} - - except ValueError as e: - logger.warning(f"Erreur métier modification article: {e}") - raise HTTPException(404, str(e)) - - except Exception as e: - logger.error(f"Erreur technique modification article: {e}") - raise HTTPException(500, str(e)) - - -@app.post("/sage/documents/generate-pdf", dependencies=[Depends(verify_token)]) -def generer_pdf_document(req: PDFGenerationRequest): - """ - 📄 Génération PDF d'un document (endpoint généralisé) - - **Supporte tous les types de documents Sage:** - - Devis (0) - - Bons de commande (10) - - Bons de livraison (30) - - Factures (60) - - Avoirs (50) - - **Process:** - 1. Charge le document depuis Sage - 2. Génère le PDF via l'état Sage correspondant - 3. Retourne le PDF en base64 - - Args: - req: Requête contenant doc_id et type_doc - - Returns: - { - "success": true, - "data": { - "pdf_base64": "JVBERi0xLjQK...", - "taille_octets": 12345, - "type_doc": 0, - "numero": "DE00001" - } - } - """ - try: - logger.info(f"📄 Génération PDF: {req.doc_id} (type={req.type_doc})") - - # Appel au connecteur Sage - pdf_bytes = sage.generer_pdf_document(req.doc_id, req.type_doc) - - if not pdf_bytes: - raise HTTPException(500, "PDF vide généré") - - # Encoder en base64 pour le transport JSON - import base64 - - pdf_base64 = base64.b64encode(pdf_bytes).decode("utf-8") - - logger.info(f" PDF généré: {len(pdf_bytes)} octets") - - return { - "success": True, - "data": { - "pdf_base64": pdf_base64, - "taille_octets": len(pdf_bytes), - "type_doc": req.type_doc, - "numero": req.doc_id, - }, - } - - except HTTPException: - raise - except Exception as e: - logger.error(f" Erreur génération PDF: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.post("/sage/admin/clean-locks") -def nettoyer_verrous_sage(): - """ - 🧹 Nettoyage des verrous Sage (cbRegFile) - - À utiliser uniquement si l'API est bloquée - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - # Forcer la fermeture de toutes les transactions en attente - for _ in range(10): - try: - sage.cial.CptaApplication.RollbackTrans() - logger.info(" Rollback effectué") - except: - break - - # Déconnecter/reconnecter - sage.deconnecter() - time.sleep(2) - sage.connecter() - - return { - "success": True, - "message": "Verrous nettoyés, connexion Sage réinitialisée" - } - - except Exception as e: - logger.error(f"Erreur nettoyage verrous: {e}") - raise HTTPException(500, str(e)) - -@app.get("/sage/diagnostic/transform-deep/{numero_source}", dependencies=[Depends(verify_token)]) -def diagnostic_transformation_approfondi( - numero_source: str, - type_source: int = Query(..., description="Type document source"), - type_cible: int = Query(..., description="Type document cible") -): - """ - 🔬 DIAGNOSTIC ULTRA-APPROFONDI : Transformation de document - - Cette route va : - 1. Créer le document cible comme dans transformer_document() - 2. Scanner TOUS les champs DO_* du document - 3. Comparer avec une facture manuelle réussie - 4. Lister les champs manquants ou invalides - 5. NE PAS commit (rollback à la fin) - - **Process** : - - Lit le devis source - - Crée une facture cible - - Associe le client - - Copie les lignes - - SCAN complet des champs AVANT Process() - - Tentative Process() et lecture des erreurs - - Rollback (rien n'est sauvegardé) - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - logger.info(f"[DIAG] === DIAGNOSTIC APPROFONDI TRANSFORMATION ===") - logger.info(f"[DIAG] Source: {numero_source} (type {type_source})") - logger.info(f"[DIAG] Cible: type {type_cible}") - - diagnostic = { - "numero_source": numero_source, - "type_source": type_source, - "type_cible": type_cible, - "etapes": [], - "champs_document": {}, - "champs_manquants": [], - "erreurs_sage": [], - "recommandations": [] - } - - # ======================================== - # ÉTAPE 1 : LIRE LE DOCUMENT SOURCE - # ======================================== - diagnostic["etapes"].append("Lecture document source") - - factory = sage.cial.FactoryDocumentVente - persist_source = factory.ReadPiece(type_source, numero_source) - - if not persist_source: - raise HTTPException(404, f"Document {numero_source} introuvable") - - doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3") - doc_source.Read() - - # Client - client_code = "" - try: - client_obj = getattr(doc_source, "Client", None) - if client_obj: - client_obj.Read() - client_code = getattr(client_obj, "CT_Num", "").strip() - except: - pass - - if not client_code: - raise HTTPException(400, "Client introuvable dans document source") - - diagnostic["client_code"] = client_code - - # Lignes - lignes_source = [] - try: - factory_lignes_source = getattr(doc_source, "FactoryDocumentLigne", None) or \ - getattr(doc_source, "FactoryDocumentVenteLigne", None) - - if factory_lignes_source: - index = 1 - while index <= 100: - try: - ligne_p = factory_lignes_source.List(index) - if ligne_p is None: - break - - ligne = win32com.client.CastTo(ligne_p, "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 - - lignes_source.append({ - "article_ref": article_ref, - "quantite": float(getattr(ligne, "DL_Qte", 0.0)), - "prix_unitaire": float(getattr(ligne, "DL_PrixUnitaire", 0.0)) - }) - - index += 1 - except: - break - except: - pass - - diagnostic["nb_lignes_source"] = len(lignes_source) - diagnostic["etapes"].append(f"Source lu: {len(lignes_source)} lignes") - - # ======================================== - # ÉTAPE 2 : TRANSACTION DE TEST - # ======================================== - try: - sage.cial.CptaApplication.BeginTrans() - diagnostic["etapes"].append("Transaction démarrée") - except: - diagnostic["etapes"].append("Transaction non supportée") - - try: - # ======================================== - # ÉTAPE 3 : CRÉER DOCUMENT CIBLE - # ======================================== - diagnostic["etapes"].append(f"Création document type {type_cible}") - - process = sage.cial.CreateProcess_Document(type_cible) - doc_cible = process.Document - - try: - doc_cible = win32com.client.CastTo(doc_cible, "IBODocumentVente3") - except: - pass - - # Date - import pywintypes - date_source = getattr(doc_source, "DO_Date", None) - if date_source: - doc_cible.DO_Date = date_source - else: - doc_cible.DO_Date = pywintypes.Time(datetime.now()) - - # ======================================== - # ÉTAPE 4 : ASSOCIER CLIENT - # ======================================== - diagnostic["etapes"].append(f"Association client {client_code}") - - factory_client = sage.cial.CptaApplication.FactoryClient - persist_client = factory_client.ReadNumero(client_code) - - if not persist_client: - raise ValueError(f"Client {client_code} introuvable") - - client_obj_cible = sage._cast_client(persist_client) - - try: - doc_cible.SetDefaultClient(client_obj_cible) - except: - doc_cible.SetClient(client_obj_cible) - - doc_cible.DO_Ref = numero_source - doc_cible.Write() - - diagnostic["etapes"].append("Client associé + 1er Write()") - - # ======================================== - # ÉTAPE 5 : COPIER LIGNES - # ======================================== - try: - factory_lignes_cible = doc_cible.FactoryDocumentLigne - except: - factory_lignes_cible = doc_cible.FactoryDocumentVenteLigne - - factory_article = sage.cial.FactoryArticle - - for idx, ligne_data in enumerate(lignes_source, 1): - article_ref = ligne_data["article_ref"] - if not article_ref: - continue - - persist_article = factory_article.ReadReference(article_ref) - if not persist_article: - continue - - article_obj = win32com.client.CastTo(persist_article, "IBOArticle3") - article_obj.Read() - - ligne_persist = factory_lignes_cible.Create() - try: - ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3") - except: - ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3") - - quantite = ligne_data["quantite"] - - try: - ligne_obj.SetDefaultArticleReference(article_ref, quantite) - except: - try: - ligne_obj.SetDefaultArticle(article_obj, quantite) - except: - ligne_obj.DL_Qte = quantite - - prix = ligne_data["prix_unitaire"] - if prix > 0: - ligne_obj.DL_PrixUnitaire = float(prix) - - ligne_obj.Write() - - diagnostic["etapes"].append(f"{len(lignes_source)} lignes copiées") - - # ======================================== - # ÉTAPE 6 : CHAMPS FACTURES - # ======================================== - if type_cible == 60: - doc_cible.DO_CodeJournal = "VTE" - doc_cible.DO_Souche = 0 - doc_cible.DO_Regime = 0 - diagnostic["etapes"].append("Champs factures définis") - - # Write final - doc_cible.Write() - doc_cible.Read() - - diagnostic["etapes"].append("Write() final effectué") - - # ======================================== - # ÉTAPE 7 : SCAN COMPLET DES CHAMPS - # ======================================== - diagnostic["etapes"].append("Scan complet des champs DO_*") - - champs = {} - champs_vides = [] - champs_null = [] - - # Liste TOUS les attributs du document - for attr in dir(doc_cible): - if attr.startswith("DO_"): - try: - valeur = getattr(doc_cible, attr, None) - - # Ignorer les méthodes - if callable(valeur): - continue - - # Catégoriser selon la valeur - if valeur is None: - champs_null.append(attr) - champs[attr] = {"valeur": "NULL", "type": "null"} - elif isinstance(valeur, str): - if valeur == "": - champs_vides.append(attr) - champs[attr] = {"valeur": "", "type": "string_vide"} - else: - champs[attr] = {"valeur": valeur, "type": "string", "len": len(valeur)} - elif isinstance(valeur, (int, float)): - if valeur == 0: - champs[attr] = {"valeur": 0, "type": "zero"} - else: - champs[attr] = {"valeur": valeur, "type": type(valeur).__name__} - else: - champs[attr] = {"valeur": str(valeur)[:50], "type": type(valeur).__name__} - except Exception as e: - champs[attr] = {"erreur": str(e)[:100]} - - diagnostic["champs_document"] = champs - diagnostic["nb_champs_total"] = len(champs) - diagnostic["nb_champs_null"] = len(champs_null) - diagnostic["nb_champs_vides"] = len(champs_vides) - diagnostic["champs_null"] = champs_null - diagnostic["champs_vides"] = champs_vides - - # ======================================== - # ÉTAPE 8 : LIRE LES ERREURS DU DOCUMENT - # ======================================== - erreurs_doc = [] - try: - if hasattr(doc_cible, "Errors"): - nb_erreurs = doc_cible.Errors.Count - if nb_erreurs > 0: - for i in range(nb_erreurs): - try: - err = doc_cible.Errors.Item(i) - erreurs_doc.append({ - "description": str(getattr(err, "Description", "?")), - "type": str(getattr(err, "Type", "?")), - "code": str(getattr(err, "Number", "?")), - "field": str(getattr(err, "Field", "?")) - }) - except: - pass - except: - pass - - diagnostic["erreurs_document"] = erreurs_doc - diagnostic["etapes"].append(f"Erreurs document: {len(erreurs_doc)}") - - # ======================================== - # ÉTAPE 9 : TENTER PROCESS() - # ======================================== - diagnostic["etapes"].append("Tentative Process()...") - - try: - process.Process() - diagnostic["process_resultat"] = "SUCCÈS" - diagnostic["etapes"].append("Process() RÉUSSI !") - - except Exception as e: - diagnostic["process_resultat"] = "ÉCHEC" - diagnostic["process_erreur"] = str(e) - diagnostic["etapes"].append(f"Process() échoué: {str(e)[:100]}") - - # Lire erreurs du process - erreurs_process = [] - try: - if hasattr(process, "Errors"): - nb_err_process = process.Errors.Count - if nb_err_process > 0: - for i in range(nb_err_process): - try: - err = process.Errors.Item(i) - erreurs_process.append({ - "description": str(getattr(err, "Description", "?")), - "type": str(getattr(err, "Type", "?")), - "field": str(getattr(err, "Field", "?")) - }) - except: - pass - except: - pass - - diagnostic["erreurs_process"] = erreurs_process - - # ======================================== - # ROLLBACK (NE PAS SAUVEGARDER) - # ======================================== - try: - sage.cial.CptaApplication.RollbackTrans() - diagnostic["etapes"].append("Rollback effectué (rien sauvegardé)") - except: - diagnostic["etapes"].append("Pas de rollback (pas de transaction)") - - # ======================================== - # RECOMMANDATIONS - # ======================================== - if diagnostic["process_resultat"] == "ÉCHEC": - diagnostic["recommandations"].append( - "Process() a échoué. Analysez les champs NULL et vides ci-dessus." - ) - - # Champs suspects - champs_suspects = [] - - if type_cible == 60: # Facture - champs_critiques_facture = [ - "DO_ModeRegl", "DO_CondRegl", "DO_CodeJournal", - "DO_Souche", "DO_Regime", "DO_TypeCalcul" - ] - - for champ in champs_critiques_facture: - if champ in champs_vides or champ in champs_null: - champs_suspects.append(champ) - - if champs_suspects: - diagnostic["champs_manquants"] = champs_suspects - diagnostic["recommandations"].append( - f"Champs critiques manquants pour facture: {', '.join(champs_suspects)}" - ) - - # Comparer avec une facture manuelle - diagnostic["recommandations"].append( - "SOLUTION : Créez une facture MANUELLEMENT dans Sage, " - "puis appelez GET /sage/diagnostic/facture-manuelle/{numero} " - "pour voir les différences" - ) - else: - diagnostic["recommandations"].append( - "Process() a RÉUSSI ! La transformation devrait fonctionner." - ) - - return { - "success": True, - "diagnostic": diagnostic - } - - except Exception as e: - # Rollback en cas d'erreur - try: - sage.cial.CptaApplication.RollbackTrans() - except: - pass - - raise HTTPException(500, f"Erreur diagnostic: {str(e)}") - - except HTTPException: - raise - except Exception as e: - logger.error(f"[DIAG] Erreur: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.get("/sage/diagnostic/facture-manuelle/{numero}", dependencies=[Depends(verify_token)]) -def analyser_facture_manuelle(numero: str): - """ - 🔍 ANALYSE D'UNE FACTURE CRÉÉE MANUELLEMENT DANS SAGE - - Scanne TOUS les champs d'une facture qui a été créée avec succès - pour voir la différence avec ce qu'on crée par API - - **Process** : - 1. Créer une facture manuellement dans Sage - 2. Noter son numéro (ex: FA00001) - 3. Appeler cette route : GET /sage/diagnostic/facture-manuelle/FA00001 - 4. Comparer les champs avec ceux du diagnostic transformation - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - factory = sage.cial.FactoryDocumentVente - - # Essayer type 60 (facture) - persist = factory.ReadPiece(60, numero) - - if not persist: - # Chercher dans List() - index = 1 - while index < 1000: - 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_Piece", "") == numero: - persist = persist_test - break - - index += 1 - except: - index += 1 - - if not persist: - raise HTTPException(404, f"Facture {numero} introuvable") - - doc = win32com.client.CastTo(persist, "IBODocumentVente3") - doc.Read() - - # Scanner TOUS les champs DO_* - champs = {} - champs_remplis = [] - champs_vides = [] - champs_null = [] - - for attr in dir(doc): - if attr.startswith("DO_"): - try: - valeur = getattr(doc, attr, None) - - if callable(valeur): - continue - - if valeur is None: - champs_null.append(attr) - champs[attr] = "NULL" - elif isinstance(valeur, str): - if valeur == "": - champs_vides.append(attr) - champs[attr] = "" - else: - champs_remplis.append(attr) - champs[attr] = valeur - elif isinstance(valeur, (int, float)): - if valeur != 0: - champs_remplis.append(attr) - champs[attr] = valeur - else: - champs[attr] = str(valeur)[:50] - except: - pass - - return { - "success": True, - "facture_numero": numero, - "type_doc": getattr(doc, "DO_Type", -1), - "statut": getattr(doc, "DO_Statut", -1), - "champs_analyse": { - "total": len(champs), - "remplis": len(champs_remplis), - "vides": len(champs_vides), - "null": len(champs_null) - }, - "champs_remplis": champs_remplis, - "champs_vides": champs_vides, - "champs_null": champs_null, - "tous_champs": champs, - "conseil": ( - "Comparez ces champs avec ceux du diagnostic transformation. " - "Les champs présents ici mais NULL/vides dans le diagnostic " - "sont probablement les champs manquants." - ) - } - - except HTTPException: - raise - except Exception as e: - logger.error(f"Erreur analyse facture: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -# À AJOUTER dans main.py - -@app.get("/sage/diagnostic/transform-final/{numero_source}", dependencies=[Depends(verify_token)]) -def diagnostic_transformation_final( - numero_source: str, - type_source: int = Query(..., description="Type document source"), - type_cible: int = Query(..., description="Type document cible") -): - """ - 🔬 DIAGNOSTIC ULTIME : Scanner le document AVANT Process() - - Cette route va : - 1. Créer le document EXACTEMENT comme transformer_document() - 2. Lire doc_cible.Errors APRÈS chaque Write() - 3. Scanner TOUS les champs DO_* du document - 4. Comparer avec une facture manuelle (FA00001) - 5. NE PAS appeler Process() (rollback à la fin) - - **Process** : - - Reproduit transformer_document() jusqu'au Process() - - S'arrête juste avant pour lire les erreurs - - Rollback (rien n'est sauvegardé) - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - logger.info(f"[DIAG-FINAL] === DIAGNOSTIC TRANSFORMATION FINALE ===") - logger.info(f"[DIAG-FINAL] Source: {numero_source} (type {type_source})") - logger.info(f"[DIAG-FINAL] Cible: type {type_cible}") - - diagnostic = { - "numero_source": numero_source, - "type_source": type_source, - "type_cible": type_cible, - "etapes": [], - "champs_document": {}, - "erreurs_sage": [], - "comparaison_facture_manuelle": {} - } - - # ======================================== - # ÉTAPE 1 : LIRE LE DOCUMENT SOURCE - # ======================================== - diagnostic["etapes"].append("Lecture document source") - - factory = sage.cial.FactoryDocumentVente - persist_source = factory.ReadPiece(type_source, numero_source) - - if not persist_source: - raise HTTPException(404, f"Document {numero_source} introuvable") - - doc_source = win32com.client.CastTo(persist_source, "IBODocumentVente3") - doc_source.Read() - - # Client - client_code = "" - try: - client_obj = getattr(doc_source, "Client", None) - if client_obj: - client_obj.Read() - client_code = getattr(client_obj, "CT_Num", "").strip() - except: - pass - - if not client_code: - raise HTTPException(400, "Client introuvable dans document source") - - diagnostic["client_code"] = client_code - - # Lignes - lignes_source = [] - try: - factory_lignes_source = getattr(doc_source, "FactoryDocumentLigne", None) or \ - getattr(doc_source, "FactoryDocumentVenteLigne", None) - - if factory_lignes_source: - index = 1 - while index <= 100: - try: - ligne_p = factory_lignes_source.List(index) - if ligne_p is None: - break - - ligne = win32com.client.CastTo(ligne_p, "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 - - lignes_source.append({ - "article_ref": article_ref, - "quantite": float(getattr(ligne, "DL_Qte", 0.0)), - "prix_unitaire": float(getattr(ligne, "DL_PrixUnitaire", 0.0)) - }) - - index += 1 - except: - break - except: - pass - - diagnostic["nb_lignes_source"] = len(lignes_source) - diagnostic["etapes"].append(f"Source lu: {len(lignes_source)} lignes") - - # ======================================== - # ÉTAPE 2 : TRANSACTION DE TEST - # ======================================== - try: - sage.cial.CptaApplication.BeginTrans() - diagnostic["etapes"].append("Transaction démarrée") - except: - diagnostic["etapes"].append("Transaction non supportée") - - try: - # ======================================== - # ÉTAPE 3 : CRÉER DOCUMENT CIBLE - # ======================================== - diagnostic["etapes"].append(f"Création document type {type_cible}") - - process = sage.cial.CreateProcess_Document(type_cible) - doc_cible = process.Document - - try: - doc_cible = win32com.client.CastTo(doc_cible, "IBODocumentVente3") - except: - pass - - # Date - import pywintypes - date_source = getattr(doc_source, "DO_Date", None) - if date_source: - doc_cible.DO_Date = date_source - else: - doc_cible.DO_Date = pywintypes.Time(datetime.now()) - - # ======================================== - # ÉTAPE 4 : ASSOCIER CLIENT - # ======================================== - diagnostic["etapes"].append(f"Association client {client_code}") - - factory_client = sage.cial.CptaApplication.FactoryClient - persist_client = factory_client.ReadNumero(client_code) - - if not persist_client: - raise ValueError(f"Client {client_code} introuvable") - - client_obj_cible = sage._cast_client(persist_client) - - try: - doc_cible.SetDefaultClient(client_obj_cible) - except: - doc_cible.SetClient(client_obj_cible) - - doc_cible.DO_Ref = numero_source - - # PREMIER WRITE - doc_cible.Write() - diagnostic["etapes"].append("1er Write() effectué") - - # LIRE ERREURS APRÈS 1ER WRITE - erreurs_apres_write1 = [] - try: - if hasattr(doc_cible, "Errors"): - nb_err = doc_cible.Errors.Count - if nb_err > 0: - for i in range(nb_err): - try: - err = doc_cible.Errors.Item(i) - erreurs_apres_write1.append({ - "description": str(getattr(err, "Description", "?")), - "type": str(getattr(err, "Type", "?")), - "field": str(getattr(err, "Field", "?")) - }) - except: - pass - except: - pass - - diagnostic["erreurs_apres_write1"] = erreurs_apres_write1 - - # ======================================== - # ÉTAPE 5 : COPIER LIGNES - # ======================================== - try: - factory_lignes_cible = doc_cible.FactoryDocumentLigne - except: - factory_lignes_cible = doc_cible.FactoryDocumentVenteLigne - - factory_article = sage.cial.FactoryArticle - - for idx, ligne_data in enumerate(lignes_source, 1): - article_ref = ligne_data["article_ref"] - if not article_ref: - continue - - persist_article = factory_article.ReadReference(article_ref) - if not persist_article: - continue - - article_obj = win32com.client.CastTo(persist_article, "IBOArticle3") - article_obj.Read() - - ligne_persist = factory_lignes_cible.Create() - try: - ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3") - except: - ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3") - - quantite = ligne_data["quantite"] - - try: - ligne_obj.SetDefaultArticleReference(article_ref, quantite) - except: - try: - ligne_obj.SetDefaultArticle(article_obj, quantite) - except: - ligne_obj.DL_Qte = quantite - - prix = ligne_data["prix_unitaire"] - if prix > 0: - ligne_obj.DL_PrixUnitaire = float(prix) - - ligne_obj.Write() - - diagnostic["etapes"].append(f"{len(lignes_source)} lignes copiées") - - # ======================================== - # ÉTAPE 6 : CHAMPS FACTURES - # ======================================== - if type_cible == 60: - # DO_Souche - try: - souche = getattr(doc_source, "DO_Souche", 0) - doc_cible.DO_Souche = souche - except: - pass - - # DO_Regime - try: - regime = getattr(doc_source, "DO_Regime", None) - if regime is not None: - doc_cible.DO_Regime = regime - except: - pass - - # DO_Transaction - try: - doc_cible.DO_Transaction = 11 - except: - pass - - diagnostic["etapes"].append("Champs factures définis") - - # Write final - doc_cible.Write() - doc_cible.Read() - - diagnostic["etapes"].append("Write() final effectué") - - # ======================================== - # ÉTAPE 7 : LIRE ERREURS DOCUMENT - # ======================================== - erreurs_document = [] - try: - if hasattr(doc_cible, "Errors"): - nb_erreurs = doc_cible.Errors.Count - if nb_erreurs > 0: - for i in range(nb_erreurs): - try: - err = doc_cible.Errors.Item(i) - erreurs_document.append({ - "description": str(getattr(err, "Description", "?")), - "type": str(getattr(err, "Type", "?")), - "code": str(getattr(err, "Number", "?")), - "field": str(getattr(err, "Field", "?")) - }) - except: - pass - except: - pass - - diagnostic["erreurs_document"] = erreurs_document - diagnostic["nb_erreurs_document"] = len(erreurs_document) - - # ======================================== - # ÉTAPE 8 : SCAN COMPLET DES CHAMPS - # ======================================== - champs = {} - champs_vides = [] - champs_null = [] - champs_zero = [] - - for attr in dir(doc_cible): - if attr.startswith("DO_"): - try: - valeur = getattr(doc_cible, attr, None) - - if callable(valeur): - continue - - if valeur is None: - champs_null.append(attr) - champs[attr] = {"valeur": "NULL", "type": "null"} - elif isinstance(valeur, str): - if valeur == "": - champs_vides.append(attr) - champs[attr] = {"valeur": "", "type": "string_vide"} - else: - champs[attr] = {"valeur": valeur, "type": "string", "len": len(valeur)} - elif isinstance(valeur, (int, float)): - if valeur == 0: - champs_zero.append(attr) - champs[attr] = {"valeur": 0, "type": "zero"} - else: - champs[attr] = {"valeur": valeur, "type": type(valeur).__name__} - else: - champs[attr] = {"valeur": str(valeur)[:50], "type": type(valeur).__name__} - except Exception as e: - champs[attr] = {"erreur": str(e)[:100]} - - diagnostic["champs_document"] = champs - diagnostic["nb_champs_total"] = len(champs) - diagnostic["nb_champs_null"] = len(champs_null) - diagnostic["nb_champs_vides"] = len(champs_vides) - diagnostic["nb_champs_zero"] = len(champs_zero) - diagnostic["champs_null"] = champs_null - diagnostic["champs_vides"] = champs_vides - diagnostic["champs_zero"] = champs_zero - - # ======================================== - # ÉTAPE 9 : COMPARER AVEC FACTURE MANUELLE - # ======================================== - # Charger FA00001 - try: - persist_ref = factory.ReadPiece(60, "FA00001") - if persist_ref: - doc_ref = win32com.client.CastTo(persist_ref, "IBODocumentVente3") - doc_ref.Read() - - champs_ref = {} - for attr in dir(doc_ref): - if attr.startswith("DO_"): - try: - valeur = getattr(doc_ref, attr, None) - if not callable(valeur): - if valeur is None: - champs_ref[attr] = "NULL" - elif isinstance(valeur, str): - champs_ref[attr] = valeur if valeur else "" - elif isinstance(valeur, (int, float)): - champs_ref[attr] = valeur - else: - champs_ref[attr] = str(valeur)[:50] - except: - pass - - # Comparer - champs_manquants = [] - champs_differents = [] - - for attr, val_ref in champs_ref.items(): - if attr in champs: - val_cible = champs[attr].get("valeur") - - # Si la référence a une valeur mais pas nous - if val_ref != "" and val_ref != 0 and val_ref != "NULL": - if val_cible == "" or val_cible == 0 or val_cible == "NULL": - champs_manquants.append({ - "champ": attr, - "valeur_ref": val_ref, - "valeur_cible": val_cible, - "severite": "CRITIQUE" - }) - elif val_ref != val_cible: - champs_differents.append({ - "champ": attr, - "valeur_ref": val_ref, - "valeur_cible": val_cible - }) - - diagnostic["comparaison_facture_manuelle"] = { - "champs_manquants": champs_manquants, - "champs_differents": champs_differents, - "nb_champs_manquants": len(champs_manquants), - "nb_champs_differents": len(champs_differents) - } - except Exception as e: - diagnostic["comparaison_facture_manuelle"] = {"erreur": str(e)} - - # ======================================== - # ROLLBACK (NE PAS SAUVEGARDER) - # ======================================== - try: - sage.cial.CptaApplication.RollbackTrans() - diagnostic["etapes"].append("Rollback effectué (rien sauvegardé)") - except: - diagnostic["etapes"].append("Pas de rollback (pas de transaction)") - - # ======================================== - # ANALYSE ET RECOMMANDATIONS - # ======================================== - recommendations = [] - - if erreurs_document: - recommendations.append( - f"CRITIQUE: {len(erreurs_document)} erreur(s) détectée(s) dans le document ! " - f"Voir 'erreurs_document' pour les détails." - ) - - for err in erreurs_document: - if err.get("field"): - recommendations.append( - f"Champ problématique: {err['field']} - {err['description']}" - ) - - if diagnostic["comparaison_facture_manuelle"].get("champs_manquants"): - recommendations.append( - f"CRITIQUE: {len(diagnostic['comparaison_facture_manuelle']['champs_manquants'])} " - f"champ(s) manquant(s) par rapport à FA00001 ! " - f"Voir 'comparaison_facture_manuelle.champs_manquants'." - ) - - if not erreurs_document and not diagnostic["comparaison_facture_manuelle"].get("champs_manquants"): - recommendations.append( - "Aucune erreur détectée ! Le document devrait pouvoir être validé." - ) - - diagnostic["recommendations"] = recommendations - - return { - "success": True, - "diagnostic": diagnostic - } - - except Exception as e: - # Rollback en cas d'erreur - try: - sage.cial.CptaApplication.RollbackTrans() - except: - pass - - raise HTTPException(500, f"Erreur diagnostic: {str(e)}") - - except HTTPException: - raise - except Exception as e: - logger.error(f"[DIAG-FINAL] Erreur: {e}", exc_info=True) - raise HTTPException(500, str(e)) - -# À ajouter dans main.py - -@app.get("/sage/diagnostic/article-requirements", dependencies=[Depends(verify_token)]) -def diagnostiquer_exigences_article(): - """ - 🔍 DIAGNOSTIC: Découvre les champs obligatoires pour créer un article - - Cette route va : - 1. Créer un article de test - 2. Scanner TOUS les champs AR_* - 3. Identifier les champs avec des valeurs par défaut - 4. NE PAS sauvegarder (rollback) - """ - try: - if not sage or not sage.cial: - raise HTTPException(503, "Service Sage indisponible") - - with sage._com_context(), sage._lock_com: - # Transaction de test - try: - sage.cial.CptaApplication.BeginTrans() - except: - pass - - try: - factory = sage.cial.FactoryArticle - - # Créer un article test - persist = factory.Create() - article_test = win32com.client.CastTo(persist, "IBOArticle3") - article_test.SetDefault() - - # Scanner TOUS les champs AR_* - champs = {} - champs_null = [] - champs_vides = [] - champs_remplis = [] - - for attr in dir(article_test): - if attr.startswith("AR_") or attr.startswith("FA_") or attr.startswith("TA_"): - try: - valeur = getattr(article_test, attr, None) - - if callable(valeur): - continue - - if valeur is None: - champs_null.append(attr) - champs[attr] = {"valeur": "NULL", "type": "null", "obligatoire_probable": True} - elif isinstance(valeur, str): - if valeur == "": - champs_vides.append(attr) - champs[attr] = {"valeur": "", "type": "string_vide", "obligatoire_probable": True} - else: - champs_remplis.append(attr) - champs[attr] = {"valeur": valeur, "type": "string", "len": len(valeur)} - elif isinstance(valeur, (int, float, bool)): - if valeur == 0 or valeur == False: - champs[attr] = {"valeur": valeur, "type": type(valeur).__name__, "obligatoire_probable": False} - else: - champs_remplis.append(attr) - champs[attr] = {"valeur": valeur, "type": type(valeur).__name__} - else: - champs[attr] = {"valeur": str(valeur)[:50], "type": type(valeur).__name__} - except Exception as e: - champs[attr] = {"erreur": str(e)[:100]} - - # Lire un article existant pour comparaison - champs_article_reel = {} - try: - # Charger le premier article de la liste - persist_reel = factory.List(1) - if persist_reel: - article_reel = win32com.client.CastTo(persist_reel, "IBOArticle3") - article_reel.Read() - - for attr in champs.keys(): - try: - val_reel = getattr(article_reel, attr, None) - if val_reel is not None and val_reel != "" and val_reel != 0: - champs_article_reel[attr] = val_reel - except: - pass - except: - pass - - # Rollback - try: - sage.cial.CptaApplication.RollbackTrans() - except: - pass - - return { - "success": True, - "champs_analyse": { - "total": len(champs), - "null": len(champs_null), - "vides": len(champs_vides), - "remplis": len(champs_remplis) - }, - "champs_probablement_obligatoires": champs_null + champs_vides, - "champs_remplis_par_defaut": champs_remplis, - "tous_champs": champs, - "exemple_article_reel": champs_article_reel, - "conseil": ( - "Les champs NULL ou vides sont probablement obligatoires. " - "Les champs remplis par défaut ont des valeurs automatiques." - ) - } - - except Exception as e: - try: - sage.cial.CptaApplication.RollbackTrans() - except: - pass - raise HTTPException(500, f"Erreur diagnostic: {str(e)}") - - except HTTPException: - raise - except Exception as e: - logger.error(f"[DIAG] Erreur: {e}", exc_info=True) - raise HTTPException(500, str(e)) - - -@app.post("/sage/diagnostic/article", dependencies=[Depends(verify_token)]) -def diagnostic_article_complet( - mode: str = Query("create", regex="^(inspect|create)$"), - reference: str = Query("TEST_DIAG"), - reference_modele: str = Query(None, description="Article existant à copier/inspecter") -): - """ - 🔧 DIAGNOSTIC UNIFIÉ pour articles Sage - - Modes: - - inspect: Analyse un article existant pour voir sa structure - - create: Crée un article de test en copiant la structure d'un modèle - - Paramètres: - - reference: Référence du nouvel article (mode create) - - reference_modele: Référence d'un article existant à analyser/copier - """ - - def est_serializable(val): - """Vérifie si une valeur est sérialisable en JSON""" - if val is None: - return True - if isinstance(val, (str, int, float, bool)): - return True - if str(type(val)).startswith(" 0: try: @@ -2216,9 +2215,9 @@ class SageConnector: ligne_data["remise_pourcentage"] ) ligne_obj.DL_Remise01REM_Type = 0 - logger.info(f" Remise définie") - except: - logger.debug(f" Remise non supportée") + logger.info(" Remise définie") + except Exception: + logger.debug(" Remise non supportée") ligne_obj.Write() logger.info(f" Ligne {idx} créée avec succès") @@ -2314,7 +2313,7 @@ class SageConnector: ) else: logger.info( - f" Pas de modification (identique ou invalide)" + " Pas de modification (identique ou invalide)" ) except Exception as e: logger.error(f" Erreur statut: {e}", exc_info=True) @@ -2344,7 +2343,7 @@ class SageConnector: resultat = _extraire_infos_devis(doc, numero, champs_modifies) - logger.info(f" Résultat extrait:") + logger.info(" Résultat extrait:") logger.info(f" Numéro: {resultat['numero']}") logger.info(f" Référence: '{resultat['reference']}'") logger.info(f" Date devis: {resultat['date_devis']}") @@ -2447,15 +2446,23 @@ class SageConnector: logger.info(f"[TRANSFORM] Méthode: Transformation.{module}.{methode}()") try: - with self._com_context(), self._lock_com, self._get_sql_connection() as conn: + with ( + self._com_context(), + self._lock_com, + self._get_sql_connection() as conn, + ): cursor = conn.cursor() if verifier_doublons: logger.info("[TRANSFORM] Vérification des doublons...") - verif = peut_etre_transforme(cursor, numero_source, type_source, type_cible) + verif = peut_etre_transforme( + cursor, numero_source, type_source, type_cible + ) if not verif["possible"]: - docs = [d["numero"] for d in verif.get("documents_existants", [])] + docs = [ + d["numero"] for d in verif.get("documents_existants", []) + ] raise ValueError( f"{verif['raison']}. Document(s) existant(s): {', '.join(docs)}" ) @@ -2484,7 +2491,7 @@ class SageConnector: if factory_lignes: lignes_list = factory_lignes.List nb_lignes_source = lignes_list.Count if lignes_list else 0 - except: + except Exception: pass logger.info( @@ -2538,7 +2545,7 @@ class SageConnector: try: can_process = getattr(transformer, "CanProcess", False) logger.info(f"[TRANSFORM] CanProcess: {can_process}") - except: + except Exception: can_process = True if not can_process: @@ -2555,7 +2562,7 @@ class SageConnector: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug("[TRANSFORM] Transaction démarrée") - except: + except Exception: pass try: @@ -2607,7 +2614,7 @@ class SageConnector: if factory_lignes_result: lignes_list = factory_lignes_result.List nb_lignes = lignes_list.Count if lignes_list else 0 - except: + except Exception: pass documents_crees.append( @@ -2626,7 +2633,7 @@ class SageConnector: index += 1 - except Exception as e: + except Exception: logger.debug(f"Fin de liste à index {index}") break @@ -2637,7 +2644,7 @@ class SageConnector: try: self.cial.CptaApplication.CommitTrans() logger.debug("[TRANSFORM] Transaction committée") - except: + except Exception: pass time.sleep(1.5) @@ -2669,12 +2676,12 @@ class SageConnector: "methode_transformation": f"{module}.{methode}", } - except Exception as e: + except Exception: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() logger.error("[TRANSFORM] Transaction annulée (rollback)") - except: + except Exception: pass raise @@ -2725,7 +2732,7 @@ class SageConnector: if persist: return _cast_client(persist) - except: + except Exception: pass return None @@ -2759,7 +2766,7 @@ class SageConnector: if telecom: contact_info["email"] = getattr(telecom, "EMail", "") contact_info["telephone"] = getattr(telecom, "Telephone", "") - except: + except Exception: pass try: @@ -2767,7 +2774,7 @@ class SageConnector: getattr(client, "CT_Contact", "") or contact_info["client_intitule"] ) - except: + except Exception: contact_info["nom"] = contact_info["client_intitule"] return contact_info @@ -2895,7 +2902,11 @@ class SageConnector: raise RuntimeError("Connexion Sage non établie") try: - with self._com_context(), self._lock_com, self._get_sql_connection() as conn: + with ( + self._com_context(), + self._lock_com, + self._get_sql_connection() as conn, + ): logger.info("=" * 80) logger.info("[CREATION CONTACT F_CONTACTT]") logger.info("=" * 80) @@ -2912,7 +2923,7 @@ class SageConnector: logger.info(f" CLIENT: {numero_client}") logger.info(f" CONTACT: {prenom} {nom}") - # Chargement du client + logger.info(f"[1] Chargement du client: {numero_client}") factory_client = self.cial.CptaApplication.FactoryClient try: @@ -2922,23 +2933,25 @@ class SageConnector: client_obj = win32com.client.CastTo(persist_client, "IBOClient3") client_obj.Read() - logger.info(f" OK Client charge") + logger.info(" OK Client charge") except Exception as e: raise ValueError(f"Client {numero_client} introuvable: {e}") - # Création du contact + logger.info("[2] Creation via FactoryTiersContact") if not hasattr(client_obj, "FactoryTiersContact"): raise RuntimeError("FactoryTiersContact non trouvee sur le client") factory_contact = client_obj.FactoryTiersContact - logger.info(f" OK FactoryTiersContact: {type(factory_contact).__name__}") + logger.info( + f" OK FactoryTiersContact: {type(factory_contact).__name__}" + ) persist = factory_contact.Create() logger.info(f" Objet cree: {type(persist).__name__}") - # Cast vers l'interface contact + contact = None interfaces_a_tester = [ "IBOTiersContact3", @@ -2964,9 +2977,11 @@ class SageConnector: if not contact: logger.error(" ERROR Aucun cast ne fonctionne") - raise RuntimeError("Impossible de caster vers une interface contact valide") + raise RuntimeError( + "Impossible de caster vers une interface contact valide" + ) - # Configuration des champs + logger.info("[3] Configuration du contact") if hasattr(contact, "_prop_map_put_"): @@ -3014,7 +3029,7 @@ class SageConnector: except Exception as e: logger.warning(f" WARN ServiceContact: {e}") - # Coordonnées Telecom + logger.info("[4] Coordonnees (Telecom)") if hasattr(contact, "Telecom"): @@ -3045,7 +3060,7 @@ class SageConnector: except Exception as e: logger.warning(f" WARN Erreur Telecom: {e}") - # Réseaux sociaux + logger.info("[5] Reseaux sociaux") if contact_data.get("facebook"): @@ -3053,7 +3068,7 @@ class SageConnector: try: contact.Facebook = facebook logger.info(f" Facebook = {facebook}") - except: + except Exception: pass if contact_data.get("linkedin"): @@ -3061,7 +3076,7 @@ class SageConnector: try: contact.LinkedIn = linkedin logger.info(f" LinkedIn = {linkedin}") - except: + except Exception: pass if contact_data.get("skype"): @@ -3069,7 +3084,7 @@ class SageConnector: try: contact.Skype = skype logger.info(f" Skype = {skype}") - except: + except Exception: pass try: @@ -3078,90 +3093,105 @@ class SageConnector: except Exception as e: logger.warning(f" WARN SetDefault(): {e}") - # ============================================================ - # ENREGISTREMENT AVEC GESTION DU BUG SAGE "existe déjà" - # ============================================================ + + + logger.info("[6] Enregistrement du contact") - + contact_cree_malgre_erreur = False contact_no = None n_contact = None erreur_com = None - + try: contact.Write() logger.info(" Write() reussi") - - # Si Write() réussit, relire immédiatement pour avoir les IDs + + try: contact.Read() logger.info(" Read() reussi") except Exception as read_err: logger.warning(f" WARN Read() échoué: {read_err}") - - # Essayer de récupérer les identifiants via l'objet COM + + try: contact_no = getattr(contact, "CT_No", None) n_contact = getattr(contact, "N_Contact", None) - logger.info(f" IDs COM: CT_No={contact_no}, N_Contact={n_contact}") - except: + logger.info( + f" IDs COM: CT_No={contact_no}, N_Contact={n_contact}" + ) + except Exception: pass - - # Si les IDs ne sont pas disponibles via COM, chercher en base + + if not contact_no: - logger.info(" 🔍 CT_No non disponible via COM - Recherche en base...") + logger.info( + " 🔍 CT_No non disponible via COM - Recherche en base..." + ) import time + time.sleep(0.3) - + contact_sql = _chercher_contact_en_base( conn, numero_client=numero_client, nom=nom, - prenom=prenom if prenom else None + prenom=prenom if prenom else None, ) - + if contact_sql: - logger.info(f" Contact trouvé en base: CT_No={contact_sql['contact_numero']}") - contact_no = contact_sql['contact_numero'] - n_contact = contact_sql['n_contact'] + logger.info( + f" Contact trouvé en base: CT_No={contact_sql['contact_numero']}" + ) + contact_no = contact_sql["contact_numero"] + n_contact = contact_sql["n_contact"] else: logger.warning(" Contact non trouvé en base immédiatement") - + except Exception as e: erreur_com = str(e) logger.warning(f" Write() a levé une exception: {erreur_com}") - - # Vérifier si c'est l'erreur "existe déjà" - if "existe déjà" in erreur_com.lower() or "already exists" in erreur_com.lower(): - logger.info(" 🔍 Erreur 'existe déjà' détectée - Vérification en base...") - - # Pause pour laisser Sage finaliser l'écriture + + + if ( + "existe déjà" in erreur_com.lower() + or "already exists" in erreur_com.lower() + ): + logger.info( + " 🔍 Erreur 'existe déjà' détectée - Vérification en base..." + ) + + import time + time.sleep(0.5) - - # Vérifier si le contact a été créé malgré l'erreur + + contact_sql = _chercher_contact_en_base( conn, numero_client=numero_client, nom=nom, - prenom=prenom if prenom else None + prenom=prenom if prenom else None, ) - + if contact_sql: - logger.info(f" Contact CRÉÉ malgré l'erreur COM !") - logger.info(f" CT_No={contact_sql['contact_numero']}, N_Contact={contact_sql['n_contact']}") + logger.info(" Contact CRÉÉ malgré l'erreur COM !") + logger.info( + f" CT_No={contact_sql['contact_numero']}, N_Contact={contact_sql['n_contact']}" + ) contact_cree_malgre_erreur = True - contact_no = contact_sql['contact_numero'] - n_contact = contact_sql['n_contact'] + contact_no = contact_sql["contact_numero"] + n_contact = contact_sql["n_contact"] else: logger.error(" Contact NON trouvé en base - Erreur réelle") raise RuntimeError(f"Echec enregistrement: {erreur_com}") else: - # Autre type d'erreur - on la remonte + logger.error(f" Erreur Write non gérée: {erreur_com}") raise RuntimeError(f"Echec enregistrement: {erreur_com}") - # Définir comme contact par défaut si demandé + est_defaut = contact_data.get("est_defaut", False) if est_defaut and (contact_no or n_contact): logger.info("[7] Definition comme contact par defaut") @@ -3169,7 +3199,9 @@ class SageConnector: nom_complet = f"{prenom} {nom}".strip() if prenom else nom persist_client = factory_client.ReadNumero(numero_client) - client_obj = win32com.client.CastTo(persist_client, "IBOClient3") + client_obj = win32com.client.CastTo( + persist_client, "IBOClient3" + ) client_obj.Read() client_obj.CT_Contact = nom_complet @@ -3179,7 +3211,7 @@ class SageConnector: try: client_obj.CT_NoContact = contact_no logger.info(f" CT_NoContact = {contact_no}") - except: + except Exception: pass client_obj.Write() @@ -3189,11 +3221,15 @@ class SageConnector: logger.warning(f" WARN Echec: {e}") est_defaut = False - # Construction du retour + logger.info("=" * 80) if contact_cree_malgre_erreur: - logger.info(f"[SUCCES] Contact créé MALGRÉ erreur COM: {prenom} {nom}") - logger.info(f" (Bug connu de Sage 100c - Le contact est bien en base)") + logger.info( + f"[SUCCES] Contact créé MALGRÉ erreur COM: {prenom} {nom}" + ) + logger.info( + " (Bug connu de Sage 100c - Le contact est bien en base)" + ) else: logger.info(f"[SUCCES] Contact créé: {prenom} {nom}") logger.info(f" Lié au client {numero_client}") @@ -3203,29 +3239,33 @@ class SageConnector: logger.info("[7] Construction du retour") contact_dict = None - - # Stratégie 1 : Si on a les identifiants, relire depuis la base + + if contact_no: logger.info(f" Stratégie 1: Lecture base (CT_No={contact_no})") try: contact_dict = _lire_contact_depuis_base( - conn, - numero_client=numero_client, - contact_no=contact_no + conn, numero_client=numero_client, contact_no=contact_no ) - + if contact_dict: - logger.info(f" Lecture base réussie: {len(contact_dict)} champs") + logger.info( + f" Lecture base réussie: {len(contact_dict)} champs" + ) logger.info(f" Type: {type(contact_dict)}") logger.info(f" Keys: {list(contact_dict.keys())}") - logger.info(f" Sample: numero={contact_dict.get('numero')}, nom={contact_dict.get('nom')}") + logger.info( + f" Sample: numero={contact_dict.get('numero')}, nom={contact_dict.get('nom')}" + ) else: - logger.warning(" _lire_contact_depuis_base() retourne None") + logger.warning( + " _lire_contact_depuis_base() retourne None" + ) except Exception as e: logger.error(f" Erreur lecture base: {e}", exc_info=True) contact_dict = None - - # Stratégie 2 : Fallback sur l'objet COM (_contact_to_dict) + + if not contact_dict: logger.info(" Stratégie 2: Lecture objet COM (_contact_to_dict)") try: @@ -3236,14 +3276,16 @@ class SageConnector: n_contact=n_contact, ) if contact_dict: - logger.info(f" _contact_to_dict réussi: {len(contact_dict)} champs") + logger.info( + f" _contact_to_dict réussi: {len(contact_dict)} champs" + ) else: logger.warning(" _contact_to_dict() retourne None/vide") except Exception as e: logger.error(f" Erreur _contact_to_dict: {e}", exc_info=True) contact_dict = None - - # Stratégie 3 : Construction manuelle en dernier recours + + if not contact_dict: logger.info(" Stratégie 3: Construction manuelle (fallback)") contact_dict = self._construire_contact_minimal( @@ -3252,27 +3294,33 @@ class SageConnector: n_contact=n_contact, nom=nom, prenom=prenom, - contact_data=contact_data + contact_data=contact_data, ) - logger.info(f" Contact minimal construit: {len(contact_dict)} champs") - - # Vérification finale avant retour + logger.info( + f" Contact minimal construit: {len(contact_dict)} champs" + ) + + if not contact_dict or not isinstance(contact_dict, dict): - logger.error(f" ERREUR: contact_dict invalide: type={type(contact_dict)}, value={contact_dict}") - raise RuntimeError("Impossible de construire le dictionnaire de retour") - - # Ajouter le flag est_defaut + logger.error( + f" ERREUR: contact_dict invalide: type={type(contact_dict)}, value={contact_dict}" + ) + raise RuntimeError( + "Impossible de construire le dictionnaire de retour" + ) + + contact_dict["est_defaut"] = est_defaut - - # Log final détaillé - logger.info(f" 📦 DICT FINAL AVANT RETURN:") + + + logger.info(" DICT FINAL AVANT RETURN:") logger.info(f" Type: {type(contact_dict)}") logger.info(f" Len: {len(contact_dict)}") logger.info(f" Keys: {list(contact_dict.keys())}") for key, value in contact_dict.items(): logger.info(f" {key}: {value} (type: {type(value).__name__})") - - logger.info(f" 🚀 RETURN contact_dict") + + logger.info(" RETURN contact_dict") return contact_dict except ValueError as e: @@ -3289,20 +3337,22 @@ class SageConnector: n_contact: Optional[int], nom: str, prenom: Optional[str], - contact_data: Dict + contact_data: Dict, ) -> Dict: - logger.info(f" _construire_contact_minimal(client={numero_client}, CT_No={contact_no})") - + logger.info( + f" _construire_contact_minimal(client={numero_client}, CT_No={contact_no})" + ) + civilite_map_reverse = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"} civilite_input = contact_data.get("civilite") civilite_code = None - + if civilite_input: if isinstance(civilite_input, int): civilite_code = civilite_map_reverse.get(civilite_input) else: civilite_code = civilite_input - + result = { "numero": numero_client, "contact_numero": contact_no, @@ -3321,8 +3371,10 @@ class SageConnector: "skype": contact_data.get("skype"), "est_defaut": False, } - - logger.info(f" Contact minimal: numero={result['numero']}, nom={result['nom']}, email={result['email']}") + + logger.info( + f" Contact minimal: numero={result['numero']}, nom={result['nom']}, email={result['email']}" + ) return result def modifier_contact(self, numero: str, contact_numero: int, updates: Dict) -> Dict: @@ -3335,12 +3387,12 @@ class SageConnector: logger.info(f"[MODIFICATION CONTACT] CT_No={contact_numero}") logger.info("=" * 80) - # ============================================================ - # [1] CHARGEMENT DU CLIENT - # ============================================================ + + + logger.info("[1] Chargement du client") factory_client = self.cial.CptaApplication.FactoryClient - + try: persist_client = factory_client.ReadNumero(numero) if not persist_client: @@ -3352,16 +3404,16 @@ class SageConnector: except Exception as e: raise ValueError(f"Client {numero} introuvable: {e}") - # ============================================================ - # [2] CHARGEMENT DU CONTACT (multi-stratégies) - # ============================================================ + + + logger.info("[2] Chargement du contact") - + contact = None nom_recherche = None prenom_recherche = None - - # Récupérer d'abord les infos depuis la base SQL (lecture seule) + + try: with self._get_sql_connection() as conn: cursor = conn.cursor() @@ -3374,146 +3426,194 @@ class SageConnector: row = cursor.fetchone() if not row: - raise ValueError(f"Contact CT_No={contact_numero} non trouve en base") + raise ValueError( + f"Contact CT_No={contact_numero} non trouve en base" + ) ct_no_sql = row.CT_No nom_recherche = row.CT_Nom.strip() if row.CT_Nom else "" - prenom_recherche = row.CT_Prenom.strip() if row.CT_Prenom else "" + prenom_recherche = ( + row.CT_Prenom.strip() if row.CT_Prenom else "" + ) cbmarq_sql = row.cbMarq - logger.info(f" Contact SQL: CT_No={ct_no_sql}, cbMarq={cbmarq_sql}") - logger.info(f" Nom='{nom_recherche}', Prenom='{prenom_recherche}'") + logger.info( + f" Contact SQL: CT_No={ct_no_sql}, cbMarq={cbmarq_sql}" + ) + logger.info( + f" Nom='{nom_recherche}', Prenom='{prenom_recherche}'" + ) except ValueError: raise except Exception as e: raise ValueError(f"Erreur lecture contact en base: {e}") - # --- Stratégie 1 : Via FactoryTiersContact du client (parcours) --- - # NOTE: CT_No et N_Contact sont None sur IBOTiersContact3, on matche par nom/prénom + + logger.info(" Strategie 1: Parcours FactoryTiersContact du client...") try: factory_contact = client_obj.FactoryTiersContact - + if hasattr(factory_contact, "List"): liste_contacts = factory_contact.List - + if liste_contacts and hasattr(liste_contacts, "Count"): count = liste_contacts.Count logger.info(f" {count} contact(s) pour ce client") - + for i in range(count): try: item = liste_contacts.Item(i + 1) - + temp_contact = None - for iface in ["IBOTiersContact3", "IBOTiersContact", "IBOContactT3"]: + for iface in [ + "IBOTiersContact3", + "IBOTiersContact", + "IBOContactT3", + ]: try: - temp_contact = win32com.client.CastTo(item, iface) + temp_contact = win32com.client.CastTo( + item, iface + ) break - except: + except Exception: continue - + if not temp_contact: continue - + temp_contact.Read() - + nom_com = getattr(temp_contact, "Nom", "") or "" - prenom_com = getattr(temp_contact, "Prenom", "") or "" - - # Correspondance par nom/prénom (insensible à la casse) - if (nom_com.strip().lower() == nom_recherche.lower() and - prenom_com.strip().lower() == prenom_recherche.lower()): + prenom_com = ( + getattr(temp_contact, "Prenom", "") or "" + ) + + + if ( + nom_com.strip().lower() == nom_recherche.lower() + and prenom_com.strip().lower() + == prenom_recherche.lower() + ): contact = temp_contact - logger.info(f" OK Contact trouve a l'index {i+1}: '{prenom_com}' '{nom_com}'") + logger.info( + f" OK Contact trouve a l'index {i + 1}: '{prenom_com}' '{nom_com}'" + ) break - + except Exception as e: - logger.debug(f" Item {i+1} erreur: {e}") + logger.debug(f" Item {i + 1} erreur: {e}") continue - + except Exception as e: logger.warning(f" Strategie 1 echouee: {e}") - # --- Stratégie 2 : Via FactoryDossierContact.ReadNomPrenom --- + if not contact: logger.info(" Strategie 2: FactoryDossierContact.ReadNomPrenom...") try: - factory_dossier = self.cial.CptaApplication.FactoryDossierContact - persist = factory_dossier.ReadNomPrenom(nom_recherche, prenom_recherche) + factory_dossier = ( + self.cial.CptaApplication.FactoryDossierContact + ) + persist = factory_dossier.ReadNomPrenom( + nom_recherche, prenom_recherche + ) if persist: - contact = win32com.client.CastTo(persist, "IBOTiersContact3") + contact = win32com.client.CastTo( + persist, "IBOTiersContact3" + ) contact.Read() - logger.info(f" OK Contact charge via ReadNomPrenom") - + logger.info(" OK Contact charge via ReadNomPrenom") + except Exception as e: logger.warning(f" Strategie 2 echouee: {e}") - # --- Stratégie 3 : Variations nom/prenom --- + if not contact: logger.info(" Strategie 3: Variations nom/prenom...") try: - factory_dossier = self.cial.CptaApplication.FactoryDossierContact - + factory_dossier = ( + self.cial.CptaApplication.FactoryDossierContact + ) + variations = [ (nom_recherche.upper(), prenom_recherche.upper()), (nom_recherche.lower(), prenom_recherche.lower()), (nom_recherche.capitalize(), prenom_recherche.capitalize()), (nom_recherche, ""), ] - + for nom_var, prenom_var in variations: try: - persist = factory_dossier.ReadNomPrenom(nom_var, prenom_var) + persist = factory_dossier.ReadNomPrenom( + nom_var, prenom_var + ) if persist: - contact = win32com.client.CastTo(persist, "IBOTiersContact3") + contact = win32com.client.CastTo( + persist, "IBOTiersContact3" + ) contact.Read() - logger.info(f" OK Contact trouve avec: '{nom_var}'/'{prenom_var}'") + logger.info( + f" OK Contact trouve avec: '{nom_var}'/'{prenom_var}'" + ) break - except: + except Exception: continue - + except Exception as e: logger.warning(f" Strategie 3 echouee: {e}") - # --- Stratégie 4 : Parcours global FactoryDossierContact --- + if not contact: - logger.info(" Strategie 4: Parcours global FactoryDossierContact...") + logger.info( + " Strategie 4: Parcours global FactoryDossierContact..." + ) try: - factory_dossier = self.cial.CptaApplication.FactoryDossierContact - + factory_dossier = ( + self.cial.CptaApplication.FactoryDossierContact + ) + if hasattr(factory_dossier, "List"): liste = factory_dossier.List - + if liste and hasattr(liste, "Count"): count = min(liste.Count, 500) logger.info(f" Parcours de {count} contacts...") - + for i in range(count): try: item = liste.Item(i + 1) - temp = win32com.client.CastTo(item, "IBOTiersContact3") + temp = win32com.client.CastTo( + item, "IBOTiersContact3" + ) temp.Read() - + nom = getattr(temp, "Nom", "") or "" prenom = getattr(temp, "Prenom", "") or "" - - if (nom.strip().lower() == nom_recherche.lower() and - prenom.strip().lower() == prenom_recherche.lower()): + + if ( + nom.strip().lower() == nom_recherche.lower() + and prenom.strip().lower() + == prenom_recherche.lower() + ): contact = temp - logger.info(f" OK Contact trouve a l'index global {i+1}") + logger.info( + f" OK Contact trouve a l'index global {i + 1}" + ) break - except: + except Exception: continue - + except Exception as e: logger.warning(f" Strategie 4 echouee: {e}") - # Échec total + if not contact: - logger.error(f" ECHEC: Impossible de charger le contact CT_No={contact_numero}") + logger.error( + f" ECHEC: Impossible de charger le contact CT_No={contact_numero}" + ) raise ValueError( f"Contact CT_No={contact_numero} introuvable via COM. " f"Nom='{nom_recherche}', Prenom='{prenom_recherche}'" @@ -3521,9 +3621,9 @@ class SageConnector: logger.info(f" OK Contact charge: {contact.Nom}") - # ============================================================ - # [3] APPLICATION DES MODIFICATIONS - # ============================================================ + + + logger.info("[3] Application des modifications") modifications_appliquees = [] @@ -3603,7 +3703,7 @@ class SageConnector: if _try_set_attribute(telecom, "Telecopie", fax): logger.info(f" Telecopie = {fax}") modifications_appliquees.append("telecopie") - + except Exception as e: logger.warning(f" WARN Telecom: {e}") @@ -3634,13 +3734,15 @@ class SageConnector: except Exception as e: logger.warning(f" WARN Skype: {e}") - logger.info(f" Modifications preparees: {', '.join(modifications_appliquees) if modifications_appliquees else 'aucune'}") + logger.info( + f" Modifications preparees: {', '.join(modifications_appliquees) if modifications_appliquees else 'aucune'}" + ) - # ============================================================ - # [4] ENREGISTREMENT - # ============================================================ + + + logger.info("[4] Enregistrement") - + try: contact.Write() logger.info(" Write() reussi") @@ -3649,8 +3751,10 @@ class SageConnector: try: sage_error = self.cial.CptaApplication.LastError if sage_error: - error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" - except: + error_detail = ( + f"{sage_error.Description} (Code: {sage_error.Number})" + ) + except Exception: pass logger.error(f" ERROR Write: {error_detail}") raise RuntimeError(f"Echec modification contact: {error_detail}") @@ -3661,9 +3765,9 @@ class SageConnector: except Exception as e: logger.warning(f" WARN Read() apres Write: {e}") - # ============================================================ - # [5] GESTION CONTACT PAR DÉFAUT - # ============================================================ + + + est_defaut_demande = updates.get("est_defaut") est_actuellement_defaut = False @@ -3677,7 +3781,9 @@ class SageConnector: ) persist_client = factory_client.ReadNumero(numero) - client_obj = win32com.client.CastTo(persist_client, "IBOClient3") + client_obj = win32com.client.CastTo( + persist_client, "IBOClient3" + ) client_obj.Read() client_obj.CT_Contact = nom_complet @@ -3698,27 +3804,25 @@ class SageConnector: except Exception as e: logger.warning(f" WARN Echec definition contact defaut: {e}") - # ============================================================ - # [6] CONSTRUCTION DU RETOUR (lecture SQL uniquement) - # ============================================================ + + + logger.info("[6] Construction du retour") - + contact_dict = None - - # Lecture depuis la base + + try: with self._get_sql_connection() as conn: contact_dict = _lire_contact_depuis_base( - conn, - numero_client=numero, - contact_no=contact_numero + conn, numero_client=numero, contact_no=contact_numero ) if contact_dict: - logger.info(f" Lecture base reussie") + logger.info(" Lecture base reussie") except Exception as e: logger.warning(f" Lecture base echouee: {e}") - - # Fallback via objet COM + + if not contact_dict: try: contact_dict = _contact_to_dict( @@ -3728,11 +3832,11 @@ class SageConnector: n_contact=None, ) if contact_dict: - logger.info(f" _contact_to_dict reussi") + logger.info(" _contact_to_dict reussi") except Exception as e: logger.warning(f" _contact_to_dict echoue: {e}") - - # Construction manuelle + + if not contact_dict: logger.info(" Construction manuelle du retour") contact_dict = { @@ -3752,22 +3856,30 @@ class SageConnector: "linkedin": None, "skype": None, } - + if hasattr(contact, "Telecom"): try: telecom = contact.Telecom - contact_dict["telephone"] = getattr(telecom, "Telephone", None) - contact_dict["portable"] = getattr(telecom, "Portable", None) + contact_dict["telephone"] = getattr( + telecom, "Telephone", None + ) + contact_dict["portable"] = getattr( + telecom, "Portable", None + ) contact_dict["email"] = getattr(telecom, "EMail", None) - contact_dict["telecopie"] = getattr(telecom, "Telecopie", None) - except: + contact_dict["telecopie"] = getattr( + telecom, "Telecopie", None + ) + except Exception: pass contact_dict["est_defaut"] = est_actuellement_defaut logger.info("=" * 80) logger.info(f"[SUCCES] Contact modifie: CT_No={contact_numero}") - logger.info(f" Modifications: {', '.join(modifications_appliquees) if modifications_appliquees else 'aucune'}") + logger.info( + f" Modifications: {', '.join(modifications_appliquees) if modifications_appliquees else 'aucune'}" + ) logger.info("=" * 80) return contact_dict @@ -3778,7 +3890,7 @@ class SageConnector: except Exception as e: logger.error(f"[ERREUR] {e}", exc_info=True) raise RuntimeError(f"Erreur technique: {e}") - + def definir_contact_defaut(self, numero: str, contact_numero: int) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -3847,7 +3959,7 @@ class SageConnector: try: client.CT_NoContact = contact_numero logger.info(f" CT_NoContact = {contact_numero}") - except: + except Exception: pass logger.info("[4] Enregistrement") @@ -3863,7 +3975,7 @@ class SageConnector: error_detail = ( f"{sage_error.Description} (Code: {sage_error.Number})" ) - except: + except Exception: pass raise RuntimeError(f"Echec mise a jour: {error_detail}") @@ -3940,7 +4052,7 @@ class SageConnector: persist = factory_dossier.ReadNomPrenom(nom_contact, prenom_contact) if not persist: - raise ValueError(f"Contact non trouvable via ReadNomPrenom") + raise ValueError("Contact non trouvable via ReadNomPrenom") contact = win32com.client.CastTo(persist, "IBOTiersContact3") contact.Read() @@ -3961,7 +4073,7 @@ class SageConnector: error_detail = ( f"{sage_error.Description} (Code: {sage_error.Number})" ) - except: + except Exception: pass logger.error(f" ERROR Remove: {error_detail}") raise RuntimeError(f"Echec suppression contact: {error_detail}") @@ -4092,7 +4204,7 @@ class SageConnector: else: client.CT_Raccourci = raccourci logger.info(f" CT_Raccourci = {raccourci} [OK]") - except Exception as e: + except Exception: try: client.CT_Raccourci = raccourci logger.info(f" CT_Raccourci = {raccourci} [OK]") @@ -4105,7 +4217,7 @@ class SageConnector: if not hasattr(client, "CT_Type") or client.CT_Type is None: client.CT_Type = type_tiers logger.info(f" CT_Type force a {type_tiers}") - except: + except Exception: pass COMPTES_DEFAUT = {0: "4110000", 1: "4010000", 2: "421", 3: "471"} @@ -4123,10 +4235,10 @@ class SageConnector: COMPTES_DEFAUT.get(type_tiers, "4110000"), "4110000", "411000", - "411", # Clients + "411", "4010000", "401000", - "401", # Fournisseurs + "401", ] for test_compte in comptes_a_tester: @@ -4173,7 +4285,7 @@ class SageConnector: client.CatTarif = cat_tarif_obj logger.info(f" CatTarif = {cat_id} [OK]") break - except: + except Exception: continue except Exception as e: logger.warning(f" CatTarif erreur: {e}") @@ -4193,7 +4305,7 @@ class SageConnector: client.CatCompta = cat_compta_obj logger.info(f" CatCompta = {cat_id} [OK]") break - except: + except Exception: continue except Exception as e: logger.warning(f" CatCompta erreur: {e}") @@ -4301,7 +4413,7 @@ class SageConnector: if client_data.get("facebook"): facebook = clean_str( client_data["facebook"], 69 - ) # URL ou @username + ) if not try_set_attribute(telecom_obj, "Facebook", facebook): try_set_attribute(client, "CT_Facebook", facebook) logger.info(f" Facebook = {facebook}") @@ -4309,7 +4421,7 @@ class SageConnector: if client_data.get("linkedin"): linkedin = clean_str( client_data["linkedin"], 69 - ) # URL ou profil + ) if not try_set_attribute(telecom_obj, "LinkedIn", linkedin): try_set_attribute(client, "CT_LinkedIn", linkedin) logger.info(f" LinkedIn = {linkedin}") @@ -4560,7 +4672,7 @@ class SageConnector: error_detail = ( f"{sage_error.Description} (Code: {sage_error.Number})" ) - except: + except Exception: pass logger.error(f"[ERREUR] {error_detail}") @@ -4685,7 +4797,7 @@ class SageConnector: else: if try_set_attribute(client, "CT_Raccourci", raccourci): champs_modifies.append("raccourci") - except: + except Exception: if try_set_attribute(client, "CT_Raccourci", raccourci): champs_modifies.append("raccourci") @@ -4924,9 +5036,6 @@ class SageConnector: if try_set_attribute(client, f"CT_Taux{i:02d}", val): champs_modifies.append(key) - stat_keys = ["statistique01", "secteur"] + [ - f"statistique{i:02d}" for i in range(2, 11) - ] stat_modifies = False stat01 = client_data.get("statistique01") or client_data.get("secteur") @@ -5238,7 +5347,7 @@ class SageConnector: error_detail = ( f"{sage_error.Description} (Code: {sage_error.Number})" ) - except: + except Exception: pass logger.error(f"[ERREUR] {error_detail}") @@ -5274,7 +5383,7 @@ class SageConnector: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug(" Transaction Sage démarrée") - except: + except Exception: pass try: @@ -5285,7 +5394,7 @@ class SageConnector: try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") - except: + except Exception: pass logger.info(" Document commande créé") @@ -5317,7 +5426,7 @@ class SageConnector: client_obj = _cast_client(persist_client) if not client_obj: - raise ValueError(f"Impossible de charger le client") + raise ValueError("Impossible de charger le client") doc.SetDefaultClient(client_obj) doc.Write() @@ -5327,12 +5436,12 @@ class SageConnector: try: doc.DO_Ref = commande_data["reference"] logger.info(f" Référence: {commande_data['reference']}") - except: + except Exception: pass try: factory_lignes = doc.FactoryDocumentLigne - except: + except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle @@ -5373,7 +5482,7 @@ class SageConnector: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) - except: + except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) @@ -5385,7 +5494,7 @@ class SageConnector: ligne_data["article_code"], quantite ) logger.info( - f" Article associé via SetDefaultArticleReference" + " Article associé via SetDefaultArticleReference" ) except Exception as e: logger.warning( @@ -5393,9 +5502,9 @@ class SageConnector: ) try: ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(f" Article associé via SetDefaultArticle") - except Exception as e2: - logger.error(f" Toutes les méthodes ont échoué") + logger.info(" Article associé via SetDefaultArticle") + except Exception: + logger.error(" Toutes les méthodes ont échoué") ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") @@ -5469,7 +5578,7 @@ class SageConnector: ) doc_result.Read() numero_commande = getattr(doc_result, "DO_Piece", "") - except: + except Exception: pass if not numero_commande: @@ -5496,7 +5605,7 @@ class SageConnector: date_livr = getattr(doc_final, "DO_DateLivr", None) if date_livr: date_livraison_final = date_livr.strftime("%Y-%m-%d") - except: + except Exception: pass else: total_ht = 0.0 @@ -5523,11 +5632,11 @@ class SageConnector: "reference": reference_finale, } - except Exception as e: + except Exception: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() - except: + except Exception: pass raise @@ -5555,7 +5664,7 @@ class SageConnector: persist = persist_test logger.info(f" Document trouvé (type={type_test})") break - except: + except Exception: continue if not persist: @@ -5597,7 +5706,7 @@ class SageConnector: break nb_lignes_initial += 1 index += 1 - except: + except Exception: break logger.info(f" Lignes initiales: {nb_lignes_initial}") @@ -5614,7 +5723,7 @@ class SageConnector: "lignes" in commande_data and commande_data["lignes"] is not None ) - logger.info(f"Modifications demandées:") + logger.info("Modifications demandées:") logger.info(f" Date commande: {modif_date}") logger.info(f" Date livraison: {modif_date_livraison}") logger.info(f" Statut: {modif_statut}") @@ -5660,7 +5769,7 @@ class SageConnector: except Exception as e: logger.error(f" Write() basique ÉCHOUE: {e}") - logger.error(f" ABANDON: Le document est VERROUILLÉ") + logger.error(" ABANDON: Le document est VERROUILLÉ") raise ValueError( f"Document verrouillé, impossible de modifier: {e}" ) @@ -5729,7 +5838,7 @@ class SageConnector: sage_error = self.cial.CptaApplication.LastError if sage_error: error_msg = f"{sage_error.Description} (Code: {sage_error.Number})" - except: + except Exception: pass logger.error(f" Write() échoue: {error_msg}") @@ -5761,7 +5870,7 @@ class SageConnector: try: factory_lignes = doc.FactoryDocumentLigne - except: + except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle @@ -5815,7 +5924,7 @@ class SageConnector: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) - except: + except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) @@ -5826,10 +5935,10 @@ class SageConnector: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) - except: + except Exception: try: ligne_obj.SetDefaultArticle(article_obj, quantite) - except: + except Exception: ligne_obj.DL_Design = ligne_data.get("designation", "") ligne_obj.DL_Qte = quantite @@ -5844,7 +5953,7 @@ class SageConnector: ligne_data["remise_pourcentage"] ) ligne_obj.DL_Remise01REM_Type = 0 - except: + except Exception: pass ligne_obj.Write() @@ -5942,7 +6051,7 @@ class SageConnector: date_livr = getattr(doc, "DO_DateLivr", None) if date_livr: date_livraison_final = date_livr.strftime("%Y-%m-%d") - except: + except Exception: pass logger.info(f" SUCCÈS: {numero} modifiée ") @@ -5979,7 +6088,7 @@ class SageConnector: error_message = ( f"Erreur Sage: {err.Description} (Code: {err.Number})" ) - except: + except Exception: pass raise RuntimeError(f"Erreur Sage: {error_message}") @@ -5999,7 +6108,7 @@ class SageConnector: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug(" Transaction Sage démarrée") - except: + except Exception: pass try: @@ -6010,7 +6119,7 @@ class SageConnector: try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") - except: + except Exception: pass logger.info(" Document livraison créé") @@ -6042,7 +6151,7 @@ class SageConnector: client_obj = _cast_client(persist_client) if not client_obj: - raise ValueError(f"Impossible de charger le client") + raise ValueError("Impossible de charger le client") doc.SetDefaultClient(client_obj) doc.Write() @@ -6052,12 +6161,12 @@ class SageConnector: try: doc.DO_Ref = livraison_data["reference"] logger.info(f" Référence: {livraison_data['reference']}") - except: + except Exception: pass try: factory_lignes = doc.FactoryDocumentLigne - except: + except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle @@ -6098,7 +6207,7 @@ class SageConnector: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) - except: + except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) @@ -6110,7 +6219,7 @@ class SageConnector: ligne_data["article_code"], quantite ) logger.info( - f" Article associé via SetDefaultArticleReference" + " Article associé via SetDefaultArticleReference" ) except Exception as e: logger.warning( @@ -6118,9 +6227,9 @@ class SageConnector: ) try: ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(f" Article associé via SetDefaultArticle") - except Exception as e2: - logger.error(f" Toutes les méthodes ont échoué") + logger.info(" Article associé via SetDefaultArticle") + except Exception: + logger.error(" Toutes les méthodes ont échoué") ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") @@ -6180,7 +6289,7 @@ class SageConnector: ) doc_result.Read() numero_livraison = getattr(doc_result, "DO_Piece", "") - except: + except Exception: pass if not numero_livraison: @@ -6209,7 +6318,7 @@ class SageConnector: date_livraison_prevue_final = date_livr.strftime( "%Y-%m-%d" ) - except: + except Exception: pass else: total_ht = 0.0 @@ -6240,11 +6349,11 @@ class SageConnector: "reference": reference_finale, } - except Exception as e: + except Exception: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() - except: + except Exception: pass raise @@ -6272,7 +6381,7 @@ class SageConnector: persist = persist_test logger.info(f" Document trouvé (type={type_test})") break - except: + except Exception: continue if not persist: @@ -6304,7 +6413,7 @@ class SageConnector: break nb_lignes_initial += 1 index += 1 - except: + except Exception: break logger.info(f" Lignes initiales: {nb_lignes_initial}") @@ -6321,7 +6430,7 @@ class SageConnector: "lignes" in livraison_data and livraison_data["lignes"] is not None ) - logger.info(f"Modifications demandées:") + logger.info("Modifications demandées:") logger.info(f" Date livraison: {modif_date}") logger.info(f" Date livraison prévue: {modif_date_livraison_prevue}") logger.info(f" Statut: {modif_statut}") @@ -6422,7 +6531,7 @@ class SageConnector: try: factory_lignes = doc.FactoryDocumentLigne - except: + except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle @@ -6473,7 +6582,7 @@ class SageConnector: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) - except: + except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) @@ -6484,10 +6593,10 @@ class SageConnector: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) - except: + except Exception: try: ligne_obj.SetDefaultArticle(article_obj, quantite) - except: + except Exception: ligne_obj.DL_Design = ligne_data.get("designation", "") ligne_obj.DL_Qte = quantite @@ -6502,7 +6611,7 @@ class SageConnector: ligne_data["remise_pourcentage"] ) ligne_obj.DL_Remise01REM_Type = 0 - except: + except Exception: pass ligne_obj.Write() @@ -6543,7 +6652,7 @@ class SageConnector: doc.Read() champs_modifies.append("reference") - logger.info(f" Référence modifiée avec succès") + logger.info(" Référence modifiée avec succès") except Exception as e: logger.warning(f"Impossible de modifier la référence: {e}") @@ -6567,7 +6676,7 @@ class SageConnector: doc.Read() champs_modifies.append("statut") - logger.info(f" Statut modifié avec succès") + logger.info(" Statut modifié avec succès") except Exception as e: logger.warning(f"Impossible de modifier le statut: {e}") @@ -6590,7 +6699,7 @@ class SageConnector: date_livr = getattr(doc, "DO_DateLivr", None) if date_livr: date_livraison_prevue_final = date_livr.strftime("%Y-%m-%d") - except: + except Exception: pass logger.info(f" SUCCÈS: {numero} modifiée ") @@ -6629,7 +6738,7 @@ class SageConnector: error_message = ( f"Erreur Sage: {err.Description} (Code: {err.Number})" ) - except: + except Exception: pass raise RuntimeError(f"Erreur Sage: {error_message}") @@ -6647,7 +6756,7 @@ class SageConnector: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug(" Transaction Sage démarrée") - except: + except Exception: pass try: @@ -6658,7 +6767,7 @@ class SageConnector: try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") - except: + except Exception: pass logger.info(" Document avoir créé") @@ -6685,7 +6794,7 @@ class SageConnector: client_obj = _cast_client(persist_client) if not client_obj: - raise ValueError(f"Impossible de charger le client") + raise ValueError("Impossible de charger le client") doc.SetDefaultClient(client_obj) doc.Write() @@ -6695,12 +6804,12 @@ class SageConnector: try: doc.DO_Ref = avoir_data["reference"] logger.info(f" Référence: {avoir_data['reference']}") - except: + except Exception: pass try: factory_lignes = doc.FactoryDocumentLigne - except: + except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle @@ -6741,7 +6850,7 @@ class SageConnector: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) - except: + except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) @@ -6753,7 +6862,7 @@ class SageConnector: ligne_data["article_code"], quantite ) logger.info( - f" Article associé via SetDefaultArticleReference" + " Article associé via SetDefaultArticleReference" ) except Exception as e: logger.warning( @@ -6761,9 +6870,9 @@ class SageConnector: ) try: ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(f" Article associé via SetDefaultArticle") - except Exception as e2: - logger.error(f" Toutes les méthodes ont échoué") + logger.info(" Article associé via SetDefaultArticle") + except Exception: + logger.error(" Toutes les méthodes ont échoué") ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") @@ -6823,7 +6932,7 @@ class SageConnector: ) doc_result.Read() numero_avoir = getattr(doc_result, "DO_Piece", "") - except: + except Exception: pass if not numero_avoir: @@ -6850,7 +6959,7 @@ class SageConnector: date_livr = getattr(doc_final, "DO_DateLivr", None) if date_livr: date_livraison_final = date_livr.strftime("%Y-%m-%d") - except: + except Exception: pass else: total_ht = 0.0 @@ -6876,11 +6985,11 @@ class SageConnector: "reference": reference_finale, } - except Exception as e: + except Exception: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() - except: + except Exception: pass raise @@ -6908,7 +7017,7 @@ class SageConnector: persist = persist_test logger.info(f" Document trouvé (type={type_test})") break - except: + except Exception: continue if not persist: @@ -6956,7 +7065,7 @@ class SageConnector: break nb_lignes_initial += 1 index += 1 - except: + except Exception: break logger.info(f" Lignes initiales: {nb_lignes_initial}") @@ -6973,7 +7082,7 @@ class SageConnector: "lignes" in avoir_data and avoir_data["lignes"] is not None ) - logger.info(f"Modifications demandées:") + logger.info("Modifications demandées:") logger.info(f" Date avoir: {modif_date}") logger.info(f" Date livraison: {modif_date_livraison}") logger.info(f" Statut: {modif_statut}") @@ -7019,7 +7128,7 @@ class SageConnector: except Exception as e: logger.error(f" Write() basique ÉCHOUE: {e}") - logger.error(f" ABANDON: Le document est VERROUILLÉ") + logger.error(" ABANDON: Le document est VERROUILLÉ") raise ValueError( f"Document verrouillé, impossible de modifier: {e}" ) @@ -7088,7 +7197,7 @@ class SageConnector: sage_error = self.cial.CptaApplication.LastError if sage_error: error_msg = f"{sage_error.Description} (Code: {sage_error.Number})" - except: + except Exception: pass logger.error(f" Write() échoue: {error_msg}") @@ -7120,7 +7229,7 @@ class SageConnector: try: factory_lignes = doc.FactoryDocumentLigne - except: + except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle @@ -7174,7 +7283,7 @@ class SageConnector: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) - except: + except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) @@ -7185,10 +7294,10 @@ class SageConnector: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) - except: + except Exception: try: ligne_obj.SetDefaultArticle(article_obj, quantite) - except: + except Exception: ligne_obj.DL_Design = ligne_data.get("designation", "") ligne_obj.DL_Qte = quantite @@ -7203,7 +7312,7 @@ class SageConnector: ligne_data["remise_pourcentage"] ) ligne_obj.DL_Remise01REM_Type = 0 - except: + except Exception: pass ligne_obj.Write() @@ -7252,7 +7361,7 @@ class SageConnector: doc.Read() champs_modifies.append("reference") - logger.info(f" Référence modifiée avec succès") + logger.info(" Référence modifiée avec succès") except Exception as e: logger.warning(f"Impossible de modifier la référence: {e}") @@ -7276,7 +7385,7 @@ class SageConnector: doc.Read() champs_modifies.append("statut") - logger.info(f" Statut modifié avec succès") + logger.info(" Statut modifié avec succès") except Exception as e: logger.warning(f"Impossible de modifier le statut: {e}") @@ -7306,7 +7415,7 @@ class SageConnector: date_livr = getattr(doc, "DO_DateLivr", None) if date_livr: date_livraison_final = date_livr.strftime("%Y-%m-%d") - except: + except Exception: pass logger.info(f" SUCCÈS: {numero} modifié ") @@ -7345,7 +7454,7 @@ class SageConnector: error_message = ( f"Erreur Sage: {err.Description} (Code: {err.Number})" ) - except: + except Exception: pass raise RuntimeError(f"Erreur Sage: {error_message}") @@ -7365,7 +7474,7 @@ class SageConnector: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug(" Transaction Sage démarrée") - except: + except Exception: pass try: @@ -7376,7 +7485,7 @@ class SageConnector: try: doc = win32com.client.CastTo(doc, "IBODocumentVente3") - except: + except Exception: pass logger.info(" Document facture créé") @@ -7408,7 +7517,7 @@ class SageConnector: client_obj = _cast_client(persist_client) if not client_obj: - raise ValueError(f"Impossible de charger le client") + raise ValueError("Impossible de charger le client") doc.SetDefaultClient(client_obj) doc.Write() @@ -7418,7 +7527,7 @@ class SageConnector: try: doc.DO_Ref = facture_data["reference"] logger.info(f" Référence: {facture_data['reference']}") - except: + except Exception: pass logger.info(" Configuration champs spécifiques factures...") @@ -7434,7 +7543,7 @@ class SageConnector: ) doc.DO_CodeJournal = journal_defaut logger.info(f" Code journal: {journal_defaut}") - except: + except Exception: doc.DO_CodeJournal = "VTE" logger.info(" Code journal: VTE (défaut)") except Exception as e: @@ -7444,19 +7553,19 @@ class SageConnector: if hasattr(doc, "DO_Souche"): doc.DO_Souche = 0 logger.debug(" Souche: 0 (défaut)") - except: + except Exception: pass try: if hasattr(doc, "DO_Regime"): doc.DO_Regime = 0 logger.debug(" Régime: 0 (défaut)") - except: + except Exception: pass try: factory_lignes = doc.FactoryDocumentLigne - except: + except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle @@ -7497,7 +7606,7 @@ class SageConnector: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) - except: + except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) @@ -7509,7 +7618,7 @@ class SageConnector: ligne_data["article_code"], quantite ) logger.info( - f" Article associé via SetDefaultArticleReference" + " Article associé via SetDefaultArticleReference" ) except Exception as e: logger.warning( @@ -7517,9 +7626,9 @@ class SageConnector: ) try: ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(f" Article associé via SetDefaultArticle") - except Exception as e2: - logger.error(f" Toutes les méthodes ont échoué") + logger.info(" Article associé via SetDefaultArticle") + except Exception: + logger.error(" Toutes les méthodes ont échoué") ligne_obj.DL_Design = ( designation_sage or ligne_data.get("designation", "") @@ -7567,10 +7676,10 @@ class SageConnector: try: doc.SetClient(client_obj) logger.debug(" Client réassocié avant validation") - except: + except Exception: try: doc.SetDefaultClient(client_obj) - except: + except Exception: pass doc.Write() @@ -7593,7 +7702,7 @@ class SageConnector: ) doc_result.Read() numero_facture = getattr(doc_result, "DO_Piece", "") - except: + except Exception: pass if not numero_facture: @@ -7625,7 +7734,7 @@ class SageConnector: date_livr = getattr(doc_final, "DO_DateLivr", None) if date_livr: date_livraison_final = date_livr.strftime("%Y-%m-%d") - except: + except Exception: pass else: total_ht = 0.0 @@ -7651,12 +7760,12 @@ class SageConnector: "reference": reference_finale, } - except Exception as e: + except Exception: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() logger.error(" Transaction annulée (rollback)") - except: + except Exception: pass raise @@ -7684,7 +7793,7 @@ class SageConnector: persist = persist_test logger.info(f" Document trouvé (type={type_test})") break - except: + except Exception: continue if not persist: @@ -7732,7 +7841,7 @@ class SageConnector: break nb_lignes_initial += 1 index += 1 - except: + except Exception: break logger.info(f" Lignes initiales: {nb_lignes_initial}") @@ -7749,7 +7858,7 @@ class SageConnector: "lignes" in facture_data and facture_data["lignes"] is not None ) - logger.info(f"Modifications demandées:") + logger.info("Modifications demandées:") logger.info(f" Date facture: {modif_date}") logger.info(f" Date livraison: {modif_date_livraison}") logger.info(f" Statut: {modif_statut}") @@ -7795,7 +7904,7 @@ class SageConnector: except Exception as e: logger.error(f" Write() basique ÉCHOUE: {e}") - logger.error(f" ABANDON: Le document est VERROUILLÉ") + logger.error(" ABANDON: Le document est VERROUILLÉ") raise ValueError( f"Document verrouillé, impossible de modifier: {e}" ) @@ -7864,7 +7973,7 @@ class SageConnector: sage_error = self.cial.CptaApplication.LastError if sage_error: error_msg = f"{sage_error.Description} (Code: {sage_error.Number})" - except: + except Exception: pass logger.error(f" Write() échoue: {error_msg}") @@ -7896,7 +8005,7 @@ class SageConnector: try: factory_lignes = doc.FactoryDocumentLigne - except: + except Exception: factory_lignes = doc.FactoryDocumentVenteLigne factory_article = self.cial.FactoryArticle @@ -7950,7 +8059,7 @@ class SageConnector: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) - except: + except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentVenteLigne3" ) @@ -7961,10 +8070,10 @@ class SageConnector: ligne_obj.SetDefaultArticleReference( ligne_data["article_code"], quantite ) - except: + except Exception: try: ligne_obj.SetDefaultArticle(article_obj, quantite) - except: + except Exception: ligne_obj.DL_Design = ligne_data.get("designation", "") ligne_obj.DL_Qte = quantite @@ -7979,7 +8088,7 @@ class SageConnector: ligne_data["remise_pourcentage"] ) ligne_obj.DL_Remise01REM_Type = 0 - except: + except Exception: pass ligne_obj.Write() @@ -8028,7 +8137,7 @@ class SageConnector: doc.Read() champs_modifies.append("reference") - logger.info(f" Référence modifiée avec succès") + logger.info(" Référence modifiée avec succès") except Exception as e: logger.warning(f"Impossible de modifier la référence: {e}") @@ -8052,7 +8161,7 @@ class SageConnector: doc.Read() champs_modifies.append("statut") - logger.info(f" Statut modifié avec succès") + logger.info(" Statut modifié avec succès") except Exception as e: logger.warning(f"Impossible de modifier le statut: {e}") @@ -8082,7 +8191,7 @@ class SageConnector: date_livr = getattr(doc, "DO_DateLivr", None) if date_livr: date_livraison_final = date_livr.strftime("%Y-%m-%d") - except: + except Exception: pass logger.info(f" SUCCÈS: {numero} modifiée ") @@ -8121,7 +8230,7 @@ class SageConnector: error_message = ( f"Erreur Sage: {err.Description} (Code: {err.Number})" ) - except: + except Exception: pass raise RuntimeError(f"Erreur Sage: {error_message}") @@ -8451,7 +8560,7 @@ class SageConnector: article.AR_Nomencl = int(getattr(article_modele, "AR_Nomencl", 0)) article.AR_SuiviStock = 2 - logger.info(f" [OK] AR_SuiviStock=2 (FIFO/LIFO)") + logger.info(" [OK] AR_SuiviStock=2 (FIFO/LIFO)") prix_vente = article_data.get("prix_vente") if prix_vente is not None: @@ -8469,7 +8578,7 @@ class SageConnector: logger.info( f" Prix achat (AR_PrixAch) : {prix_achat} EUR" ) - except: + except Exception: article.AR_PrixAchat = float(prix_achat) logger.info( f" Prix achat (AR_PrixAchat) : {prix_achat} EUR" @@ -8486,8 +8595,8 @@ class SageConnector: if description: try: article.AR_Commentaire = description - logger.info(f" Description définie") - except: + logger.info(" Description définie") + except Exception: pass logger.info("[ARTICLE] Écriture dans Sage...") @@ -8502,7 +8611,7 @@ class SageConnector: sage_error = self.cial.CptaApplication.LastError if sage_error: error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" - except: + except Exception: pass logger.error(f" [ERREUR] Write() échoué : {error_detail}") @@ -8535,7 +8644,7 @@ class SageConnector: f" Factory trouvée : {factory_name}" ) break - except: + except Exception: continue if not factory_stock: @@ -8706,7 +8815,7 @@ class SageConnector: resultat["famille_libelle"] = getattr( famille_obj, "FA_Intitule", "" ) - except: + except Exception: pass if stocks_par_depot: @@ -8733,7 +8842,7 @@ class SageConnector: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() - except: + except Exception: pass raise @@ -8741,7 +8850,7 @@ class SageConnector: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() - except: + except Exception: pass logger.error(f"Erreur creation article : {e}", exc_info=True) @@ -8828,7 +8937,7 @@ class SageConnector: raise ValueError(f"Impossible de vérifier la famille : {e}") if famille_existe_sql and famille_code_exact: - logger.info(f" [COM] Recherche via scanner...") + logger.info(" [COM] Recherche via scanner...") factory_famille = self.cial.FactoryFamille famille_obj = None @@ -8897,7 +9006,7 @@ class SageConnector: if "designation" in article_data: designation = str(article_data["designation"])[:69].strip() article.AR_Design = designation - champs_modifies.append(f"designation") + champs_modifies.append("designation") logger.info(f" [OK] Désignation : {designation}") if "prix_vente" in article_data: @@ -8919,7 +9028,7 @@ class SageConnector: logger.info( f" [OK] Prix achat (AR_PrixAch) : {prix_achat} EUR" ) - except: + except Exception: article.AR_PrixAchat = prix_achat champs_modifies.append("prix_achat") logger.info( @@ -8980,7 +9089,7 @@ class SageConnector: description = str(article_data["description"])[:255].strip() article.AR_Commentaire = description champs_modifies.append("description") - logger.info(f" [OK] Description définie") + logger.info(" [OK] Description définie") except Exception as e: logger.warning(f" [WARN] Description : {e}") @@ -9007,7 +9116,7 @@ class SageConnector: error_detail = ( f"{sage_error.Description} (Code: {sage_error.Number})" ) - except: + except Exception: pass logger.error(f"[ARTICLE] Erreur Write() : {error_detail}") @@ -9069,7 +9178,7 @@ class SageConnector: err = self.cial.CptaApplication.LastError if err: error_message = f"Erreur Sage: {err.Description}" - except: + except Exception: pass raise RuntimeError(f"Erreur technique Sage : {error_message}") @@ -9128,8 +9237,8 @@ class SageConnector: index += 1 except ValueError: - raise # Re-raise si c'est notre erreur - except: + raise + except Exception: index += 1 except ValueError: raise @@ -9142,8 +9251,8 @@ class SageConnector: famille.FA_Intitule = intitule try: - famille.FA_Type = 0 # Toujours Détail - logger.info(f"[FAMILLE] Type : 0 (Détail)") + famille.FA_Type = 0 + logger.info("[FAMILLE] Type : 0 (Détail)") except Exception as e: logger.warning(f"[FAMILLE] FA_Type non défini : {e}") @@ -9195,7 +9304,7 @@ class SageConnector: error_detail = ( f"{sage_error.Description} (Code: {sage_error.Number})" ) - except: + except Exception: pass logger.error(f"[FAMILLE] Erreur Write() : {error_detail}") @@ -9206,7 +9315,7 @@ class SageConnector: resultat = { "code": getattr(famille, "FA_CodeFamille", "").strip(), "intitule": getattr(famille, "FA_Intitule", "").strip(), - "type": 0, # Toujours Détail + "type": 0, "type_libelle": "Détail", } @@ -9227,7 +9336,7 @@ class SageConnector: err = self.cial.CptaApplication.LastError if err: error_message = f"Erreur Sage: {err.Description}" - except: + except Exception: pass raise RuntimeError(f"Erreur technique Sage : {error_message}") @@ -9589,10 +9698,8 @@ class SageConnector: "cb_createur": to_str(row[idx + 1]), "cb_modification": row[ idx + 2 - ], # datetime - garder tel quel - "cb_creation": row[ - idx + 3 - ], # datetime - garder tel quel + ], + "cb_creation": row[idx + 3], "cb_creation_user": to_str(row[idx + 4]), } ) @@ -9605,7 +9712,7 @@ class SageConnector: "tva_vente_1": to_str(row[idx + 2]), "tva_vente_2": to_str(row[idx + 3]), "tva_vente_3": to_str(row[idx + 4]), - "tva_vente_date_1": row[idx + 5], # datetime + "tva_vente_date_1": row[idx + 5], "tva_vente_date_2": row[idx + 6], "tva_vente_date_3": row[idx + 7], "type_facture_vente": to_int(row[idx + 8]), @@ -9671,9 +9778,7 @@ class SageConnector: familles.append(famille) - type_msg = ( - "DÉTAIL uniquement" if not inclure_totaux else "TOUS types" - ) + type_msg = "DÉTAIL uniquement" if not inclure_totaux else "TOUS types" logger.info(f" {len(familles)} familles chargées ({type_msg})") return familles @@ -9855,19 +9960,19 @@ class SageConnector: def creer_entree_stock(self, entree_data: Dict) -> Dict: try: with self._com_context(), self._lock_com: - logger.info(f"[STOCK] === CRÉATION ENTRÉE STOCK (COM pur) ===") + logger.info("[STOCK] === CRÉATION ENTRÉE STOCK (COM pur) ===") transaction_active = False try: self.cial.CptaApplication.BeginTrans() transaction_active = True logger.debug("Transaction démarrée") - except: + except Exception: pass try: factory_doc = self.cial.FactoryDocumentStock - persist_doc = factory_doc.CreateType(180) # 180 = Entrée + persist_doc = factory_doc.CreateType(180) doc = win32com.client.CastTo(persist_doc, "IBODocumentStock3") doc.SetDefault() @@ -9883,7 +9988,7 @@ class SageConnector: doc.DO_Ref = entree_data["reference"] doc.Write() - logger.info(f"[STOCK] Document créé") + logger.info("[STOCK] Document créé") factory_article = self.cial.FactoryArticle factory_depot = self.cial.FactoryDepot @@ -9906,7 +10011,7 @@ class SageConnector: try: factory_lignes = doc.FactoryDocumentLigne - except: + except Exception: factory_lignes = doc.FactoryDocumentStockLigne for idx, ligne_data in enumerate(entree_data["lignes"], 1): @@ -9931,7 +10036,7 @@ class SageConnector: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) - except: + except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentStockLigne3" ) @@ -9942,12 +10047,12 @@ class SageConnector: ligne_obj.SetDefaultArticleReference( article_ref, float(quantite) ) - except: + except Exception: try: ligne_obj.SetDefaultArticle( article_obj, float(quantite) ) - except: + except Exception: raise ValueError( f"Impossible de lier l'article {article_ref}" ) @@ -9956,7 +10061,7 @@ class SageConnector: if prix: try: ligne_obj.DL_PrixUnitaire = float(prix) - except: + except Exception: pass ligne_obj.Write() @@ -9968,7 +10073,7 @@ class SageConnector: try: logger.info( - f" [COM] Méthode A : Article.FactoryArticleStock" + " [COM] Méthode A : Article.FactoryArticleStock" ) factory_article = self.cial.FactoryArticle @@ -10035,7 +10140,7 @@ class SageConnector: logger.info( f" Stock trouvé pour dépôt {depot_code}" ) - except: + except Exception: pass index_stock += 1 @@ -10061,7 +10166,7 @@ class SageConnector: logger.info( " Dépôt principal lié" ) - except: + except Exception: pass logger.info(" Nouvel ArticleStock créé") @@ -10074,7 +10179,7 @@ class SageConnector: if stock_trouve: try: stock_trouve.Read() - except: + except Exception: pass if stock_mini is not None: @@ -10139,7 +10244,7 @@ class SageConnector: try: stock_trouve.Write() - logger.info(f" ArticleStock sauvegardé") + logger.info(" ArticleStock sauvegardé") except Exception as e: logger.error( f" Erreur Write() ArticleStock: {e}" @@ -10150,7 +10255,7 @@ class SageConnector: stock_mini is not None or stock_maxi is not None ): logger.info( - f" [COM] Méthode B : Depot.FactoryDepotStock (alternative)" + " [COM] Méthode B : Depot.FactoryDepotStock (alternative)" ) try: @@ -10168,7 +10273,7 @@ class SageConnector: f" Factory trouvée: {factory_name}" ) break - except: + except Exception: continue if factory_depot_stock: @@ -10201,7 +10306,7 @@ class SageConnector: break index_ds += 1 - except: + except Exception: index_ds += 1 if not stock_depot_trouve: @@ -10288,7 +10393,7 @@ class SageConnector: numero = getattr(doc, "DO_Piece", "") logger.info(f"[STOCK] Document finalisé: {numero}") - logger.info(f"[STOCK] Vérification finale via COM...") + logger.info("[STOCK] Vérification finale via COM...") for stock_info in stocks_mis_a_jour: article_ref = stock_info["article_ref"] @@ -10310,7 +10415,7 @@ class SageConnector: if val is not None: stock_total = float(val) break - except: + except Exception: pass for attr in ["AR_StockMini", "AS_QteMini", "StockMini"]: @@ -10319,7 +10424,7 @@ class SageConnector: if val is not None: stock_mini_lu = float(val) break - except: + except Exception: pass for attr in ["AR_StockMaxi", "AS_QteMaxi", "StockMaxi"]: @@ -10328,7 +10433,7 @@ class SageConnector: if val is not None: stock_maxi_lu = float(val) break - except: + except Exception: pass logger.info( @@ -10350,9 +10455,9 @@ class SageConnector: if transaction_active: try: self.cial.CptaApplication.CommitTrans() - logger.info(f"[STOCK] Transaction committée") - except: - logger.info(f"[STOCK] Changements sauvegardés") + logger.info("[STOCK] Transaction committée") + except Exception: + logger.info("[STOCK] Changements sauvegardés") return { "article_ref": article_ref, @@ -10368,8 +10473,8 @@ class SageConnector: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() - logger.info(f"[STOCK] Transaction annulée") - except: + logger.info("[STOCK] Transaction annulée") + except Exception: pass logger.error(f"[STOCK] ERREUR : {e}", exc_info=True) @@ -10387,17 +10492,17 @@ class SageConnector: self._get_sql_connection() as conn, ): cursor = conn.cursor() - logger.info(f"[STOCK] === CRÉATION SORTIE STOCK ===") + logger.info("[STOCK] === CRÉATION SORTIE STOCK ===") logger.info(f"[STOCK] {len(sortie_data.get('lignes', []))} ligne(s)") try: self.cial.CptaApplication.BeginTrans() - except: + except Exception: pass try: factory = self.cial.FactoryDocumentStock - persist = factory.CreateType(181) # 181 = Sortie + persist = factory.CreateType(181) doc = win32com.client.CastTo(persist, "IBODocumentStock3") doc.SetDefault() @@ -10419,7 +10524,7 @@ class SageConnector: try: factory_lignes = doc.FactoryDocumentLigne - except: + except Exception: factory_lignes = doc.FactoryDocumentStockLigne factory_article = self.cial.FactoryArticle @@ -10464,12 +10569,12 @@ class SageConnector: numero_lot = ligne_data.get("numero_lot") - if ar_suivi == 1: # CMUP + if ar_suivi == 1: if numero_lot: - logger.warning(f"[STOCK] CMUP : Suppression du lot") + logger.warning("[STOCK] CMUP : Suppression du lot") numero_lot = None - elif ar_suivi == 2: # FIFO/LIFO + elif ar_suivi == 2: if not numero_lot: import uuid @@ -10484,7 +10589,7 @@ class SageConnector: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentLigne3" ) - except: + except Exception: ligne_obj = win32com.client.CastTo( ligne_persist, "IBODocumentStockLigne3" ) @@ -10498,15 +10603,15 @@ class SageConnector: article_ref, float(quantite) ) article_lie = True - logger.info(f"[STOCK] SetDefaultArticleReference()") - except: + logger.info("[STOCK] SetDefaultArticleReference()") + except Exception: try: ligne_obj.SetDefaultArticle( article_obj, float(quantite) ) article_lie = True - logger.info(f"[STOCK] SetDefaultArticle()") - except: + logger.info("[STOCK] SetDefaultArticle()") + except Exception: pass if not article_lie: @@ -10517,26 +10622,26 @@ class SageConnector: if numero_lot and ar_suivi == 2: try: ligne_obj.SetDefaultLot(numero_lot) - logger.info(f"[STOCK] Lot défini") - except: + logger.info("[STOCK] Lot défini") + except Exception: try: ligne_obj.LS_NoSerie = numero_lot - logger.info(f"[STOCK] Lot via LS_NoSerie") - except: + logger.info("[STOCK] Lot via LS_NoSerie") + except Exception: pass prix = ligne_data.get("prix_unitaire") if prix: try: ligne_obj.DL_PrixUnitaire = float(prix) - except: + except Exception: pass ligne_obj.Write() - logger.info(f"[STOCK] Write() réussi") + logger.info("[STOCK] Write() réussi") ligne_obj.Read() - ref_verifiee = article_ref # Supposer OK si Write() réussi + ref_verifiee = article_ref try: article_lie_obj = getattr(ligne_obj, "Article", None) @@ -10546,7 +10651,7 @@ class SageConnector: getattr(article_lie_obj, "AR_Ref", "").strip() or article_ref ) - except: + except Exception: pass logger.info(f"[STOCK] LIGNE {idx} COMPLÈTE") @@ -10570,8 +10675,8 @@ class SageConnector: try: self.cial.CptaApplication.CommitTrans() - logger.info(f"[STOCK] Transaction committée") - except: + logger.info("[STOCK] Transaction committée") + except Exception: pass return { @@ -10587,7 +10692,7 @@ class SageConnector: logger.error(f"[STOCK] ERREUR : {e}", exc_info=True) try: self.cial.CptaApplication.RollbackTrans() - except: + except Exception: pass raise ValueError(f"Erreur création sortie stock : {str(e)}") @@ -10622,7 +10727,7 @@ class SageConnector: break index += 1 - except: + except Exception: index += 1 if not persist: @@ -10666,7 +10771,7 @@ class SageConnector: ligne = win32com.client.CastTo( ligne_p, "IBODocumentLigne3" ) - except: + except Exception: ligne = win32com.client.CastTo( ligne_p, "IBODocumentStockLigne3" ) @@ -10681,7 +10786,7 @@ class SageConnector: article_ref = getattr( article_obj, "AR_Ref", "" ).strip() - except: + except Exception: pass ligne_info = { @@ -10700,7 +10805,7 @@ class SageConnector: mouvement["lignes"].append(ligne_info) idx += 1 - except: + except Exception: break except Exception as e: logger.warning(f"[MOUVEMENT] Erreur lecture lignes : {e}") diff --git a/schemas/documents/avoirs.py b/schemas/documents/avoirs.py index 48d6066..2f261a1 100644 --- a/schemas/documents/avoirs.py +++ b/schemas/documents/avoirs.py @@ -1,9 +1,7 @@ - - -from pydantic import BaseModel, Field, validator, EmailStr, field_validator +from pydantic import BaseModel from typing import Optional, List, Dict -from enum import Enum, IntEnum -from datetime import datetime, date +from datetime import date + class AvoirCreateGatewayRequest(BaseModel): """Création d'un avoir côté gateway""" diff --git a/schemas/documents/commandes.py b/schemas/documents/commandes.py index 7024090..e7ca3df 100644 --- a/schemas/documents/commandes.py +++ b/schemas/documents/commandes.py @@ -1,8 +1,7 @@ - -from pydantic import BaseModel, Field, validator, EmailStr, field_validator +from pydantic import BaseModel from typing import Optional, List, Dict -from enum import Enum, IntEnum -from datetime import datetime, date +from datetime import date + class CommandeCreateRequest(BaseModel): """Création d'une commande""" diff --git a/schemas/documents/devis.py b/schemas/documents/devis.py index ec0d60e..2f0e2a1 100644 --- a/schemas/documents/devis.py +++ b/schemas/documents/devis.py @@ -1,9 +1,7 @@ - - -from pydantic import BaseModel, Field, validator, EmailStr, field_validator +from pydantic import BaseModel from typing import Optional, List, Dict -from enum import Enum, IntEnum -from datetime import datetime, date +from datetime import date + class DevisRequest(BaseModel): client_id: str @@ -12,8 +10,9 @@ class DevisRequest(BaseModel): reference: Optional[str] = None lignes: List[Dict] + class DevisUpdateGatewayRequest(BaseModel): """Modèle pour modification devis côté gateway""" numero: str - devis_data: Dict \ No newline at end of file + devis_data: Dict diff --git a/schemas/documents/documents.py b/schemas/documents/documents.py index 989ea90..4dc6ae8 100644 --- a/schemas/documents/documents.py +++ b/schemas/documents/documents.py @@ -1,8 +1,6 @@ +from pydantic import BaseModel, Field +from enum import Enum -from pydantic import BaseModel, Field, validator, EmailStr, field_validator -from typing import Optional, List, Dict -from enum import Enum, IntEnum -from datetime import datetime, date class TypeDocument(int, Enum): DEVIS = 0 @@ -17,16 +15,15 @@ class DocumentGetRequest(BaseModel): numero: str type_doc: int + class TransformationRequest(BaseModel): numero_source: str type_source: int type_cible: int - class PDFGenerationRequest(BaseModel): """Modèle pour génération PDF""" doc_id: str = Field(..., description="Numéro du document") type_doc: int = Field(..., ge=0, le=60, description="Type de document Sage") - diff --git a/schemas/documents/factures.py b/schemas/documents/factures.py index 9401a68..4d7e940 100644 --- a/schemas/documents/factures.py +++ b/schemas/documents/factures.py @@ -1,9 +1,7 @@ - - -from pydantic import BaseModel, Field, validator, EmailStr, field_validator +from pydantic import BaseModel from typing import Optional, List, Dict -from enum import Enum, IntEnum -from datetime import datetime, date +from datetime import date + class FactureCreateGatewayRequest(BaseModel): """Création d'une facture côté gateway""" diff --git a/schemas/documents/livraisons.py b/schemas/documents/livraisons.py index 3125f6c..0d629c2 100644 --- a/schemas/documents/livraisons.py +++ b/schemas/documents/livraisons.py @@ -1,8 +1,7 @@ - -from pydantic import BaseModel, Field, validator, EmailStr, field_validator +from pydantic import BaseModel from typing import Optional, List, Dict -from enum import Enum, IntEnum -from datetime import datetime, date +from datetime import date + class LivraisonCreateGatewayRequest(BaseModel): """Création d'une livraison côté gateway""" diff --git a/schemas/others/general_schema.py b/schemas/others/general_schema.py index 39ff91f..62d2b34 100644 --- a/schemas/others/general_schema.py +++ b/schemas/others/general_schema.py @@ -1,6 +1,6 @@ +from pydantic import BaseModel +from typing import Optional -from pydantic import BaseModel, Field, validator, EmailStr, field_validator -from typing import Optional, List, Dict class FiltreRequest(BaseModel): filtre: Optional[str] = "" diff --git a/schemas/tiers/clients.py b/schemas/tiers/clients.py index c80bf63..7b12d60 100644 --- a/schemas/tiers/clients.py +++ b/schemas/tiers/clients.py @@ -1,5 +1,6 @@ -from pydantic import BaseModel, Field, validator, EmailStr, field_validator -from typing import Optional, List, Dict +from pydantic import BaseModel, Field, field_validator +from typing import Optional, Dict + class ClientCreateRequest(BaseModel): intitule: str = Field( @@ -348,7 +349,7 @@ class ClientCreateRequest(BaseModel): "statistique08": self.statistique08, "statistique09": self.statistique09, "statistique10": self.statistique10, - "secteur": self.secteur, # Gardé pour compatibilité + "secteur": self.secteur, "encours_autorise": self.encours_autorise, "assurance_credit": self.assurance_credit, "langue": self.langue, diff --git a/schemas/tiers/contact.py b/schemas/tiers/contact.py index bd6e56b..5736846 100644 --- a/schemas/tiers/contact.py +++ b/schemas/tiers/contact.py @@ -1,9 +1,10 @@ +from pydantic import BaseModel +from typing import Optional, Dict -from pydantic import BaseModel, Field, validator, EmailStr, field_validator -from typing import Optional, List, Dict class ContactCreateRequest(BaseModel): """Requête de création de contact""" + numero: str civilite: Optional[str] = None nom: str @@ -21,17 +22,20 @@ class ContactCreateRequest(BaseModel): class ContactListRequest(BaseModel): """Requête de liste des contacts""" + numero: str class ContactGetRequest(BaseModel): """Requête de récupération d'un contact""" + numero: str contact_numero: int class ContactUpdateRequest(BaseModel): """Requête de modification d'un contact""" + numero: str contact_numero: int updates: Dict @@ -39,5 +43,6 @@ class ContactUpdateRequest(BaseModel): class ContactDeleteRequest(BaseModel): """Requête de suppression d'un contact""" + numero: str - contact_numero: int \ No newline at end of file + contact_numero: int diff --git a/schemas/tiers/fournisseurs.py b/schemas/tiers/fournisseurs.py index 7a0776d..721a7a5 100644 --- a/schemas/tiers/fournisseurs.py +++ b/schemas/tiers/fournisseurs.py @@ -1,21 +1,5 @@ - -from pydantic import BaseModel, Field, validator, EmailStr, field_validator -from typing import Optional, List, Dict -from enum import Enum, IntEnum -from datetime import datetime, date - -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 +from pydantic import BaseModel, Field +from typing import Optional, Dict class FournisseurCreateRequest(BaseModel): @@ -37,5 +21,3 @@ class FournisseurUpdateGatewayRequest(BaseModel): code: str fournisseur_data: Dict - - diff --git a/schemas/tiers/tiers.py b/schemas/tiers/tiers.py index f7e6ea2..ed6dfeb 100644 --- a/schemas/tiers/tiers.py +++ b/schemas/tiers/tiers.py @@ -1,17 +1,16 @@ -from pydantic import BaseModel, Field, validator, EmailStr, field_validator -from typing import Optional, List, Dict -from enum import Enum, IntEnum +from pydantic import BaseModel, Field +from typing import Optional +from enum import IntEnum + class TiersListRequest(BaseModel): """Requête de listage des tiers""" + type_tiers: Optional[str] = Field( - None, - description="Type: client, fournisseur, prospect, all" - ) - filtre: str = Field( - "", - description="Filtre sur code ou intitulé" + None, description="Type: client, fournisseur, prospect, all" ) + filtre: str = Field("", description="Filtre sur code ou intitulé") + class TypeTiers(IntEnum): """CT_Type - Type de tiers""" @@ -19,4 +18,4 @@ class TypeTiers(IntEnum): CLIENT = 0 FOURNISSEUR = 1 SALARIE = 2 - AUTRE = 3 \ No newline at end of file + AUTRE = 3 diff --git a/test.py b/test.py deleted file mode 100644 index 65faaf9..0000000 --- a/test.py +++ /dev/null @@ -1,323 +0,0 @@ -""" -🔬 DIAGNOSTIC APPROFONDI CRYSTAL REPORTS -Découvre pourquoi Crystal ne fonctionne pas après redémarrage -""" - -import os -import winreg -import subprocess -import sys - -def diagnostic_complet_crystal(): - """Diagnostic exhaustif de l'installation Crystal""" - - print("="*70) - print("🔬 DIAGNOSTIC APPROFONDI CRYSTAL REPORTS") - print("="*70) - - problemes = [] - solutions = [] - - # ========================================== - # 1. VÉRIFIER SI CRYSTAL EST INSTALLÉ - # ========================================== - print("\n📁 1. Vérification présence des fichiers...") - - chemins_installation = [ - r"C:\Program Files\SAP BusinessObjects", - r"C:\Program Files (x86)\SAP BusinessObjects", - r"C:\Program Files\SAP\Crystal Reports", - r"C:\Program Files (x86)\SAP\Crystal Reports", - ] - - crystal_trouve = False - chemin_crystal = None - - for chemin in chemins_installation: - if os.path.exists(chemin): - print(f" Dossier trouvé : {chemin}") - crystal_trouve = True - chemin_crystal = chemin - - # Afficher taille - try: - total_size = 0 - for dirpath, dirnames, filenames in os.walk(chemin): - for f in filenames: - fp = os.path.join(dirpath, f) - try: - total_size += os.path.getsize(fp) - except: - pass - - size_mb = total_size / (1024 * 1024) - print(f" Taille : {size_mb:.1f} MB") - - if size_mb < 100: - print(f" Taille suspecte (attendu: 300-800 MB)") - problemes.append("Installation incomplète (taille trop petite)") - - except Exception as e: - print(f" Impossible de calculer taille : {e}") - else: - print(f" Absent : {chemin}") - - if not crystal_trouve: - print("\n PROBLÈME MAJEUR : Crystal Reports n'est pas installé") - problemes.append("Crystal Reports non installé") - solutions.append("Télécharger et installer SAP Crystal Reports Runtime") - return {"problemes": problemes, "solutions": solutions, "installe": False} - - # ========================================== - # 2. CHERCHER LES DLL CRITIQUES - # ========================================== - print("\n📦 2. Recherche DLL critiques...") - - dll_critiques = { - 'crpe32.dll': 'Crystal Reports Print Engine (CRITIQUE)', - 'crxf_pdf.dll': 'Export PDF (CRITIQUE)', - 'crdb_adoplus.dll': 'Connexion base de données', - 'CrystalDecisions.CrystalReports.Engine.dll': 'Moteur Crystal .NET', - } - - dll_trouvees = {} - - for dll_nom, description in dll_critiques.items(): - trouve = False - - for root, dirs, files in os.walk(chemin_crystal): - if dll_nom.lower() in [f.lower() for f in files]: - dll_path = os.path.join(root, dll_nom) - dll_trouvees[dll_nom] = dll_path - print(f" {dll_nom}") - print(f" {dll_path}") - trouve = True - break - - if not trouve: - print(f" {dll_nom} - {description}") - if "CRITIQUE" in description: - problemes.append(f"{dll_nom} manquante") - - if len(dll_trouvees) < 2: - print("\n Trop peu de DLL trouvées - Installation corrompue") - problemes.append("DLL manquantes - Installation corrompue") - solutions.append("Réinstaller Crystal Reports Runtime") - - # ========================================== - # 3. VÉRIFIER LE REGISTRE - # ========================================== - print("\n📋 3. Vérification registre Windows...") - - prog_ids = [ - "CrystalRuntime.Application.140", - "CrystalRuntime.Application.13", - "CrystalRuntime.Application.12", - "CrystalRuntime.Application", - "CrystalDesignRunTime.Application", - ] - - prog_ids_trouves = [] - - for prog_id in prog_ids: - try: - # Vérifier existence - key = winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, prog_id) - print(f" {prog_id}") - - # Lire le CLSID - try: - clsid_key = winreg.OpenKey(key, "CLSID") - clsid, _ = winreg.QueryValueEx(clsid_key, "") - print(f" CLSID: {clsid}") - - # Vérifier que le CLSID existe aussi - try: - clsid_path = f"CLSID\\{clsid}" - clsid_key_check = winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, clsid_path) - - # Lire InprocServer32 (chemin DLL) - try: - server_key = winreg.OpenKey(clsid_key_check, "InprocServer32") - dll_path, _ = winreg.QueryValueEx(server_key, "") - print(f" DLL: {dll_path}") - - # Vérifier que la DLL existe - if not os.path.exists(dll_path): - print(f" DLL INTROUVABLE: {dll_path}") - problemes.append(f"{prog_id}: DLL manquante ({dll_path})") - else: - prog_ids_trouves.append(prog_id) - - except: - print(f" InprocServer32 non trouvé") - - except: - print(f" CLSID {clsid} non trouvé dans registre") - problemes.append(f"{prog_id}: CLSID cassé") - - except: - print(f" Pas de CLSID") - - winreg.CloseKey(key) - - except: - print(f" {prog_id}") - - if not prog_ids_trouves: - print("\n Aucun ProgID valide - Enregistrement COM échoué") - problemes.append("ProgID non enregistrés correctement") - solutions.append("Réenregistrer les DLL Crystal avec regsvr32") - - # ========================================== - # 4. VÉRIFIER ARCHITECTURE (32 vs 64 bit) - # ========================================== - print("\n🔧 4. Vérification architecture...") - - # Architecture Python - python_arch = "64-bit" if sys.maxsize > 2**32 else "32-bit" - print(f" Python : {python_arch}") - - # Architecture système - import platform - sys_arch = platform.machine() - print(f" Système : {sys_arch}") - - # Détecter architecture Crystal installée - crystal_arch = None - if dll_trouvees: - first_dll = list(dll_trouvees.values())[0] - if "win64_x64" in first_dll or "x64" in first_dll: - crystal_arch = "64-bit" - elif "win32_x86" in first_dll or "x86" in first_dll: - crystal_arch = "32-bit" - - if crystal_arch: - print(f" Crystal : {crystal_arch}") - - if python_arch != crystal_arch: - print(f"\n INCOMPATIBILITÉ ARCHITECTURE") - print(f" Python {python_arch} ne peut pas utiliser Crystal {crystal_arch}") - problemes.append(f"Incompatibilité: Python {python_arch} vs Crystal {crystal_arch}") - solutions.append(f"Réinstaller Crystal en version {python_arch}") - - # ========================================== - # 5. VÉRIFIER SERVICES WINDOWS - # ========================================== - print("\n🔄 5. Vérification services Windows...") - - try: - result = subprocess.run( - ['sc', 'query', 'type=', 'service'], - capture_output=True, - text=True, - timeout=10 - ) - - services_crystal_attendus = [ - "SAP Crystal Reports", - "Crystal Reports", - "CrystalReports", - ] - - services_trouves = [] - for service in services_crystal_attendus: - if service.lower() in result.stdout.lower(): - services_trouves.append(service) - print(f" Service trouvé: {service}") - - if not services_trouves: - print(f" Aucun service Crystal trouvé") - print(f" (Normal pour Runtime léger)") - - except Exception as e: - print(f" Impossible de vérifier services: {e}") - - # ========================================== - # 6. TEST INSTANCIATION COM DÉTAILLÉ - # ========================================== - print("\n🧪 6. Test instanciation COM détaillé...") - - import win32com.client - - for prog_id in prog_ids_trouves: - print(f"\n Test: {prog_id}") - try: - obj = win32com.client.Dispatch(prog_id) - print(f" Instanciation RÉUSSIE") - - # Lister méthodes disponibles - print(f" Méthodes disponibles:") - for attr in dir(obj): - if not attr.startswith('_') and callable(getattr(obj, attr, None)): - print(f" - {attr}()") - - return { - "problemes": problemes, - "solutions": solutions, - "installe": True, - "prog_id_fonctionnel": prog_id - } - - except Exception as e: - print(f" Échec: {e}") - print(f" Type erreur: {type(e).__name__}") - print(f" Code: {e.args if hasattr(e, 'args') else 'N/A'}") - - # ========================================== - # 7. RÉSUMÉ ET RECOMMANDATIONS - # ========================================== - print("\n" + "="*70) - print("📊 RÉSUMÉ DU DIAGNOSTIC") - print("="*70) - - print(f"\n📁 Installation détectée: {'OUI' if crystal_trouve else 'NON'}") - print(f"📦 DLL trouvées: {len(dll_trouvees)}/{len(dll_critiques)}") - print(f"📋 ProgID valides: {len(prog_ids_trouves)}") - print(f"🔧 Architecture: Python {python_arch}, Crystal {crystal_arch or 'INCONNUE'}") - - if problemes: - print(f"\n PROBLÈMES DÉTECTÉS ({len(problemes)}):") - for i, pb in enumerate(problemes, 1): - print(f" {i}. {pb}") - - if solutions: - print(f"\n💡 SOLUTIONS RECOMMANDÉES:") - for i, sol in enumerate(solutions, 1): - print(f" {i}. {sol}") - - print("\n" + "="*70) - - return { - "problemes": problemes, - "solutions": solutions, - "installe": crystal_trouve, - "dll_trouvees": list(dll_trouvees.keys()), - "prog_ids_valides": prog_ids_trouves, - "architecture_ok": python_arch == crystal_arch if crystal_arch else False - } - - -if __name__ == "__main__": - resultats = diagnostic_complet_crystal() - - print("\n💾 Résultats sauvegardés dans diagnostic_crystal.txt") - - # Sauvegarder dans fichier - with open("diagnostic_crystal.txt", "w", encoding="utf-8") as f: - f.write("="*70 + "\n") - f.write("DIAGNOSTIC CRYSTAL REPORTS\n") - f.write("="*70 + "\n\n") - - f.write(f"Installation détectée: {resultats['installe']}\n") - f.write(f"DLL trouvées: {', '.join(resultats.get('dll_trouvees', []))}\n") - f.write(f"ProgID valides: {', '.join(resultats.get('prog_ids_valides', []))}\n") - f.write(f"Architecture OK: {resultats.get('architecture_ok', False)}\n\n") - - f.write("PROBLÈMES:\n") - for pb in resultats['problemes']: - f.write(f" - {pb}\n") - - f.write("\nSOLUTIONS:\n") - for sol in resultats['solutions']: - f.write(f" - {sol}\n") \ No newline at end of file diff --git a/utils/articles/articles_data_com.py b/utils/articles/articles_data_com.py index c50f001..fa0e6ed 100644 --- a/utils/articles/articles_data_com.py +++ b/utils/articles/articles_data_com.py @@ -2,6 +2,7 @@ import logging logger = logging.getLogger(__name__) + def _extraire_article(article_obj): try: data = { @@ -23,68 +24,64 @@ def _extraire_article(article_obj): if code_barre1: data["code_ean"] = code_barre1 data["code_barre"] = code_barre1 - except: + except Exception: pass try: data["prix_vente"] = float(getattr(article_obj, "AR_PrixVen", 0.0)) - except: + except Exception: data["prix_vente"] = 0.0 try: data["prix_achat"] = float(getattr(article_obj, "AR_PrixAch", 0.0)) - except: + except Exception: data["prix_achat"] = 0.0 try: - data["prix_revient"] = float( - getattr(article_obj, "AR_PrixRevient", 0.0) - ) - except: + data["prix_revient"] = float(getattr(article_obj, "AR_PrixRevient", 0.0)) + except Exception: data["prix_revient"] = 0.0 try: data["stock_reel"] = float(getattr(article_obj, "AR_Stock", 0.0)) - except: + except Exception: data["stock_reel"] = 0.0 try: data["stock_mini"] = float(getattr(article_obj, "AR_StockMini", 0.0)) - except: + except Exception: data["stock_mini"] = 0.0 try: data["stock_maxi"] = float(getattr(article_obj, "AR_StockMaxi", 0.0)) - except: + except Exception: data["stock_maxi"] = 0.0 try: data["stock_reserve"] = float(getattr(article_obj, "AR_QteCom", 0.0)) - except: + except Exception: data["stock_reserve"] = 0.0 try: - data["stock_commande"] = float( - getattr(article_obj, "AR_QteComFou", 0.0) - ) - except: + data["stock_commande"] = float(getattr(article_obj, "AR_QteComFou", 0.0)) + except Exception: data["stock_commande"] = 0.0 try: data["stock_disponible"] = data["stock_reel"] - data["stock_reserve"] - except: + except Exception: data["stock_disponible"] = data["stock_reel"] try: commentaire = getattr(article_obj, "AR_Commentaire", "").strip() data["description"] = commentaire - except: + except Exception: data["description"] = "" try: design2 = getattr(article_obj, "AR_Design2", "").strip() data["designation_complementaire"] = design2 - except: + except Exception: data["designation_complementaire"] = "" try: @@ -95,7 +92,7 @@ def _extraire_article(article_obj): 1: "Prestation", 2: "Divers", }.get(type_art, "Inconnu") - except: + except Exception: data["type_article"] = 0 data["type_article_libelle"] = "Article" @@ -113,11 +110,11 @@ def _extraire_article(article_obj): ).strip() else: data["famille_libelle"] = "" - except: + except Exception: data["famille_libelle"] = "" else: data["famille_libelle"] = "" - except: + except Exception: data["famille_code"] = "" data["famille_libelle"] = "" @@ -135,39 +132,39 @@ def _extraire_article(article_obj): ).strip() else: data["fournisseur_nom"] = "" - except: + except Exception: data["fournisseur_nom"] = "" else: data["fournisseur_nom"] = "" - except: + except Exception: data["fournisseur_principal"] = "" data["fournisseur_nom"] = "" try: data["unite_vente"] = getattr(article_obj, "AR_UniteVen", "").strip() - except: + except Exception: data["unite_vente"] = "" try: data["unite_achat"] = getattr(article_obj, "AR_UniteAch", "").strip() - except: + except Exception: data["unite_achat"] = "" try: data["poids"] = float(getattr(article_obj, "AR_Poids", 0.0)) - except: + except Exception: data["poids"] = 0.0 try: data["volume"] = float(getattr(article_obj, "AR_Volume", 0.0)) - except: + except Exception: data["volume"] = 0.0 try: sommeil = getattr(article_obj, "AR_Sommeil", 0) data["est_actif"] = sommeil == 0 data["en_sommeil"] = sommeil == 1 - except: + except Exception: data["est_actif"] = True data["en_sommeil"] = False @@ -182,22 +179,22 @@ def _extraire_article(article_obj): data["tva_taux"] = float(getattr(tva_obj, "TA_Taux", 20.0)) else: data["tva_taux"] = 20.0 - except: + except Exception: data["tva_taux"] = 20.0 - except: + except Exception: data["tva_code"] = "" data["tva_taux"] = 20.0 try: date_creation = getattr(article_obj, "AR_DateCreate", None) data["date_creation"] = str(date_creation) if date_creation else "" - except: + except Exception: data["date_creation"] = "" try: date_modif = getattr(article_obj, "AR_DateModif", None) data["date_modification"] = str(date_modif) if date_modif else "" - except: + except Exception: data["date_modification"] = "" return data @@ -238,7 +235,7 @@ def _extraire_article(article_obj): "date_modification": "", } - + __all__ = [ "_extraire_article", -] \ No newline at end of file +] diff --git a/utils/articles/articles_data_sql.py b/utils/articles/articles_data_sql.py index f9aada4..12de60a 100644 --- a/utils/articles/articles_data_sql.py +++ b/utils/articles/articles_data_sql.py @@ -1,18 +1,19 @@ -from typing import Dict, List, Optional, Any +from typing import Dict, List import win32com.client import logging from utils.functions.functions import _safe_strip logger = logging.getLogger(__name__) + def _enrichir_stock_emplacements(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement stock emplacements...") - + logger.info(" → Enrichissement stock emplacements...") + references = [a["reference"] for a in articles if a["reference"]] if not references: return articles - + placeholders = ",".join(["?"] * len(references)) query = f""" SELECT @@ -28,36 +29,38 @@ def _enrichir_stock_emplacements(articles: List[Dict], cursor) -> List[Dict]: WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, DE_No, DP_No """ - + cursor.execute(query, references) rows = cursor.fetchall() - + emplacements_map = {} for row in rows: ref = _safe_strip(row[0]) if not ref: continue - + if ref not in emplacements_map: emplacements_map[ref] = [] - - emplacements_map[ref].append({ - "depot": _safe_strip(row[1]), - "emplacement": _safe_strip(row[2]), - "qte_stockee": float(row[3]) if row[3] else 0.0, - "qte_preparee": float(row[4]) if row[4] else 0.0, - "qte_a_controler": float(row[5]) if row[5] else 0.0, - "date_creation": row[6], - "date_modification": row[7], - }) - + + emplacements_map[ref].append( + { + "depot": _safe_strip(row[1]), + "emplacement": _safe_strip(row[2]), + "qte_stockee": float(row[3]) if row[3] else 0.0, + "qte_preparee": float(row[4]) if row[4] else 0.0, + "qte_a_controler": float(row[5]) if row[5] else 0.0, + "date_creation": row[6], + "date_modification": row[7], + } + ) + for article in articles: article["emplacements"] = emplacements_map.get(article["reference"], []) article["nb_emplacements"] = len(article["emplacements"]) - + logger.info(f" {len(emplacements_map)} articles avec emplacements") return articles - + except Exception as e: logger.error(f" Erreur stock emplacements: {e}") for article in articles: @@ -65,14 +68,15 @@ def _enrichir_stock_emplacements(articles: List[Dict], cursor) -> List[Dict]: article["nb_emplacements"] = 0 return articles + def _enrichir_gammes_articles(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement gammes articles...") - + logger.info(" → Enrichissement gammes articles...") + references = [a["reference"] for a in articles if a["reference"]] if not references: return articles - + placeholders = ",".join(["?"] * len(references)) query = f""" SELECT @@ -86,34 +90,36 @@ def _enrichir_gammes_articles(articles: List[Dict], cursor) -> List[Dict]: WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, AG_No, EG_Enumere """ - + cursor.execute(query, references) rows = cursor.fetchall() - + gammes_map = {} for row in rows: ref = _safe_strip(row[0]) if not ref: continue - + if ref not in gammes_map: gammes_map[ref] = [] - - gammes_map[ref].append({ - "numero_gamme": int(row[1]) if row[1] else 0, - "enumere": _safe_strip(row[2]), - "type_gamme": int(row[3]) if row[3] else 0, - "date_creation": row[4], - "date_modification": row[5], - }) - + + gammes_map[ref].append( + { + "numero_gamme": int(row[1]) if row[1] else 0, + "enumere": _safe_strip(row[2]), + "type_gamme": int(row[3]) if row[3] else 0, + "date_creation": row[4], + "date_modification": row[5], + } + ) + for article in articles: article["gammes"] = gammes_map.get(article["reference"], []) article["nb_gammes"] = len(article["gammes"]) - + logger.info(f" {len(gammes_map)} articles avec gammes") return articles - + except Exception as e: logger.error(f" Erreur gammes: {e}") for article in articles: @@ -121,14 +127,15 @@ def _enrichir_gammes_articles(articles: List[Dict], cursor) -> List[Dict]: article["nb_gammes"] = 0 return articles + def _enrichir_tarifs_clients(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement tarifs clients...") - + logger.info(" → Enrichissement tarifs clients...") + references = [a["reference"] for a in articles if a["reference"]] if not references: return articles - + placeholders = ",".join(["?"] * len(references)) query = f""" SELECT @@ -158,50 +165,52 @@ def _enrichir_tarifs_clients(articles: List[Dict], cursor) -> List[Dict]: WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, AC_Categorie, CT_Num """ - + cursor.execute(query, references) rows = cursor.fetchall() - + tarifs_map = {} for row in rows: ref = _safe_strip(row[0]) if not ref: continue - + if ref not in tarifs_map: tarifs_map[ref] = [] - - tarifs_map[ref].append({ - "categorie": int(row[1]) if row[1] else 0, - "client_num": _safe_strip(row[2]), - "prix_vente": float(row[3]) if row[3] else 0.0, - "coefficient": float(row[4]) if row[4] else 0.0, - "prix_ttc": float(row[5]) if row[5] else 0.0, - "arrondi": float(row[6]) if row[6] else 0.0, - "qte_montant": float(row[7]) if row[7] else 0.0, - "enumere_gamme": int(row[8]) if row[8] else 0, - "prix_devise": float(row[9]) if row[9] else 0.0, - "devise": int(row[10]) if row[10] else 0, - "remise": float(row[11]) if row[11] else 0.0, - "mode_calcul": int(row[12]) if row[12] else 0, - "type_remise": int(row[13]) if row[13] else 0, - "ref_client": _safe_strip(row[14]), - "coef_nouveau": float(row[15]) if row[15] else 0.0, - "prix_vente_nouveau": float(row[16]) if row[16] else 0.0, - "prix_devise_nouveau": float(row[17]) if row[17] else 0.0, - "remise_nouvelle": float(row[18]) if row[18] else 0.0, - "date_application": row[19], - "date_creation": row[20], - "date_modification": row[21], - }) - + + tarifs_map[ref].append( + { + "categorie": int(row[1]) if row[1] else 0, + "client_num": _safe_strip(row[2]), + "prix_vente": float(row[3]) if row[3] else 0.0, + "coefficient": float(row[4]) if row[4] else 0.0, + "prix_ttc": float(row[5]) if row[5] else 0.0, + "arrondi": float(row[6]) if row[6] else 0.0, + "qte_montant": float(row[7]) if row[7] else 0.0, + "enumere_gamme": int(row[8]) if row[8] else 0, + "prix_devise": float(row[9]) if row[9] else 0.0, + "devise": int(row[10]) if row[10] else 0, + "remise": float(row[11]) if row[11] else 0.0, + "mode_calcul": int(row[12]) if row[12] else 0, + "type_remise": int(row[13]) if row[13] else 0, + "ref_client": _safe_strip(row[14]), + "coef_nouveau": float(row[15]) if row[15] else 0.0, + "prix_vente_nouveau": float(row[16]) if row[16] else 0.0, + "prix_devise_nouveau": float(row[17]) if row[17] else 0.0, + "remise_nouvelle": float(row[18]) if row[18] else 0.0, + "date_application": row[19], + "date_creation": row[20], + "date_modification": row[21], + } + ) + for article in articles: article["tarifs_clients"] = tarifs_map.get(article["reference"], []) article["nb_tarifs_clients"] = len(article["tarifs_clients"]) - + logger.info(f" {len(tarifs_map)} articles avec tarifs clients") return articles - + except Exception as e: logger.error(f" Erreur tarifs clients: {e}") for article in articles: @@ -209,14 +218,15 @@ def _enrichir_tarifs_clients(articles: List[Dict], cursor) -> List[Dict]: article["nb_tarifs_clients"] = 0 return articles + def _enrichir_nomenclature(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement nomenclature...") - + logger.info(" → Enrichissement nomenclature...") + references = [a["reference"] for a in articles if a["reference"]] if not references: return articles - + placeholders = ",".join(["?"] * len(references)) query = f""" SELECT @@ -241,45 +251,47 @@ def _enrichir_nomenclature(articles: List[Dict], cursor) -> List[Dict]: WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, AT_Ordre, AT_Operation """ - + cursor.execute(query, references) rows = cursor.fetchall() - + composants_map = {} for row in rows: ref = _safe_strip(row[0]) if not ref: continue - + if ref not in composants_map: composants_map[ref] = [] - - composants_map[ref].append({ - "operation": _safe_strip(row[1]), - "code_ressource": _safe_strip(row[2]), - "temps": float(row[3]) if row[3] else 0.0, - "type": int(row[4]) if row[4] else 0, - "description": _safe_strip(row[5]), - "ordre": int(row[6]) if row[6] else 0, - "gamme_1_comp": int(row[7]) if row[7] else 0, - "gamme_2_comp": int(row[8]) if row[8] else 0, - "type_ressource": int(row[9]) if row[9] else 0, - "chevauche": int(row[10]) if row[10] else 0, - "demarre": int(row[11]) if row[11] else 0, - "operation_chevauche": _safe_strip(row[12]), - "valeur_chevauche": float(row[13]) if row[13] else 0.0, - "type_chevauche": int(row[14]) if row[14] else 0, - "date_creation": row[15], - "date_modification": row[16], - }) - + + composants_map[ref].append( + { + "operation": _safe_strip(row[1]), + "code_ressource": _safe_strip(row[2]), + "temps": float(row[3]) if row[3] else 0.0, + "type": int(row[4]) if row[4] else 0, + "description": _safe_strip(row[5]), + "ordre": int(row[6]) if row[6] else 0, + "gamme_1_comp": int(row[7]) if row[7] else 0, + "gamme_2_comp": int(row[8]) if row[8] else 0, + "type_ressource": int(row[9]) if row[9] else 0, + "chevauche": int(row[10]) if row[10] else 0, + "demarre": int(row[11]) if row[11] else 0, + "operation_chevauche": _safe_strip(row[12]), + "valeur_chevauche": float(row[13]) if row[13] else 0.0, + "type_chevauche": int(row[14]) if row[14] else 0, + "date_creation": row[15], + "date_modification": row[16], + } + ) + for article in articles: article["composants"] = composants_map.get(article["reference"], []) article["nb_composants"] = len(article["composants"]) - + logger.info(f" {len(composants_map)} articles avec nomenclature") return articles - + except Exception as e: logger.error(f" Erreur nomenclature: {e}") for article in articles: @@ -287,14 +299,15 @@ def _enrichir_nomenclature(articles: List[Dict], cursor) -> List[Dict]: article["nb_composants"] = 0 return articles + def _enrichir_compta_articles(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement comptabilité articles...") - + logger.info(" → Enrichissement comptabilité articles...") + references = [a["reference"] for a in articles if a["reference"]] if not references: return articles - + placeholders = ",".join(["?"] * len(references)) query = f""" SELECT @@ -319,22 +332,22 @@ def _enrichir_compta_articles(articles: List[Dict], cursor) -> List[Dict]: WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, ACP_Type, ACP_Champ """ - + cursor.execute(query, references) rows = cursor.fetchall() - + compta_map = {} for row in rows: ref = _safe_strip(row[0]) if not ref: continue - + if ref not in compta_map: compta_map[ref] = {"vente": [], "achat": [], "stock": []} - + type_compta = int(row[1]) if row[1] else 0 type_key = {0: "vente", 1: "achat", 2: "stock"}.get(type_compta, "autre") - + compta_entry = { "champ": int(row[2]) if row[2] else 0, "compte_general": _safe_strip(row[3]), @@ -352,19 +365,21 @@ def _enrichir_compta_articles(articles: List[Dict], cursor) -> List[Dict]: "date_creation": row[15], "date_modification": row[16], } - + if type_key in compta_map[ref]: compta_map[ref][type_key].append(compta_entry) - + for article in articles: - compta = compta_map.get(article["reference"], {"vente": [], "achat": [], "stock": []}) + compta = compta_map.get( + article["reference"], {"vente": [], "achat": [], "stock": []} + ) article["compta_vente"] = compta["vente"] article["compta_achat"] = compta["achat"] article["compta_stock"] = compta["stock"] - + logger.info(f" {len(compta_map)} articles avec compta spécifique") return articles - + except Exception as e: logger.error(f" Erreur comptabilité articles: {e}") for article in articles: @@ -373,14 +388,15 @@ def _enrichir_compta_articles(articles: List[Dict], cursor) -> List[Dict]: article["compta_stock"] = [] return articles + def _enrichir_fournisseurs_multiples(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement fournisseurs multiples...") - + logger.info(" → Enrichissement fournisseurs multiples...") + references = [a["reference"] for a in articles if a["reference"]] if not references: return articles - + placeholders = ",".join(["?"] * len(references)) query = f""" SELECT @@ -413,53 +429,55 @@ def _enrichir_fournisseurs_multiples(articles: List[Dict], cursor) -> List[Dict] WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, AF_Principal DESC, CT_Num """ - + cursor.execute(query, references) rows = cursor.fetchall() - + fournisseurs_map = {} for row in rows: ref = _safe_strip(row[0]) if not ref: continue - + if ref not in fournisseurs_map: fournisseurs_map[ref] = [] - - fournisseurs_map[ref].append({ - "fournisseur_num": _safe_strip(row[1]), - "ref_fournisseur": _safe_strip(row[2]), - "prix_achat": float(row[3]) if row[3] else 0.0, - "unite": _safe_strip(row[4]), - "conversion": float(row[5]) if row[5] else 0.0, - "delai_appro": int(row[6]) if row[6] else 0, - "garantie": int(row[7]) if row[7] else 0, - "colisage": int(row[8]) if row[8] else 0, - "qte_mini": float(row[9]) if row[9] else 0.0, - "qte_montant": float(row[10]) if row[10] else 0.0, - "enumere_gamme": int(row[11]) if row[11] else 0, - "est_principal": bool(row[12]), - "prix_devise": float(row[13]) if row[13] else 0.0, - "devise": int(row[14]) if row[14] else 0, - "remise": float(row[15]) if row[15] else 0.0, - "conversion_devise": float(row[16]) if row[16] else 0.0, - "type_remise": int(row[17]) if row[17] else 0, - "code_barre_fournisseur": _safe_strip(row[18]), - "prix_achat_nouveau": float(row[19]) if row[19] else 0.0, - "prix_devise_nouveau": float(row[20]) if row[20] else 0.0, - "remise_nouvelle": float(row[21]) if row[21] else 0.0, - "date_application": row[22], - "date_creation": row[23], - "date_modification": row[24], - }) - + + fournisseurs_map[ref].append( + { + "fournisseur_num": _safe_strip(row[1]), + "ref_fournisseur": _safe_strip(row[2]), + "prix_achat": float(row[3]) if row[3] else 0.0, + "unite": _safe_strip(row[4]), + "conversion": float(row[5]) if row[5] else 0.0, + "delai_appro": int(row[6]) if row[6] else 0, + "garantie": int(row[7]) if row[7] else 0, + "colisage": int(row[8]) if row[8] else 0, + "qte_mini": float(row[9]) if row[9] else 0.0, + "qte_montant": float(row[10]) if row[10] else 0.0, + "enumere_gamme": int(row[11]) if row[11] else 0, + "est_principal": bool(row[12]), + "prix_devise": float(row[13]) if row[13] else 0.0, + "devise": int(row[14]) if row[14] else 0, + "remise": float(row[15]) if row[15] else 0.0, + "conversion_devise": float(row[16]) if row[16] else 0.0, + "type_remise": int(row[17]) if row[17] else 0, + "code_barre_fournisseur": _safe_strip(row[18]), + "prix_achat_nouveau": float(row[19]) if row[19] else 0.0, + "prix_devise_nouveau": float(row[20]) if row[20] else 0.0, + "remise_nouvelle": float(row[21]) if row[21] else 0.0, + "date_application": row[22], + "date_creation": row[23], + "date_modification": row[24], + } + ) + for article in articles: article["fournisseurs"] = fournisseurs_map.get(article["reference"], []) article["nb_fournisseurs"] = len(article["fournisseurs"]) - + logger.info(f" {len(fournisseurs_map)} articles avec fournisseurs multiples") return articles - + except Exception as e: logger.error(f" Erreur fournisseurs multiples: {e}") for article in articles: @@ -467,10 +485,11 @@ def _enrichir_fournisseurs_multiples(articles: List[Dict], cursor) -> List[Dict] article["nb_fournisseurs"] = 0 return articles + def _enrichir_depots_details(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement détails dépôts...") - + logger.info(" → Enrichissement détails dépôts...") + query = """ SELECT DE_No, @@ -492,16 +511,16 @@ def _enrichir_depots_details(articles: List[Dict], cursor) -> List[Dict]: DE_Exclure FROM F_DEPOT """ - + cursor.execute(query) rows = cursor.fetchall() - + depots_map = {} for row in rows: de_no = _safe_strip(row[0]) if not de_no: continue - + depots_map[de_no] = { "depot_num": de_no, "depot_nom": _safe_strip(row[1]), @@ -521,26 +540,27 @@ def _enrichir_depots_details(articles: List[Dict], cursor) -> List[Dict]: "depot_emplacement_defaut": _safe_strip(row[15]), "depot_exclu": bool(row[16]), } - + logger.info(f" → {len(depots_map)} dépôts chargés") - + for article in articles: for empl in article.get("emplacements", []): depot_num = empl.get("depot") if depot_num and depot_num in depots_map: empl.update(depots_map[depot_num]) - - logger.info(f" Emplacements enrichis avec détails dépôts") + + logger.info(" Emplacements enrichis avec détails dépôts") return articles - + except Exception as e: logger.error(f" Erreur détails dépôts: {e}") return articles + def _enrichir_emplacements_details(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement détails emplacements...") - + logger.info(" → Enrichissement détails emplacements...") + query = """ SELECT DE_No, @@ -551,18 +571,18 @@ def _enrichir_emplacements_details(articles: List[Dict], cursor) -> List[Dict]: DP_Type FROM F_DEPOTEMPL """ - + cursor.execute(query) rows = cursor.fetchall() - + emplacements_map = {} for row in rows: de_no = _safe_strip(row[0]) dp_no = _safe_strip(row[1]) - + if not de_no or not dp_no: continue - + key = f"{de_no}_{dp_no}" emplacements_map[key] = { "emplacement_code": _safe_strip(row[2]), @@ -570,9 +590,9 @@ def _enrichir_emplacements_details(articles: List[Dict], cursor) -> List[Dict]: "emplacement_zone": _safe_strip(row[4]), "emplacement_type": int(row[5]) if row[5] else 0, } - + logger.info(f" → {len(emplacements_map)} emplacements détaillés chargés") - + for article in articles: for empl in article.get("emplacements", []): depot = empl.get("depot") @@ -581,31 +601,32 @@ def _enrichir_emplacements_details(articles: List[Dict], cursor) -> List[Dict]: key = f"{depot}_{emplacement}" if key in emplacements_map: empl.update(emplacements_map[key]) - - logger.info(f" Emplacements enrichis avec détails") + + logger.info(" Emplacements enrichis avec détails") return articles - + except Exception as e: logger.error(f" Erreur détails emplacements: {e}") return articles + def _enrichir_gammes_enumeres(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement énumérés gammes...") - + logger.info(" → Enrichissement énumérés gammes...") + query_pgamme = "SELECT G_Intitule, G_Type FROM P_GAMME ORDER BY G_Type" cursor.execute(query_pgamme) pgamme_rows = cursor.fetchall() - + gammes_config = {} for idx, row in enumerate(pgamme_rows): gammes_config[idx + 1] = { "nom": _safe_strip(row[0]), "type": int(row[1]) if row[1] else 0, } - + logger.info(f" → Configuration gammes: {gammes_config}") - + query_enum = """ SELECT EG_Champ, @@ -615,18 +636,18 @@ def _enrichir_gammes_enumeres(articles: List[Dict], cursor) -> List[Dict]: FROM F_ENUMGAMME ORDER BY EG_Champ, EG_Ligne """ - + cursor.execute(query_enum) enum_rows = cursor.fetchall() - + enumeres_map = {} for row in enum_rows: champ = int(row[0]) if row[0] else 0 enumere = _safe_strip(row[2]) - + if not enumere: continue - + key = f"{champ}_{enumere}" enumeres_map[key] = { "ligne": int(row[1]) if row[1] else 0, @@ -634,34 +655,35 @@ def _enrichir_gammes_enumeres(articles: List[Dict], cursor) -> List[Dict]: "borne_sup": float(row[3]) if row[3] else 0.0, "gamme_nom": gammes_config.get(champ, {}).get("nom", f"Gamme {champ}"), } - + logger.info(f" → {len(enumeres_map)} énumérés chargés") - + for article in articles: for gamme in article.get("gammes", []): num_gamme = gamme.get("numero_gamme") enumere = gamme.get("enumere") - + if num_gamme and enumere: key = f"{num_gamme}_{enumere}" if key in enumeres_map: gamme.update(enumeres_map[key]) - - logger.info(f" Gammes enrichies avec énumérés") + + logger.info(" Gammes enrichies avec énumérés") return articles - + except Exception as e: logger.error(f" Erreur énumérés gammes: {e}") return articles + def _enrichir_references_enumerees(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement références énumérées...") - + logger.info(" → Enrichissement références énumérées...") + references = [a["reference"] for a in articles if a["reference"]] if not references: return articles - + placeholders = ",".join(["?"] * len(references)) query = f""" SELECT @@ -680,39 +702,41 @@ def _enrichir_references_enumerees(articles: List[Dict], cursor) -> List[Dict]: WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, AG_No1, AG_No2 """ - + cursor.execute(query, references) rows = cursor.fetchall() - + refs_enum_map = {} for row in rows: ref = _safe_strip(row[0]) if not ref: continue - + if ref not in refs_enum_map: refs_enum_map[ref] = [] - - refs_enum_map[ref].append({ - "gamme_1": int(row[1]) if row[1] else 0, - "gamme_2": int(row[2]) if row[2] else 0, - "reference_enumeree": _safe_strip(row[3]), - "prix_achat": float(row[4]) if row[4] else 0.0, - "code_barre": _safe_strip(row[5]), - "prix_achat_nouveau": float(row[6]) if row[6] else 0.0, - "edi_code": _safe_strip(row[7]), - "en_sommeil": bool(row[8]), - "date_creation": row[9], - "date_modification": row[10], - }) - + + refs_enum_map[ref].append( + { + "gamme_1": int(row[1]) if row[1] else 0, + "gamme_2": int(row[2]) if row[2] else 0, + "reference_enumeree": _safe_strip(row[3]), + "prix_achat": float(row[4]) if row[4] else 0.0, + "code_barre": _safe_strip(row[5]), + "prix_achat_nouveau": float(row[6]) if row[6] else 0.0, + "edi_code": _safe_strip(row[7]), + "en_sommeil": bool(row[8]), + "date_creation": row[9], + "date_modification": row[10], + } + ) + for article in articles: article["refs_enumerees"] = refs_enum_map.get(article["reference"], []) article["nb_refs_enumerees"] = len(article["refs_enumerees"]) - + logger.info(f" {len(refs_enum_map)} articles avec références énumérées") return articles - + except Exception as e: logger.error(f" Erreur références énumérées: {e}") for article in articles: @@ -720,14 +744,15 @@ def _enrichir_references_enumerees(articles: List[Dict], cursor) -> List[Dict]: article["nb_refs_enumerees"] = 0 return articles + def _enrichir_medias_articles(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement médias articles...") - + logger.info(" → Enrichissement médias articles...") + references = [a["reference"] for a in articles if a["reference"]] if not references: return articles - + placeholders = ",".join(["?"] * len(references)) query = f""" SELECT @@ -743,36 +768,38 @@ def _enrichir_medias_articles(articles: List[Dict], cursor) -> List[Dict]: WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, cbCreation """ - + cursor.execute(query, references) rows = cursor.fetchall() - + medias_map = {} for row in rows: ref = _safe_strip(row[0]) if not ref: continue - + if ref not in medias_map: medias_map[ref] = [] - - medias_map[ref].append({ - "commentaire": _safe_strip(row[1]), - "fichier": _safe_strip(row[2]), - "type_mime": _safe_strip(row[3]), - "origine": int(row[4]) if row[4] else 0, - "ged_id": _safe_strip(row[5]), - "date_creation": row[6], - "date_modification": row[7], - }) - + + medias_map[ref].append( + { + "commentaire": _safe_strip(row[1]), + "fichier": _safe_strip(row[2]), + "type_mime": _safe_strip(row[3]), + "origine": int(row[4]) if row[4] else 0, + "ged_id": _safe_strip(row[5]), + "date_creation": row[6], + "date_modification": row[7], + } + ) + for article in articles: article["medias"] = medias_map.get(article["reference"], []) article["nb_medias"] = len(article["medias"]) - + logger.info(f" {len(medias_map)} articles avec médias") return articles - + except Exception as e: logger.error(f" Erreur médias: {e}") for article in articles: @@ -780,14 +807,15 @@ def _enrichir_medias_articles(articles: List[Dict], cursor) -> List[Dict]: article["nb_medias"] = 0 return articles + def _enrichir_prix_gammes(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement prix par gammes...") - + logger.info(" → Enrichissement prix par gammes...") + references = [a["reference"] for a in articles if a["reference"]] if not references: return articles - + placeholders = ",".join(["?"] * len(references)) query = f""" SELECT @@ -802,35 +830,37 @@ def _enrichir_prix_gammes(articles: List[Dict], cursor) -> List[Dict]: WHERE AR_Ref IN ({placeholders}) ORDER BY AR_Ref, AG_No1, AG_No2 """ - + cursor.execute(query, references) rows = cursor.fetchall() - + prix_gammes_map = {} for row in rows: ref = _safe_strip(row[0]) if not ref: continue - + if ref not in prix_gammes_map: prix_gammes_map[ref] = [] - - prix_gammes_map[ref].append({ - "gamme_1": int(row[1]) if row[1] else 0, - "gamme_2": int(row[2]) if row[2] else 0, - "prix_net": float(row[3]) if row[3] else 0.0, - "cout_standard": float(row[4]) if row[4] else 0.0, - "date_creation": row[5], - "date_modification": row[6], - }) - + + prix_gammes_map[ref].append( + { + "gamme_1": int(row[1]) if row[1] else 0, + "gamme_2": int(row[2]) if row[2] else 0, + "prix_net": float(row[3]) if row[3] else 0.0, + "cout_standard": float(row[4]) if row[4] else 0.0, + "date_creation": row[5], + "date_modification": row[6], + } + ) + for article in articles: article["prix_gammes"] = prix_gammes_map.get(article["reference"], []) article["nb_prix_gammes"] = len(article["prix_gammes"]) - + logger.info(f" {len(prix_gammes_map)} articles avec prix par gammes") return articles - + except Exception as e: logger.error(f" Erreur prix gammes: {e}") for article in articles: @@ -838,10 +868,11 @@ def _enrichir_prix_gammes(articles: List[Dict], cursor) -> List[Dict]: article["nb_prix_gammes"] = 0 return articles + def _enrichir_conditionnements(articles: List[Dict], cursor) -> List[Dict]: try: - logger.info(f" → Enrichissement conditionnements...") - + logger.info(" → Enrichissement conditionnements...") + query = """ SELECT EC_Champ, @@ -851,18 +882,18 @@ def _enrichir_conditionnements(articles: List[Dict], cursor) -> List[Dict]: FROM F_ENUMCOND ORDER BY EC_Champ, EC_Enumere """ - + cursor.execute(query) rows = cursor.fetchall() - + cond_map = {} for row in rows: champ = int(row[0]) if row[0] else 0 enumere = _safe_strip(row[1]) - + if not enumere: continue - + key = f"{champ}_{enumere}" cond_map[key] = { "champ": champ, @@ -870,9 +901,9 @@ def _enrichir_conditionnements(articles: List[Dict], cursor) -> List[Dict]: "quantite": float(row[2]) if row[2] else 0.0, "edi_code": _safe_strip(row[3]), } - + logger.info(f" → {len(cond_map)} conditionnements chargés") - + for article in articles: conditionnement = article.get("conditionnement") if conditionnement: @@ -881,40 +912,41 @@ def _enrichir_conditionnements(articles: List[Dict], cursor) -> List[Dict]: article["conditionnement_qte"] = cond_data["quantite"] article["conditionnement_edi"] = cond_data["edi_code"] break - - logger.info(f" Conditionnements enrichis") + + logger.info(" Conditionnements enrichis") return articles - + except Exception as e: logger.error(f" Erreur conditionnements: {e}") return articles - + + def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article = {} - + def get_val(sql_col, default=None, convert_type=None): val = row_data.get(sql_col, default) if val is None: return default - - if convert_type == float: + + if isinstance(convert_type, float): return float(val) if val not in (None, "") else (default or 0.0) - elif convert_type == int: + elif isinstance(convert_type, int): return int(val) if val not in (None, "") else (default or 0) - elif convert_type == bool: + elif isinstance(convert_type, bool): return bool(val) if val not in (None, "") else (default or False) - elif convert_type == str: + elif isinstance(convert_type, str): return _safe_strip(val) - + return val - + article["reference"] = get_val("AR_Ref", convert_type=str) article["designation"] = get_val("AR_Design", convert_type=str) article["code_ean"] = get_val("AR_CodeBarre", convert_type=str) article["code_barre"] = get_val("AR_CodeBarre", convert_type=str) article["edi_code"] = get_val("AR_EdiCode", convert_type=str) article["raccourci"] = get_val("AR_Raccourci", convert_type=str) - + article["prix_vente"] = get_val("AR_PrixVen", 0.0, float) article["prix_achat"] = get_val("AR_PrixAch", 0.0, float) article["coef"] = get_val("AR_Coef", 0.0, float) @@ -922,20 +954,20 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["prix_achat_nouveau"] = get_val("AR_PrixAchNouv", 0.0, float) article["coef_nouveau"] = get_val("AR_CoefNouv", 0.0, float) article["prix_vente_nouveau"] = get_val("AR_PrixVenNouv", 0.0, float) - + date_app = get_val("AR_DateApplication") article["date_application_prix"] = str(date_app) if date_app else None - + article["cout_standard"] = get_val("AR_CoutStd", 0.0, float) - + article["unite_vente"] = get_val("AR_UniteVen", convert_type=str) article["unite_poids"] = get_val("AR_UnitePoids", convert_type=str) article["poids_net"] = get_val("AR_PoidsNet", 0.0, float) article["poids_brut"] = get_val("AR_PoidsBrut", 0.0, float) - + article["gamme_1"] = get_val("AR_Gamme1", convert_type=str) article["gamme_2"] = get_val("AR_Gamme2", convert_type=str) - + type_val = get_val("AR_Type", 0, int) article["type_article"] = type_val article["type_article_libelle"] = _get_type_article_libelle(type_val) @@ -944,39 +976,39 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["garantie"] = get_val("AR_Garantie", 0, int) article["code_fiscal"] = get_val("AR_CodeFiscal", convert_type=str) article["pays"] = get_val("AR_Pays", convert_type=str) - + article["fournisseur_principal"] = get_val("CO_No", 0, int) article["conditionnement"] = get_val("AR_Condition", convert_type=str) article["nb_colis"] = get_val("AR_NbColis", 0, int) article["prevision"] = get_val("AR_Prevision", False, bool) - + article["suivi_stock"] = get_val("AR_SuiviStock", False, bool) article["nomenclature"] = get_val("AR_Nomencl", False, bool) article["qte_composant"] = get_val("AR_QteComp", 0.0, float) article["qte_operatoire"] = get_val("AR_QteOperatoire", 0.0, float) - + sommeil = get_val("AR_Sommeil", 0, int) - article["est_actif"] = (sommeil == 0) - article["en_sommeil"] = (sommeil == 1) + article["est_actif"] = sommeil == 0 + article["en_sommeil"] = sommeil == 1 article["article_substitut"] = get_val("AR_Substitut", convert_type=str) article["soumis_escompte"] = get_val("AR_Escompte", False, bool) article["delai"] = get_val("AR_Delai", 0, int) - + article["stat_01"] = get_val("AR_Stat01", convert_type=str) article["stat_02"] = get_val("AR_Stat02", convert_type=str) article["stat_03"] = get_val("AR_Stat03", convert_type=str) article["stat_04"] = get_val("AR_Stat04", convert_type=str) article["stat_05"] = get_val("AR_Stat05", convert_type=str) article["hors_statistique"] = get_val("AR_HorsStat", False, bool) - + article["categorie_1"] = get_val("CL_No1", 0, int) article["categorie_2"] = get_val("CL_No2", 0, int) article["categorie_3"] = get_val("CL_No3", 0, int) article["categorie_4"] = get_val("CL_No4", 0, int) - + date_modif = get_val("AR_DateModif") article["date_modification"] = str(date_modif) if date_modif else None - + article["vente_debit"] = get_val("AR_VteDebit", False, bool) article["non_imprimable"] = get_val("AR_NotImp", False, bool) article["transfere"] = get_val("AR_Transfere", False, bool) @@ -988,49 +1020,59 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["fictif"] = get_val("AR_Fictif", False, bool) article["sous_traitance"] = get_val("AR_SousTraitance", False, bool) article["criticite"] = get_val("AR_Criticite", 0, int) - + article["reprise_code_defaut"] = get_val("RP_CodeDefaut", convert_type=str) article["delai_fabrication"] = get_val("AR_DelaiFabrication", 0, int) article["delai_peremption"] = get_val("AR_DelaiPeremption", 0, int) article["delai_securite"] = get_val("AR_DelaiSecurite", 0, int) article["type_lancement"] = get_val("AR_TypeLancement", 0, int) article["cycle"] = get_val("AR_Cycle", 1, int) - + article["photo"] = get_val("AR_Photo", convert_type=str) article["langue_1"] = get_val("AR_Langue1", convert_type=str) article["langue_2"] = get_val("AR_Langue2", convert_type=str) - - article["frais_01_denomination"] = get_val("AR_Frais01FR_Denomination", convert_type=str) - article["frais_02_denomination"] = get_val("AR_Frais02FR_Denomination", convert_type=str) - article["frais_03_denomination"] = get_val("AR_Frais03FR_Denomination", convert_type=str) - + + article["frais_01_denomination"] = get_val( + "AR_Frais01FR_Denomination", convert_type=str + ) + article["frais_02_denomination"] = get_val( + "AR_Frais02FR_Denomination", convert_type=str + ) + article["frais_03_denomination"] = get_val( + "AR_Frais03FR_Denomination", convert_type=str + ) + article["marque_commerciale"] = get_val("Marque commerciale", convert_type=str) - + objectif_val = get_val("Objectif / Qtés vendues") if objectif_val is not None: - article["objectif_qtes_vendues"] = str(float(objectif_val)) if objectif_val not in ("", 0, 0.0) else None + article["objectif_qtes_vendues"] = ( + str(float(objectif_val)) if objectif_val not in ("", 0, 0.0) else None + ) else: article["objectif_qtes_vendues"] = None - + pourcentage_val = get_val("Pourcentage teneur en or") if pourcentage_val is not None: - article["pourcentage_or"] = str(float(pourcentage_val)) if pourcentage_val not in ("", 0, 0.0) else None + article["pourcentage_or"] = ( + str(float(pourcentage_val)) if pourcentage_val not in ("", 0, 0.0) else None + ) else: article["pourcentage_or"] = None - + date_com = get_val("1ère commercialisation") article["premiere_commercialisation"] = str(date_com) if date_com else None - + article["interdire_commande"] = get_val("AR_InterdireCommande", False, bool) article["exclure"] = get_val("AR_Exclure", False, bool) - + article["stock_reel"] = 0.0 article["stock_mini"] = 0.0 article["stock_maxi"] = 0.0 article["stock_reserve"] = 0.0 article["stock_commande"] = 0.0 article["stock_disponible"] = 0.0 - + article["famille_libelle"] = None article["famille_type"] = None article["famille_unite_vente"] = None @@ -1046,23 +1088,24 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict: article["famille_nature"] = None article["famille_hors_stat"] = None article["famille_pays"] = None - + article["fournisseur_nom"] = None article["tva_code"] = None article["tva_taux"] = None - + return article + def _enrichir_stocks_articles(articles: List[Dict], cursor) -> List[Dict]: """Enrichit les articles avec les données de stock depuis F_ARTSTOCK""" try: logger.info(f" → Enrichissement stocks pour {len(articles)} articles...") - + references = [a["reference"] for a in articles if a["reference"]] - + if not references: return articles - + placeholders = ",".join(["?"] * len(references)) stock_query = f""" SELECT @@ -1076,10 +1119,10 @@ def _enrichir_stocks_articles(articles: List[Dict], cursor) -> List[Dict]: WHERE AR_Ref IN ({placeholders}) GROUP BY AR_Ref """ - + cursor.execute(stock_query, references) stock_rows = cursor.fetchall() - + stock_map = {} for stock_row in stock_rows: ref = _safe_strip(stock_row[0]) @@ -1091,9 +1134,9 @@ def _enrichir_stocks_articles(articles: List[Dict], cursor) -> List[Dict]: "stock_reserve": float(stock_row[4]) if stock_row[4] else 0.0, "stock_commande": float(stock_row[5]) if stock_row[5] else 0.0, } - + logger.info(f" → {len(stock_map)} articles avec stock trouvés dans F_ARTSTOCK") - + for article in articles: if article["reference"] in stock_map: stock_data = stock_map[article["reference"]] @@ -1101,32 +1144,38 @@ def _enrichir_stocks_articles(articles: List[Dict], cursor) -> List[Dict]: article["stock_disponible"] = ( article["stock_reel"] - article["stock_reserve"] ) - + return articles - + except Exception as e: logger.error(f" Erreur enrichissement stocks: {e}", exc_info=True) return articles + def _enrichir_fournisseurs_articles(articles: List[Dict], cursor) -> List[Dict]: """Enrichit les articles avec le nom du fournisseur principal""" try: - logger.info(f" → Enrichissement fournisseurs...") - - nums_fournisseurs = list(set([ - a["fournisseur_principal"] for a in articles - if a.get("fournisseur_principal") and a["fournisseur_principal"] > 0 - ])) - + logger.info(" → Enrichissement fournisseurs...") + + nums_fournisseurs = list( + set( + [ + a["fournisseur_principal"] + for a in articles + if a.get("fournisseur_principal") and a["fournisseur_principal"] > 0 + ] + ) + ) + if not nums_fournisseurs: logger.warning(" ⚠ Aucun numéro de fournisseur trouvé dans les articles") for article in articles: article["fournisseur_nom"] = None return articles - + logger.info(f" → {len(nums_fournisseurs)} fournisseurs uniques à chercher") logger.info(f" → Exemples CO_No : {nums_fournisseurs[:5]}") - + placeholders = ",".join(["?"] * len(nums_fournisseurs)) fournisseur_query = f""" SELECT @@ -1137,27 +1186,34 @@ def _enrichir_fournisseurs_articles(articles: List[Dict], cursor) -> List[Dict]: WHERE CT_Num IN ({placeholders}) AND CT_Type = 1 """ - + cursor.execute(fournisseur_query, nums_fournisseurs) fournisseur_rows = cursor.fetchall() - + logger.info(f" → {len(fournisseur_rows)} fournisseurs trouvés dans F_COMPTET") - + if len(fournisseur_rows) == 0: - logger.warning(f" ⚠ Aucun fournisseur trouvé pour CT_Type=1 et CT_Num IN {nums_fournisseurs[:5]}") - cursor.execute(f"SELECT CT_Num, CT_Type FROM F_COMPTET WHERE CT_Num IN ({placeholders})", nums_fournisseurs) + logger.warning( + f" ⚠ Aucun fournisseur trouvé pour CT_Type=1 et CT_Num IN {nums_fournisseurs[:5]}" + ) + cursor.execute( + f"SELECT CT_Num, CT_Type FROM F_COMPTET WHERE CT_Num IN ({placeholders})", + nums_fournisseurs, + ) tous_types = cursor.fetchall() if tous_types: - logger.info(f" → Trouvé {len(tous_types)} comptes (tous types) : {[(r[0], r[1]) for r in tous_types[:5]]}") - + logger.info( + f" → Trouvé {len(tous_types)} comptes (tous types) : {[(r[0], r[1]) for r in tous_types[:5]]}" + ) + fournisseur_map = {} for fourn_row in fournisseur_rows: - num = int(fourn_row[0]) # CT_Num - nom = _safe_strip(fourn_row[1]) # CT_Intitule - type_ct = int(fourn_row[2]) # CT_Type + num = int(fourn_row[0]) + nom = _safe_strip(fourn_row[1]) + type_ct = int(fourn_row[2]) fournisseur_map[num] = nom logger.debug(f" → Fournisseur mappé : {num} = {nom} (Type={type_ct})") - + nb_enrichis = 0 for article in articles: num_fourn = article.get("fournisseur_principal") @@ -1166,45 +1222,47 @@ def _enrichir_fournisseurs_articles(articles: List[Dict], cursor) -> List[Dict]: nb_enrichis += 1 else: article["fournisseur_nom"] = None - + logger.info(f" {nb_enrichis} articles enrichis avec nom fournisseur") - + return articles - + except Exception as e: logger.error(f" Erreur enrichissement fournisseurs: {e}", exc_info=True) for article in articles: article["fournisseur_nom"] = None return articles + def _enrichir_familles_articles(articles: List[Dict], cursor) -> List[Dict]: """Enrichit les articles avec les informations de famille depuis F_FAMILLE""" try: logger.info(f" → Enrichissement familles pour {len(articles)} articles...") - + codes_familles_bruts = [ - a.get("famille_code") for a in articles + a.get("famille_code") + for a in articles if a.get("famille_code") not in (None, "", " ") ] - + if codes_familles_bruts: logger.info(f" → Exemples de codes familles : {codes_familles_bruts[:5]}") - - codes_familles = list(set([ - str(code).strip() for code in codes_familles_bruts if code - ])) - + + codes_familles = list( + set([str(code).strip() for code in codes_familles_bruts if code]) + ) + if not codes_familles: logger.warning(" ⚠ Aucun code famille trouvé dans les articles") for article in articles: _init_champs_famille_vides(article) return articles - + logger.info(f" → {len(codes_familles)} codes famille uniques") - + cursor.execute("SELECT TOP 1 * FROM F_FAMILLE") colonnes_disponibles = [column[0] for column in cursor.description] - + colonnes_souhaitees = [ "FA_CodeFamille", "FA_Intitule", @@ -1235,39 +1293,44 @@ def _enrichir_familles_articles(articles: List[Dict], cursor) -> List[Dict]: "FA_Fictif", "FA_Criticite", ] - - colonnes_a_lire = [col for col in colonnes_souhaitees if col in colonnes_disponibles] - - if "FA_CodeFamille" not in colonnes_a_lire or "FA_Intitule" not in colonnes_a_lire: + + colonnes_a_lire = [ + col for col in colonnes_souhaitees if col in colonnes_disponibles + ] + + if ( + "FA_CodeFamille" not in colonnes_a_lire + or "FA_Intitule" not in colonnes_a_lire + ): logger.error(" Colonnes essentielles manquantes !") return articles - + logger.info(f" → Colonnes disponibles : {len(colonnes_a_lire)}") - + colonnes_str = ", ".join(colonnes_a_lire) placeholders = ",".join(["?"] * len(codes_familles)) - + famille_query = f""" SELECT {colonnes_str} FROM F_FAMILLE WHERE FA_CodeFamille IN ({placeholders}) """ - + cursor.execute(famille_query, codes_familles) famille_rows = cursor.fetchall() - + logger.info(f" → {len(famille_rows)} familles trouvées") - + famille_map = {} for fam_row in famille_rows: famille_data = {} for idx, col in enumerate(colonnes_a_lire): famille_data[col] = fam_row[idx] - + code = _safe_strip(famille_data.get("FA_CodeFamille")) if not code: continue - + famille_map[code] = { "famille_libelle": _safe_strip(famille_data.get("FA_Intitule")), "famille_type": int(famille_data.get("FA_Type", 0) or 0), @@ -1285,29 +1348,30 @@ def _enrichir_familles_articles(articles: List[Dict], cursor) -> List[Dict]: "famille_hors_stat": bool(famille_data.get("FA_HorsStat", 0)), "famille_pays": _safe_strip(famille_data.get("FA_Pays")), } - + logger.info(f" → {len(famille_map)} familles mappées") - + nb_enrichis = 0 for article in articles: code_fam = str(article.get("famille_code", "")).strip() - + if code_fam and code_fam in famille_map: article.update(famille_map[code_fam]) nb_enrichis += 1 else: _init_champs_famille_vides(article) - + logger.info(f" {nb_enrichis} articles enrichis avec infos famille") - + return articles - + except Exception as e: logger.error(f" Erreur enrichissement familles: {e}", exc_info=True) for article in articles: _init_champs_famille_vides(article) return articles + def _init_champs_famille_vides(article: Dict): """Initialise les champs famille à None/0""" article["famille_libelle"] = None @@ -1326,22 +1390,22 @@ def _init_champs_famille_vides(article: Dict): article["famille_hors_stat"] = None article["famille_pays"] = None + def _enrichir_tva_articles(articles: List[Dict], cursor) -> List[Dict]: """Enrichit les articles avec le taux de TVA""" try: - logger.info(f" → Enrichissement TVA...") - - codes_tva = list(set([ - a["code_fiscal"] for a in articles - if a.get("code_fiscal") - ])) - + logger.info(" → Enrichissement TVA...") + + codes_tva = list( + set([a["code_fiscal"] for a in articles if a.get("code_fiscal")]) + ) + if not codes_tva: for article in articles: article["tva_code"] = None article["tva_taux"] = None return articles - + placeholders = ",".join(["?"] * len(codes_tva)) tva_query = f""" SELECT @@ -1350,17 +1414,17 @@ def _enrichir_tva_articles(articles: List[Dict], cursor) -> List[Dict]: FROM F_TAXE WHERE TA_Code IN ({placeholders}) """ - + cursor.execute(tva_query, codes_tva) tva_rows = cursor.fetchall() - + tva_map = {} for tva_row in tva_rows: code = _safe_strip(tva_row[0]) tva_map[code] = float(tva_row[1]) if tva_row[1] else 0.0 - + logger.info(f" → {len(tva_map)} codes TVA trouvés") - + for article in articles: code_tva = article.get("code_fiscal") if code_tva and code_tva in tva_map: @@ -1369,9 +1433,9 @@ def _enrichir_tva_articles(articles: List[Dict], cursor) -> List[Dict]: else: article["tva_code"] = code_tva article["tva_taux"] = None - + return articles - + except Exception as e: logger.error(f" Erreur enrichissement TVA: {e}", exc_info=True) for article in articles: @@ -1379,24 +1443,22 @@ def _enrichir_tva_articles(articles: List[Dict], cursor) -> List[Dict]: article["tva_taux"] = None return articles + def _get_type_article_libelle(type_val: int) -> str: """Retourne le libellé du type d'article""" - types = { - 0: "Article", - 1: "Prestation", - 2: "Divers / Frais", - 3: "Nomenclature" - } + types = {0: "Article", 1: "Prestation", 2: "Divers / Frais", 3: "Nomenclature"} return types.get(type_val, f"Type {type_val}") + def _cast_article(persist_obj): try: obj = win32com.client.CastTo(persist_obj, "IBOArticle3") obj.Read() return obj - except: + except Exception: return None - + + __all__ = [ "_enrichir_stock_emplacements", "_enrichir_gammes_articles", @@ -1417,5 +1479,5 @@ __all__ = [ "_enrichir_familles_articles", "_init_champs_famille_vides", "_enrichir_tva_articles", - "_cast_article" -] \ No newline at end of file + "_cast_article", +] diff --git a/utils/articles/stock_check.py b/utils/articles/stock_check.py index 2cb31e3..bc831fe 100644 --- a/utils/articles/stock_check.py +++ b/utils/articles/stock_check.py @@ -2,39 +2,39 @@ import logging logger = logging.getLogger(__name__) + def verifier_stock_suffisant(article_ref, quantite, cursor, depot=None): """Version thread-safe avec lock SQL""" try: + cursor.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE") + cursor.execute("BEGIN TRANSACTION") - cursor.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE") - cursor.execute("BEGIN TRANSACTION") - - try: - cursor.execute( - """ + try: + cursor.execute( + """ SELECT SUM(AS_QteSto) FROM F_ARTSTOCK WITH (UPDLOCK, ROWLOCK) WHERE AR_Ref = ? """, - (article_ref.upper(),), - ) + (article_ref.upper(),), + ) - row = cursor.fetchone() - stock_dispo = float(row[0]) if row and row[0] else 0.0 + row = cursor.fetchone() + stock_dispo = float(row[0]) if row and row[0] else 0.0 - suffisant = stock_dispo >= quantite + suffisant = stock_dispo >= quantite - cursor.execute("COMMIT") + cursor.execute("COMMIT") - return { - "suffisant": suffisant, - "stock_disponible": stock_dispo, - "quantite_demandee": quantite, - } + return { + "suffisant": suffisant, + "stock_disponible": stock_dispo, + "quantite_demandee": quantite, + } - except: - cursor.execute("ROLLBACK") - raise + except Exception: + cursor.execute("ROLLBACK") + raise except Exception as e: logger.error(f"Erreur vérification stock: {e}") diff --git a/utils/documents/devis/devis_check.py b/utils/documents/devis/devis_check.py index 4e74a18..314b47f 100644 --- a/utils/documents/devis/devis_check.py +++ b/utils/documents/devis/devis_check.py @@ -3,6 +3,7 @@ import logging logger = logging.getLogger(__name__) + def _rechercher_devis_dans_liste(numero_devis, factory_doc): """Recherche un devis dans les 100 premiers éléments de la liste.""" index = 1 @@ -23,11 +24,12 @@ def _rechercher_devis_dans_liste(numero_devis, factory_doc): return persist_test index += 1 - except: + except Exception: index += 1 return None - + + def _recuperer_numero_devis(process, doc): """Récupère le numéro du devis créé via plusieurs méthodes.""" numero_devis = None @@ -38,7 +40,7 @@ def _recuperer_numero_devis(process, doc): doc_result = win32com.client.CastTo(doc_result, "IBODocumentVente3") doc_result.Read() numero_devis = getattr(doc_result, "DO_Piece", "") - except: + except Exception: pass if not numero_devis: @@ -50,12 +52,10 @@ def _recuperer_numero_devis(process, doc): doc.Write() doc.Read() numero_devis = getattr(doc, "DO_Piece", "") - except: + except Exception: pass return numero_devis -__all__ = [ - "_recuperer_numero_devis", - "_rechercher_devis_dans_liste" -] \ No newline at end of file + +__all__ = ["_recuperer_numero_devis", "_rechercher_devis_dans_liste"] diff --git a/utils/documents/devis/devis_extraction.py b/utils/documents/devis/devis_extraction.py index ddc56f5..48de6d4 100644 --- a/utils/documents/devis/devis_extraction.py +++ b/utils/documents/devis/devis_extraction.py @@ -1,5 +1,6 @@ from typing import Dict, List, Optional, Any + def _extraire_infos_devis(doc, numero: str, champs_modifies: list) -> Dict: """Extrait les informations complètes du devis.""" total_ht = float(getattr(doc, "DO_TotalHT", 0.0)) @@ -12,7 +13,7 @@ def _extraire_infos_devis(doc, numero: str, champs_modifies: list) -> Dict: date_doc = getattr(doc, "DO_Date", None) if date_doc: date_devis = date_doc.strftime("%Y-%m-%d") - except: + except Exception: pass date_livraison = None @@ -20,7 +21,7 @@ def _extraire_infos_devis(doc, numero: str, champs_modifies: list) -> Dict: date_livr = getattr(doc, "DO_DateLivr", None) if date_livr: date_livraison = date_livr.strftime("%Y-%m-%d") - except: + except Exception: pass client_code = "" @@ -29,7 +30,7 @@ def _extraire_infos_devis(doc, numero: str, champs_modifies: list) -> Dict: if client_obj: client_obj.Read() client_code = getattr(client_obj, "CT_Num", "") - except: + except Exception: pass return { @@ -44,6 +45,7 @@ def _extraire_infos_devis(doc, numero: str, champs_modifies: list) -> Dict: "client_code": client_code, } + __all__ = [ "_extraire_infos_devis", -] \ No newline at end of file +] diff --git a/utils/documents/documents_data_sql.py b/utils/documents/documents_data_sql.py index 37f66f9..1d5bc16 100644 --- a/utils/documents/documents_data_sql.py +++ b/utils/documents/documents_data_sql.py @@ -2,14 +2,16 @@ import win32com.client from typing import Optional import logging -logger = logging.getLogger(__name__) - from utils.functions.functions import ( _convertir_type_depuis_sql, _convertir_type_pour_sql, - _safe_strip + _safe_strip, ) + +logger = logging.getLogger(__name__) + + def _afficher_etat_document(doc, titre: str): """Affiche l'état complet d'un document.""" logger.info("-" * 80) @@ -19,15 +21,15 @@ def _afficher_etat_document(doc, titre: str): logger.info(f" DO_Piece: {getattr(doc, 'DO_Piece', 'N/A')}") logger.info(f" DO_Ref: '{getattr(doc, 'DO_Ref', 'N/A')}'") logger.info(f" DO_Statut: {getattr(doc, 'DO_Statut', 'N/A')}") - - date_doc = getattr(doc, 'DO_Date', None) - date_str = date_doc.strftime('%Y-%m-%d') if date_doc else 'None' + + date_doc = getattr(doc, "DO_Date", None) + date_str = date_doc.strftime("%Y-%m-%d") if date_doc else "None" logger.info(f" DO_Date: {date_str}") - - date_livr = getattr(doc, 'DO_DateLivr', None) - date_livr_str = date_livr.strftime('%Y-%m-%d') if date_livr else 'None' + + date_livr = getattr(doc, "DO_DateLivr", None) + date_livr_str = date_livr.strftime("%Y-%m-%d") if date_livr else "None" logger.info(f" DO_DateLivr: {date_livr_str}") - + logger.info(f" DO_TotalHT: {getattr(doc, 'DO_TotalHT', 0)}€") logger.info(f" DO_TotalTTC: {getattr(doc, 'DO_TotalTTC', 0)}€") except Exception as e: @@ -40,7 +42,7 @@ def _compter_lignes_document(doc) -> int: try: try: factory_lignes = doc.FactoryDocumentLigne - except: + except Exception: factory_lignes = doc.FactoryDocumentVenteLigne count = 0 @@ -52,7 +54,7 @@ def _compter_lignes_document(doc) -> int: break count += 1 index += 1 - except: + except Exception: break return count except Exception as e: @@ -63,7 +65,7 @@ def _compter_lignes_document(doc) -> int: def _rechercher_devis_par_numero(numero: str, factory): """Recherche un devis par numéro dans la liste.""" logger.info(f" Recherche de {numero} dans la liste...") - + index = 1 while index < 10000: try: @@ -82,7 +84,7 @@ def _rechercher_devis_par_numero(numero: str, factory): return persist_test index += 1 - except: + except Exception: index += 1 logger.error(f" Devis {numero} non trouvé dans la liste") @@ -91,7 +93,6 @@ def _rechercher_devis_par_numero(numero: str, factory): def _lire_document_sql(cursor, numero: str, type_doc: int): try: - query = """ SELECT d.DO_Piece, d.DO_Date, d.DO_Ref, d.DO_TotalHT, d.DO_TotalTTC, @@ -126,71 +127,56 @@ def _lire_document_sql(cursor, numero: str, type_doc: int): numero_piece = _safe_strip(row[0]) logger.info(f"[SQL READ] Document trouvé: {numero_piece}") - doc = { "numero": numero_piece, - "reference": _safe_strip(row[2]), # DO_Ref - "date": str(row[1]) if row[1] else "", # DO_Date - "date_livraison": (str(row[7]) if row[7] else ""), # DO_DateLivr - "date_expedition": ( - str(row[8]) if row[8] else "" - ), # DO_DateExpedition - "client_code": _safe_strip(row[6]), # DO_Tiers - "client_intitule": _safe_strip(row[39]), # CT_Intitule - "client_adresse": _safe_strip(row[40]), # CT_Adresse - "client_code_postal": _safe_strip(row[41]), # CT_CodePostal - "client_ville": _safe_strip(row[42]), # CT_Ville - "client_telephone": _safe_strip(row[43]), # CT_Telephone - "client_email": _safe_strip(row[44]), # CT_EMail - "contact": _safe_strip(row[9]), # DO_Contact - "total_ht": float(row[3]) if row[3] else 0.0, # DO_TotalHT - "total_ht_net": float(row[10]) if row[10] else 0.0, # DO_TotalHTNet - "total_ttc": float(row[4]) if row[4] else 0.0, # DO_TotalTTC - "net_a_payer": float(row[11]) if row[11] else 0.0, # DO_NetAPayer - "montant_regle": ( - float(row[12]) if row[12] else 0.0 - ), # DO_MontantRegle - "reliquat": float(row[13]) if row[13] else 0.0, # DO_Reliquat - "taux_escompte": ( - float(row[14]) if row[14] else 0.0 - ), # DO_TxEscompte - "escompte": float(row[15]) if row[15] else 0.0, # DO_Escompte - "taxe1": float(row[16]) if row[16] else 0.0, # DO_Taxe1 - "taxe2": float(row[17]) if row[17] else 0.0, # DO_Taxe2 - "taxe3": float(row[18]) if row[18] else 0.0, # DO_Taxe3 - "code_taxe1": _safe_strip(row[19]), # DO_CodeTaxe1 - "code_taxe2": _safe_strip(row[20]), # DO_CodeTaxe2 - "code_taxe3": _safe_strip(row[21]), # DO_CodeTaxe3 - "statut": int(row[5]) if row[5] is not None else 0, # DO_Statut + "reference": _safe_strip(row[2]), + "date": str(row[1]) if row[1] else "", + "date_livraison": (str(row[7]) if row[7] else ""), + "date_expedition": (str(row[8]) if row[8] else ""), + "client_code": _safe_strip(row[6]), + "client_intitule": _safe_strip(row[39]), + "client_adresse": _safe_strip(row[40]), + "client_code_postal": _safe_strip(row[41]), + "client_ville": _safe_strip(row[42]), + "client_telephone": _safe_strip(row[43]), + "client_email": _safe_strip(row[44]), + "contact": _safe_strip(row[9]), + "total_ht": float(row[3]) if row[3] else 0.0, + "total_ht_net": float(row[10]) if row[10] else 0.0, + "total_ttc": float(row[4]) if row[4] else 0.0, + "net_a_payer": float(row[11]) if row[11] else 0.0, + "montant_regle": (float(row[12]) if row[12] else 0.0), + "reliquat": float(row[13]) if row[13] else 0.0, + "taux_escompte": (float(row[14]) if row[14] else 0.0), + "escompte": float(row[15]) if row[15] else 0.0, + "taxe1": float(row[16]) if row[16] else 0.0, + "taxe2": float(row[17]) if row[17] else 0.0, + "taxe3": float(row[18]) if row[18] else 0.0, + "code_taxe1": _safe_strip(row[19]), + "code_taxe2": _safe_strip(row[20]), + "code_taxe3": _safe_strip(row[21]), + "statut": int(row[5]) if row[5] is not None else 0, "statut_estatut": ( int(row[22]) if row[22] is not None else 0 - ), # DO_EStatut - "imprime": int(row[23]) if row[23] is not None else 0, # DO_Imprim - "valide": int(row[24]) if row[24] is not None else 0, # DO_Valide - "cloture": int(row[25]) if row[25] is not None else 0, # DO_Cloture - "transfere": ( - int(row[26]) if row[26] is not None else 0 - ), # DO_Transfere - "souche": int(row[27]) if row[27] is not None else 0, # DO_Souche - "piece_origine": _safe_strip(row[28]), # DO_PieceOrig - "guid": _safe_strip(row[29]), # DO_GUID - "ca_num": _safe_strip(row[30]), # CA_Num - "cg_num": _safe_strip(row[31]), # CG_Num - "expedition": ( - int(row[32]) if row[32] is not None else 1 - ), # DO_Expedit - "condition": ( - int(row[33]) if row[33] is not None else 1 - ), # DO_Condition - "tarif": int(row[34]) if row[34] is not None else 1, # DO_Tarif - "type_frais": ( - int(row[35]) if row[35] is not None else 0 - ), # DO_TypeFrais - "valeur_frais": float(row[36]) if row[36] else 0.0, # DO_ValFrais + ), + "imprime": int(row[23]) if row[23] is not None else 0, + "valide": int(row[24]) if row[24] is not None else 0, + "cloture": int(row[25]) if row[25] is not None else 0, + "transfere": (int(row[26]) if row[26] is not None else 0), + "souche": int(row[27]) if row[27] is not None else 0, + "piece_origine": _safe_strip(row[28]), + "guid": _safe_strip(row[29]), + "ca_num": _safe_strip(row[30]), + "cg_num": _safe_strip(row[31]), + "expedition": (int(row[32]) if row[32] is not None else 1), + "condition": (int(row[33]) if row[33] is not None else 1), + "tarif": int(row[34]) if row[34] is not None else 1, + "type_frais": (int(row[35]) if row[35] is not None else 0), + "valeur_frais": float(row[36]) if row[36] else 0.0, "type_franco": ( int(row[37]) if row[37] is not None else 0 - ), # DO_TypeFranco - "valeur_franco": float(row[38]) if row[38] else 0.0, # DO_ValFranco + ), + "valeur_franco": float(row[38]) if row[38] else 0.0, } cursor.execute( @@ -216,8 +202,7 @@ def _lire_document_sql(cursor, numero: str, type_doc: int): ) montant_net = ( float(ligne_row.DL_MontantNet) - if hasattr(ligne_row, "DL_MontantNet") - and ligne_row.DL_MontantNet + if hasattr(ligne_row, "DL_MontantNet") and ligne_row.DL_MontantNet else montant_ht ) @@ -245,15 +230,11 @@ def _lire_document_sql(cursor, numero: str, type_doc: int): montant_taxe3 = montant_net * (taux_taxe3 / 100) ligne = { - "numero_ligne": ( - int(ligne_row.DL_Ligne) if ligne_row.DL_Ligne else 0 - ), + "numero_ligne": (int(ligne_row.DL_Ligne) if ligne_row.DL_Ligne else 0), "article_code": _safe_strip(ligne_row.AR_Ref), "designation": _safe_strip(ligne_row.DL_Design), "designation_article": _safe_strip(ligne_row.AR_Design), - "quantite": ( - float(ligne_row.DL_Qte) if ligne_row.DL_Qte else 0.0 - ), + "quantite": (float(ligne_row.DL_Qte) if ligne_row.DL_Qte else 0.0), "quantite_livree": ( float(ligne_row.DL_QteLiv) if hasattr(ligne_row, "DL_QteLiv") and ligne_row.DL_QteLiv @@ -311,9 +292,7 @@ def _lire_document_sql(cursor, numero: str, type_doc: int): else 0 ), "remise_article": ( - float(ligne_row.AR_Escompte) - if ligne_row.AR_Escompte - else 0.0 + float(ligne_row.AR_Escompte) if ligne_row.AR_Escompte else 0.0 ), "taux_taxe1": taux_taxe1, "montant_taxe1": montant_taxe1, @@ -333,20 +312,15 @@ def _lire_document_sql(cursor, numero: str, type_doc: int): float(ligne_row.AR_CoutStd) if ligne_row.AR_CoutStd else 0.0 ), "poids_net": ( - float(ligne_row.AR_PoidsNet) - if ligne_row.AR_PoidsNet - else 0.0 + float(ligne_row.AR_PoidsNet) if ligne_row.AR_PoidsNet else 0.0 ), "poids_brut": ( - float(ligne_row.AR_PoidsBrut) - if ligne_row.AR_PoidsBrut - else 0.0 + float(ligne_row.AR_PoidsBrut) if ligne_row.AR_PoidsBrut else 0.0 ), "unite_vente": _safe_strip(ligne_row.AR_UniteVen), "date_livraison_ligne": ( str(ligne_row.DL_DateLivr) - if hasattr(ligne_row, "DL_DateLivr") - and ligne_row.DL_DateLivr + if hasattr(ligne_row, "DL_DateLivr") and ligne_row.DL_DateLivr else "" ), "statut_ligne": ( @@ -356,9 +330,7 @@ def _lire_document_sql(cursor, numero: str, type_doc: int): else 0 ), "depot": ( - _safe_strip(ligne_row.DE_No) - if hasattr(ligne_row, "DE_No") - else "" + _safe_strip(ligne_row.DE_No) if hasattr(ligne_row, "DE_No") else "" ), "numero_commande": ( _safe_strip(ligne_row.DL_NoColis) @@ -376,9 +348,9 @@ def _lire_document_sql(cursor, numero: str, type_doc: int): doc["lignes"] = lignes doc["nb_lignes"] = len(lignes) - total_ht_calcule = sum(l.get("montant_ligne_ht", 0) for l in lignes) - total_ttc_calcule = sum(l.get("montant_ligne_ttc", 0) for l in lignes) - total_taxes_calcule = sum(l.get("total_taxes", 0) for l in lignes) + total_ht_calcule = sum(ligne.get("montant_ligne_ht", 0) for ligne in lignes) + total_ttc_calcule = sum(ligne.get("montant_ligne_ttc", 0) for ligne in lignes) + total_taxes_calcule = sum(ligne.get("total_taxes", 0) for ligne in lignes) doc["total_ht_calcule"] = total_ht_calcule doc["total_ttc_calcule"] = total_ttc_calcule @@ -390,6 +362,7 @@ def _lire_document_sql(cursor, numero: str, type_doc: int): logger.error(f" Erreur SQL lecture document {numero}: {e}", exc_info=True) return None + def _lister_documents_avec_lignes_sql( cursor, type_doc: int, @@ -423,7 +396,9 @@ def _lister_documents_avec_lignes_sql( params = [type_doc_sql] if filtre: - query += " AND (d.DO_Piece LIKE ? OR c.CT_Intitule LIKE ? OR d.DO_Ref LIKE ?)" + query += ( + " AND (d.DO_Piece LIKE ? OR c.CT_Intitule LIKE ? OR d.DO_Ref LIKE ?)" + ) params.extend([f"%{filtre}%", f"%{filtre}%", f"%{filtre}%"]) query += " ORDER BY d.DO_Date DESC" @@ -450,7 +425,7 @@ def _lister_documents_avec_lignes_sql( for idx, entete in enumerate(entetes): numero = _safe_strip(entete.DO_Piece) logger.info( - f"[SQL LIST] [{idx+1}/{len(entetes)}] Traitement {numero}..." + f"[SQL LIST] [{idx + 1}/{len(entetes)}] Traitement {numero}..." ) try: @@ -469,9 +444,7 @@ def _lister_documents_avec_lignes_sql( numero.upper().startswith(p) for p in prefixes_acceptes ) if not est_vente: - logger.info( - f"[SQL LIST] {numero} : exclu (préfixe achat)" - ) + logger.info(f"[SQL LIST] {numero} : exclu (préfixe achat)") stats["exclus_prefixe"] += 1 continue @@ -488,9 +461,7 @@ def _lister_documents_avec_lignes_sql( "reference": _safe_strip(entete.DO_Ref), "date": str(entete.DO_Date) if entete.DO_Date else "", "date_livraison": ( - str(entete.DO_DateLivr) - if entete.DO_DateLivr - else "" + str(entete.DO_DateLivr) if entete.DO_DateLivr else "" ), "date_expedition": ( str(entete.DO_DateExpedition) @@ -500,34 +471,22 @@ def _lister_documents_avec_lignes_sql( "client_code": _safe_strip(entete.DO_Tiers), "client_intitule": _safe_strip(entete.CT_Intitule), "client_adresse": _safe_strip(entete.CT_Adresse), - "client_code_postal": _safe_strip( - entete.CT_CodePostal - ), + "client_code_postal": _safe_strip(entete.CT_CodePostal), "client_ville": _safe_strip(entete.CT_Ville), - "client_telephone": _safe_strip( - entete.CT_Telephone - ), + "client_telephone": _safe_strip(entete.CT_Telephone), "client_email": _safe_strip(entete.CT_EMail), "contact": _safe_strip(entete.DO_Contact), "total_ht": ( - float(entete.DO_TotalHT) - if entete.DO_TotalHT - else 0.0 + float(entete.DO_TotalHT) if entete.DO_TotalHT else 0.0 ), "total_ht_net": ( - float(entete.DO_TotalHTNet) - if entete.DO_TotalHTNet - else 0.0 + float(entete.DO_TotalHTNet) if entete.DO_TotalHTNet else 0.0 ), "total_ttc": ( - float(entete.DO_TotalTTC) - if entete.DO_TotalTTC - else 0.0 + float(entete.DO_TotalTTC) if entete.DO_TotalTTC else 0.0 ), "net_a_payer": ( - float(entete.DO_NetAPayer) - if entete.DO_NetAPayer - else 0.0 + float(entete.DO_NetAPayer) if entete.DO_NetAPayer else 0.0 ), "montant_regle": ( float(entete.DO_MontantRegle) @@ -535,36 +494,22 @@ def _lister_documents_avec_lignes_sql( else 0.0 ), "reliquat": ( - float(entete.DO_Reliquat) - if entete.DO_Reliquat - else 0.0 + float(entete.DO_Reliquat) if entete.DO_Reliquat else 0.0 ), "taux_escompte": ( - float(entete.DO_TxEscompte) - if entete.DO_TxEscompte - else 0.0 + float(entete.DO_TxEscompte) if entete.DO_TxEscompte else 0.0 ), "escompte": ( - float(entete.DO_Escompte) - if entete.DO_Escompte - else 0.0 - ), - "taxe1": ( - float(entete.DO_Taxe1) if entete.DO_Taxe1 else 0.0 - ), - "taxe2": ( - float(entete.DO_Taxe2) if entete.DO_Taxe2 else 0.0 - ), - "taxe3": ( - float(entete.DO_Taxe3) if entete.DO_Taxe3 else 0.0 + float(entete.DO_Escompte) if entete.DO_Escompte else 0.0 ), + "taxe1": (float(entete.DO_Taxe1) if entete.DO_Taxe1 else 0.0), + "taxe2": (float(entete.DO_Taxe2) if entete.DO_Taxe2 else 0.0), + "taxe3": (float(entete.DO_Taxe3) if entete.DO_Taxe3 else 0.0), "code_taxe1": _safe_strip(entete.DO_CodeTaxe1), "code_taxe2": _safe_strip(entete.DO_CodeTaxe2), "code_taxe3": _safe_strip(entete.DO_CodeTaxe3), "statut": ( - int(entete.DO_Statut) - if entete.DO_Statut is not None - else 0 + int(entete.DO_Statut) if entete.DO_Statut is not None else 0 ), "statut_estatut": ( int(entete.DO_EStatut) @@ -572,14 +517,10 @@ def _lister_documents_avec_lignes_sql( else 0 ), "imprime": ( - int(entete.DO_Imprim) - if entete.DO_Imprim is not None - else 0 + int(entete.DO_Imprim) if entete.DO_Imprim is not None else 0 ), "valide": ( - int(entete.DO_Valide) - if entete.DO_Valide is not None - else 0 + int(entete.DO_Valide) if entete.DO_Valide is not None else 0 ), "cloture": ( int(entete.DO_Cloture) @@ -605,9 +546,7 @@ def _lister_documents_avec_lignes_sql( else 0 ), "valeur_frais": ( - float(entete.DO_ValFrais) - if entete.DO_ValFrais - else 0.0 + float(entete.DO_ValFrais) if entete.DO_ValFrais else 0.0 ), "type_franco": ( int(entete.DO_TypeFranco) @@ -615,16 +554,12 @@ def _lister_documents_avec_lignes_sql( else 0 ), "valeur_franco": ( - float(entete.DO_ValFranco) - if entete.DO_ValFranco - else 0.0 + float(entete.DO_ValFranco) if entete.DO_ValFranco else 0.0 ), "lignes": [], } - logger.debug( - f"[SQL LIST] {numero} : document de base créé" - ) + logger.debug(f"[SQL LIST] {numero} : document de base créé") except Exception as e: logger.error( @@ -665,20 +600,17 @@ def _lister_documents_avec_lignes_sql( taux_taxe1 = ( float(ligne_row.DL_Taxe1) - if hasattr(ligne_row, "DL_Taxe1") - and ligne_row.DL_Taxe1 + if hasattr(ligne_row, "DL_Taxe1") and ligne_row.DL_Taxe1 else 0.0 ) taux_taxe2 = ( float(ligne_row.DL_Taxe2) - if hasattr(ligne_row, "DL_Taxe2") - and ligne_row.DL_Taxe2 + if hasattr(ligne_row, "DL_Taxe2") and ligne_row.DL_Taxe2 else 0.0 ) taux_taxe3 = ( float(ligne_row.DL_Taxe3) - if hasattr(ligne_row, "DL_Taxe3") - and ligne_row.DL_Taxe3 + if hasattr(ligne_row, "DL_Taxe3") and ligne_row.DL_Taxe3 else 0.0 ) @@ -691,21 +623,13 @@ def _lister_documents_avec_lignes_sql( ligne = { "numero_ligne": ( - int(ligne_row.DL_Ligne) - if ligne_row.DL_Ligne - else 0 + int(ligne_row.DL_Ligne) if ligne_row.DL_Ligne else 0 ), "article_code": _safe_strip(ligne_row.AR_Ref), - "designation": _safe_strip( - ligne_row.DL_Design - ), - "designation_article": _safe_strip( - ligne_row.AR_Design - ), + "designation": _safe_strip(ligne_row.DL_Design), + "designation_article": _safe_strip(ligne_row.AR_Design), "quantite": ( - float(ligne_row.DL_Qte) - if ligne_row.DL_Qte - else 0.0 + float(ligne_row.DL_Qte) if ligne_row.DL_Qte else 0.0 ), "quantite_livree": ( float(ligne_row.DL_QteLiv) @@ -785,18 +709,12 @@ def _lister_documents_avec_lignes_sql( "total_taxes": montant_taxe1 + montant_taxe2 + montant_taxe3, - "famille_article": _safe_strip( - ligne_row.FA_CodeFamille - ), + "famille_article": _safe_strip(ligne_row.FA_CodeFamille), "gamme1": _safe_strip(ligne_row.AR_Gamme1), "gamme2": _safe_strip(ligne_row.AR_Gamme2), - "code_barre": _safe_strip( - ligne_row.AR_CodeBarre - ), + "code_barre": _safe_strip(ligne_row.AR_CodeBarre), "type_article": _safe_strip(ligne_row.AR_Type), - "nature_article": _safe_strip( - ligne_row.AR_Nature - ), + "nature_article": _safe_strip(ligne_row.AR_Nature), "garantie": _safe_strip(ligne_row.AR_Garantie), "cout_standard": ( float(ligne_row.AR_CoutStd) @@ -813,9 +731,7 @@ def _lister_documents_avec_lignes_sql( if ligne_row.AR_PoidsBrut else 0.0 ), - "unite_vente": _safe_strip( - ligne_row.AR_UniteVen - ), + "unite_vente": _safe_strip(ligne_row.AR_UniteVen), "date_livraison_ligne": ( str(ligne_row.DL_DateLivr) if hasattr(ligne_row, "DL_DateLivr") @@ -848,13 +764,13 @@ def _lister_documents_avec_lignes_sql( doc["nb_lignes"] = len(doc["lignes"]) doc["total_ht_calcule"] = sum( - l.get("montant_ligne_ht", 0) for l in doc["lignes"] + ligne.get("montant_ligne_ht", 0) for ligne in doc["lignes"] ) doc["total_ttc_calcule"] = sum( - l.get("montant_ligne_ttc", 0) for l in doc["lignes"] + ligne.get("montant_ligne_ttc", 0) for ligne in doc["lignes"] ) doc["total_taxes_calcule"] = sum( - l.get("total_taxes", 0) for l in doc["lignes"] + ligne.get("total_taxes", 0) for ligne in doc["lignes"] ) logger.debug( @@ -881,13 +797,11 @@ def _lister_documents_avec_lignes_sql( ) continue - logger.info(f"[SQL LIST] ═══════════════════════════") - logger.info(f"[SQL LIST] STATISTIQUES FINALES:") + logger.info("[SQL LIST] ═══════════════════════════") + logger.info("[SQL LIST] STATISTIQUES FINALES:") logger.info(f"[SQL LIST] Total SQL: {stats['total']}") logger.info(f"[SQL LIST] Exclus préfixe: {stats['exclus_prefixe']}") - logger.info( - f"[SQL LIST] Erreur construction: {stats['erreur_construction']}" - ) + logger.info(f"[SQL LIST] Erreur construction: {stats['erreur_construction']}") logger.info(f"[SQL LIST] Erreur lignes: {stats['erreur_lignes']}") logger.info( f"[SQL LIST] Erreur transformations: {stats['erreur_transformations']}" @@ -895,7 +809,7 @@ def _lister_documents_avec_lignes_sql( logger.info(f"[SQL LIST] Erreur liaisons: {stats['erreur_liaisons']}") logger.info(f"[SQL LIST] SUCCÈS: {stats['succes']}") logger.info(f"[SQL LIST] Documents retournés: {len(documents)}") - logger.info(f"[SQL LIST] ═══════════════════════════") + logger.info("[SQL LIST] ═══════════════════════════") return documents @@ -910,4 +824,4 @@ __all__ = [ "_rechercher_devis_par_numero", "_lire_document_sql", "_lister_documents_avec_lignes_sql", -] \ No newline at end of file +] diff --git a/utils/functions/functions.py b/utils/functions/functions.py index 05b1bfa..5858f13 100644 --- a/utils/functions/functions.py +++ b/utils/functions/functions.py @@ -4,9 +4,10 @@ import logging logger = logging.getLogger(__name__) + def _clean_str(value, max_len: int) -> str: """Nettoie et tronque une chaîne""" - if value is None or str(value).lower() in ('none', 'null', ''): + if value is None or str(value).lower() in ("none", "null", ""): return "" return str(value)[:max_len].strip() @@ -37,7 +38,7 @@ def _try_set_attribute(obj, attr_name, value, variants=None): variants = [attr_name] else: variants = [attr_name] + variants - + for variant in variants: try: if hasattr(obj, variant): @@ -45,7 +46,7 @@ def _try_set_attribute(obj, attr_name, value, variants=None): return True except Exception as e: logger.debug(f" {variant} échec: {str(e)[:50]}") - + return False @@ -77,12 +78,13 @@ def _get_type_libelle(type_doc: int) -> str: return f"Type {type_doc}" - + def _convertir_type_pour_sql(type_doc: int) -> int: """COM → SQL : 0, 10, 20, 30... → 0, 1, 2, 3...""" mapping = {0: 0, 10: 1, 20: 2, 30: 3, 40: 4, 50: 5, 60: 6} return mapping.get(type_doc, type_doc) + def _convertir_type_depuis_sql(type_sql: int) -> int: """SQL → COM : 0, 1, 2, 3... → 0, 10, 20, 30...""" mapping = {0: 0, 1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 6: 60} @@ -96,16 +98,17 @@ def _normaliser_type_document(type_doc: int) -> int: return type_doc mapping_normalisation = { - 1: 10, # Commande - 2: 20, # Préparation - 3: 30, # BL - 4: 40, # Retour - 5: 50, # Avoir - 6: 60, # Facture + 1: 10, + 2: 20, + 3: 30, + 4: 40, + 5: 50, + 6: 60, } return mapping_normalisation.get(type_doc, type_doc) + def normaliser_date(valeur): if isinstance(valeur, str): try: @@ -121,7 +124,8 @@ def normaliser_date(valeur): else: return datetime.now() - + + __all__ = [ "_clean_str", "_safe_strip", @@ -131,5 +135,5 @@ __all__ = [ "_normaliser_type_document", "_convertir_type_depuis_sql", "_convertir_type_pour_sql", - "normaliser_date" -] \ No newline at end of file + "normaliser_date", +] diff --git a/utils/functions/items_to_dict.py b/utils/functions/items_to_dict.py index fec0293..f275a07 100644 --- a/utils/functions/items_to_dict.py +++ b/utils/functions/items_to_dict.py @@ -4,27 +4,32 @@ from utils.functions.functions import _safe_strip logger = logging.getLogger(__name__) -def _contact_to_dict(contact, numero_client=None, contact_numero=None, n_contact=None) -> Dict: + +def _contact_to_dict( + contact, numero_client=None, contact_numero=None, n_contact=None +) -> Dict: try: civilite_code = getattr(contact, "Civilite", None) civilite_map = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"} - civilite = civilite_map.get(civilite_code) if civilite_code is not None else None - + civilite = ( + civilite_map.get(civilite_code) if civilite_code is not None else None + ) + telephone = None portable = None telecopie = None email = None - - if hasattr(contact, 'Telecom'): + + if hasattr(contact, "Telecom"): try: telecom = contact.Telecom telephone = _safe_strip(getattr(telecom, "Telephone", None)) portable = _safe_strip(getattr(telecom, "Portable", None)) telecopie = _safe_strip(getattr(telecom, "Telecopie", None)) email = _safe_strip(getattr(telecom, "EMail", None)) - except: + except Exception: pass - + return { "numero": numero_client, "contact_numero": contact_numero, @@ -46,16 +51,19 @@ def _contact_to_dict(contact, numero_client=None, contact_numero=None, n_contact logger.warning(f"Erreur conversion contact: {e}") return {} + def _row_to_contact_dict(row) -> Dict: """Convertit une ligne SQL en dictionnaire contact""" civilite_code = row.CT_Civilite civilite_map = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"} - + return { "numero": _safe_strip(row.CT_Num), "contact_numero": row.CT_No, "n_contact": row.N_Contact, - "civilite": civilite_map.get(civilite_code) if civilite_code is not None else None, + "civilite": civilite_map.get(civilite_code) + if civilite_code is not None + else None, "nom": _safe_strip(row.CT_Nom), "prenom": _safe_strip(row.CT_Prenom), "fonction": _safe_strip(row.CT_Fonction), @@ -69,6 +77,7 @@ def _row_to_contact_dict(row) -> Dict: "skype": _safe_strip(row.CT_Skype), } + def _row_to_tiers_dict(row) -> Dict: """Convertit une ligne SQL en dictionnaire tiers (factorisation DRY)""" return { @@ -81,7 +90,6 @@ def _row_to_tiers_dict(row) -> Dict: "siret": _safe_strip(row.CT_Siret), "tva_intra": _safe_strip(row.CT_Identifiant), "code_naf": _safe_strip(row.CT_Ape), - "contact": _safe_strip(row.CT_Contact), "adresse": _safe_strip(row.CT_Adresse), "complement": _safe_strip(row.CT_Complement), @@ -89,19 +97,16 @@ def _row_to_tiers_dict(row) -> Dict: "ville": _safe_strip(row.CT_Ville), "region": _safe_strip(row.CT_CodeRegion), "pays": _safe_strip(row.CT_Pays), - "telephone": _safe_strip(row.CT_Telephone), "telecopie": _safe_strip(row.CT_Telecopie), "email": _safe_strip(row.CT_EMail), "site_web": _safe_strip(row.CT_Site), "facebook": _safe_strip(row.CT_Facebook), "linkedin": _safe_strip(row.CT_LinkedIn), - "taux01": row.CT_Taux01, "taux02": row.CT_Taux02, "taux03": row.CT_Taux03, "taux04": row.CT_Taux04, - "statistique01": _safe_strip(row.CT_Statistique01), "statistique02": _safe_strip(row.CT_Statistique02), "statistique03": _safe_strip(row.CT_Statistique03), @@ -112,12 +117,10 @@ def _row_to_tiers_dict(row) -> Dict: "statistique08": _safe_strip(row.CT_Statistique08), "statistique09": _safe_strip(row.CT_Statistique09), "statistique10": _safe_strip(row.CT_Statistique10), - "encours_autorise": row.CT_Encours, "assurance_credit": row.CT_Assurance, "langue": row.CT_Langue, "commercial_code": row.CO_No, - "lettrage_auto": (row.CT_Lettrage == 1), "est_actif": (row.CT_Sommeil == 0), "type_facture": row.CT_Facture, @@ -129,16 +132,12 @@ def _row_to_tiers_dict(row) -> Dict: "exclure_relance": (row.CT_NotRappel == 1), "exclure_penalites": (row.CT_NotPenal == 1), "bon_a_payer": row.CT_BonAPayer, - "priorite_livraison": row.CT_PrioriteLivr, "livraison_partielle": row.CT_LivrPartielle, "delai_transport": row.CT_DelaiTransport, "delai_appro": row.CT_DelaiAppro, - "commentaire": _safe_strip(row.CT_Commentaire), - "section_analytique": _safe_strip(row.CA_Num), - "mode_reglement_code": row.MR_No, "surveillance_active": (row.CT_Surveillance == 1), "coface": _safe_strip(row.CT_Coface), @@ -149,14 +148,14 @@ def _row_to_tiers_dict(row) -> Dict: "sv_objet_maj": _safe_strip(row.CT_SvObjetMaj), "sv_chiffre_affaires": row.CT_SvCA, "sv_resultat": row.CT_SvResultat, - "compte_general": _safe_strip(row.CG_NumPrinc), "categorie_tarif": row.N_CatTarif, "categorie_compta": row.N_CatCompta, } + __all__ = [ "_contact_to_dict", "_row_to_contact_dict", "_row_to_tiers_dict", -] \ No newline at end of file +] diff --git a/utils/functions/sage_utilities.py b/utils/functions/sage_utilities.py index a5f809a..9574802 100644 --- a/utils/functions/sage_utilities.py +++ b/utils/functions/sage_utilities.py @@ -3,7 +3,7 @@ from utils.functions.functions import ( _convertir_type_depuis_sql, _convertir_type_pour_sql, _normaliser_type_document, - _get_type_libelle + _get_type_libelle, ) logger = logging.getLogger(__name__) @@ -23,7 +23,8 @@ def _verifier_devis_non_transforme(numero: str, doc, cursor): statut_actuel = getattr(doc, "DO_Statut", 0) if statut_actuel == 5: raise ValueError(f" Devis {numero} déjà transformé (statut=5)") - + + def verifier_si_deja_transforme_sql(numero_source, cursor, type_source): """Version corrigée avec normalisation des types""" logger.info( @@ -57,7 +58,6 @@ def verifier_si_deja_transforme_sql(numero_source, cursor, type_source): return {"deja_transforme": False, "documents_cibles": []} try: - query = f""" SELECT DISTINCT dc.DO_Piece, @@ -81,8 +81,8 @@ def verifier_si_deja_transforme_sql(numero_source, cursor, type_source): doc = { "numero": row.DO_Piece.strip() if row.DO_Piece else "", - "type": type_normalise, # ← TYPE NORMALISÉ - "type_brut": type_brut, # Garder aussi le type original + "type": type_normalise, + "type_brut": type_brut, "type_libelle": _get_type_libelle(type_brut), "statut": int(row.DO_Statut) if row.DO_Statut else 0, "nb_lignes": int(row.NbLignes) if row.NbLignes else 0, @@ -101,9 +101,7 @@ def verifier_si_deja_transforme_sql(numero_source, cursor, type_source): f"[VERIF] Document {numero_source} a {len(documents_cibles)} transformation(s)" ) else: - logger.info( - f"[VERIF] Document {numero_source} pas encore transformé" - ) + logger.info(f"[VERIF] Document {numero_source} pas encore transformé") return { "deja_transforme": deja_transforme, @@ -114,21 +112,19 @@ def verifier_si_deja_transforme_sql(numero_source, cursor, type_source): logger.error(f"[VERIF] Erreur vérification: {e}") return {"deja_transforme": False, "documents_cibles": []} + def peut_etre_transforme(cursor, numero_source, type_source, type_cible): """Version corrigée avec normalisation""" type_source = _normaliser_type_document(type_source) type_cible = _normaliser_type_document(type_cible) logger.info( - f"[VERIF_TRANSFO] {numero_source} " - f"(type {type_source}) → type {type_cible}" + f"[VERIF_TRANSFO] {numero_source} (type {type_source}) → type {type_cible}" ) verif = verifier_si_deja_transforme_sql(cursor, numero_source, type_source) - docs_meme_type = [ - d for d in verif["documents_cibles"] if d["type"] == type_cible - ] + docs_meme_type = [d for d in verif["documents_cibles"] if d["type"] == type_cible] if docs_meme_type: nums = [d["numero"] for d in docs_meme_type] @@ -145,6 +141,7 @@ def peut_etre_transforme(cursor, numero_source, type_source, type_cible): "documents_existants": [], } + def lire_erreurs_sage(obj, nom_obj=""): erreurs = [] try: @@ -154,7 +151,7 @@ def lire_erreurs_sage(obj, nom_obj=""): nb_erreurs = 0 try: nb_erreurs = obj.Errors.Count - except: + except Exception: return erreurs if nb_erreurs == 0: @@ -165,13 +162,13 @@ def lire_erreurs_sage(obj, nom_obj=""): err = None try: err = obj.Errors.Item(i) - except: + except Exception: try: err = obj.Errors(i) - except: + except Exception: try: err = obj.Errors.Item(i - 1) - except: + except Exception: pass if err is not None: @@ -185,7 +182,7 @@ def lire_erreurs_sage(obj, nom_obj=""): if val: description = str(val) break - except: + except Exception: pass for attr in ["Field", "FieldName", "Champ", "Property"]: @@ -194,7 +191,7 @@ def lire_erreurs_sage(obj, nom_obj=""): if val: field = str(val) break - except: + except Exception: pass for attr in ["Number", "Code", "ErrorCode", "Numero"]: @@ -203,7 +200,7 @@ def lire_erreurs_sage(obj, nom_obj=""): if val is not None: number = str(val) break - except: + except Exception: pass if description or field or number: @@ -226,9 +223,10 @@ def lire_erreurs_sage(obj, nom_obj=""): return erreurs + __all__ = [ "_verifier_devis_non_transforme", "verifier_si_deja_transforme_sql", "peut_etre_transforme", - "lire_erreurs_sage" -] \ No newline at end of file + "lire_erreurs_sage", +] diff --git a/utils/tiers/clients/clients_data.py b/utils/tiers/clients/clients_data.py index 961da5d..dda2ded 100644 --- a/utils/tiers/clients/clients_data.py +++ b/utils/tiers/clients/clients_data.py @@ -3,6 +3,7 @@ import logging logger = logging.getLogger(__name__) + def _cast_client(persist_obj): try: obj = win32com.client.CastTo(persist_obj, "IBOClient3") @@ -11,7 +12,8 @@ def _cast_client(persist_obj): except Exception as e: logger.debug(f" _cast_client échoue: {e}") return None - + + def _extraire_client(client_obj): try: try: @@ -40,23 +42,23 @@ def _extraire_client(client_obj): qualite_code = getattr(client_obj, "CT_Type", None) qualite_map = { - 0: "CLI", # Client - 1: "FOU", # Fournisseur - 2: "CLIFOU", # Client + Fournisseur - 3: "SAL", # Salarié - 4: "PRO", # Prospect + 0: "CLI", + 1: "FOU", + 2: "CLIFOU", + 3: "SAL", + 4: "PRO", } data["qualite"] = qualite_map.get(qualite_code, "CLI") data["est_fournisseur"] = qualite_code in [1, 2] - except: + except Exception: data["qualite"] = "CLI" data["est_fournisseur"] = False try: data["est_prospect"] = getattr(client_obj, "CT_Prospect", 0) == 1 - except: + except Exception: data["est_prospect"] = False if data["est_prospect"]: @@ -72,7 +74,7 @@ def _extraire_client(client_obj): sommeil = getattr(client_obj, "CT_Sommeil", 0) data["est_actif"] = sommeil == 0 data["est_en_sommeil"] = sommeil == 1 - except: + except Exception: data["est_actif"] = True data["est_en_sommeil"] = False @@ -81,24 +83,24 @@ def _extraire_client(client_obj): data["forme_juridique"] = forme_juridique data["est_entreprise"] = bool(forme_juridique) data["est_particulier"] = not bool(forme_juridique) - except: + except Exception: data["forme_juridique"] = "" data["est_entreprise"] = False data["est_particulier"] = True try: data["civilite"] = getattr(client_obj, "CT_Civilite", "").strip() - except: + except Exception: data["civilite"] = "" try: data["nom"] = getattr(client_obj, "CT_Nom", "").strip() - except: + except Exception: data["nom"] = "" try: data["prenom"] = getattr(client_obj, "CT_Prenom", "").strip() - except: + except Exception: data["prenom"] = "" if data.get("nom") or data.get("prenom"): @@ -115,7 +117,7 @@ def _extraire_client(client_obj): try: data["contact"] = getattr(client_obj, "CT_Contact", "").strip() - except: + except Exception: data["contact"] = "" try: @@ -123,36 +125,32 @@ def _extraire_client(client_obj): if adresse_obj: try: data["adresse"] = getattr(adresse_obj, "Adresse", "").strip() - except: + except Exception: data["adresse"] = "" try: - data["complement"] = getattr( - adresse_obj, "Complement", "" - ).strip() - except: + data["complement"] = getattr(adresse_obj, "Complement", "").strip() + except Exception: data["complement"] = "" try: - data["code_postal"] = getattr( - adresse_obj, "CodePostal", "" - ).strip() - except: + data["code_postal"] = getattr(adresse_obj, "CodePostal", "").strip() + except Exception: data["code_postal"] = "" try: data["ville"] = getattr(adresse_obj, "Ville", "").strip() - except: + except Exception: data["ville"] = "" try: data["region"] = getattr(adresse_obj, "Region", "").strip() - except: + except Exception: data["region"] = "" try: data["pays"] = getattr(adresse_obj, "Pays", "").strip() - except: + except Exception: data["pays"] = "" else: data["adresse"] = "" @@ -175,22 +173,22 @@ def _extraire_client(client_obj): if telecom: try: data["telephone"] = getattr(telecom, "Telephone", "").strip() - except: + except Exception: data["telephone"] = "" try: data["portable"] = getattr(telecom, "Portable", "").strip() - except: + except Exception: data["portable"] = "" try: data["telecopie"] = getattr(telecom, "Telecopie", "").strip() - except: + except Exception: data["telecopie"] = "" try: data["email"] = getattr(telecom, "EMail", "").strip() - except: + except Exception: data["email"] = "" try: @@ -200,7 +198,7 @@ def _extraire_client(client_obj): or getattr(telecom, "SiteWeb", "") ) data["site_web"] = str(site).strip() if site else "" - except: + except Exception: data["site_web"] = "" else: data["telephone"] = "" @@ -218,17 +216,17 @@ def _extraire_client(client_obj): try: data["siret"] = getattr(client_obj, "CT_Siret", "").strip() - except: + except Exception: data["siret"] = "" try: data["siren"] = getattr(client_obj, "CT_Siren", "").strip() - except: + except Exception: data["siren"] = "" try: data["tva_intra"] = getattr(client_obj, "CT_Identifiant", "").strip() - except: + except Exception: data["tva_intra"] = "" try: @@ -236,34 +234,34 @@ def _extraire_client(client_obj): getattr(client_obj, "CT_CodeNAF", "").strip() or getattr(client_obj, "CT_APE", "").strip() ) - except: + except Exception: data["code_naf"] = "" try: data["secteur"] = getattr(client_obj, "CT_Secteur", "").strip() - except: + except Exception: data["secteur"] = "" try: effectif = getattr(client_obj, "CT_Effectif", None) data["effectif"] = int(effectif) if effectif is not None else None - except: + except Exception: data["effectif"] = None try: ca = getattr(client_obj, "CT_ChiffreAffaire", None) data["ca_annuel"] = float(ca) if ca is not None else None - except: + except Exception: data["ca_annuel"] = None try: data["commercial_code"] = getattr(client_obj, "CO_No", "").strip() - except: + except Exception: try: data["commercial_code"] = getattr( client_obj, "CT_Commercial", "" ).strip() - except: + except Exception: data["commercial_code"] = "" if data.get("commercial_code"): @@ -276,48 +274,46 @@ def _extraire_client(client_obj): ).strip() else: data["commercial_nom"] = "" - except: + except Exception: data["commercial_nom"] = "" else: data["commercial_nom"] = "" try: data["categorie_tarifaire"] = getattr(client_obj, "N_CatTarif", None) - except: + except Exception: data["categorie_tarifaire"] = None try: data["categorie_comptable"] = getattr(client_obj, "N_CatCompta", None) - except: + except Exception: data["categorie_comptable"] = None try: data["encours_autorise"] = float(getattr(client_obj, "CT_Encours", 0.0)) - except: + except Exception: data["encours_autorise"] = 0.0 try: - data["assurance_credit"] = float( - getattr(client_obj, "CT_Assurance", 0.0) - ) - except: + data["assurance_credit"] = float(getattr(client_obj, "CT_Assurance", 0.0)) + except Exception: data["assurance_credit"] = 0.0 try: data["compte_general"] = getattr(client_obj, "CG_Num", "").strip() - except: + except Exception: data["compte_general"] = "" try: date_creation = getattr(client_obj, "CT_DateCreate", None) data["date_creation"] = str(date_creation) if date_creation else "" - except: + except Exception: data["date_creation"] = "" try: date_modif = getattr(client_obj, "CT_DateModif", None) data["date_modification"] = str(date_modif) if date_modif else "" - except: + except Exception: data["date_modification"] = "" return data @@ -325,8 +321,6 @@ def _extraire_client(client_obj): except Exception as e: logger.error(f" ERREUR GLOBALE _extraire_client: {e}", exc_info=True) return None - -__all__ = [ - "_extraire_client", - "_cast_client" -] + + +__all__ = ["_extraire_client", "_cast_client"] diff --git a/utils/tiers/fournisseurs/fournisseurs_extraction.py b/utils/tiers/fournisseurs/fournisseurs_extraction.py index 2933942..0d852b2 100644 --- a/utils/tiers/fournisseurs/fournisseurs_extraction.py +++ b/utils/tiers/fournisseurs/fournisseurs_extraction.py @@ -1,9 +1,9 @@ - import win32com.client import logging logger = logging.getLogger(__name__) + def _extraire_fournisseur_enrichi(fourn_obj): try: numero = getattr(fourn_obj, "CT_Num", "").strip() @@ -15,7 +15,7 @@ def _extraire_fournisseur_enrichi(fourn_obj): data = { "numero": numero, "intitule": intitule, - "type": 1, # Fournisseur + "type": 1, "est_fournisseur": True, } @@ -23,7 +23,7 @@ def _extraire_fournisseur_enrichi(fourn_obj): sommeil = getattr(fourn_obj, "CT_Sommeil", 0) data["est_actif"] = sommeil == 0 data["en_sommeil"] = sommeil == 1 - except: + except Exception: data["est_actif"] = True data["en_sommeil"] = False @@ -83,7 +83,7 @@ def _extraire_fournisseur_enrichi(fourn_obj): or getattr(telecom_obj, "SiteWeb", "") ) data["site_web"] = str(site).strip() if site else "" - except: + except Exception: data["site_web"] = "" else: data["telephone"] = "" @@ -101,7 +101,7 @@ def _extraire_fournisseur_enrichi(fourn_obj): try: data["siret"] = getattr(fourn_obj, "CT_Siret", "").strip() - except: + except Exception: data["siret"] = "" try: @@ -109,12 +109,12 @@ def _extraire_fournisseur_enrichi(fourn_obj): data["siren"] = data["siret"][:9] else: data["siren"] = getattr(fourn_obj, "CT_Siren", "").strip() - except: + except Exception: data["siren"] = "" try: data["tva_intra"] = getattr(fourn_obj, "CT_Identifiant", "").strip() - except: + except Exception: data["tva_intra"] = "" try: @@ -122,14 +122,14 @@ def _extraire_fournisseur_enrichi(fourn_obj): getattr(fourn_obj, "CT_CodeNAF", "").strip() or getattr(fourn_obj, "CT_APE", "").strip() ) - except: + except Exception: data["code_naf"] = "" try: data["forme_juridique"] = getattr( fourn_obj, "CT_FormeJuridique", "" ).strip() - except: + except Exception: data["forme_juridique"] = "" try: @@ -137,7 +137,7 @@ def _extraire_fournisseur_enrichi(fourn_obj): data["categorie_tarifaire"] = ( int(cat_tarif) if cat_tarif is not None else None ) - except: + except Exception: data["categorie_tarifaire"] = None try: @@ -145,7 +145,7 @@ def _extraire_fournisseur_enrichi(fourn_obj): data["categorie_comptable"] = ( int(cat_compta) if cat_compta is not None else None ) - except: + except Exception: data["categorie_comptable"] = None try: @@ -162,11 +162,11 @@ def _extraire_fournisseur_enrichi(fourn_obj): ).strip() else: data["conditions_reglement_libelle"] = "" - except: + except Exception: data["conditions_reglement_libelle"] = "" else: data["conditions_reglement_libelle"] = "" - except: + except Exception: data["conditions_reglement_code"] = "" data["conditions_reglement_libelle"] = "" @@ -184,11 +184,11 @@ def _extraire_fournisseur_enrichi(fourn_obj): ).strip() else: data["mode_reglement_libelle"] = "" - except: + except Exception: data["mode_reglement_libelle"] = "" else: data["mode_reglement_libelle"] = "" - except: + except Exception: data["mode_reglement_code"] = "" data["mode_reglement_libelle"] = "" @@ -199,53 +199,36 @@ def _extraire_fournisseur_enrichi(fourn_obj): if factory_banque: index = 1 - while index <= 5: # Max 5 comptes bancaires + while index <= 5: try: banque_persist = factory_banque.List(index) if banque_persist is None: break - banque = win32com.client.CastTo( - banque_persist, "IBOBanque3" - ) + banque = win32com.client.CastTo(banque_persist, "IBOBanque3") banque.Read() compte_bancaire = { - "banque_nom": getattr( - banque, "BI_Intitule", "" - ).strip(), + "banque_nom": getattr(banque, "BI_Intitule", "").strip(), "iban": getattr(banque, "RIB_Iban", "").strip(), "bic": getattr(banque, "RIB_Bic", "").strip(), - "code_banque": getattr( - banque, "RIB_Banque", "" - ).strip(), - "code_guichet": getattr( - banque, "RIB_Guichet", "" - ).strip(), - "numero_compte": getattr( - banque, "RIB_Compte", "" - ).strip(), + "code_banque": getattr(banque, "RIB_Banque", "").strip(), + "code_guichet": getattr(banque, "RIB_Guichet", "").strip(), + "numero_compte": getattr(banque, "RIB_Compte", "").strip(), "cle_rib": getattr(banque, "RIB_Cle", "").strip(), } - if ( - compte_bancaire["iban"] - or compte_bancaire["numero_compte"] - ): + if compte_bancaire["iban"] or compte_bancaire["numero_compte"]: data["coordonnees_bancaires"].append(compte_bancaire) index += 1 - except: + except Exception: break except Exception as e: - logger.debug( - f"Erreur coordonnées bancaires fournisseur {numero}: {e}" - ) + logger.debug(f"Erreur coordonnées bancaires fournisseur {numero}: {e}") if data["coordonnees_bancaires"]: - data["iban_principal"] = data["coordonnees_bancaires"][0].get( - "iban", "" - ) + data["iban_principal"] = data["coordonnees_bancaires"][0].get("iban", "") data["bic_principal"] = data["coordonnees_bancaires"][0].get("bic", "") else: data["iban_principal"] = "" @@ -258,15 +241,13 @@ def _extraire_fournisseur_enrichi(fourn_obj): if factory_contact: index = 1 - while index <= 20: # Max 20 contacts + while index <= 20: try: contact_persist = factory_contact.List(index) if contact_persist is None: break - contact = win32com.client.CastTo( - contact_persist, "IBOContact3" - ) + contact = win32com.client.CastTo(contact_persist, "IBOContact3") contact.Read() contact_data = { @@ -274,14 +255,14 @@ def _extraire_fournisseur_enrichi(fourn_obj): "prenom": getattr(contact, "CO_Prenom", "").strip(), "fonction": getattr(contact, "CO_Fonction", "").strip(), "service": getattr(contact, "CO_Service", "").strip(), - "telephone": getattr( - contact, "CO_Telephone", "" - ).strip(), + "telephone": getattr(contact, "CO_Telephone", "").strip(), "portable": getattr(contact, "CO_Portable", "").strip(), "email": getattr(contact, "CO_EMail", "").strip(), } - nom_complet = f"{contact_data['prenom']} {contact_data['nom']}".strip() + nom_complet = ( + f"{contact_data['prenom']} {contact_data['nom']}".strip() + ) if nom_complet: contact_data["nom_complet"] = nom_complet else: @@ -291,7 +272,7 @@ def _extraire_fournisseur_enrichi(fourn_obj): data["contacts"].append(contact_data) index += 1 - except: + except Exception: break except Exception as e: logger.debug(f"Erreur contacts fournisseur {numero}: {e}") @@ -305,29 +286,29 @@ def _extraire_fournisseur_enrichi(fourn_obj): try: data["encours_autorise"] = float(getattr(fourn_obj, "CT_Encours", 0.0)) - except: + except Exception: data["encours_autorise"] = 0.0 try: data["ca_annuel"] = float(getattr(fourn_obj, "CT_ChiffreAffaire", 0.0)) - except: + except Exception: data["ca_annuel"] = 0.0 try: data["compte_general"] = getattr(fourn_obj, "CG_Num", "").strip() - except: + except Exception: data["compte_general"] = "" try: date_creation = getattr(fourn_obj, "CT_DateCreate", None) data["date_creation"] = str(date_creation) if date_creation else "" - except: + except Exception: data["date_creation"] = "" try: date_modif = getattr(fourn_obj, "CT_DateModif", None) data["date_modification"] = str(date_modif) if date_modif else "" - except: + except Exception: data["date_modification"] = "" return data @@ -378,6 +359,4 @@ def _extraire_fournisseur_enrichi(fourn_obj): } -__all__ = [ - "_extraire_fournisseur_enrichi" -] \ No newline at end of file +__all__ = ["_extraire_fournisseur_enrichi"] diff --git a/utils/tiers/tiers_data_sql.py b/utils/tiers/tiers_data_sql.py index 81dc2d6..31ccb3b 100644 --- a/utils/tiers/tiers_data_sql.py +++ b/utils/tiers/tiers_data_sql.py @@ -50,6 +50,6 @@ def _build_tiers_select_query() -> str: -- COMPTE GENERAL ET CATEGORIES (3) CG_NumPrinc, N_CatTarif, N_CatCompta """ -__all__ = [ - "_build_tiers_select_query" -] \ No newline at end of file + + +__all__ = ["_build_tiers_select_query"]