diff --git a/main.py b/main.py index e1648fc..30eeb13 100644 --- a/main.py +++ b/main.py @@ -13,7 +13,43 @@ from config import settings, validate_settings from sage_connector import SageConnector import pyodbc import os -from schemas import TiersListRequest +from schemas import ( + TiersListRequest, + ContactCreateRequest, + ContactDeleteRequest, + ContactGetRequest, + ContactListRequest, + ContactUpdateRequest, + ClientCreateRequest, + ClientUpdateGatewayRequest, + FiltreRequest, + ChampLibreRequest, + CodeRequest, + TransformationRequest, + TypeDocument, + DevisRequest, + DocumentGetRequest, + StatutRequest, + TypeTiers, + FournisseurCreateRequest, + FournisseurUpdateGatewayRequest, + AvoirCreateGatewayRequest, + AvoirUpdateGatewayRequest, + CommandeCreateRequest, + CommandeUpdateGatewayRequest, + FactureCreateGatewayRequest, + FactureUpdateGatewayRequest, + LivraisonCreateGatewayRequest, + LivraisonUpdateGatewayRequest, + ArticleCreateRequest, + ArticleUpdateGatewayRequest, + MouvementStockLigneRequest, + EntreeStockRequest, + SortieStockRequest, + FamilleCreate, + PDFGenerationRequest, + DevisUpdateGatewayRequest + ) logging.basicConfig( level=logging.INFO, @@ -22,882 +58,6 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) - -class TypeDocument(int, Enum): - DEVIS = 0 - BON_LIVRAISON = 1 - BON_RETOUR = 2 - COMMANDE = 3 - PREPARATION = 4 - FACTURE = 5 - - - - -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 - date_livraison: Optional[date] = None - reference: Optional[str] = None - lignes: List[Dict] - - -class TransformationRequest(BaseModel): - numero_source: str - type_source: int - type_cible: int - - -class StatutRequest(BaseModel): - nouveau_statut: int - - -class TypeTiers(IntEnum): - """CT_Type - Type de tiers""" - CLIENT = 0 - FOURNISSEUR = 1 - SALARIE = 2 - AUTRE = 3 - - -class ClientCreateRequest(BaseModel): - - intitule: str = Field( - ..., - max_length=69, - description="Nom du client (CT_Intitule) - OBLIGATOIRE" - ) - - numero: Optional[str] = Field( - None, - max_length=17, - description="Numéro client CT_Num (auto si None)" - ) - - type_tiers: int = Field( - 0, - ge=0, - le=3, - description="CT_Type: 0=Client, 1=Fournisseur, 2=Salarié, 3=Autre" - ) - - qualite: Optional[str] = Field( - "CLI", - max_length=17, - description="CT_Qualite: CLI/FOU/SAL/DIV/AUT" - ) - - classement: Optional[str] = Field( - None, - max_length=17, - description="CT_Classement" - ) - - raccourci: Optional[str] = Field( - None, - max_length=7, - description="CT_Raccourci (7 chars max, unique)" - ) - - siret: Optional[str] = Field( - None, - max_length=15, - description="CT_Siret (14-15 chars)" - ) - - tva_intra: Optional[str] = Field( - None, - max_length=25, - description="CT_Identifiant (TVA intracommunautaire)" - ) - - code_naf: Optional[str] = Field( - None, - max_length=7, - description="CT_Ape (Code NAF/APE)" - ) - - contact: Optional[str] = Field( - None, - max_length=35, - description="CT_Contact (double affectation: client + adresse)" - ) - - adresse: Optional[str] = Field( - None, - max_length=35, - description="Adresse.Adresse" - ) - - complement: Optional[str] = Field( - None, - max_length=35, - description="Adresse.Complement" - ) - - code_postal: Optional[str] = Field( - None, - max_length=9, - description="Adresse.CodePostal" - ) - - ville: Optional[str] = Field( - None, - max_length=35, - description="Adresse.Ville" - ) - - region: Optional[str] = Field( - None, - max_length=25, - description="Adresse.CodeRegion" - ) - - pays: Optional[str] = Field( - None, - max_length=35, - description="Adresse.Pays" - ) - - telephone: Optional[str] = Field( - None, - max_length=21, - description="Telecom.Telephone" - ) - - telecopie: Optional[str] = Field( - None, - max_length=21, - description="Telecom.Telecopie (fax)" - ) - - email: Optional[str] = Field( - None, - max_length=69, - description="Telecom.EMail" - ) - - site_web: Optional[str] = Field( - None, - max_length=69, - description="Telecom.Site" - ) - - portable: Optional[str] = Field( - None, - max_length=21, - description="Telecom.Portable" - ) - - facebook: Optional[str] = Field( - None, - max_length=69, - description="Telecom.Facebook ou CT_Facebook" - ) - - linkedin: Optional[str] = Field( - None, - max_length=69, - description="Telecom.LinkedIn ou CT_LinkedIn" - ) - - compte_general: Optional[str] = Field( - None, - max_length=13, - description="CompteGPrinc (défaut selon type_tiers: 4110000, 4010000, 421, 471)" - ) - - categorie_tarifaire: Optional[str] = Field( - None, - description="N_CatTarif (ID catégorie tarifaire, défaut '0' ou '1')" - ) - - categorie_comptable: Optional[str] = Field( - None, - description="N_CatCompta (ID catégorie comptable, défaut '0' ou '1')" - ) - - taux01: Optional[float] = Field(None, description="CT_Taux01") - taux02: Optional[float] = Field(None, description="CT_Taux02") - taux03: Optional[float] = Field(None, description="CT_Taux03") - taux04: Optional[float] = Field(None, description="CT_Taux04") - - secteur: Optional[str] = Field( - None, - max_length=21, - description="Alias de statistique01 (CT_Statistique01)" - ) - - statistique01: Optional[str] = Field(None, max_length=21, description="CT_Statistique01") - statistique02: Optional[str] = Field(None, max_length=21, description="CT_Statistique02") - statistique03: Optional[str] = Field(None, max_length=21, description="CT_Statistique03") - statistique04: Optional[str] = Field(None, max_length=21, description="CT_Statistique04") - statistique05: Optional[str] = Field(None, max_length=21, description="CT_Statistique05") - statistique06: Optional[str] = Field(None, max_length=21, description="CT_Statistique06") - statistique07: Optional[str] = Field(None, max_length=21, description="CT_Statistique07") - statistique08: Optional[str] = Field(None, max_length=21, description="CT_Statistique08") - statistique09: Optional[str] = Field(None, max_length=21, description="CT_Statistique09") - statistique10: Optional[str] = Field(None, max_length=21, description="CT_Statistique10") - - encours_autorise: Optional[float] = Field( - None, - description="CT_Encours (montant max autorisé)" - ) - - assurance_credit: Optional[float] = Field( - None, - description="CT_Assurance (montant assurance crédit)" - ) - - langue: Optional[int] = Field( - None, - ge=0, - description="CT_Langue (0=Français, 1=Anglais, etc.)" - ) - - commercial_code: Optional[int] = Field( - None, - description="CO_No (ID du collaborateur commercial)" - ) - - lettrage_auto: Optional[bool] = Field( - True, - description="CT_Lettrage (1=oui, 0=non)" - ) - - est_actif: Optional[bool] = Field( - True, - description="Inverse de CT_Sommeil (True=actif, False=en sommeil)" - ) - - type_facture: Optional[int] = Field( - 1, - ge=0, - le=2, - description="CT_Facture: 0=aucune, 1=normale, 2=regroupée" - ) - - est_prospect: Optional[bool] = Field( - False, - description="CT_Prospect (1=oui, 0=non)" - ) - - bl_en_facture: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_BLFact (impression BL sur facture)" - ) - - saut_page: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_Saut (saut de page après impression)" - ) - - validation_echeance: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_ValidEch" - ) - - controle_encours: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_ControlEnc" - ) - - exclure_relance: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_NotRappel" - ) - - exclure_penalites: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_NotPenal" - ) - - bon_a_payer: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_BonAPayer" - ) - - priorite_livraison: Optional[int] = Field( - None, - ge=0, - le=5, - description="CT_PrioriteLivr" - ) - - livraison_partielle: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_LivrPartielle" - ) - - delai_transport: Optional[int] = Field( - None, - ge=0, - description="CT_DelaiTransport (jours)" - ) - - delai_appro: Optional[int] = Field( - None, - ge=0, - description="CT_DelaiAppro (jours)" - ) - - commentaire: Optional[str] = Field( - None, - max_length=35, - description="CT_Commentaire" - ) - - section_analytique: Optional[str] = Field( - None, - max_length=13, - description="CA_Num" - ) - - mode_reglement_code: Optional[int] = Field( - None, - description="MR_No (ID du mode de règlement)" - ) - - surveillance_active: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_Surveillance (DOIT être défini AVANT coface)" - ) - - coface: Optional[str] = Field( - None, - max_length=25, - description="CT_Coface (code Coface)" - ) - - forme_juridique: Optional[str] = Field( - None, - max_length=33, - description="CT_SvFormeJuri (SARL, SA, etc.)" - ) - - effectif: Optional[str] = Field( - None, - max_length=11, - description="CT_SvEffectif" - ) - - sv_regularite: Optional[str] = Field( - None, - max_length=3, - description="CT_SvRegul" - ) - - sv_cotation: Optional[str] = Field( - None, - max_length=5, - description="CT_SvCotation" - ) - - sv_objet_maj: Optional[str] = Field( - None, - max_length=61, - description="CT_SvObjetMaj" - ) - - ca_annuel: Optional[float] = Field( - None, - description="CT_SvCA (Chiffre d'affaires annuel) - alias: sv_chiffre_affaires" - ) - - sv_chiffre_affaires: Optional[float] = Field( - None, - description="CT_SvCA (alias de ca_annuel)" - ) - - sv_resultat: Optional[float] = Field( - None, - description="CT_SvResultat" - ) - - - @field_validator('siret') - @classmethod - def validate_siret(cls, v): - """Valide et nettoie le SIRET""" - if v and v.lower() not in ('none', 'null', ''): - cleaned = v.replace(' ', '').replace('-', '') - if len(cleaned) not in (14, 15): - raise ValueError('Le SIRET doit contenir 14 ou 15 caractères') - return cleaned - return None - - @field_validator('email') - @classmethod - def validate_email(cls, v): - """Valide le format email""" - if v and v.lower() not in ('none', 'null', ''): - v = v.strip() - if '@' not in v: - raise ValueError('Format email invalide') - return v - return None - - @field_validator('raccourci') - @classmethod - def validate_raccourci(cls, v): - """Force le raccourci en majuscules""" - if v and v.lower() not in ('none', 'null', ''): - return v.upper().strip()[:7] - return None - - @field_validator( - 'adresse', 'code_postal', 'ville', 'pays', 'telephone', - 'tva_intra', 'contact', 'complement', mode='before' - ) - @classmethod - def clean_none_strings(cls, v): - """Convertit les chaînes 'None'/'null'/'' en None""" - if isinstance(v, str) and v.lower() in ('none', 'null', ''): - return None - return v - - def to_sage_dict(self) -> dict: - """ - Convertit le modèle en dictionnaire compatible avec creer_client() - Mapping 1:1 avec les paramètres réels de la fonction - """ - stat01 = self.statistique01 or self.secteur - - ca = self.ca_annuel or self.sv_chiffre_affaires - - return { - "intitule": self.intitule, - "numero": self.numero, - "type_tiers": self.type_tiers, - "qualite": self.qualite, - "classement": self.classement, - "raccourci": self.raccourci, - - "siret": self.siret, - "tva_intra": self.tva_intra, - "code_naf": self.code_naf, - - "contact": self.contact, - "adresse": self.adresse, - "complement": self.complement, - "code_postal": self.code_postal, - "ville": self.ville, - "region": self.region, - "pays": self.pays, - - "telephone": self.telephone, - "telecopie": self.telecopie, - "email": self.email, - "site_web": self.site_web, - "portable": self.portable, - "facebook": self.facebook, - "linkedin": self.linkedin, - - "compte_general": self.compte_general, - - "categorie_tarifaire": self.categorie_tarifaire, - "categorie_comptable": self.categorie_comptable, - - "taux01": self.taux01, - "taux02": self.taux02, - "taux03": self.taux03, - "taux04": self.taux04, - - "statistique01": stat01, - "statistique02": self.statistique02, - "statistique03": self.statistique03, - "statistique04": self.statistique04, - "statistique05": self.statistique05, - "statistique06": self.statistique06, - "statistique07": self.statistique07, - "statistique08": self.statistique08, - "statistique09": self.statistique09, - "statistique10": self.statistique10, - "secteur": self.secteur, # Gardé pour compatibilité - - "encours_autorise": self.encours_autorise, - "assurance_credit": self.assurance_credit, - "langue": self.langue, - "commercial_code": self.commercial_code, - - "lettrage_auto": self.lettrage_auto, - "est_actif": self.est_actif, - "type_facture": self.type_facture, - "est_prospect": self.est_prospect, - "bl_en_facture": self.bl_en_facture, - "saut_page": self.saut_page, - "validation_echeance": self.validation_echeance, - "controle_encours": self.controle_encours, - "exclure_relance": self.exclure_relance, - "exclure_penalites": self.exclure_penalites, - "bon_a_payer": self.bon_a_payer, - - "priorite_livraison": self.priorite_livraison, - "livraison_partielle": self.livraison_partielle, - "delai_transport": self.delai_transport, - "delai_appro": self.delai_appro, - - "commentaire": self.commentaire, - - "section_analytique": self.section_analytique, - - "mode_reglement_code": self.mode_reglement_code, - - "surveillance_active": self.surveillance_active, - "coface": self.coface, - "forme_juridique": self.forme_juridique, - "effectif": self.effectif, - "sv_regularite": self.sv_regularite, - "sv_cotation": self.sv_cotation, - "sv_objet_maj": self.sv_objet_maj, - "ca_annuel": ca, - "sv_chiffre_affaires": self.sv_chiffre_affaires, - "sv_resultat": self.sv_resultat, - } - - class Config: - json_schema_extra = { - "example": { - "intitule": "ENTREPRISE EXEMPLE SARL", - "numero": "CLI00123", - "type_tiers": 0, - "qualite": "CLI", - "compte_general": "411000", - "est_prospect": False, - "est_actif": True, - "email": "contact@exemple.fr", - "telephone": "0123456789", - "adresse": "123 Rue de la Paix", - "code_postal": "75001", - "ville": "Paris", - "pays": "France" - } - } - - -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 - date_livraison: 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 - date_livraison_prevue: 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 - date_livraison: 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 - date_livraison: 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 - - -class MouvementStockLigneRequest(BaseModel): - article_ref: str = Field(..., description="Référence de l'article") - quantite: float = Field(..., gt=0, description="Quantité (>0)") - depot_code: Optional[str] = Field(None, description="Code du dépôt (ex: '01')") - prix_unitaire: Optional[float] = Field( - None, ge=0, description="Prix unitaire (optionnel)" - ) - commentaire: Optional[str] = Field(None, description="Commentaire ligne") - numero_lot: Optional[str] = Field( - None, description="Numéro de lot (pour FIFO/LIFO)" - ) - stock_mini: Optional[float] = Field( - None, - ge=0, - description="""Stock minimum à définir pour cet article. - Si fourni, met à jour AS_QteMini dans F_ARTSTOCK. - Laisser None pour ne pas modifier.""", - ) - stock_maxi: Optional[float] = Field( - None, - ge=0, - description="""Stock maximum à définir pour cet article. - Doit être > stock_mini si les deux sont fournis.""", - ) - - class Config: - schema_extra = { - "example": { - "article_ref": "ARTS-001", - "quantite": 50.0, - "depot_code": "01", - "prix_unitaire": 100.0, - "commentaire": "Réapprovisionnement", - "numero_lot": "LOT20241217", - "stock_mini": 10.0, - "stock_maxi": 200.0, - } - } - - @validator("stock_maxi") - def validate_stock_maxi(cls, v, values): - """Valide que stock_maxi > stock_mini si les deux sont fournis""" - if ( - v is not None - and "stock_mini" in values - and values["stock_mini"] is not None - ): - if v <= values["stock_mini"]: - raise ValueError( - "stock_maxi doit être strictement supérieur à stock_mini" - ) - return v - - -class EntreeStockRequest(BaseModel): - """Création d'un bon d'entrée en stock""" - - date_entree: Optional[date] = Field( - None, description="Date du mouvement (aujourd'hui par défaut)" - ) - reference: Optional[str] = Field(None, description="Référence externe") - depot_code: Optional[str] = Field( - None, description="Dépôt principal (si applicable)" - ) - lignes: List[MouvementStockLigneRequest] = Field( - ..., min_items=1, description="Lignes du mouvement" - ) - commentaire: Optional[str] = Field(None, description="Commentaire général") - - -class SortieStockRequest(BaseModel): - """Création d'un bon de sortie de stock""" - - date_sortie: Optional[date] = Field( - None, description="Date du mouvement (aujourd'hui par défaut)" - ) - reference: Optional[str] = Field(None, description="Référence externe") - depot_code: Optional[str] = Field( - None, description="Dépôt principal (si applicable)" - ) - lignes: List[MouvementStockLigneRequest] = Field( - ..., min_items=1, description="Lignes du mouvement" - ) - commentaire: Optional[str] = Field(None, description="Commentaire général") - - -class FamilleCreate(BaseModel): - """Modèle pour créer une famille d'articles""" - - code: str = Field(..., description="Code famille (max 18 car)", max_length=18) - intitule: str = Field(..., description="Intitulé (max 69 car)", max_length=69) - type: int = Field(0, description="0=Détail, 1=Total") - compte_achat: Optional[str] = Field( - None, description="Compte général d'achat (ex: 607000)" - ) - compte_vente: Optional[str] = Field( - None, description="Compte général de vente (ex: 707000)" - ) - - -class ContactCreateRequest(BaseModel): - """Requête de création de contact""" - numero: str - civilite: Optional[str] = None - nom: str - prenom: Optional[str] = None - fonction: Optional[str] = None - service_code: Optional[int] = None - telephone: Optional[str] = None - portable: Optional[str] = None - telecopie: Optional[str] = None - email: Optional[str] = None - facebook: Optional[str] = None - linkedin: Optional[str] = None - skype: Optional[str] = None - - -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 - - -class ContactDeleteRequest(BaseModel): - """Requête de suppression d'un contact""" - numero: str - contact_numero: int - def verify_token(x_sage_token: str = Header(...)): """Vérification du token d'authentification""" if x_sage_token != settings.sage_gateway_token: @@ -1171,9 +331,7 @@ def transformer_document( } if (type_source, type_cible) not in transformations_valides: - logger.error( - f" Transformation non autorisée: {type_source} → {type_cible}" - ) + 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}. " @@ -1389,7 +547,6 @@ def create_fournisseur_endpoint(req: FournisseurCreateRequest): @app.post("/sage/fournisseurs/update", dependencies=[Depends(verify_token)]) def modifier_fournisseur_endpoint(req: FournisseurUpdateGatewayRequest): - try: resultat = sage.modifier_fournisseur(req.code, req.fournisseur_data) return {"success": True, "data": resultat} @@ -1405,7 +562,7 @@ def modifier_fournisseur_endpoint(req: FournisseurUpdateGatewayRequest): @app.post("/sage/fournisseurs/get", dependencies=[Depends(verify_token)]) def fournisseur_get(req: CodeRequest): """ - NOUVEAU : Lecture d'un fournisseur par code + NOUVEAU : Lecture d'un fournisseur par code """ try: fournisseur = sage.lire_fournisseur(req.code) @@ -1527,8 +684,6 @@ def modifier_devis_endpoint(req: DevisUpdateGatewayRequest): raise HTTPException(500, str(e)) - - @app.post("/sage/commandes/create", dependencies=[Depends(verify_token)]) def creer_commande_endpoint(req: CommandeCreateRequest): try: @@ -1741,8 +896,6 @@ async def creer_famille(famille: FamilleCreate): raise HTTPException(status_code=500, detail=f"Erreur serveur : {str(e)}") - - @app.get( "/sage/familles", response_model=dict, @@ -1767,8 +920,6 @@ async def lister_familles(filtre: str = ""): raise HTTPException(status_code=500, detail=f"Erreur serveur : {str(e)}") - - @app.get( "/sage/familles/{code}", response_model=dict, @@ -1791,8 +942,6 @@ async def lire_famille(code: str): raise HTTPException(status_code=500, detail=f"Erreur serveur : {str(e)}") - - @app.get("/sage/familles/stats", response_model=dict) async def stats_familles(): try: @@ -2165,10 +1314,7 @@ def contacts_set_default(req: ContactGetRequest): def tiers_list(req: TiersListRequest): """Liste des tiers avec filtres optionnels""" try: - tiers = sage.lister_tous_tiers( - type_tiers=req.type_tiers, - filtre=req.filtre - ) + tiers = sage.lister_tous_tiers(type_tiers=req.type_tiers, filtre=req.filtre) return {"success": True, "data": tiers} except Exception as e: logger.error(f" Erreur liste tiers: {e}") @@ -2188,7 +1334,7 @@ def tiers_get(req: CodeRequest): except Exception as e: logger.error(f" Erreur lecture tiers: {e}") raise HTTPException(500, str(e)) - + if __name__ == "__main__": uvicorn.run( diff --git a/sage_connector.py b/sage_connector.py index 07d0b19..6697620 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -40,7 +40,6 @@ from utils.articles.articles_data_sql import ( from utils.tiers.clients.clients_data import ( _extraire_client, _cast_client, - ) from utils.articles.stock_check import verifier_stock_suffisant @@ -52,7 +51,7 @@ from utils.functions.functions import ( _safe_int, _clean_str, _try_set_attribute, - normaliser_date + normaliser_date, ) from utils.functions.items_to_dict import ( @@ -64,7 +63,7 @@ from utils.functions.items_to_dict import ( from utils.functions.sage_utilities import ( _verifier_devis_non_transforme, peut_etre_transforme, - lire_erreurs_sage + lire_erreurs_sage, ) from utils.documents.documents_data_sql import ( @@ -72,14 +71,11 @@ from utils.documents.documents_data_sql import ( _compter_lignes_document, _rechercher_devis_par_numero, _lire_document_sql, - _lister_documents_avec_lignes_sql + _lister_documents_avec_lignes_sql, ) from utils.documents.devis.devis_extraction import _extraire_infos_devis -from utils.documents.devis.devis_check import ( - _relire_devis, - _recuperer_numero_devis -) +from utils.documents.devis.devis_check import _relire_devis, _recuperer_numero_devis from utils.tiers.contacts.contacts import _get_contacts_client @@ -106,7 +102,6 @@ class SageConnector: self._lock_com = threading.RLock() self._thread_local = threading.local() - @contextmanager def _com_context(self): @@ -140,7 +135,6 @@ class SageConnector: if conn: conn.close() - def _cleanup_com_thread(self): """Nettoie COM pour le thread actuel (à appeler à la fin)""" if hasattr(self._thread_local, "com_initialized"): @@ -153,7 +147,6 @@ class SageConnector: except: pass - def connecter(self): """Connexion initiale à Sage - VERSION HYBRIDE""" try: @@ -192,7 +185,6 @@ class SageConnector: pass def lister_tous_fournisseurs(self, filtre=""): - try: with self._get_sql_connection() as conn: cursor = conn.cursor() @@ -274,7 +266,6 @@ class SageConnector: "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), @@ -282,19 +273,16 @@ class SageConnector: "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), @@ -305,12 +293,10 @@ class SageConnector: "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, @@ -322,16 +308,12 @@ class SageConnector: "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), @@ -342,17 +324,18 @@ class SageConnector: "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, } fournisseur["contacts"] = _get_contacts_client(row.CT_Num, conn) - + fournisseurs.append(fournisseur) - logger.info(f" SQL: {len(fournisseurs)} fournisseurs avec {len(fournisseur)} champs") + logger.info( + f" SQL: {len(fournisseurs)} fournisseurs avec {len(fournisseur)} champs" + ) return fournisseurs except Exception as e: @@ -434,7 +417,6 @@ class SageConnector: "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), @@ -442,19 +424,16 @@ class SageConnector: "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), @@ -465,12 +444,10 @@ class SageConnector: "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, @@ -482,16 +459,12 @@ class SageConnector: "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), @@ -502,21 +475,22 @@ class SageConnector: "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, } fournisseur["contacts"] = _get_contacts_client(row.CT_Num, conn) - - logger.info(f" SQL: Fournisseur {code_fournisseur} avec {len(fournisseur)} champs") + + logger.info( + f" SQL: Fournisseur {code_fournisseur} avec {len(fournisseur)} champs" + ) return fournisseur except Exception as e: logger.error(f" Erreur SQL fournisseur {code_fournisseur}: {e}") return None - + def creer_fournisseur(self, fournisseur_data: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -756,7 +730,6 @@ class SageConnector: "tva_intra": tva_intra or None, } - return resultat except ValueError as e: @@ -1049,7 +1022,6 @@ class SageConnector: "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), @@ -1057,19 +1029,16 @@ class SageConnector: "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), @@ -1080,12 +1049,10 @@ class SageConnector: "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, @@ -1097,16 +1064,12 @@ class SageConnector: "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), @@ -1117,14 +1080,13 @@ class SageConnector: "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, } client["contacts"] = _get_contacts_client(row.CT_Num, conn) - + clients.append(client) logger.info(f" SQL: {len(clients)} clients avec {len(client)} champs") @@ -1133,7 +1095,7 @@ class SageConnector: except Exception as e: logger.error(f" Erreur SQL clients: {e}") raise RuntimeError(f"Erreur lecture clients: {str(e)}") - + def lire_client(self, code_client): """ Lit un client avec TOUS les champs (identique à lister_tous_clients) @@ -1213,7 +1175,6 @@ class SageConnector: "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), @@ -1221,19 +1182,16 @@ class SageConnector: "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), @@ -1244,12 +1202,10 @@ class SageConnector: "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, @@ -1261,16 +1217,12 @@ class SageConnector: "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), @@ -1281,38 +1233,38 @@ class SageConnector: "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, } client["contacts"] = _get_contacts_client(row.CT_Num, conn) - + logger.info(f" SQL: Client {code_client} avec {len(client)} champs") return client except Exception as e: logger.error(f" Erreur SQL client {code_client}: {e}") return None - + def lister_tous_articles(self, filtre=""): try: with self._get_sql_connection() as conn: cursor = conn.cursor() - + logger.info("[SQL] Détection des colonnes de F_ARTICLE...") cursor.execute("SELECT TOP 1 * FROM F_ARTICLE") colonnes_disponibles = [column[0] for column in cursor.description] - logger.info(f"[SQL] Colonnes F_ARTICLE trouvées : {len(colonnes_disponibles)}") - + logger.info( + f"[SQL] Colonnes F_ARTICLE trouvées : {len(colonnes_disponibles)}" + ) + colonnes_config = { "AR_Ref": "reference", "AR_Design": "designation", "AR_CodeBarre": "code_barre", "AR_EdiCode": "edi_code", "AR_Raccourci": "raccourci", - "AR_PrixVen": "prix_vente", "AR_PrixAch": "prix_achat", "AR_Coef": "coef", @@ -1322,51 +1274,41 @@ class SageConnector: "AR_PrixVenNouv": "prix_vente_nouveau", "AR_DateApplication": "date_application_prix", "AR_CoutStd": "cout_standard", - "AR_UniteVen": "unite_vente", "AR_UnitePoids": "unite_poids", "AR_PoidsNet": "poids_net", "AR_PoidsBrut": "poids_brut", - "AR_Gamme1": "gamme_1", "AR_Gamme2": "gamme_2", - "FA_CodeFamille": "famille_code", "AR_Type": "type_article", "AR_Nature": "nature", "AR_Garantie": "garantie", "AR_CodeFiscal": "code_fiscal", "AR_Pays": "pays", - "CO_No": "fournisseur_principal", "AR_Condition": "conditionnement", "AR_NbColis": "nb_colis", "AR_Prevision": "prevision", - "AR_SuiviStock": "suivi_stock", "AR_Nomencl": "nomenclature", "AR_QteComp": "qte_composant", "AR_QteOperatoire": "qte_operatoire", - "AR_Sommeil": "sommeil", "AR_Substitut": "article_substitut", "AR_Escompte": "soumis_escompte", "AR_Delai": "delai", - "AR_Stat01": "stat_01", "AR_Stat02": "stat_02", "AR_Stat03": "stat_03", "AR_Stat04": "stat_04", "AR_Stat05": "stat_05", "AR_HorsStat": "hors_statistique", - "CL_No1": "categorie_1", "CL_No2": "categorie_2", "CL_No3": "categorie_3", "CL_No4": "categorie_4", - "AR_DateModif": "date_modification", - "AR_VteDebit": "vente_debit", "AR_NotImp": "non_imprimable", "AR_Transfere": "transfere", @@ -1378,22 +1320,18 @@ class SageConnector: "AR_Fictif": "fictif", "AR_SousTraitance": "sous_traitance", "AR_Criticite": "criticite", - "RP_CodeDefaut": "reprise_code_defaut", "AR_DelaiFabrication": "delai_fabrication", "AR_DelaiPeremption": "delai_peremption", "AR_DelaiSecurite": "delai_securite", "AR_TypeLancement": "type_lancement", "AR_Cycle": "cycle", - "AR_Photo": "photo", "AR_Langue1": "langue_1", "AR_Langue2": "langue_2", - "AR_Frais01FR_Denomination": "frais_01_denomination", "AR_Frais02FR_Denomination": "frais_02_denomination", "AR_Frais03FR_Denomination": "frais_03_denomination", - "Marque commerciale": "marque_commerciale", "Objectif / Qtés vendues": "objectif_qtes_vendues", "Pourcentage teneur en or": "pourcentage_or", @@ -1401,30 +1339,31 @@ class SageConnector: "AR_InterdireCommande": "interdire_commande", "AR_Exclure": "exclure", } - + colonnes_a_lire = [ - col_sql for col_sql in colonnes_config.keys() + col_sql + for col_sql in colonnes_config.keys() if col_sql in colonnes_disponibles ] - + if not colonnes_a_lire: logger.error("[SQL] Aucune colonne mappée trouvée !") colonnes_a_lire = ["AR_Ref", "AR_Design", "AR_PrixVen"] - + logger.info(f"[SQL] Colonnes sélectionnées : {len(colonnes_a_lire)}") - + colonnes_sql = [] for col in colonnes_a_lire: if " " in col or "/" in col or "è" in col: colonnes_sql.append(f"[{col}]") else: colonnes_sql.append(col) - + colonnes_str = ", ".join(colonnes_sql) query = f"SELECT {colonnes_str} FROM F_ARTICLE WHERE 1=1" - + params = [] - + if filtre: conditions = [] if "AR_Ref" in colonnes_a_lire: @@ -1436,20 +1375,20 @@ class SageConnector: if "AR_CodeBarre" in colonnes_a_lire: conditions.append("AR_CodeBarre LIKE ?") params.append(f"%{filtre}%") - + if conditions: query += " AND (" + " OR ".join(conditions) + ")" - + query += " ORDER BY AR_Ref" - + logger.debug(f"[SQL] Requête : {query[:200]}...") cursor.execute(query, params) rows = cursor.fetchall() - + logger.info(f"[SQL] {len(rows)} lignes récupérées") - + articles = [] - + for row in rows: row_data = {} for idx, col_sql in enumerate(colonnes_a_lire): @@ -1457,13 +1396,15 @@ class SageConnector: if isinstance(valeur, str): valeur = valeur.strip() row_data[col_sql] = valeur - + if "Marque commerciale" in row_data: - logger.debug(f"[DEBUG] Marque commerciale trouvée: {row_data['Marque commerciale']}") - + logger.debug( + f"[DEBUG] Marque commerciale trouvée: {row_data['Marque commerciale']}" + ) + article_data = _mapper_article_depuis_row(row_data, colonnes_config) articles.append(article_data) - + articles = _enrichir_stocks_articles(articles, cursor) articles = _enrichir_familles_articles(articles, cursor) articles = _enrichir_fournisseurs_articles(articles, cursor) @@ -1482,9 +1423,9 @@ class SageConnector: articles = _enrichir_medias_articles(articles, cursor) articles = _enrichir_prix_gammes(articles, cursor) articles = _enrichir_conditionnements(articles, cursor) - + return articles - + except Exception as e: logger.error(f" Erreur SQL articles: {e}", exc_info=True) raise RuntimeError(f"Erreur lecture articles: {str(e)}") @@ -1496,7 +1437,7 @@ class SageConnector: try: with self._get_sql_connection() as conn: cursor = conn.cursor() - + query = """ SELECT CT_Num, CT_No, N_Contact, @@ -1507,19 +1448,19 @@ class SageConnector: FROM F_CONTACTT WHERE CT_Num = ? AND CT_No = ? """ - + cursor.execute(query, [numero, contact_numero]) row = cursor.fetchone() - + if not row: return None - + return _row_to_contact_dict(row) - + except Exception as e: logger.error(f"Erreur obtention contact: {e}") raise RuntimeError(f"Erreur lecture contact: {str(e)}") - + def lister_tous_devis_cache(self, filtre=""): with self._get_sql_connection() as conn: cursor = conn.cursor() @@ -1809,26 +1750,30 @@ class SageConnector: except Exception as e: logger.error(f" ERREUR CRÉATION DEVIS: {e}", exc_info=True) raise RuntimeError(f"Échec création devis: {str(e)}") - + def modifier_devis(self, numero: str, devis_data: Dict) -> Dict: logger.info("=" * 100) logger.info("=" * 100) logger.info(f" NOUVELLE MÉTHODE modifier_devis() APPELÉE POUR {numero} ") logger.info(f" Données reçues: {devis_data}") logger.info("=" * 100) - + if not self.cial: logger.error(" Connexion Sage non établie") 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, + ): cursor = conn.cursor() logger.info("") logger.info("=" * 80) logger.info(f" [ÉTAPE 1] CHARGEMENT DU DEVIS {numero}") logger.info("=" * 80) - + doc = self._charger_devis(numero) logger.info(f" Devis {numero} chargé avec succès") @@ -1843,7 +1788,7 @@ class SageConnector: logger.info("=" * 80) logger.info(" [ÉTAPE 2] ANALYSE DOCUMENT ACTUEL") logger.info("=" * 80) - + client_code_initial = "" try: client_obj = getattr(doc, "Client", None) @@ -1863,34 +1808,38 @@ class SageConnector: logger.info("=" * 80) logger.info(" [ÉTAPE 3] ANALYSE MODIFICATIONS DEMANDÉES") logger.info("=" * 80) - + modif_date = "date_devis" in devis_data modif_date_livraison = "date_livraison" in devis_data modif_statut = "statut" in devis_data modif_ref = "reference" in devis_data - modif_lignes = "lignes" in devis_data and devis_data["lignes"] is not None + modif_lignes = ( + "lignes" in devis_data and devis_data["lignes"] is not None + ) logger.info(f" Date devis: {modif_date}") if modif_date: logger.info(f" → Valeur: {devis_data['date_devis']}") - + logger.info(f" Date livraison: {modif_date_livraison}") if modif_date_livraison: logger.info(f" → Valeur: {devis_data['date_livraison']}") - + logger.info(f" Référence: {modif_ref}") if modif_ref: logger.info(f" → Valeur: '{devis_data['reference']}'") - + logger.info(f" Statut: {modif_statut}") if modif_statut: logger.info(f" → Valeur: {devis_data['statut']}") - + logger.info(f" Lignes: {modif_lignes}") if modif_lignes: logger.info(f" → Nombre: {len(devis_data['lignes'])}") - for i, ligne in enumerate(devis_data['lignes'], 1): - logger.info(f" → Ligne {i}: {ligne.get('article_code')} (Qté: {ligne.get('quantite')})") + for i, ligne in enumerate(devis_data["lignes"], 1): + logger.info( + f" → Ligne {i}: {ligne.get('article_code')} (Qté: {ligne.get('quantite')})" + ) devis_data_temp = devis_data.copy() reference_a_modifier = None @@ -1898,8 +1847,10 @@ class SageConnector: if modif_lignes: logger.info("") - logger.info(" STRATÉGIE: Report référence/statut APRÈS modification lignes") - + logger.info( + " STRATÉGIE: Report référence/statut APRÈS modification lignes" + ) + if modif_ref: reference_a_modifier = devis_data_temp.pop("reference") logger.info(f" Référence '{reference_a_modifier}' reportée") @@ -1915,11 +1866,11 @@ class SageConnector: logger.info(" [ÉTAPE 4] TEST WRITE() BASIQUE") logger.info("=" * 80) logger.info("Test sans modification pour vérifier le verrouillage...") - + try: doc.Write() logger.info(" Write() basique OK - Document NON verrouillé") - + time.sleep(0.3) doc.Read() logger.info(" Read() après Write() OK") @@ -1930,7 +1881,9 @@ class SageConnector: champs_modifies = [] - if not modif_lignes and (modif_date or modif_date_livraison or modif_statut or modif_ref): + if not modif_lignes and ( + modif_date or modif_date_livraison or modif_statut or modif_ref + ): logger.info("") logger.info("=" * 80) logger.info(" [ÉTAPE 5A] MODIFICATIONS SIMPLES (sans lignes)") @@ -1941,18 +1894,26 @@ class SageConnector: logger.info(" Modification DATE_DEVIS...") try: ancienne_date = getattr(doc, "DO_Date", None) - ancienne_date_str = ancienne_date.strftime("%Y-%m-%d") if ancienne_date else "None" + ancienne_date_str = ( + ancienne_date.strftime("%Y-%m-%d") + if ancienne_date + else "None" + ) logger.info(f" Actuelle: {ancienne_date_str}") - - nouvelle_date = normaliser_date(devis_data_temp["date_devis"]) + + nouvelle_date = normaliser_date( + devis_data_temp["date_devis"] + ) nouvelle_date_str = nouvelle_date.strftime("%Y-%m-%d") logger.info(f" Cible: {nouvelle_date_str}") - + doc.DO_Date = pywintypes.Time(nouvelle_date) logger.info(" doc.DO_Date affecté") - + champs_modifies.append("date_devis") - logger.info(f" Date devis sera modifiée: {ancienne_date_str} → {nouvelle_date_str}") + logger.info( + f" Date devis sera modifiée: {ancienne_date_str} → {nouvelle_date_str}" + ) except Exception as e: logger.error(f" Erreur date devis: {e}", exc_info=True) @@ -1961,25 +1922,35 @@ class SageConnector: logger.info(" Modification DATE_LIVRAISON...") try: ancienne_date_livr = getattr(doc, "DO_DateLivr", None) - ancienne_date_livr_str = ancienne_date_livr.strftime("%Y-%m-%d") if ancienne_date_livr else "None" + ancienne_date_livr_str = ( + ancienne_date_livr.strftime("%Y-%m-%d") + if ancienne_date_livr + else "None" + ) logger.info(f" Actuelle: {ancienne_date_livr_str}") - + if devis_data_temp["date_livraison"]: - nouvelle_date_livr = normaliser_date(devis_data_temp["date_livraison"]) - nouvelle_date_livr_str = nouvelle_date_livr.strftime("%Y-%m-%d") + nouvelle_date_livr = normaliser_date( + devis_data_temp["date_livraison"] + ) + nouvelle_date_livr_str = nouvelle_date_livr.strftime( + "%Y-%m-%d" + ) logger.info(f" Cible: {nouvelle_date_livr_str}") - + doc.DO_DateLivr = pywintypes.Time(nouvelle_date_livr) logger.info(" doc.DO_DateLivr affecté") else: logger.info(" Cible: Effacement (None)") doc.DO_DateLivr = None logger.info(" doc.DO_DateLivr = None") - + champs_modifies.append("date_livraison") logger.info(" Date livraison sera modifiée") except Exception as e: - logger.error(f" Erreur date livraison: {e}", exc_info=True) + logger.error( + f" Erreur date livraison: {e}", exc_info=True + ) if modif_ref: logger.info("") @@ -1987,15 +1958,21 @@ class SageConnector: try: ancienne_ref = getattr(doc, "DO_Ref", "") logger.info(f" Actuelle: '{ancienne_ref}'") - - nouvelle_ref = str(devis_data_temp["reference"]) if devis_data_temp["reference"] else "" + + nouvelle_ref = ( + str(devis_data_temp["reference"]) + if devis_data_temp["reference"] + else "" + ) logger.info(f" Cible: '{nouvelle_ref}'") - + doc.DO_Ref = nouvelle_ref logger.info(" doc.DO_Ref affecté") - + champs_modifies.append("reference") - logger.info(f" Référence sera modifiée: '{ancienne_ref}' → '{nouvelle_ref}'") + logger.info( + f" Référence sera modifiée: '{ancienne_ref}' → '{nouvelle_ref}'" + ) except Exception as e: logger.error(f" Erreur référence: {e}", exc_info=True) @@ -2005,18 +1982,22 @@ class SageConnector: try: statut_actuel = getattr(doc, "DO_Statut", 0) logger.info(f" Actuel: {statut_actuel}") - + nouveau_statut = int(devis_data_temp["statut"]) logger.info(f" Cible: {nouveau_statut}") - + if nouveau_statut in [0, 1, 2, 3]: doc.DO_Statut = nouveau_statut logger.info(" doc.DO_Statut affecté") - + champs_modifies.append("statut") - logger.info(f" Statut sera modifié: {statut_actuel} → {nouveau_statut}") + logger.info( + f" Statut sera modifié: {statut_actuel} → {nouveau_statut}" + ) else: - logger.warning(f" Statut {nouveau_statut} invalide (doit être 0,1,2 ou 3)") + logger.warning( + f" Statut {nouveau_statut} invalide (doit être 0,1,2 ou 3)" + ) except Exception as e: logger.error(f" Erreur statut: {e}", exc_info=True) @@ -2025,7 +2006,7 @@ class SageConnector: try: doc.Write() logger.info(" Write() réussi") - + time.sleep(0.5) doc.Read() logger.info(" Read() après Write() OK") @@ -2042,10 +2023,14 @@ class SageConnector: if modif_date: logger.info(" Modification date devis (avant lignes)...") try: - nouvelle_date = normaliser_date(devis_data_temp["date_devis"]) + nouvelle_date = normaliser_date( + devis_data_temp["date_devis"] + ) doc.DO_Date = pywintypes.Time(nouvelle_date) champs_modifies.append("date_devis") - logger.info(f" Date: {nouvelle_date.strftime('%Y-%m-%d')}") + logger.info( + f" Date: {nouvelle_date.strftime('%Y-%m-%d')}" + ) except Exception as e: logger.error(f" Erreur: {e}") @@ -2053,9 +2038,13 @@ class SageConnector: logger.info(" Modification date livraison (avant lignes)...") try: if devis_data_temp["date_livraison"]: - nouvelle_date_livr = normaliser_date(devis_data_temp["date_livraison"]) + nouvelle_date_livr = normaliser_date( + devis_data_temp["date_livraison"] + ) doc.DO_DateLivr = pywintypes.Time(nouvelle_date_livr) - logger.info(f" Date livraison: {nouvelle_date_livr.strftime('%Y-%m-%d')}") + logger.info( + f" Date livraison: {nouvelle_date_livr.strftime('%Y-%m-%d')}" + ) else: doc.DO_DateLivr = None logger.info(" Date livraison effacée") @@ -2067,7 +2056,9 @@ class SageConnector: nb_nouvelles = len(nouvelles_lignes) logger.info("") - logger.info(f" Remplacement: {nb_lignes_initial} lignes → {nb_nouvelles} lignes") + logger.info( + f" Remplacement: {nb_lignes_initial} lignes → {nb_nouvelles} lignes" + ) try: factory_lignes = doc.FactoryDocumentLigne @@ -2078,17 +2069,23 @@ class SageConnector: if nb_lignes_initial > 0: logger.info("") - logger.info(f" Suppression de {nb_lignes_initial} lignes existantes...") + logger.info( + f" Suppression de {nb_lignes_initial} lignes existantes..." + ) for idx in range(nb_lignes_initial, 0, -1): try: ligne_p = factory_lignes.List(idx) if ligne_p: try: - ligne = win32com.client.CastTo(ligne_p, "IBODocumentLigne3") + ligne = win32com.client.CastTo( + ligne_p, "IBODocumentLigne3" + ) except: - ligne = win32com.client.CastTo(ligne_p, "IBODocumentVenteLigne3") - + ligne = win32com.client.CastTo( + ligne_p, "IBODocumentVenteLigne3" + ) + ligne.Read() ligne.Remove() logger.debug(f" Ligne {idx} supprimée") @@ -2103,50 +2100,74 @@ class SageConnector: for idx, ligne_data in enumerate(nouvelles_lignes, 1): article_code = ligne_data["article_code"] quantite = float(ligne_data["quantite"]) - + logger.info("") logger.info(f" Ligne {idx}/{nb_nouvelles}: {article_code}") logger.info(f" Quantité: {quantite}") if ligne_data.get("prix_unitaire_ht"): - logger.info(f" Prix HT: {ligne_data['prix_unitaire_ht']}€") + logger.info( + f" Prix HT: {ligne_data['prix_unitaire_ht']}€" + ) if ligne_data.get("remise_pourcentage"): - logger.info(f" Remise: {ligne_data['remise_pourcentage']}%") + logger.info( + f" Remise: {ligne_data['remise_pourcentage']}%" + ) try: - persist_article = factory_article.ReadReference(article_code) + persist_article = factory_article.ReadReference( + article_code + ) if not persist_article: raise ValueError(f"Article {article_code} INTROUVABLE") - article_obj = win32com.client.CastTo(persist_article, "IBOArticle3") + article_obj = win32com.client.CastTo( + persist_article, "IBOArticle3" + ) article_obj.Read() logger.info(f" Article chargé") ligne_persist = factory_lignes.Create() try: - ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3") + ligne_obj = win32com.client.CastTo( + ligne_persist, "IBODocumentLigne3" + ) except: - ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3") + ligne_obj = win32com.client.CastTo( + ligne_persist, "IBODocumentVenteLigne3" + ) try: - ligne_obj.SetDefaultArticleReference(article_code, quantite) - logger.info(f" Article associé via SetDefaultArticleReference") + ligne_obj.SetDefaultArticleReference( + article_code, quantite + ) + logger.info( + f" Article associé via SetDefaultArticleReference" + ) except: try: ligne_obj.SetDefaultArticle(article_obj, quantite) - logger.info(f" Article associé via SetDefaultArticle") + logger.info( + f" Article associé via SetDefaultArticle" + ) except: - ligne_obj.DL_Design = ligne_data.get("designation", "") + ligne_obj.DL_Design = ligne_data.get( + "designation", "" + ) ligne_obj.DL_Qte = quantite logger.info(f" Article associé manuellement") if ligne_data.get("prix_unitaire_ht"): - ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"]) + ligne_obj.DL_PrixUnitaire = float( + ligne_data["prix_unitaire_ht"] + ) logger.info(f" Prix unitaire défini") if ligne_data.get("remise_pourcentage", 0) > 0: try: - ligne_obj.DL_Remise01REM_Valeur = float(ligne_data["remise_pourcentage"]) + ligne_obj.DL_Remise01REM_Valeur = float( + ligne_data["remise_pourcentage"] + ) ligne_obj.DL_Remise01REM_Type = 0 logger.info(f" Remise définie") except: @@ -2156,7 +2177,9 @@ class SageConnector: logger.info(f" Ligne {idx} créée avec succès") except Exception as e: - logger.error(f" ERREUR ligne {idx}: {e}", exc_info=True) + logger.error( + f" ERREUR ligne {idx}: {e}", exc_info=True + ) raise logger.info("") @@ -2167,7 +2190,7 @@ class SageConnector: try: doc.Write() logger.info(" Write() réussi") - + time.sleep(0.5) doc.Read() logger.info(" Read() après Write() OK") @@ -2182,26 +2205,30 @@ class SageConnector: logger.info("=" * 80) logger.info(" [ÉTAPE 6] MODIFICATION RÉFÉRENCE (après lignes)") logger.info("=" * 80) - + try: ancienne_ref = getattr(doc, "DO_Ref", "") - nouvelle_ref = str(reference_a_modifier) if reference_a_modifier else "" - + nouvelle_ref = ( + str(reference_a_modifier) if reference_a_modifier else "" + ) + logger.info(f" Actuelle: '{ancienne_ref}'") logger.info(f" Cible: '{nouvelle_ref}'") doc.DO_Ref = nouvelle_ref logger.info(" doc.DO_Ref affecté") - + doc.Write() logger.info(" Write()") - + time.sleep(0.5) doc.Read() logger.info(" Read()") champs_modifies.append("reference") - logger.info(f" Référence modifiée: '{ancienne_ref}' → '{nouvelle_ref}'") + logger.info( + f" Référence modifiée: '{ancienne_ref}' → '{nouvelle_ref}'" + ) except Exception as e: logger.error(f" Erreur référence: {e}", exc_info=True) @@ -2210,29 +2237,38 @@ class SageConnector: logger.info("=" * 80) logger.info(" [ÉTAPE 7] MODIFICATION STATUT (en dernier)") logger.info("=" * 80) - + try: statut_actuel = getattr(doc, "DO_Statut", 0) nouveau_statut = int(statut_a_modifier) - + logger.info(f" Actuel: {statut_actuel}") logger.info(f" Cible: {nouveau_statut}") - if nouveau_statut != statut_actuel and nouveau_statut in [0, 1, 2, 3]: + if nouveau_statut != statut_actuel and nouveau_statut in [ + 0, + 1, + 2, + 3, + ]: doc.DO_Statut = nouveau_statut logger.info(" doc.DO_Statut affecté") - + doc.Write() logger.info(" Write()") - + time.sleep(0.5) doc.Read() logger.info(" Read()") champs_modifies.append("statut") - logger.info(f" Statut modifié: {statut_actuel} → {nouveau_statut}") + logger.info( + f" Statut modifié: {statut_actuel} → {nouveau_statut}" + ) else: - logger.info(f" Pas de modification (identique ou invalide)") + logger.info( + f" Pas de modification (identique ou invalide)" + ) except Exception as e: logger.error(f" Erreur statut: {e}", exc_info=True) @@ -2258,9 +2294,9 @@ class SageConnector: logger.info("=" * 80) logger.info(" [ÉTAPE 9] EXTRACTION RÉSULTAT") logger.info("=" * 80) - + resultat = _extraire_infos_devis(doc, numero, champs_modifies) - + logger.info(f" Résultat extrait:") logger.info(f" Numéro: {resultat['numero']}") logger.info(f" Référence: '{resultat['reference']}'") @@ -2289,9 +2325,9 @@ class SageConnector: def _charger_devis(self, numero: str): """Charge un devis depuis Sage.""" logger.info(f" Chargement devis {numero}...") - + factory = self.cial.FactoryDocumentVente - + logger.info(" Tentative ReadPiece(0, numero)...") persist = factory.ReadPiece(0, numero) @@ -2304,7 +2340,7 @@ class SageConnector: doc = win32com.client.CastTo(persist, "IBODocumentVente3") doc.Read() - + logger.info(f" Devis {numero} chargé") return doc @@ -2802,7 +2838,7 @@ class SageConnector: return _lire_document_sql(cursor, numero, type_doc=50) def lire_livraison(self, numero): - """ Lit UNE livraison via SQL (avec lignes)""" + """Lit UNE livraison via SQL (avec lignes)""" with self._get_sql_connection() as conn: cursor = conn.cursor() return _lire_document_sql(cursor, numero, type_doc=30) @@ -2810,49 +2846,51 @@ class SageConnector: def creer_contact(self, contact_data: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") - + try: with self._com_context(), self._lock_com: logger.info("=" * 80) logger.info("[CREATION CONTACT F_CONTACTT]") logger.info("=" * 80) - + if not contact_data.get("numero"): raise ValueError("numero (code client) obligatoire") if not contact_data.get("nom"): raise ValueError("nom obligatoire") - + numero_client = _clean_str(contact_data["numero"], 17).upper() nom = _clean_str(contact_data["nom"], 35) prenom = _clean_str(contact_data.get("prenom", ""), 35) - + logger.info(f" CLIENT: {numero_client}") logger.info(f" CONTACT: {prenom} {nom}") - + logger.info(f"[1] Chargement du client: {numero_client}") factory_client = self.cial.CptaApplication.FactoryClient try: persist_client = factory_client.ReadNumero(numero_client) if not persist_client: raise ValueError(f"Client {numero_client} non trouve") - + client_obj = win32com.client.CastTo(persist_client, "IBOClient3") client_obj.Read() logger.info(f" OK Client charge") except Exception as e: raise ValueError(f"Client {numero_client} introuvable: {e}") - + logger.info("[2] Creation via FactoryTiersContact") - - if not hasattr(client_obj, '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__}") - + contact = None interfaces_a_tester = [ "IBOTiersContact3", @@ -2860,46 +2898,48 @@ class SageConnector: "IBOContactT3", "IBOContactT", ] - + for interface_name in interfaces_a_tester: try: temp = win32com.client.CastTo(persist, interface_name) - - if hasattr(temp, '_prop_map_put_'): + + if hasattr(temp, "_prop_map_put_"): props = list(temp._prop_map_put_.keys()) logger.info(f" Test {interface_name}: props={props[:15]}") - - if 'Nom' in props or 'CT_Nom' in props: + + if "Nom" in props or "CT_Nom" in props: contact = temp logger.info(f" OK Cast reussi vers {interface_name}") break except Exception as e: logger.debug(f" {interface_name}: {str(e)[:50]}") - + 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" + ) + logger.info("[3] Configuration du contact") - - if hasattr(contact, '_prop_map_put_'): + + if hasattr(contact, "_prop_map_put_"): props = list(contact._prop_map_put_.keys()) logger.info(f" Proprietes disponibles: {props}") - + try: contact.Nom = nom logger.info(f" OK Nom = {nom}") except Exception as e: logger.error(f" ERROR Impossible de definir Nom: {e}") raise RuntimeError(f"Echec definition Nom: {e}") - + if prenom: try: contact.Prenom = prenom logger.info(f" OK Prenom = {prenom}") except Exception as e: logger.warning(f" WARN Prenom: {e}") - + if contact_data.get("civilite"): civilite_map = {"M.": 0, "Mme": 1, "Mlle": 2, "Societe": 3} civilite_code = civilite_map.get(contact_data["civilite"]) @@ -2909,7 +2949,7 @@ class SageConnector: logger.info(f" OK Civilite = {civilite_code}") except Exception as e: logger.warning(f" WARN Civilite: {e}") - + if contact_data.get("fonction"): fonction = _clean_str(contact_data["fonction"], 35) try: @@ -2917,48 +2957,48 @@ class SageConnector: logger.info(f" OK Fonction = {fonction}") except Exception as e: logger.warning(f" WARN Fonction: {e}") - + if contact_data.get("service_code") is not None: try: service = _safe_int(contact_data["service_code"]) - if service is not None and hasattr(contact, 'ServiceContact'): + if service is not None and hasattr(contact, "ServiceContact"): contact.ServiceContact = service logger.info(f" OK ServiceContact = {service}") except Exception as e: logger.warning(f" WARN ServiceContact: {e}") - + logger.info("[4] Coordonnees (Telecom)") - - if hasattr(contact, 'Telecom'): + + if hasattr(contact, "Telecom"): try: telecom = contact.Telecom logger.info(f" Type Telecom: {type(telecom).__name__}") - + if contact_data.get("telephone"): telephone = _clean_str(contact_data["telephone"], 21) if _try_set_attribute(telecom, "Telephone", telephone): logger.info(f" Telephone = {telephone}") - + if contact_data.get("portable"): portable = _clean_str(contact_data["portable"], 21) if _try_set_attribute(telecom, "Portable", portable): logger.info(f" Portable = {portable}") - + if contact_data.get("email"): email = _clean_str(contact_data["email"], 69) if _try_set_attribute(telecom, "EMail", email): logger.info(f" EMail = {email}") - + if contact_data.get("telecopie"): fax = _clean_str(contact_data["telecopie"], 21) if _try_set_attribute(telecom, "Telecopie", fax): logger.info(f" Telecopie = {fax}") - + except Exception as e: logger.warning(f" WARN Erreur Telecom: {e}") - + logger.info("[5] Reseaux sociaux") - + if contact_data.get("facebook"): facebook = _clean_str(contact_data["facebook"], 69) try: @@ -2966,7 +3006,7 @@ class SageConnector: logger.info(f" Facebook = {facebook}") except: pass - + if contact_data.get("linkedin"): linkedin = _clean_str(contact_data["linkedin"], 69) try: @@ -2974,7 +3014,7 @@ class SageConnector: logger.info(f" LinkedIn = {linkedin}") except: pass - + if contact_data.get("skype"): skype = _clean_str(contact_data["skype"], 69) try: @@ -2982,18 +3022,18 @@ class SageConnector: logger.info(f" Skype = {skype}") except: pass - + try: contact.SetDefault() logger.info(" OK SetDefault() applique") except Exception as e: logger.warning(f" WARN SetDefault(): {e}") - + logger.info("[6] Enregistrement du contact") try: contact.Write() logger.info(" OK Write() reussi") - + contact.Read() logger.info(" OK Read() reussi") except Exception as e: @@ -3001,63 +3041,67 @@ class SageConnector: try: sage_error = self.cial.CptaApplication.LastError if sage_error: - error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" + error_detail = ( + f"{sage_error.Description} (Code: {sage_error.Number})" + ) except: pass logger.error(f" ERROR Write: {error_detail}") raise RuntimeError(f"Echec enregistrement: {error_detail}") - + contact_no = None n_contact = None try: - contact_no = getattr(contact, 'CT_No', None) - n_contact = getattr(contact, 'N_Contact', None) + contact_no = getattr(contact, "CT_No", None) + n_contact = getattr(contact, "N_Contact", None) logger.info(f" Contact CT_No={contact_no}, N_Contact={n_contact}") except: pass - + 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") try: 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 logger.info(f" CT_Contact = '{nom_complet}'") - - if contact_no and hasattr(client_obj, 'CT_NoContact'): + + if contact_no and hasattr(client_obj, "CT_NoContact"): try: client_obj.CT_NoContact = contact_no logger.info(f" CT_NoContact = {contact_no}") except: pass - + client_obj.Write() client_obj.Read() logger.info(" OK Contact par defaut defini") except Exception as e: logger.warning(f" WARN Echec: {e}") est_defaut = False - + logger.info("=" * 80) logger.info(f"[SUCCES] Contact cree: {prenom} {nom}") logger.info(f" Lie au client {numero_client}") if contact_no: logger.info(f" CT_No={contact_no}") logger.info("=" * 80) - + contact_dict = _contact_to_dict( contact, numero_client=numero_client, contact_numero=contact_no, - n_contact=n_contact + n_contact=n_contact, ) contact_dict["est_defaut"] = est_defaut - + logger.info("=" * 80) logger.info("[DEBUG RETOUR]") logger.info(f" numero_client = {numero_client}") @@ -3065,9 +3109,9 @@ class SageConnector: logger.info(f" n_contact = {n_contact}") logger.info(f" contact_dict COMPLET = {contact_dict}") logger.info("=" * 80) - + return contact_dict - + except ValueError as e: logger.error(f"[ERREUR VALIDATION] {e}") raise @@ -3078,64 +3122,70 @@ class SageConnector: def modifier_contact(self, numero: str, contact_numero: int, updates: Dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") - + try: with self._com_context(), self._lock_com: logger.info("=" * 80) logger.info(f"[MODIFICATION CONTACT] CT_No={contact_numero}") logger.info("=" * 80) - + logger.info("[1] Chargement du client") factory_client = self.cial.CptaApplication.FactoryClient try: persist_client = factory_client.ReadNumero(numero) if not persist_client: raise ValueError(f"Client {numero} non trouve") - + client_obj = win32com.client.CastTo(persist_client, "IBOClient3") client_obj.Read() logger.info(f" OK Client charge") except Exception as e: raise ValueError(f"Client {numero} introuvable: {e}") - + logger.info("[2] Chargement du contact") - + nom_recherche = None prenom_recherche = None - + try: with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( "SELECT CT_Nom, CT_Prenom FROM F_CONTACTT WHERE CT_Num = ? AND CT_No = ?", - [numero, contact_numero] + [numero, contact_numero], ) row = cursor.fetchone() - + if not row: - raise ValueError(f"Contact CT_No={contact_numero} non trouve") - + raise ValueError( + f"Contact CT_No={contact_numero} non trouve" + ) + nom_recherche = row.CT_Nom.strip() if row.CT_Nom else "" - prenom_recherche = row.CT_Prenom.strip() if row.CT_Prenom else "" - - logger.info(f" Contact trouve en SQL: {prenom_recherche} {nom_recherche}") - + prenom_recherche = ( + row.CT_Prenom.strip() if row.CT_Prenom else "" + ) + + logger.info( + f" Contact trouve en SQL: {prenom_recherche} {nom_recherche}" + ) + except Exception as e: raise ValueError(f"Contact introuvable: {e}") - + factory_dossier = self.cial.CptaApplication.FactoryDossierContact persist = factory_dossier.ReadNomPrenom(nom_recherche, prenom_recherche) - + if not persist: raise ValueError(f"Contact non trouvable via ReadNomPrenom") - + contact = win32com.client.CastTo(persist, "IBOTiersContact3") contact.Read() logger.info(f" OK Contact charge: {contact.Nom}") - + logger.info("[3] Application des modifications") modifications_appliquees = [] - + if "civilite" in updates: civilite_map = {"M.": 0, "Mme": 1, "Mlle": 2, "Societe": 3} civilite_code = civilite_map.get(updates["civilite"]) @@ -3146,7 +3196,7 @@ class SageConnector: modifications_appliquees.append("civilite") except: pass - + if "nom" in updates: nom = _clean_str(updates["nom"], 35) if nom: @@ -3156,7 +3206,7 @@ class SageConnector: modifications_appliquees.append("nom") except: pass - + if "prenom" in updates: prenom = _clean_str(updates["prenom"], 35) try: @@ -3165,7 +3215,7 @@ class SageConnector: modifications_appliquees.append("prenom") except: pass - + if "fonction" in updates: fonction = _clean_str(updates["fonction"], 35) try: @@ -3174,39 +3224,39 @@ class SageConnector: modifications_appliquees.append("fonction") except: pass - + if "service_code" in updates: service = _safe_int(updates["service_code"]) - if service is not None and hasattr(contact, 'ServiceContact'): + if service is not None and hasattr(contact, "ServiceContact"): try: contact.ServiceContact = service logger.info(f" ServiceContact = {service}") modifications_appliquees.append("service_code") except: pass - - if hasattr(contact, 'Telecom'): + + if hasattr(contact, "Telecom"): try: telecom = contact.Telecom - + if "telephone" in updates: telephone = _clean_str(updates["telephone"], 21) if _try_set_attribute(telecom, "Telephone", telephone): logger.info(f" Telephone = {telephone}") modifications_appliquees.append("telephone") - + if "portable" in updates: portable = _clean_str(updates["portable"], 21) if _try_set_attribute(telecom, "Portable", portable): logger.info(f" Portable = {portable}") modifications_appliquees.append("portable") - + if "email" in updates: email = _clean_str(updates["email"], 69) if _try_set_attribute(telecom, "EMail", email): logger.info(f" EMail = {email}") modifications_appliquees.append("email") - + if "telecopie" in updates: fax = _clean_str(updates["telecopie"], 21) if _try_set_attribute(telecom, "Telecopie", fax): @@ -3214,7 +3264,7 @@ class SageConnector: modifications_appliquees.append("telecopie") except: pass - + if "facebook" in updates: facebook = _clean_str(updates["facebook"], 69) try: @@ -3223,7 +3273,7 @@ class SageConnector: modifications_appliquees.append("facebook") except: pass - + if "linkedin" in updates: linkedin = _clean_str(updates["linkedin"], 69) try: @@ -3232,7 +3282,7 @@ class SageConnector: modifications_appliquees.append("linkedin") except: pass - + if "skype" in updates: skype = _clean_str(updates["skype"], 69) try: @@ -3241,7 +3291,7 @@ class SageConnector: modifications_appliquees.append("skype") except: pass - + logger.info("[4] Enregistrement") try: contact.Write() @@ -3252,58 +3302,68 @@ class SageConnector: try: sage_error = self.cial.CptaApplication.LastError if sage_error: - error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" + error_detail = ( + f"{sage_error.Description} (Code: {sage_error.Number})" + ) except: pass logger.error(f" ERROR Write: {error_detail}") raise RuntimeError(f"Echec modification contact: {error_detail}") - - logger.info(f" Modifications appliquees: {', '.join(modifications_appliquees)}") - + + logger.info( + f" Modifications appliquees: {', '.join(modifications_appliquees)}" + ) + est_defaut_demande = updates.get("est_defaut") est_actuellement_defaut = False - + if est_defaut_demande is not None and est_defaut_demande: logger.info("[5] Gestion contact par defaut") try: - nom_complet = f"{contact.Prenom} {contact.Nom}".strip() if contact.Prenom else contact.Nom - + nom_complet = ( + f"{contact.Prenom} {contact.Nom}".strip() + if contact.Prenom + else contact.Nom + ) + 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 logger.info(f" CT_Contact = '{nom_complet}'") - - if hasattr(client_obj, 'CT_NoContact'): + + if hasattr(client_obj, "CT_NoContact"): try: client_obj.CT_NoContact = contact_numero logger.info(f" CT_NoContact = {contact_numero}") except: pass - + client_obj.Write() client_obj.Read() logger.info(" OK Contact par defaut defini") est_actuellement_defaut = True - + except Exception as e: logger.warning(f" WARN Echec: {e}") - + logger.info("=" * 80) logger.info(f"[SUCCES] Contact modifie: CT_No={contact_numero}") logger.info("=" * 80) - + contact_dict = _contact_to_dict( contact, numero_client=numero, contact_numero=contact_numero, - n_contact=None + n_contact=None, ) contact_dict["est_defaut"] = est_actuellement_defaut - + return contact_dict - + except ValueError as e: logger.error(f"[ERREUR VALIDATION] {e}") raise @@ -3314,66 +3374,74 @@ class SageConnector: def definir_contact_defaut(self, numero: str, contact_numero: int) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") - + try: with self._com_context(), self._lock_com: logger.info("=" * 80) - logger.info(f"[DEFINIR CONTACT PAR DEFAUT] Client={numero}, Contact={contact_numero}") + logger.info( + f"[DEFINIR CONTACT PAR DEFAUT] Client={numero}, Contact={contact_numero}" + ) logger.info("=" * 80) - + logger.info("[1] Recuperation infos contact") nom_contact = None prenom_contact = None - + try: with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( "SELECT CT_Nom, CT_Prenom FROM F_CONTACTT WHERE CT_Num = ? AND CT_No = ?", - [numero, contact_numero] + [numero, contact_numero], ) row = cursor.fetchone() - + if not row: - raise ValueError(f"Contact CT_No={contact_numero} non trouve") - + raise ValueError( + f"Contact CT_No={contact_numero} non trouve" + ) + nom_contact = row.CT_Nom.strip() if row.CT_Nom else "" prenom_contact = row.CT_Prenom.strip() if row.CT_Prenom else "" - - nom_complet = f"{prenom_contact} {nom_contact}".strip() if prenom_contact else nom_contact + + nom_complet = ( + f"{prenom_contact} {nom_contact}".strip() + if prenom_contact + else nom_contact + ) logger.info(f" OK Contact trouve: {nom_complet}") - + except Exception as e: raise ValueError(f"Contact introuvable: {e}") - + logger.info("[2] Chargement du client") factory_client = self.cial.CptaApplication.FactoryClient - + try: persist_client = factory_client.ReadNumero(numero) if not persist_client: raise ValueError(f"Client {numero} non trouve") - + client = win32com.client.CastTo(persist_client, "IBOClient3") client.Read() logger.info(f" OK Client charge: {client.CT_Intitule}") - + except Exception as e: raise ValueError(f"Client introuvable: {e}") - + logger.info("[3] Definition du contact par defaut") - + ancien_contact = getattr(client, "CT_Contact", "") client.CT_Contact = nom_complet logger.info(f" CT_Contact: '{ancien_contact}' -> '{nom_complet}'") - - if hasattr(client, 'CT_NoContact'): + + if hasattr(client, "CT_NoContact"): try: client.CT_NoContact = contact_numero logger.info(f" CT_NoContact = {contact_numero}") except: pass - + logger.info("[4] Enregistrement") try: client.Write() @@ -3384,24 +3452,26 @@ class SageConnector: try: sage_error = self.cial.CptaApplication.LastError if sage_error: - error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" + error_detail = ( + f"{sage_error.Description} (Code: {sage_error.Number})" + ) except: pass raise RuntimeError(f"Echec mise a jour: {error_detail}") - + logger.info("=" * 80) logger.info(f"[SUCCES] Contact par defaut: {nom_complet}") logger.info("=" * 80) - + return { "numero": numero, "contact_numero": contact_numero, "contact_nom": nom_complet, "client_intitule": client.CT_Intitule, "est_defaut": True, - "date_modification": datetime.now().isoformat() + "date_modification": datetime.now().isoformat(), } - + except ValueError as e: logger.error(f"[ERREUR VALIDATION] {e}") raise @@ -3416,57 +3486,61 @@ class SageConnector: except Exception as e: logger.error(f"Erreur liste contacts: {e}") raise RuntimeError(f"Erreur lecture contacts: {str(e)}") - + def supprimer_contact(self, numero: str, contact_numero: int) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") - + try: with self._com_context(), self._lock_com: logger.info("=" * 80) logger.info(f"[SUPPRESSION CONTACT] CT_No={contact_numero}") logger.info("=" * 80) - + logger.info("[1] Recuperation infos contact") nom_contact = None prenom_contact = None - + try: with self._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( "SELECT CT_Nom, CT_Prenom FROM F_CONTACTT WHERE CT_Num = ? AND CT_No = ?", - [numero, contact_numero] + [numero, contact_numero], ) row = cursor.fetchone() - + if not row: - raise ValueError(f"Contact CT_No={contact_numero} non trouve") - + raise ValueError( + f"Contact CT_No={contact_numero} non trouve" + ) + nom_contact = row.CT_Nom.strip() if row.CT_Nom else "" prenom_contact = row.CT_Prenom.strip() if row.CT_Prenom else "" - - logger.info(f" OK Contact trouve: {prenom_contact} {nom_contact}") - + + logger.info( + f" OK Contact trouve: {prenom_contact} {nom_contact}" + ) + except Exception as e: raise ValueError(f"Contact introuvable: {e}") - + logger.info("[2] Chargement du contact") factory_dossier = self.cial.CptaApplication.FactoryDossierContact - + try: persist = factory_dossier.ReadNomPrenom(nom_contact, prenom_contact) - + if not persist: raise ValueError(f"Contact non trouvable via ReadNomPrenom") - + contact = win32com.client.CastTo(persist, "IBOTiersContact3") contact.Read() logger.info(f" OK Contact charge: {contact.Nom}") - + except Exception as e: raise ValueError(f"Contact introuvable: {e}") - + logger.info("[3] Suppression") try: contact.Remove() @@ -3476,25 +3550,29 @@ class SageConnector: try: sage_error = self.cial.CptaApplication.LastError if sage_error: - error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" + error_detail = ( + f"{sage_error.Description} (Code: {sage_error.Number})" + ) except: pass logger.error(f" ERROR Remove: {error_detail}") raise RuntimeError(f"Echec suppression contact: {error_detail}") - + logger.info("=" * 80) - logger.info(f"[SUCCES] Contact supprime: {prenom_contact} {nom_contact}") + logger.info( + f"[SUCCES] Contact supprime: {prenom_contact} {nom_contact}" + ) logger.info("=" * 80) - + return { "numero": numero, "contact_numero": contact_numero, "nom": nom_contact, "prenom": prenom_contact, "supprime": True, - "date_suppression": datetime.now().isoformat() + "date_suppression": datetime.now().isoformat(), } - + except ValueError as e: logger.error(f"[ERREUR VALIDATION] {e}") raise @@ -3516,10 +3594,10 @@ class SageConnector: logger.info("=" * 80) def clean_str(value, max_len: int) -> str: - 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() - + def safe_int(value, default=None): if value is None: return default @@ -3527,7 +3605,7 @@ class SageConnector: return int(value) except (ValueError, TypeError): return default - + def safe_float(value, default=None): if value is None: return default @@ -3535,14 +3613,14 @@ class SageConnector: return float(value) except (ValueError, TypeError): return default - + def try_set_attribute(obj, attr_name, value, variants=None): """Essaie de definir un attribut avec plusieurs variantes de noms""" if variants is None: variants = [attr_name] else: variants = [attr_name] + variants - + for variant in variants: try: if hasattr(obj, variant): @@ -3551,12 +3629,12 @@ class SageConnector: return True except Exception as e: logger.debug(f" {variant} echec: {str(e)[:50]}") - + return False if not client_data.get("intitule"): raise ValueError("intitule obligatoire") - + if not client_data.get("numero"): raise ValueError("numero obligatoire") @@ -3565,28 +3643,28 @@ class SageConnector: type_tiers = safe_int(client_data.get("type_tiers"), 0) logger.info("[ETAPE 1] CREATION OBJET") - + factory_map = { 0: ("FactoryClient", "IBOClient3"), 1: ("FactoryFourniss", "IBOFournisseur3"), 2: ("FactorySalarie", "IBOSalarie3"), 3: ("FactoryAutre", "IBOAutre3"), } - + factory_name, interface_name = factory_map[type_tiers] factory = getattr(self.cial.CptaApplication, factory_name) persist = factory.Create() client = win32com.client.CastTo(persist, interface_name) - + logger.info(f" Objet cree: {interface_name}") logger.info("[ETAPE 2] CONFIGURATION OBLIGATOIRE") - + client.CT_Intitule = intitule client.CT_Num = numero logger.info(f" CT_Num = {numero}") logger.info(f" CT_Intitule = {intitule}") - + qualite = clean_str(client_data.get("qualite", "CLI"), 17) if qualite: client.CT_Qualite = qualite @@ -3597,13 +3675,15 @@ class SageConnector: if client_data.get("raccourci"): raccourci = clean_str(client_data["raccourci"], 7).upper().strip() - + try: factory_client = self.cial.CptaApplication.FactoryClient exist_client = factory_client.ReadRaccourci(raccourci) - + if exist_client: - logger.warning(f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]") + logger.warning( + f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]" + ) else: client.CT_Raccourci = raccourci logger.info(f" CT_Raccourci = {raccourci} [OK]") @@ -3612,10 +3692,12 @@ class SageConnector: client.CT_Raccourci = raccourci logger.info(f" CT_Raccourci = {raccourci} [OK]") except Exception as e2: - logger.warning(f" CT_Raccourci = {raccourci} [ECHEC: {e2}]") - + logger.warning( + f" CT_Raccourci = {raccourci} [ECHEC: {e2}]" + ) + try: - if not hasattr(client, 'CT_Type') or client.CT_Type is None: + 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: @@ -3623,37 +3705,48 @@ class SageConnector: COMPTES_DEFAUT = {0: "4110000", 1: "4010000", 2: "421", 3: "471"} compte = clean_str( - client_data.get("compte_general") or COMPTES_DEFAUT.get(type_tiers, "4110000"), - 13 + client_data.get("compte_general") + or COMPTES_DEFAUT.get(type_tiers, "4110000"), + 13, ) - + factory_compte = self.cial.CptaApplication.FactoryCompteG compte_trouve = False - + comptes_a_tester = [ compte, COMPTES_DEFAUT.get(type_tiers, "4110000"), - "4110000", "411000", "411", # Clients - "4010000", "401000", "401", # Fournisseurs + "4110000", + "411000", + "411", # Clients + "4010000", + "401000", + "401", # Fournisseurs ] for test_compte in comptes_a_tester: try: persist_compte = factory_compte.ReadNumero(test_compte) if persist_compte: - compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3") + compte_obj = win32com.client.CastTo( + persist_compte, "IBOCompteG3" + ) compte_obj.Read() - - type_compte = getattr(compte_obj, 'CG_Type', None) - + + type_compte = getattr(compte_obj, "CG_Type", None) + if type_compte == 0: client.CompteGPrinc = compte_obj compte = test_compte compte_trouve = True - logger.info(f" CompteGPrinc = {test_compte} (Type: {type_compte}) [OK]") + logger.info( + f" CompteGPrinc = {test_compte} (Type: {type_compte}) [OK]" + ) break else: - logger.debug(f" Compte {test_compte} - Type {type_compte} incompatible") + logger.debug( + f" Compte {test_compte} - Type {type_compte} incompatible" + ) except Exception as e: logger.debug(f" Compte {test_compte} - erreur: {e}") @@ -3661,14 +3754,16 @@ class SageConnector: raise RuntimeError("Aucun compte general valide trouve") logger.info(" Configuration categories:") - + try: factory_cat_tarif = self.cial.CptaApplication.FactoryCategorieTarif for cat_id in ["0", "1"]: try: persist_cat = factory_cat_tarif.ReadIntitule(cat_id) if persist_cat: - cat_tarif_obj = win32com.client.CastTo(persist_cat, "IBOCategorieTarif3") + cat_tarif_obj = win32com.client.CastTo( + persist_cat, "IBOCategorieTarif3" + ) cat_tarif_obj.Read() client.CatTarif = cat_tarif_obj logger.info(f" CatTarif = {cat_id} [OK]") @@ -3679,12 +3774,16 @@ class SageConnector: logger.warning(f" CatTarif erreur: {e}") try: - factory_cat_compta = self.cial.CptaApplication.FactoryCategorieCompta + factory_cat_compta = ( + self.cial.CptaApplication.FactoryCategorieCompta + ) for cat_id in ["0", "1"]: try: persist_cat = factory_cat_compta.ReadIntitule(cat_id) if persist_cat: - cat_compta_obj = win32com.client.CastTo(persist_cat, "IBOCategorieCompta3") + cat_compta_obj = win32com.client.CastTo( + persist_cat, "IBOCategorieCompta3" + ) cat_compta_obj.Read() client.CatCompta = cat_compta_obj logger.info(f" CatCompta = {cat_id} [OK]") @@ -3695,34 +3794,46 @@ class SageConnector: logger.warning(f" CatCompta erreur: {e}") logger.info("[ETAPE 3] IDENTIFICATION") - + if client_data.get("classement"): - try_set_attribute(client, "CT_Classement", clean_str(client_data["classement"], 17)) + try_set_attribute( + client, + "CT_Classement", + clean_str(client_data["classement"], 17), + ) if client_data.get("raccourci"): raccourci = clean_str(client_data["raccourci"], 7).upper() try_set_attribute(client, "CT_Raccourci", raccourci) if client_data.get("siret"): - try_set_attribute(client, "CT_Siret", clean_str(client_data["siret"], 15)) - + try_set_attribute( + client, "CT_Siret", clean_str(client_data["siret"], 15) + ) + if client_data.get("tva_intra"): - try_set_attribute(client, "CT_Identifiant", clean_str(client_data["tva_intra"], 25)) - + try_set_attribute( + client, + "CT_Identifiant", + clean_str(client_data["tva_intra"], 25), + ) + if client_data.get("code_naf"): - try_set_attribute(client, "CT_Ape", clean_str(client_data["code_naf"], 7)) + try_set_attribute( + client, "CT_Ape", clean_str(client_data["code_naf"], 7) + ) logger.info("[ETAPE 4] ADRESSE") if client_data.get("contact"): contact_nom = clean_str(client_data["contact"], 35) - + try: client.CT_Contact = contact_nom logger.info(f" CT_Contact (client) = {contact_nom} [OK]") except Exception as e: logger.warning(f" CT_Contact (client) [ECHEC: {e}]") - + try: adresse_obj = client.Adresse adresse_obj.Contact = contact_nom @@ -3733,25 +3844,29 @@ class SageConnector: try: adresse_obj = client.Adresse logger.info(" Objet Adresse OK") - + if client_data.get("adresse"): adresse_obj.Adresse = clean_str(client_data["adresse"], 35) - + if client_data.get("complement"): - adresse_obj.Complement = clean_str(client_data["complement"], 35) - + adresse_obj.Complement = clean_str( + client_data["complement"], 35 + ) + if client_data.get("code_postal"): - adresse_obj.CodePostal = clean_str(client_data["code_postal"], 9) - + adresse_obj.CodePostal = clean_str( + client_data["code_postal"], 9 + ) + if client_data.get("ville"): adresse_obj.Ville = clean_str(client_data["ville"], 35) - + if client_data.get("region"): adresse_obj.CodeRegion = clean_str(client_data["region"], 25) - + if client_data.get("pays"): adresse_obj.Pays = clean_str(client_data["pays"], 35) - + except Exception as e: logger.error(f" Adresse erreur: {e}") @@ -3760,91 +3875,123 @@ class SageConnector: try: telecom_obj = client.Telecom logger.info(" Objet Telecom OK") - + if client_data.get("telephone"): telecom_obj.Telephone = clean_str(client_data["telephone"], 21) - + if client_data.get("telecopie"): telecom_obj.Telecopie = clean_str(client_data["telecopie"], 21) - + if client_data.get("email"): telecom_obj.EMail = clean_str(client_data["email"], 69) - + if client_data.get("site_web"): telecom_obj.Site = clean_str(client_data["site_web"], 69) - + if client_data.get("portable"): portable = clean_str(client_data["portable"], 21) try_set_attribute(telecom_obj, "Portable", portable) logger.info(f" Portable = {portable}") - + if client_data.get("facebook"): - facebook = clean_str(client_data["facebook"], 69) # URL ou @username + 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}") - + if client_data.get("linkedin"): - linkedin = clean_str(client_data["linkedin"], 69) # URL ou profil + 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}") - + except Exception as e: logger.error(f" Telecom erreur: {e}") logger.info("[ETAPE 6] TAUX") - + for i in range(1, 5): val = client_data.get(f"taux{i:02d}") if val is not None: try_set_attribute(client, f"CT_Taux{i:02d}", safe_float(val)) logger.info("[ETAPE 7] STATISTIQUES") - + stat01 = client_data.get("statistique01") or client_data.get("secteur") if stat01: try_set_attribute(client, "CT_Statistique01", clean_str(stat01, 21)) - + for i in range(2, 11): val = client_data.get(f"statistique{i:02d}") if val: - try_set_attribute(client, f"CT_Statistique{i:02d}", clean_str(val, 21)) + try_set_attribute( + client, f"CT_Statistique{i:02d}", clean_str(val, 21) + ) logger.info("[ETAPE 8] COMMERCIAL") - + if client_data.get("encours_autorise"): - try_set_attribute(client, "CT_Encours", safe_float(client_data["encours_autorise"])) - + try_set_attribute( + client, + "CT_Encours", + safe_float(client_data["encours_autorise"]), + ) + if client_data.get("assurance_credit"): - try_set_attribute(client, "CT_Assurance", safe_float(client_data["assurance_credit"])) - + try_set_attribute( + client, + "CT_Assurance", + safe_float(client_data["assurance_credit"]), + ) + if client_data.get("langue") is not None: - try_set_attribute(client, "CT_Langue", safe_int(client_data["langue"])) - + try_set_attribute( + client, "CT_Langue", safe_int(client_data["langue"]) + ) + if client_data.get("commercial_code") is not None: co_no = safe_int(client_data["commercial_code"]) if not try_set_attribute(client, "CO_No", co_no): try: - factory_collab = self.cial.CptaApplication.FactoryCollaborateur + factory_collab = ( + self.cial.CptaApplication.FactoryCollaborateur + ) persist_collab = factory_collab.ReadIntitule(str(co_no)) if persist_collab: - collab_obj = win32com.client.CastTo(persist_collab, "IBOCollaborateur3") + collab_obj = win32com.client.CastTo( + persist_collab, "IBOCollaborateur3" + ) collab_obj.Read() client.Collaborateur = collab_obj - logger.debug(f" Collaborateur (objet) = {co_no} [OK]") + logger.debug( + f" Collaborateur (objet) = {co_no} [OK]" + ) except Exception as e: logger.debug(f" Collaborateur echec: {e}") logger.info("[ETAPE 9] FACTURATION") - - try_set_attribute(client, "CT_Lettrage", 1 if client_data.get("lettrage_auto", True) else 0) - try_set_attribute(client, "CT_Sommeil", 0 if client_data.get("est_actif", True) else 1) - try_set_attribute(client, "CT_Facture", safe_int(client_data.get("type_facture", 1))) - + + try_set_attribute( + client, + "CT_Lettrage", + 1 if client_data.get("lettrage_auto", True) else 0, + ) + try_set_attribute( + client, "CT_Sommeil", 0 if client_data.get("est_actif", True) else 1 + ) + try_set_attribute( + client, "CT_Facture", safe_int(client_data.get("type_facture", 1)) + ) + if client_data.get("est_prospect") is not None: - try_set_attribute(client, "CT_Prospect", 1 if client_data["est_prospect"] else 0) - + try_set_attribute( + client, "CT_Prospect", 1 if client_data["est_prospect"] else 0 + ) + factu_map = { "CT_BLFact": "bl_en_facture", "CT_Saut": "saut_page", @@ -3859,7 +4006,7 @@ class SageConnector: try_set_attribute(client, attr, safe_int(client_data[key])) logger.info("[ETAPE 10] LOGISTIQUE") - + logistique_map = { "CT_PrioriteLivr": "priorite_livraison", "CT_LivrPartielle": "livraison_partielle", @@ -3871,15 +4018,23 @@ class SageConnector: try_set_attribute(client, attr, safe_int(client_data[key])) if client_data.get("commentaire"): - try_set_attribute(client, "CT_Commentaire", clean_str(client_data["commentaire"], 35)) + try_set_attribute( + client, + "CT_Commentaire", + clean_str(client_data["commentaire"], 35), + ) logger.info("[ETAPE 12] ANALYTIQUE") - + if client_data.get("section_analytique"): - try_set_attribute(client, "CA_Num", clean_str(client_data["section_analytique"], 13)) + try_set_attribute( + client, + "CA_Num", + clean_str(client_data["section_analytique"], 13), + ) logger.info("[ETAPE 13] ORGANISATION") - + if client_data.get("mode_reglement_code") is not None: mr_no = safe_int(client_data["mode_reglement_code"]) if not try_set_attribute(client, "MR_No", mr_no): @@ -3887,7 +4042,9 @@ class SageConnector: factory_mr = self.cial.CptaApplication.FactoryModeRegl persist_mr = factory_mr.ReadIntitule(str(mr_no)) if persist_mr: - mr_obj = win32com.client.CastTo(persist_mr, "IBOModeRegl3") + mr_obj = win32com.client.CastTo( + persist_mr, "IBOModeRegl3" + ) mr_obj.Read() client.ModeRegl = mr_obj logger.debug(f" ModeRegl (objet) = {mr_no} [OK]") @@ -3911,33 +4068,58 @@ class SageConnector: logger.warning(f" CT_Coface [ECHEC: {e}]") if client_data.get("forme_juridique"): - try_set_attribute(client, "CT_SvFormeJuri", clean_str(client_data["forme_juridique"], 33)) + try_set_attribute( + client, + "CT_SvFormeJuri", + clean_str(client_data["forme_juridique"], 33), + ) if client_data.get("effectif"): - try_set_attribute(client, "CT_SvEffectif", clean_str(client_data["effectif"], 11)) + try_set_attribute( + client, "CT_SvEffectif", clean_str(client_data["effectif"], 11) + ) if client_data.get("sv_regularite"): - try_set_attribute(client, "CT_SvRegul", clean_str(client_data["sv_regularite"], 3)) + try_set_attribute( + client, "CT_SvRegul", clean_str(client_data["sv_regularite"], 3) + ) if client_data.get("sv_cotation"): - try_set_attribute(client, "CT_SvCotation", clean_str(client_data["sv_cotation"], 5)) + try_set_attribute( + client, + "CT_SvCotation", + clean_str(client_data["sv_cotation"], 5), + ) if client_data.get("sv_objet_maj"): - try_set_attribute(client, "CT_SvObjetMaj", clean_str(client_data["sv_objet_maj"], 61)) + try_set_attribute( + client, + "CT_SvObjetMaj", + clean_str(client_data["sv_objet_maj"], 61), + ) - ca = client_data.get("ca_annuel") or client_data.get("sv_chiffre_affaires") + ca = client_data.get("ca_annuel") or client_data.get( + "sv_chiffre_affaires" + ) if ca: try_set_attribute(client, "CT_SvCA", safe_float(ca)) if client_data.get("sv_resultat"): - try_set_attribute(client, "CT_SvResultat", safe_float(client_data["sv_resultat"])) - + try_set_attribute( + client, "CT_SvResultat", safe_float(client_data["sv_resultat"]) + ) + logger.info("=" * 80) logger.info("[DIAGNOSTIC PRE-WRITE]") - + champs_diagnostic = [ - 'CT_Num', 'CT_Intitule', 'CT_Type', 'CT_Qualite', - 'CT_Facture', 'CT_Lettrage', 'CT_Sommeil', + "CT_Num", + "CT_Intitule", + "CT_Type", + "CT_Qualite", + "CT_Facture", + "CT_Lettrage", + "CT_Sommeil", ] for champ in champs_diagnostic: @@ -3960,7 +4142,7 @@ class SageConnector: logger.info("=" * 80) logger.info("[WRITE]") - + try: client.Write() client.Read() @@ -3970,15 +4152,17 @@ class SageConnector: try: sage_error = self.cial.CptaApplication.LastError if sage_error: - error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" + error_detail = ( + f"{sage_error.Description} (Code: {sage_error.Number})" + ) except: pass - + logger.error(f"[ERREUR] {error_detail}") raise RuntimeError(f"Echec Write(): {error_detail}") num_final = getattr(client, "CT_Num", numero) - + logger.info("=" * 80) logger.info(f"[SUCCES] CLIENT CREE: {num_final}") logger.info("=" * 80) @@ -3998,7 +4182,7 @@ class SageConnector: except Exception as e: logger.error(f"[ERREUR] {e}", exc_info=True) raise RuntimeError(f"Erreur technique: {e}") - + def modifier_client(self, code: str, client_data: Dict) -> Dict: """ Modification client Sage - Version complète alignée sur creer_client @@ -4013,10 +4197,10 @@ class SageConnector: logger.info("=" * 80) def clean_str(value, max_len: int) -> str: - 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() - + def safe_int(value, default=None): if value is None: return default @@ -4024,7 +4208,7 @@ class SageConnector: return int(value) except (ValueError, TypeError): return default - + def safe_float(value, default=None): if value is None: return default @@ -4032,14 +4216,14 @@ class SageConnector: return float(value) except (ValueError, TypeError): return default - + def try_set_attribute(obj, attr_name, value, variants=None): """Essaie de définir un attribut avec plusieurs variantes de noms""" if variants is None: variants = [attr_name] else: variants = [attr_name] + variants - + for variant in variants: try: if hasattr(obj, variant): @@ -4048,13 +4232,13 @@ class SageConnector: return True except Exception as e: logger.debug(f" {variant} echec: {str(e)[:50]}") - + return False champs_modifies = [] logger.info("[ETAPE 1] CHARGEMENT CLIENT") - + factory_client = self.cial.CptaApplication.FactoryClient persist = factory_client.ReadNumero(code) @@ -4067,64 +4251,88 @@ class SageConnector: logger.info(f" Client chargé: {getattr(client, 'CT_Intitule', '')}") logger.info("[ETAPE 2] IDENTIFICATION") - + if "intitule" in client_data: intitule = clean_str(client_data["intitule"], 69) client.CT_Intitule = intitule champs_modifies.append("intitule") logger.info(f" CT_Intitule = {intitule}") - + if "qualite" in client_data: qualite = clean_str(client_data["qualite"], 17) if try_set_attribute(client, "CT_Qualite", qualite): champs_modifies.append("qualite") - + if "classement" in client_data: - if try_set_attribute(client, "CT_Classement", clean_str(client_data["classement"], 17)): + if try_set_attribute( + client, + "CT_Classement", + clean_str(client_data["classement"], 17), + ): champs_modifies.append("classement") - + if "raccourci" in client_data: raccourci = clean_str(client_data["raccourci"], 7).upper() - + try: exist_client = factory_client.ReadRaccourci(raccourci) if exist_client and exist_client.CT_Num != code: - logger.warning(f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]") + logger.warning( + f" CT_Raccourci = {raccourci} [EXISTE DEJA - ignoré]" + ) else: if try_set_attribute(client, "CT_Raccourci", raccourci): champs_modifies.append("raccourci") except: if try_set_attribute(client, "CT_Raccourci", raccourci): champs_modifies.append("raccourci") - + if "siret" in client_data: - if try_set_attribute(client, "CT_Siret", clean_str(client_data["siret"], 15)): + if try_set_attribute( + client, "CT_Siret", clean_str(client_data["siret"], 15) + ): champs_modifies.append("siret") - + if "tva_intra" in client_data: - if try_set_attribute(client, "CT_Identifiant", clean_str(client_data["tva_intra"], 25)): + if try_set_attribute( + client, + "CT_Identifiant", + clean_str(client_data["tva_intra"], 25), + ): champs_modifies.append("tva_intra") - + if "code_naf" in client_data: - if try_set_attribute(client, "CT_Ape", clean_str(client_data["code_naf"], 7)): + if try_set_attribute( + client, "CT_Ape", clean_str(client_data["code_naf"], 7) + ): champs_modifies.append("code_naf") - adresse_keys = ["contact", "adresse", "complement", "code_postal", "ville", "region", "pays"] - + adresse_keys = [ + "contact", + "adresse", + "complement", + "code_postal", + "ville", + "region", + "pays", + ] + if any(k in client_data for k in adresse_keys): logger.info("[ETAPE 3] ADRESSE") - + try: if "contact" in client_data: contact_nom = clean_str(client_data["contact"], 35) - + try: client.CT_Contact = contact_nom champs_modifies.append("contact (client)") - logger.info(f" CT_Contact (client) = {contact_nom} [OK]") + logger.info( + f" CT_Contact (client) = {contact_nom} [OK]" + ) except Exception as e: logger.warning(f" CT_Contact (client) [ECHEC: {e}]") - + try: adresse_obj = client.Adresse adresse_obj.Contact = contact_nom @@ -4132,132 +4340,169 @@ class SageConnector: logger.info(f" Contact (adresse) = {contact_nom} [OK]") except Exception as e: logger.warning(f" Contact (adresse) [ECHEC: {e}]") - + adresse_obj = client.Adresse - + if "adresse" in client_data: adresse_obj.Adresse = clean_str(client_data["adresse"], 35) champs_modifies.append("adresse") - + if "complement" in client_data: - adresse_obj.Complement = clean_str(client_data["complement"], 35) + adresse_obj.Complement = clean_str( + client_data["complement"], 35 + ) champs_modifies.append("complement") - + if "code_postal" in client_data: - adresse_obj.CodePostal = clean_str(client_data["code_postal"], 9) + adresse_obj.CodePostal = clean_str( + client_data["code_postal"], 9 + ) champs_modifies.append("code_postal") - + if "ville" in client_data: adresse_obj.Ville = clean_str(client_data["ville"], 35) champs_modifies.append("ville") - + if "region" in client_data: - adresse_obj.CodeRegion = clean_str(client_data["region"], 25) + adresse_obj.CodeRegion = clean_str( + client_data["region"], 25 + ) champs_modifies.append("region") - + if "pays" in client_data: adresse_obj.Pays = clean_str(client_data["pays"], 35) champs_modifies.append("pays") - - logger.info(f" Adresse mise à jour ({len([k for k in adresse_keys if k in client_data])} champs)") - + + logger.info( + f" Adresse mise à jour ({len([k for k in adresse_keys if k in client_data])} champs)" + ) + except Exception as e: logger.error(f" Adresse erreur: {e}") - telecom_keys = ["telephone", "telecopie", "email", "site_web", "portable", "facebook", "linkedin"] - + telecom_keys = [ + "telephone", + "telecopie", + "email", + "site_web", + "portable", + "facebook", + "linkedin", + ] + if any(k in client_data for k in telecom_keys): logger.info("[ETAPE 4] TELECOM") - + try: telecom_obj = client.Telecom - + if "telephone" in client_data: - telecom_obj.Telephone = clean_str(client_data["telephone"], 21) + telecom_obj.Telephone = clean_str( + client_data["telephone"], 21 + ) champs_modifies.append("telephone") - + if "telecopie" in client_data: - telecom_obj.Telecopie = clean_str(client_data["telecopie"], 21) + telecom_obj.Telecopie = clean_str( + client_data["telecopie"], 21 + ) champs_modifies.append("telecopie") - + if "email" in client_data: telecom_obj.EMail = clean_str(client_data["email"], 69) champs_modifies.append("email") - + if "site_web" in client_data: telecom_obj.Site = clean_str(client_data["site_web"], 69) champs_modifies.append("site_web") - + if "portable" in client_data: portable = clean_str(client_data["portable"], 21) if try_set_attribute(telecom_obj, "Portable", portable): champs_modifies.append("portable") - + if "facebook" in client_data: facebook = clean_str(client_data["facebook"], 69) if not try_set_attribute(telecom_obj, "Facebook", facebook): try_set_attribute(client, "CT_Facebook", facebook) champs_modifies.append("facebook") - + if "linkedin" in client_data: linkedin = clean_str(client_data["linkedin"], 69) if not try_set_attribute(telecom_obj, "LinkedIn", linkedin): try_set_attribute(client, "CT_LinkedIn", linkedin) champs_modifies.append("linkedin") - - logger.info(f" Telecom mis à jour ({len([k for k in telecom_keys if k in client_data])} champs)") - + + logger.info( + f" Telecom mis à jour ({len([k for k in telecom_keys if k in client_data])} champs)" + ) + except Exception as e: logger.error(f" Telecom erreur: {e}") if "compte_general" in client_data: logger.info("[ETAPE 5] COMPTE GENERAL") - + compte = clean_str(client_data["compte_general"], 13) factory_compte = self.cial.CptaApplication.FactoryCompteG - + try: persist_compte = factory_compte.ReadNumero(compte) if persist_compte: - compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3") + compte_obj = win32com.client.CastTo( + persist_compte, "IBOCompteG3" + ) compte_obj.Read() - - type_compte = getattr(compte_obj, 'CG_Type', None) + + type_compte = getattr(compte_obj, "CG_Type", None) if type_compte == 0: client.CompteGPrinc = compte_obj champs_modifies.append("compte_general") logger.info(f" CompteGPrinc = {compte} [OK]") else: - logger.warning(f" Compte {compte} - Type {type_compte} incompatible") + logger.warning( + f" Compte {compte} - Type {type_compte} incompatible" + ) except Exception as e: logger.warning(f" CompteGPrinc erreur: {e}") - if "categorie_tarifaire" in client_data or "categorie_comptable" in client_data: + if ( + "categorie_tarifaire" in client_data + or "categorie_comptable" in client_data + ): logger.info("[ETAPE 6] CATEGORIES") - + if "categorie_tarifaire" in client_data: try: cat_id = str(client_data["categorie_tarifaire"]) - factory_cat_tarif = self.cial.CptaApplication.FactoryCategorieTarif + factory_cat_tarif = ( + self.cial.CptaApplication.FactoryCategorieTarif + ) persist_cat = factory_cat_tarif.ReadIntitule(cat_id) - + if persist_cat: - cat_tarif_obj = win32com.client.CastTo(persist_cat, "IBOCategorieTarif3") + cat_tarif_obj = win32com.client.CastTo( + persist_cat, "IBOCategorieTarif3" + ) cat_tarif_obj.Read() client.CatTarif = cat_tarif_obj champs_modifies.append("categorie_tarifaire") logger.info(f" CatTarif = {cat_id} [OK]") except Exception as e: logger.warning(f" CatTarif erreur: {e}") - + if "categorie_comptable" in client_data: try: cat_id = str(client_data["categorie_comptable"]) - factory_cat_compta = self.cial.CptaApplication.FactoryCategorieCompta + factory_cat_compta = ( + self.cial.CptaApplication.FactoryCategorieCompta + ) persist_cat = factory_cat_compta.ReadIntitule(cat_id) - + if persist_cat: - cat_compta_obj = win32com.client.CastTo(persist_cat, "IBOCategorieCompta3") + cat_compta_obj = win32com.client.CastTo( + persist_cat, "IBOCategorieCompta3" + ) cat_compta_obj.Read() client.CatCompta = cat_compta_obj champs_modifies.append("categorie_comptable") @@ -4272,58 +4517,85 @@ class SageConnector: if not taux_modifies: logger.info("[ETAPE 7] TAUX") taux_modifies = True - + val = safe_float(client_data[key]) 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_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") if stat01: if not stat_modifies: logger.info("[ETAPE 8] STATISTIQUES") stat_modifies = True - - if try_set_attribute(client, "CT_Statistique01", clean_str(stat01, 21)): + + if try_set_attribute( + client, "CT_Statistique01", clean_str(stat01, 21) + ): champs_modifies.append("statistique01") - + for i in range(2, 11): key = f"statistique{i:02d}" if key in client_data: if not stat_modifies: logger.info("[ETAPE 8] STATISTIQUES") stat_modifies = True - - if try_set_attribute(client, f"CT_Statistique{i:02d}", clean_str(client_data[key], 21)): + + if try_set_attribute( + client, + f"CT_Statistique{i:02d}", + clean_str(client_data[key], 21), + ): champs_modifies.append(key) - commercial_keys = ["encours_autorise", "assurance_credit", "langue", "commercial_code"] - + commercial_keys = [ + "encours_autorise", + "assurance_credit", + "langue", + "commercial_code", + ] + if any(k in client_data for k in commercial_keys): logger.info("[ETAPE 9] COMMERCIAL") - + if "encours_autorise" in client_data: - if try_set_attribute(client, "CT_Encours", safe_float(client_data["encours_autorise"])): + if try_set_attribute( + client, + "CT_Encours", + safe_float(client_data["encours_autorise"]), + ): champs_modifies.append("encours_autorise") - + if "assurance_credit" in client_data: - if try_set_attribute(client, "CT_Assurance", safe_float(client_data["assurance_credit"])): + if try_set_attribute( + client, + "CT_Assurance", + safe_float(client_data["assurance_credit"]), + ): champs_modifies.append("assurance_credit") - + if "langue" in client_data: - if try_set_attribute(client, "CT_Langue", safe_int(client_data["langue"])): + if try_set_attribute( + client, "CT_Langue", safe_int(client_data["langue"]) + ): champs_modifies.append("langue") - + if "commercial_code" in client_data: co_no = safe_int(client_data["commercial_code"]) if not try_set_attribute(client, "CO_No", co_no): try: - factory_collab = self.cial.CptaApplication.FactoryCollaborateur + factory_collab = ( + self.cial.CptaApplication.FactoryCollaborateur + ) persist_collab = factory_collab.ReadIntitule(str(co_no)) if persist_collab: - collab_obj = win32com.client.CastTo(persist_collab, "IBOCollaborateur3") + collab_obj = win32com.client.CastTo( + persist_collab, "IBOCollaborateur3" + ) collab_obj.Read() client.Collaborateur = collab_obj champs_modifies.append("commercial_code") @@ -4332,30 +4604,50 @@ class SageConnector: logger.warning(f" Collaborateur erreur: {e}") facturation_keys = [ - "lettrage_auto", "est_actif", "type_facture", "est_prospect", - "bl_en_facture", "saut_page", "validation_echeance", "controle_encours", - "exclure_relance", "exclure_penalites", "bon_a_payer" + "lettrage_auto", + "est_actif", + "type_facture", + "est_prospect", + "bl_en_facture", + "saut_page", + "validation_echeance", + "controle_encours", + "exclure_relance", + "exclure_penalites", + "bon_a_payer", ] - + if any(k in client_data for k in facturation_keys): logger.info("[ETAPE 10] FACTURATION") - + if "lettrage_auto" in client_data: - if try_set_attribute(client, "CT_Lettrage", 1 if client_data["lettrage_auto"] else 0): + if try_set_attribute( + client, + "CT_Lettrage", + 1 if client_data["lettrage_auto"] else 0, + ): champs_modifies.append("lettrage_auto") - + if "est_actif" in client_data: - if try_set_attribute(client, "CT_Sommeil", 0 if client_data["est_actif"] else 1): + if try_set_attribute( + client, "CT_Sommeil", 0 if client_data["est_actif"] else 1 + ): champs_modifies.append("est_actif") - + if "type_facture" in client_data: - if try_set_attribute(client, "CT_Facture", safe_int(client_data["type_facture"])): + if try_set_attribute( + client, "CT_Facture", safe_int(client_data["type_facture"]) + ): champs_modifies.append("type_facture") - + if "est_prospect" in client_data: - if try_set_attribute(client, "CT_Prospect", 1 if client_data["est_prospect"] else 0): + if try_set_attribute( + client, + "CT_Prospect", + 1 if client_data["est_prospect"] else 0, + ): champs_modifies.append("est_prospect") - + factu_map = { "CT_BLFact": "bl_en_facture", "CT_Saut": "saut_page", @@ -4365,48 +4657,73 @@ class SageConnector: "CT_NotPenal": "exclure_penalites", "CT_BonAPayer": "bon_a_payer", } - + for attr, key in factu_map.items(): if key in client_data: - if try_set_attribute(client, attr, safe_int(client_data[key])): + if try_set_attribute( + client, attr, safe_int(client_data[key]) + ): champs_modifies.append(key) - logistique_keys = ["priorite_livraison", "livraison_partielle", "delai_transport", "delai_appro"] - + logistique_keys = [ + "priorite_livraison", + "livraison_partielle", + "delai_transport", + "delai_appro", + ] + if any(k in client_data for k in logistique_keys): logger.info("[ETAPE 11] LOGISTIQUE") - + logistique_map = { "CT_PrioriteLivr": "priorite_livraison", "CT_LivrPartielle": "livraison_partielle", "CT_DelaiTransport": "delai_transport", "CT_DelaiAppro": "delai_appro", } - + for attr, key in logistique_map.items(): if key in client_data: - if try_set_attribute(client, attr, safe_int(client_data[key])): + if try_set_attribute( + client, attr, safe_int(client_data[key]) + ): champs_modifies.append(key) if "commentaire" in client_data: logger.info("[ETAPE 12] COMMENTAIRE") - if try_set_attribute(client, "CT_Commentaire", clean_str(client_data["commentaire"], 35)): + if try_set_attribute( + client, + "CT_Commentaire", + clean_str(client_data["commentaire"], 35), + ): champs_modifies.append("commentaire") if "section_analytique" in client_data: logger.info("[ETAPE 13] ANALYTIQUE") - if try_set_attribute(client, "CA_Num", clean_str(client_data["section_analytique"], 13)): + if try_set_attribute( + client, + "CA_Num", + clean_str(client_data["section_analytique"], 13), + ): champs_modifies.append("section_analytique") organisation_keys = [ - "mode_reglement_code", "surveillance_active", "coface", - "forme_juridique", "effectif", "sv_regularite", "sv_cotation", - "sv_objet_maj", "ca_annuel", "sv_chiffre_affaires", "sv_resultat" + "mode_reglement_code", + "surveillance_active", + "coface", + "forme_juridique", + "effectif", + "sv_regularite", + "sv_cotation", + "sv_objet_maj", + "ca_annuel", + "sv_chiffre_affaires", + "sv_resultat", ] - + if any(k in client_data for k in organisation_keys): logger.info("[ETAPE 14] ORGANISATION & SURVEILLANCE") - + if "mode_reglement_code" in client_data: mr_no = safe_int(client_data["mode_reglement_code"]) if not try_set_attribute(client, "MR_No", mr_no): @@ -4414,14 +4731,16 @@ class SageConnector: factory_mr = self.cial.CptaApplication.FactoryModeRegl persist_mr = factory_mr.ReadIntitule(str(mr_no)) if persist_mr: - mr_obj = win32com.client.CastTo(persist_mr, "IBOModeRegl3") + mr_obj = win32com.client.CastTo( + persist_mr, "IBOModeRegl3" + ) mr_obj.Read() client.ModeRegl = mr_obj champs_modifies.append("mode_reglement_code") logger.info(f" ModeRegl = {mr_no} [OK]") except Exception as e: logger.warning(f" ModeRegl erreur: {e}") - + if "surveillance_active" in client_data: surveillance = 1 if client_data["surveillance_active"] else 0 try: @@ -4430,7 +4749,7 @@ class SageConnector: logger.info(f" CT_Surveillance = {surveillance} [OK]") except Exception as e: logger.warning(f" CT_Surveillance [ECHEC: {e}]") - + if "coface" in client_data: coface = clean_str(client_data["coface"], 25) try: @@ -4439,34 +4758,60 @@ class SageConnector: logger.info(f" CT_Coface = {coface} [OK]") except Exception as e: logger.warning(f" CT_Coface [ECHEC: {e}]") - + if "forme_juridique" in client_data: - if try_set_attribute(client, "CT_SvFormeJuri", clean_str(client_data["forme_juridique"], 33)): + if try_set_attribute( + client, + "CT_SvFormeJuri", + clean_str(client_data["forme_juridique"], 33), + ): champs_modifies.append("forme_juridique") - + if "effectif" in client_data: - if try_set_attribute(client, "CT_SvEffectif", clean_str(client_data["effectif"], 11)): + if try_set_attribute( + client, + "CT_SvEffectif", + clean_str(client_data["effectif"], 11), + ): champs_modifies.append("effectif") - + if "sv_regularite" in client_data: - if try_set_attribute(client, "CT_SvRegul", clean_str(client_data["sv_regularite"], 3)): + if try_set_attribute( + client, + "CT_SvRegul", + clean_str(client_data["sv_regularite"], 3), + ): champs_modifies.append("sv_regularite") - + if "sv_cotation" in client_data: - if try_set_attribute(client, "CT_SvCotation", clean_str(client_data["sv_cotation"], 5)): + if try_set_attribute( + client, + "CT_SvCotation", + clean_str(client_data["sv_cotation"], 5), + ): champs_modifies.append("sv_cotation") - + if "sv_objet_maj" in client_data: - if try_set_attribute(client, "CT_SvObjetMaj", clean_str(client_data["sv_objet_maj"], 61)): + if try_set_attribute( + client, + "CT_SvObjetMaj", + clean_str(client_data["sv_objet_maj"], 61), + ): champs_modifies.append("sv_objet_maj") - - ca = client_data.get("ca_annuel") or client_data.get("sv_chiffre_affaires") + + ca = client_data.get("ca_annuel") or client_data.get( + "sv_chiffre_affaires" + ) if ca: if try_set_attribute(client, "CT_SvCA", safe_float(ca)): champs_modifies.append("ca_annuel/sv_chiffre_affaires") - + if "sv_resultat" in client_data: - if try_set_attribute(client, "CT_SvResultat", safe_float(client_data["sv_resultat"])): + if try_set_attribute( + client, + "CT_SvResultat", + safe_float(client_data["sv_resultat"]), + ): champs_modifies.append("sv_resultat") if not champs_modifies: @@ -4488,15 +4833,19 @@ class SageConnector: try: sage_error = self.cial.CptaApplication.LastError if sage_error: - error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" + error_detail = ( + f"{sage_error.Description} (Code: {sage_error.Number})" + ) except: pass - + logger.error(f"[ERREUR] {error_detail}") raise RuntimeError(f"Echec Write(): {error_detail}") logger.info("=" * 80) - logger.info(f"[SUCCES] CLIENT MODIFIÉ: {code} ({len(champs_modifies)} champs)") + logger.info( + f"[SUCCES] CLIENT MODIFIÉ: {code} ({len(champs_modifies)} champs)" + ) logger.info("=" * 80) return _extraire_client(client) @@ -4507,7 +4856,7 @@ class SageConnector: except Exception as e: logger.error(f"[ERREUR] {e}", exc_info=True) raise RuntimeError(f"Erreur technique: {e}") - + def creer_commande_enrichi(self, commande_data: dict) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -4543,7 +4892,10 @@ class SageConnector: normaliser_date(commande_data.get("date_commande")) ) - if ("date_livraison" in commande_data and commande_data["date_livraison"]): + if ( + "date_livraison" in commande_data + and commande_data["date_livraison"] + ): doc.DO_DateLivr = pywintypes.Time( normaliser_date(commande_data["date_livraison"]) ) @@ -4881,9 +5233,7 @@ class SageConnector: if modif_statut: statut_a_modifier = commande_data_temp.pop("statut") - logger.info( - " Modification du statut reportée après les lignes" - ) + logger.info(" Modification du statut reportée après les lignes") modif_statut = False logger.info(" Test Write() basique (sans modification)...") @@ -4914,19 +5264,14 @@ class SageConnector: ) if not modif_lignes and ( - modif_date - or modif_date_livraison - or modif_statut - or modif_ref + modif_date or modif_date_livraison or modif_statut or modif_ref ): logger.info(" Modifications simples (sans lignes)...") if modif_date: logger.info(" Modification date commande...") doc.DO_Date = pywintypes.Time( - normaliser_date( - commande_data_temp.get("date_commande") - ) + normaliser_date(commande_data_temp.get("date_commande")) ) champs_modifies.append("date") @@ -4993,9 +5338,7 @@ class SageConnector: if modif_date: doc.DO_Date = pywintypes.Time( - normaliser_date( - commande_data_temp.get("date_commande") - ) + normaliser_date(commande_data_temp.get("date_commande")) ) champs_modifies.append("date") logger.info(" Date commande modifiée") @@ -5279,9 +5622,7 @@ class SageConnector: and livraison_data["date_livraison_prevue"] ): doc.DO_DateLivr = pywintypes.Time( - normaliser_date( - livraison_data["date_livraison_prevue"] - ) + normaliser_date(livraison_data["date_livraison_prevue"]) ) logger.info( f" Date livraison prévue: {livraison_data['date_livraison_prevue']}" @@ -5319,9 +5660,7 @@ class SageConnector: factory_article = self.cial.FactoryArticle - logger.info( - f" Ajout de {len(livraison_data['lignes'])} lignes..." - ) + logger.info(f" Ajout de {len(livraison_data['lignes'])} lignes...") for idx, ligne_data in enumerate(livraison_data["lignes"], 1): logger.info( @@ -5601,9 +5940,7 @@ class SageConnector: if modif_statut: statut_a_modifier = livraison_data_temp.pop("statut") - logger.info( - " Modification du statut reportée après les lignes" - ) + logger.info(" Modification du statut reportée après les lignes") modif_statut = False if not modif_lignes and ( @@ -5617,9 +5954,7 @@ class SageConnector: if modif_date: logger.info(" Modification date livraison...") doc.DO_Date = pywintypes.Time( - normaliser_date( - livraison_data_temp.get("date_livraison") - ) + normaliser_date(livraison_data_temp.get("date_livraison")) ) champs_modifies.append("date") @@ -5662,12 +5997,10 @@ class SageConnector: if modif_date: doc.DO_Date = pywintypes.Time( - normaliser_date( - livraison_data_temp.get("date_livraison") - ) + normaliser_date(livraison_data_temp.get("date_livraison")) ) champs_modifies.append("date") - logger.info(" Date livraison modifiée") + logger.info(" Date livraison modifiée") if modif_date_livraison_prevue: doc.DO_DateLivr = pywintypes.Time( @@ -5862,7 +6195,7 @@ class SageConnector: logger.info(f" Totaux: {total_ht}€ HT / {total_ttc}€ TTC") logger.info(f" Référence: {reference_finale}") logger.info(f" Statut: {statut_final}") - + if date_livraison_prevue_final: logger.info( f" Date livraison prévue: {date_livraison_prevue_final}" @@ -5903,9 +6236,7 @@ class SageConnector: if not self.cial: raise RuntimeError("Connexion Sage non établie") - logger.info( - f" Début création avoir pour client {avoir_data['client']['code']}" - ) + logger.info(f" Début création avoir pour client {avoir_data['client']['code']}") try: with self._com_context(), self._lock_com: @@ -5938,9 +6269,7 @@ class SageConnector: doc.DO_DateLivr = pywintypes.Time( normaliser_date(avoir_data["date_livraison"]) ) - logger.info( - f" Date livraison: {avoir_data['date_livraison']}" - ) + logger.info(f" Date livraison: {avoir_data['date_livraison']}") factory_client = self.cial.CptaApplication.FactoryClient persist_client = factory_client.ReadNumero( @@ -6127,10 +6456,8 @@ class SageConnector: reference_finale = avoir_data.get("reference", "") date_livraison_final = avoir_data.get("date_livraison") - logger.info( - f" AVOIR CRÉÉ: {numero_avoir} - {total_ttc}€ TTC " - ) - + logger.info(f" AVOIR CRÉÉ: {numero_avoir} - {total_ttc}€ TTC ") + if date_livraison_final: logger.info(f" Date livraison: {date_livraison_final}") @@ -6265,9 +6592,7 @@ class SageConnector: if modif_statut: statut_a_modifier = avoir_data_temp.pop("statut") - logger.info( - " Modification du statut reportée après les lignes" - ) + logger.info(" Modification du statut reportée après les lignes") modif_statut = False logger.info(" Test Write() basique (sans modification)...") @@ -6298,10 +6623,7 @@ class SageConnector: ) if not modif_lignes and ( - modif_date - or modif_date_livraison - or modif_statut - or modif_ref + modif_date or modif_date_livraison or modif_statut or modif_ref ): logger.info(" Modifications simples (sans lignes)...") @@ -6590,7 +6912,7 @@ class SageConnector: logger.info(f" Client final: {client_final}") logger.info(f" Référence: {reference_finale}") logger.info(f" Statut: {statut_final}") - + if date_livraison_final: logger.info(f" Date livraison: {date_livraison_final}") logger.info(f" Champs modifiés: {champs_modifies}") @@ -6909,10 +7231,8 @@ class SageConnector: reference_finale = facture_data.get("reference", "") date_livraison_final = facture_data.get("date_livraison") - logger.info( - f" FACTURE CRÉÉE: {numero_facture} - {total_ttc}€ TTC " - ) - + logger.info(f" FACTURE CRÉÉE: {numero_facture} - {total_ttc}€ TTC ") + if date_livraison_final: logger.info(f" Date livraison: {date_livraison_final}") @@ -7048,9 +7368,7 @@ class SageConnector: if modif_statut: statut_a_modifier = facture_data_temp.pop("statut") - logger.info( - " Modification du statut reportée après les lignes" - ) + logger.info(" Modification du statut reportée après les lignes") modif_statut = False logger.info(" Test Write() basique (sans modification)...") @@ -7081,10 +7399,7 @@ class SageConnector: ) if not modif_lignes and ( - modif_date - or modif_date_livraison - or modif_statut - or modif_ref + modif_date or modif_date_livraison or modif_statut or modif_ref ): logger.info(" Modifications simples (sans lignes)...") @@ -7373,7 +7688,7 @@ class SageConnector: logger.info(f" Client final: {client_final}") logger.info(f" Référence: {reference_finale}") logger.info(f" Statut: {statut_final}") - + if date_livraison_final: logger.info(f" Date livraison: {date_livraison_final}") logger.info(f" Champs modifiés: {champs_modifies}") @@ -7918,9 +8233,7 @@ class SageConnector: stocks_par_depot.append( { - "depot_code": _safe_strip( - depot_row[0] - ), + "depot_code": _safe_strip(depot_row[0]), "quantite": qte, "qte_mini": ( float(depot_row[2]) @@ -8087,9 +8400,7 @@ class SageConnector: row = cursor.fetchone() if row: - famille_code_exact = _safe_strip( - row.FA_CodeFamille - ) + famille_code_exact = _safe_strip(row.FA_CodeFamille) famille_type = row.FA_Type if len(row) > 1 else 0 famille_existe_sql = True @@ -8527,7 +8838,9 @@ class SageConnector: with self._get_sql_connection() as conn: cursor = conn.cursor() - logger.info("[SQL] Chargement des familles avec F_FAMCOMPTA et F_FAMFOURNISS...") + logger.info( + "[SQL] Chargement des familles avec F_FAMCOMPTA et F_FAMFOURNISS..." + ) query = """ SELECT @@ -8737,7 +9050,7 @@ class SageConnector: if val is None: return "" return str(val).strip() if isinstance(val, str) else str(val) - + def to_float(val): """Convertit en float, gère None""" if val is None or val == "": @@ -8746,7 +9059,7 @@ class SageConnector: return float(val) except (ValueError, TypeError): return 0.0 - + def to_int(val): """Convertit en int, gère None""" if val is None or val == "": @@ -8755,7 +9068,7 @@ class SageConnector: return int(val) except (ValueError, TypeError): return 0 - + def to_bool(val): """Convertit en bool""" if val is None: @@ -8767,168 +9080,198 @@ class SageConnector: return bool(val) familles = [] - + for row in rows: idx = 0 - + famille = { "code": to_str(row[idx]), - "type": to_int(row[idx+1]), - "intitule": to_str(row[idx+2]), - "unite_vente": to_str(row[idx+3]), - "coef": to_float(row[idx+4]), - "suivi_stock": to_bool(row[idx+5]), - "garantie": to_int(row[idx+6]), - "est_centrale": to_bool(row[idx+7]), + "type": to_int(row[idx + 1]), + "intitule": to_str(row[idx + 2]), + "unite_vente": to_str(row[idx + 3]), + "coef": to_float(row[idx + 4]), + "suivi_stock": to_bool(row[idx + 5]), + "garantie": to_int(row[idx + 6]), + "est_centrale": to_bool(row[idx + 7]), } idx += 8 - - famille.update({ - "stat_01": to_str(row[idx]), - "stat_02": to_str(row[idx+1]), - "stat_03": to_str(row[idx+2]), - "stat_04": to_str(row[idx+3]), - "stat_05": to_str(row[idx+4]), - }) + + famille.update( + { + "stat_01": to_str(row[idx]), + "stat_02": to_str(row[idx + 1]), + "stat_03": to_str(row[idx + 2]), + "stat_04": to_str(row[idx + 3]), + "stat_05": to_str(row[idx + 4]), + } + ) idx += 5 - - famille.update({ - "code_fiscal": to_str(row[idx]), - "pays": to_str(row[idx+1]), - "unite_poids": to_str(row[idx+2]), - "escompte": to_bool(row[idx+3]), - "delai": to_int(row[idx+4]), - "hors_statistique": to_bool(row[idx+5]), - "vente_debit": to_bool(row[idx+6]), - "non_imprimable": to_bool(row[idx+7]), - }) + + famille.update( + { + "code_fiscal": to_str(row[idx]), + "pays": to_str(row[idx + 1]), + "unite_poids": to_str(row[idx + 2]), + "escompte": to_bool(row[idx + 3]), + "delai": to_int(row[idx + 4]), + "hors_statistique": to_bool(row[idx + 5]), + "vente_debit": to_bool(row[idx + 6]), + "non_imprimable": to_bool(row[idx + 7]), + } + ) idx += 8 - - famille.update({ - "frais_01_libelle": to_str(row[idx]), - "frais_01_remise_1_valeur": to_float(row[idx+1]), - "frais_01_remise_1_type": to_int(row[idx+2]), - "frais_01_remise_2_valeur": to_float(row[idx+3]), - "frais_01_remise_2_type": to_int(row[idx+4]), - "frais_01_remise_3_valeur": to_float(row[idx+5]), - "frais_01_remise_3_type": to_int(row[idx+6]), - "frais_02_libelle": to_str(row[idx+7]), - "frais_02_remise_1_valeur": to_float(row[idx+8]), - "frais_02_remise_1_type": to_int(row[idx+9]), - "frais_02_remise_2_valeur": to_float(row[idx+10]), - "frais_02_remise_2_type": to_int(row[idx+11]), - "frais_02_remise_3_valeur": to_float(row[idx+12]), - "frais_02_remise_3_type": to_int(row[idx+13]), - "frais_03_libelle": to_str(row[idx+14]), - "frais_03_remise_1_valeur": to_float(row[idx+15]), - "frais_03_remise_1_type": to_int(row[idx+16]), - "frais_03_remise_2_valeur": to_float(row[idx+17]), - "frais_03_remise_2_type": to_int(row[idx+18]), - "frais_03_remise_3_valeur": to_float(row[idx+19]), - "frais_03_remise_3_type": to_int(row[idx+20]), - }) + + famille.update( + { + "frais_01_libelle": to_str(row[idx]), + "frais_01_remise_1_valeur": to_float(row[idx + 1]), + "frais_01_remise_1_type": to_int(row[idx + 2]), + "frais_01_remise_2_valeur": to_float(row[idx + 3]), + "frais_01_remise_2_type": to_int(row[idx + 4]), + "frais_01_remise_3_valeur": to_float(row[idx + 5]), + "frais_01_remise_3_type": to_int(row[idx + 6]), + "frais_02_libelle": to_str(row[idx + 7]), + "frais_02_remise_1_valeur": to_float(row[idx + 8]), + "frais_02_remise_1_type": to_int(row[idx + 9]), + "frais_02_remise_2_valeur": to_float(row[idx + 10]), + "frais_02_remise_2_type": to_int(row[idx + 11]), + "frais_02_remise_3_valeur": to_float(row[idx + 12]), + "frais_02_remise_3_type": to_int(row[idx + 13]), + "frais_03_libelle": to_str(row[idx + 14]), + "frais_03_remise_1_valeur": to_float(row[idx + 15]), + "frais_03_remise_1_type": to_int(row[idx + 16]), + "frais_03_remise_2_valeur": to_float(row[idx + 17]), + "frais_03_remise_2_type": to_int(row[idx + 18]), + "frais_03_remise_3_valeur": to_float(row[idx + 19]), + "frais_03_remise_3_type": to_int(row[idx + 20]), + } + ) idx += 21 - - famille.update({ - "contremarque": to_bool(row[idx]), - "fact_poids": to_bool(row[idx+1]), - "fact_forfait": to_bool(row[idx+2]), - "publie": to_bool(row[idx+3]), - "racine_reference": to_str(row[idx+4]), - "racine_code_barre": to_str(row[idx+5]), - }) + + famille.update( + { + "contremarque": to_bool(row[idx]), + "fact_poids": to_bool(row[idx + 1]), + "fact_forfait": to_bool(row[idx + 2]), + "publie": to_bool(row[idx + 3]), + "racine_reference": to_str(row[idx + 4]), + "racine_code_barre": to_str(row[idx + 5]), + } + ) idx += 6 - - famille.update({ - "categorie_1": to_int(row[idx]), - "categorie_2": to_int(row[idx+1]), - "categorie_3": to_int(row[idx+2]), - "categorie_4": to_int(row[idx+3]), - }) + + famille.update( + { + "categorie_1": to_int(row[idx]), + "categorie_2": to_int(row[idx + 1]), + "categorie_3": to_int(row[idx + 2]), + "categorie_4": to_int(row[idx + 3]), + } + ) idx += 4 - - famille.update({ - "nature": to_int(row[idx]), - "nb_colis": to_int(row[idx+1]), - "sous_traitance": to_bool(row[idx+2]), - "fictif": to_bool(row[idx+3]), - "criticite": to_int(row[idx+4]), - }) + + famille.update( + { + "nature": to_int(row[idx]), + "nb_colis": to_int(row[idx + 1]), + "sous_traitance": to_bool(row[idx + 2]), + "fictif": to_bool(row[idx + 3]), + "criticite": to_int(row[idx + 4]), + } + ) idx += 5 - - famille.update({ - "cb_marq": to_int(row[idx]), - "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_user": to_str(row[idx+4]), - }) + + famille.update( + { + "cb_marq": to_int(row[idx]), + "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_user": to_str(row[idx + 4]), + } + ) idx += 5 - - famille.update({ - "compte_vente": to_str(row[idx]), - "compte_auxiliaire_vente": to_str(row[idx+1]), - "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_2": row[idx+6], - "tva_vente_date_3": row[idx+7], - "type_facture_vente": to_int(row[idx+8]), - }) + + famille.update( + { + "compte_vente": to_str(row[idx]), + "compte_auxiliaire_vente": to_str(row[idx + 1]), + "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_2": row[idx + 6], + "tva_vente_date_3": row[idx + 7], + "type_facture_vente": to_int(row[idx + 8]), + } + ) idx += 9 - - famille.update({ - "compte_achat": to_str(row[idx]), - "compte_auxiliaire_achat": to_str(row[idx+1]), - "tva_achat_1": to_str(row[idx+2]), - "tva_achat_2": to_str(row[idx+3]), - "tva_achat_3": to_str(row[idx+4]), - "tva_achat_date_1": row[idx+5], - "tva_achat_date_2": row[idx+6], - "tva_achat_date_3": row[idx+7], - "type_facture_achat": to_int(row[idx+8]), - }) + + famille.update( + { + "compte_achat": to_str(row[idx]), + "compte_auxiliaire_achat": to_str(row[idx + 1]), + "tva_achat_1": to_str(row[idx + 2]), + "tva_achat_2": to_str(row[idx + 3]), + "tva_achat_3": to_str(row[idx + 4]), + "tva_achat_date_1": row[idx + 5], + "tva_achat_date_2": row[idx + 6], + "tva_achat_date_3": row[idx + 7], + "type_facture_achat": to_int(row[idx + 8]), + } + ) idx += 9 - - famille.update({ - "compte_stock": to_str(row[idx]), - "compte_auxiliaire_stock": to_str(row[idx+1]), - }) + + famille.update( + { + "compte_stock": to_str(row[idx]), + "compte_auxiliaire_stock": to_str(row[idx + 1]), + } + ) idx += 2 - - famille.update({ - "fournisseur_principal": to_str(row[idx]), - "fournisseur_unite": to_str(row[idx+1]), - "fournisseur_conversion": to_float(row[idx+2]), - "fournisseur_delai_appro": to_int(row[idx+3]), - "fournisseur_garantie": to_int(row[idx+4]), - "fournisseur_colisage": to_int(row[idx+5]), - "fournisseur_qte_mini": to_float(row[idx+6]), - "fournisseur_qte_mont": to_float(row[idx+7]), - "fournisseur_enumere_gamme": to_int(row[idx+8]), - "fournisseur_devise": to_int(row[idx+9]), - "fournisseur_remise": to_float(row[idx+10]), - "fournisseur_conv_div": to_float(row[idx+11]), - "fournisseur_type_remise": to_int(row[idx+12]), - }) + + famille.update( + { + "fournisseur_principal": to_str(row[idx]), + "fournisseur_unite": to_str(row[idx + 1]), + "fournisseur_conversion": to_float(row[idx + 2]), + "fournisseur_delai_appro": to_int(row[idx + 3]), + "fournisseur_garantie": to_int(row[idx + 4]), + "fournisseur_colisage": to_int(row[idx + 5]), + "fournisseur_qte_mini": to_float(row[idx + 6]), + "fournisseur_qte_mont": to_float(row[idx + 7]), + "fournisseur_enumere_gamme": to_int(row[idx + 8]), + "fournisseur_devise": to_int(row[idx + 9]), + "fournisseur_remise": to_float(row[idx + 10]), + "fournisseur_conv_div": to_float(row[idx + 11]), + "fournisseur_type_remise": to_int(row[idx + 12]), + } + ) idx += 13 - + famille["nb_articles"] = to_int(row[idx]) - - famille["type_libelle"] = "Total" if famille["type"] == 1 else "Détail" + + famille["type_libelle"] = ( + "Total" if famille["type"] == 1 else "Détail" + ) famille["est_total"] = famille["type"] == 1 famille["est_detail"] = famille["type"] == 0 - + famille["FA_CodeFamille"] = famille["code"] famille["FA_Intitule"] = famille["intitule"] famille["FA_Type"] = famille["type"] famille["CG_NumVte"] = famille["compte_vente"] famille["CG_NumAch"] = famille["compte_achat"] - + 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 @@ -8936,80 +9279,73 @@ class SageConnector: except Exception as e: logger.error(f"Erreur SQL familles: {e}", exc_info=True) raise RuntimeError(f"Erreur lecture familles: {str(e)}") - + def lire_famille(self, code: str) -> Dict: - try: - with self._get_sql_connection() as conn: - cursor = conn.cursor() + try: + with self._get_sql_connection() as conn: + cursor = conn.cursor() - logger.info(f"[SQL] Lecture famille : {code}") + logger.info(f"[SQL] Lecture famille : {code}") - cursor.execute("SELECT TOP 1 * FROM F_FAMILLE") - colonnes_disponibles = [column[0] for column in cursor.description] + cursor.execute("SELECT TOP 1 * FROM F_FAMILLE") + colonnes_disponibles = [column[0] for column in cursor.description] - logger.info(f"[SQL] Colonnes trouvées : {len(colonnes_disponibles)}") + logger.info(f"[SQL] Colonnes trouvées : {len(colonnes_disponibles)}") - colonnes_souhaitees = [ - "FA_CodeFamille", - "FA_Intitule", - "FA_Type", - - "FA_UniteVen", - "FA_Coef", - "FA_SuiviStock", - "FA_Garantie", - "FA_UnitePoids", - "FA_Delai", - "FA_NbColis", - - "CG_NumAch", - "CG_NumVte", - "FA_CodeFiscal", - "FA_Escompte", - - "FA_Central", - "FA_Nature", - "CL_No1", - "CL_No2", - "CL_No3", - "CL_No4", - - "FA_Stat01", - "FA_Stat02", - "FA_Stat03", - "FA_Stat04", - "FA_Stat05", - "FA_HorsStat", - - "FA_Pays", - "FA_VteDebit", - "FA_NotImp", - "FA_Contremarque", - "FA_FactPoids", - "FA_FactForfait", - "FA_Publie", - - "FA_RacineRef", - "FA_RacineCB", - "FA_Raccourci", - - "FA_SousTraitance", - "FA_Fictif", - "FA_Criticite" - ] + colonnes_souhaitees = [ + "FA_CodeFamille", + "FA_Intitule", + "FA_Type", + "FA_UniteVen", + "FA_Coef", + "FA_SuiviStock", + "FA_Garantie", + "FA_UnitePoids", + "FA_Delai", + "FA_NbColis", + "CG_NumAch", + "CG_NumVte", + "FA_CodeFiscal", + "FA_Escompte", + "FA_Central", + "FA_Nature", + "CL_No1", + "CL_No2", + "CL_No3", + "CL_No4", + "FA_Stat01", + "FA_Stat02", + "FA_Stat03", + "FA_Stat04", + "FA_Stat05", + "FA_HorsStat", + "FA_Pays", + "FA_VteDebit", + "FA_NotImp", + "FA_Contremarque", + "FA_FactPoids", + "FA_FactForfait", + "FA_Publie", + "FA_RacineRef", + "FA_RacineCB", + "FA_Raccourci", + "FA_SousTraitance", + "FA_Fictif", + "FA_Criticite", + ] - colonnes_a_lire = [ - col for col in colonnes_souhaitees if col in colonnes_disponibles - ] + colonnes_a_lire = [ + col for col in colonnes_souhaitees if col in colonnes_disponibles + ] - if not colonnes_a_lire: - colonnes_a_lire = colonnes_disponibles + if not colonnes_a_lire: + colonnes_a_lire = colonnes_disponibles - logger.info(f"[SQL] Colonnes sélectionnées : {len(colonnes_a_lire)}") + logger.info(f"[SQL] Colonnes sélectionnées : {len(colonnes_a_lire)}") - colonnes_str = ", ".join([f"f.{col}" for col in colonnes_a_lire]) + colonnes_str = ", ".join([f"f.{col}" for col in colonnes_a_lire]) - query = f""" + query = f""" SELECT {colonnes_str}, ISNULL(COUNT(a.AR_Ref), 0) as nb_articles FROM F_FAMILLE f @@ -9018,99 +9354,101 @@ class SageConnector: GROUP BY {colonnes_str} """ - cursor.execute(query, (code.upper().strip(),)) - row = cursor.fetchone() + cursor.execute(query, (code.upper().strip(),)) + row = cursor.fetchone() - if not row: - raise ValueError(f"Famille '{code}' introuvable dans Sage") + if not row: + raise ValueError(f"Famille '{code}' introuvable dans Sage") - famille = {} + famille = {} - for idx, colonne in enumerate(colonnes_a_lire): - valeur = row[idx] + for idx, colonne in enumerate(colonnes_a_lire): + valeur = row[idx] - if isinstance(valeur, str): - valeur = valeur.strip() + if isinstance(valeur, str): + valeur = valeur.strip() - famille[colonne] = valeur + famille[colonne] = valeur - famille["nb_articles"] = row[-1] + famille["nb_articles"] = row[-1] - if "FA_CodeFamille" in famille: - famille["code"] = famille["FA_CodeFamille"] + if "FA_CodeFamille" in famille: + famille["code"] = famille["FA_CodeFamille"] - if "FA_Intitule" in famille: - famille["intitule"] = famille["FA_Intitule"] + if "FA_Intitule" in famille: + famille["intitule"] = famille["FA_Intitule"] - if "FA_Type" in famille: - type_val = famille["FA_Type"] - famille["type"] = type_val - famille["type_libelle"] = "Total" if type_val == 1 else "Détail" - famille["est_total"] = type_val == 1 - else: - famille["type"] = 0 - famille["type_libelle"] = "Détail" - famille["est_total"] = False + if "FA_Type" in famille: + type_val = famille["FA_Type"] + famille["type"] = type_val + famille["type_libelle"] = "Total" if type_val == 1 else "Détail" + famille["est_total"] = type_val == 1 + else: + famille["type"] = 0 + famille["type_libelle"] = "Détail" + famille["est_total"] = False - famille["unite_vente"] = str(famille.get("FA_UniteVen", "")) - famille["unite_poids"] = str(famille.get("FA_UnitePoids", "")) - famille["coef"] = ( - float(famille.get("FA_Coef", 0.0)) - if famille.get("FA_Coef") is not None - else 0.0 - ) + famille["unite_vente"] = str(famille.get("FA_UniteVen", "")) + famille["unite_poids"] = str(famille.get("FA_UnitePoids", "")) + famille["coef"] = ( + float(famille.get("FA_Coef", 0.0)) + if famille.get("FA_Coef") is not None + else 0.0 + ) - famille["suivi_stock"] = bool(famille.get("FA_SuiviStock", 0)) - famille["garantie"] = int(famille.get("FA_Garantie", 0)) - famille["delai"] = int(famille.get("FA_Delai", 0)) - famille["nb_colis"] = int(famille.get("FA_NbColis", 0)) + famille["suivi_stock"] = bool(famille.get("FA_SuiviStock", 0)) + famille["garantie"] = int(famille.get("FA_Garantie", 0)) + famille["delai"] = int(famille.get("FA_Delai", 0)) + famille["nb_colis"] = int(famille.get("FA_NbColis", 0)) - famille["compte_achat"] = famille.get("CG_NumAch", "") - famille["compte_vente"] = famille.get("CG_NumVte", "") - famille["code_fiscal"] = famille.get("FA_CodeFiscal", "") - famille["escompte"] = bool(famille.get("FA_Escompte", 0)) + famille["compte_achat"] = famille.get("CG_NumAch", "") + famille["compte_vente"] = famille.get("CG_NumVte", "") + famille["code_fiscal"] = famille.get("FA_CodeFiscal", "") + famille["escompte"] = bool(famille.get("FA_Escompte", 0)) - famille["est_centrale"] = bool(famille.get("FA_Central", 0)) - famille["nature"] = famille.get("FA_Nature", 0) - famille["pays"] = famille.get("FA_Pays", "") + famille["est_centrale"] = bool(famille.get("FA_Central", 0)) + famille["nature"] = famille.get("FA_Nature", 0) + famille["pays"] = famille.get("FA_Pays", "") - famille["categorie_1"] = famille.get("CL_No1", 0) - famille["categorie_2"] = famille.get("CL_No2", 0) - famille["categorie_3"] = famille.get("CL_No3", 0) - famille["categorie_4"] = famille.get("CL_No4", 0) + famille["categorie_1"] = famille.get("CL_No1", 0) + famille["categorie_2"] = famille.get("CL_No2", 0) + famille["categorie_3"] = famille.get("CL_No3", 0) + famille["categorie_4"] = famille.get("CL_No4", 0) - famille["stat_01"] = famille.get("FA_Stat01", "") - famille["stat_02"] = famille.get("FA_Stat02", "") - famille["stat_03"] = famille.get("FA_Stat03", "") - famille["stat_04"] = famille.get("FA_Stat04", "") - famille["stat_05"] = famille.get("FA_Stat05", "") - famille["hors_statistique"] = bool(famille.get("FA_HorsStat", 0)) + famille["stat_01"] = famille.get("FA_Stat01", "") + famille["stat_02"] = famille.get("FA_Stat02", "") + famille["stat_03"] = famille.get("FA_Stat03", "") + famille["stat_04"] = famille.get("FA_Stat04", "") + famille["stat_05"] = famille.get("FA_Stat05", "") + famille["hors_statistique"] = bool(famille.get("FA_HorsStat", 0)) - famille["vente_debit"] = bool(famille.get("FA_VteDebit", 0)) - famille["non_imprimable"] = bool(famille.get("FA_NotImp", 0)) - famille["contremarque"] = bool(famille.get("FA_Contremarque", 0)) - famille["fact_poids"] = bool(famille.get("FA_FactPoids", 0)) - famille["fact_forfait"] = bool(famille.get("FA_FactForfait", 0)) - famille["publie"] = bool(famille.get("FA_Publie", 0)) + famille["vente_debit"] = bool(famille.get("FA_VteDebit", 0)) + famille["non_imprimable"] = bool(famille.get("FA_NotImp", 0)) + famille["contremarque"] = bool(famille.get("FA_Contremarque", 0)) + famille["fact_poids"] = bool(famille.get("FA_FactPoids", 0)) + famille["fact_forfait"] = bool(famille.get("FA_FactForfait", 0)) + famille["publie"] = bool(famille.get("FA_Publie", 0)) - famille["racine_reference"] = famille.get("FA_RacineRef", "") - famille["racine_code_barre"] = famille.get("FA_RacineCB", "") - famille["raccourci"] = famille.get("FA_Raccourci", "") + famille["racine_reference"] = famille.get("FA_RacineRef", "") + famille["racine_code_barre"] = famille.get("FA_RacineCB", "") + famille["raccourci"] = famille.get("FA_Raccourci", "") - famille["sous_traitance"] = bool(famille.get("FA_SousTraitance", 0)) - famille["fictif"] = bool(famille.get("FA_Fictif", 0)) - famille["criticite"] = int(famille.get("FA_Criticite", 0)) + famille["sous_traitance"] = bool(famille.get("FA_SousTraitance", 0)) + famille["fictif"] = bool(famille.get("FA_Fictif", 0)) + famille["criticite"] = int(famille.get("FA_Criticite", 0)) - logger.info(f"SQL: Famille '{famille['code']}' chargée ({famille['nb_articles']} articles)") + logger.info( + f"SQL: Famille '{famille['code']}' chargée ({famille['nb_articles']} articles)" + ) - return famille + return famille - except ValueError as e: - logger.error(f"Erreur famille: {e}") - raise - except Exception as e: - logger.error(f"Erreur SQL famille: {e}", exc_info=True) - raise RuntimeError(f"Erreur lecture famille: {str(e)}") + except ValueError as e: + logger.error(f"Erreur famille: {e}") + raise + except Exception as e: + logger.error(f"Erreur SQL famille: {e}", exc_info=True) + raise RuntimeError(f"Erreur lecture famille: {str(e)}") def creer_entree_stock(self, entree_data: Dict) -> Dict: try: @@ -9399,9 +9737,7 @@ class SageConnector: try: stock_trouve.Write() - logger.info( - f" ArticleStock sauvegardé" - ) + logger.info(f" ArticleStock sauvegardé") except Exception as e: logger.error( f" Erreur Write() ArticleStock: {e}" @@ -9874,7 +10210,6 @@ class SageConnector: except Exception as e: logger.warning(f"[STOCK] Méthode 2 échouée : {e}") - if not calcul_complet: logger.warning( f"[STOCK] Méthodes rapides échouées pour {reference}" @@ -9896,7 +10231,6 @@ class SageConnector: "[STOCK] CALCUL DEPUIS MOUVEMENTS ACTIVÉ - PEUT PRENDRE 20+ MINUTES" ) - except ValueError: raise except Exception as e: @@ -9905,7 +10239,11 @@ class SageConnector: def creer_sortie_stock(self, sortie_data: Dict) -> Dict: 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() logger.info(f"[STOCK] === CRÉATION SORTIE STOCK ===") logger.info(f"[STOCK] {len(sortie_data.get('lignes', []))} ligne(s)") @@ -9921,7 +10259,6 @@ class SageConnector: doc = win32com.client.CastTo(persist, "IBODocumentStock3") doc.SetDefault() - date_mouv = sortie_data.get("date_mouvement") if isinstance(date_mouv, date): doc.DO_Date = pywintypes.Time( @@ -10239,21 +10576,19 @@ class SageConnector: except Exception as e: logger.error(f"[MOUVEMENT] Erreur : {e}", exc_info=True) raise ValueError(f"Erreur lecture mouvement : {str(e)}") - + def lister_tous_tiers( - self, - type_tiers: Optional[str] = None, - filtre: str = "" + self, type_tiers: Optional[str] = None, filtre: str = "" ) -> List[Dict]: try: with self._get_sql_connection() as conn: cursor = conn.cursor() - + query = _build_tiers_select_query() query += " FROM F_COMPTET WHERE 1=1" - + params = [] - + if type_tiers and type_tiers != "all": if type_tiers == "prospect": query += " AND CT_Type = 0 AND CT_Prospect = 1" @@ -10261,25 +10596,27 @@ class SageConnector: query += " AND CT_Type = 0 AND CT_Prospect = 0" elif type_tiers == "fournisseur": query += " AND CT_Type = 1" - + if filtre: query += " AND (CT_Num LIKE ? OR CT_Intitule LIKE ?)" params.extend([f"%{filtre}%", f"%{filtre}%"]) - + query += " ORDER BY CT_Intitule" - + cursor.execute(query, params) rows = cursor.fetchall() - + tiers_list = [] for row in rows: tiers = _row_to_tiers_dict(row) tiers["contacts"] = _get_contacts_client(row.CT_Num, conn) tiers_list.append(tiers) - - logger.info(f" SQL: {len(tiers_list)} tiers retournés (type={type_tiers}, filtre={filtre})") + + logger.info( + f" SQL: {len(tiers_list)} tiers retournés (type={type_tiers}, filtre={filtre})" + ) return tiers_list - + except Exception as e: logger.error(f" Erreur SQL tiers: {e}") raise RuntimeError(f"Erreur lecture tiers: {str(e)}") @@ -10289,24 +10626,22 @@ class SageConnector: try: with self._get_sql_connection() as conn: cursor = conn.cursor() - + query = _build_tiers_select_query() query += " FROM F_COMPTET WHERE CT_Num = ?" - + cursor.execute(query, (code.upper(),)) row = cursor.fetchone() - + if not row: return None - + tiers = _row_to_tiers_dict(row) tiers["contacts"] = _get_contacts_client(row.CT_Num, conn) - + logger.info(f" SQL: Tiers {code} lu avec succès") return tiers - + except Exception as e: logger.error(f" Erreur SQL tiers {code}: {e}") return None - - diff --git a/schemas/__init__.py b/schemas/__init__.py index f4fb7c7..4300324 100644 --- a/schemas/__init__.py +++ b/schemas/__init__.py @@ -1,5 +1,107 @@ -from schemas.tiers.tiers import (TiersListRequest,) +from schemas.tiers.tiers import ( + TiersListRequest, + TypeTiers +) +from schemas.tiers.contact import ( + ContactCreateRequest, + ContactDeleteRequest, + ContactGetRequest, + ContactListRequest, + ContactUpdateRequest, +) +from schemas.tiers.clients import ( + ClientCreateRequest, + ClientUpdateGatewayRequest +) + +from schemas.others.general_schema import ( + FiltreRequest, + ChampLibreRequest, + CodeRequest, + StatutRequest +) + +from schemas.documents.documents import ( + TransformationRequest, + TypeDocument, + DocumentGetRequest, + PDFGenerationRequest +) + +from schemas.documents.devis import ( + DevisRequest, + DevisUpdateGatewayRequest +) + +from schemas.tiers.fournisseurs import ( + FournisseurCreateRequest, + FournisseurUpdateGatewayRequest +) + +from schemas.documents.avoirs import ( + AvoirCreateGatewayRequest, + AvoirUpdateGatewayRequest +) + +from schemas.documents.commandes import ( + CommandeCreateRequest, + CommandeUpdateGatewayRequest +) + +from schemas.documents.factures import ( + FactureCreateGatewayRequest, + FactureUpdateGatewayRequest +) + +from schemas.documents.livraisons import ( + LivraisonCreateGatewayRequest, + LivraisonUpdateGatewayRequest +) + +from schemas.articles.articles import ( + ArticleCreateRequest, + ArticleUpdateGatewayRequest, + MouvementStockLigneRequest, + EntreeStockRequest, + SortieStockRequest +) + +from schemas.articles.famille_d_articles import FamilleCreate __all__ = [ - "TiersListRequest", - ] \ No newline at end of file + "TiersListRequest", + "ContactCreateRequest", + "ContactDeleteRequest", + "ContactGetRequest", + "ContactListRequest", + "ContactUpdateRequest", + "ClientCreateRequest", + "ClientUpdateGatewayRequest", + "FiltreRequest", + "ChampLibreRequest", + "CodeRequest", + "TransformationRequest", + "TypeDocument", + "DevisRequest", + "DocumentGetRequest", + "StatutRequest", + "TypeTiers", + "DevisUpdateGatewayRequest", + "FournisseurCreateRequest", + "FournisseurUpdateGatewayRequest", + "AvoirCreateGatewayRequest", + "AvoirUpdateGatewayRequest", + "CommandeCreateRequest", + "CommandeUpdateGatewayRequest", + "FactureCreateGatewayRequest", + "FactureUpdateGatewayRequest", + "LivraisonCreateGatewayRequest", + "LivraisonUpdateGatewayRequest", + "ArticleCreateRequest", + "ArticleUpdateGatewayRequest", + "MouvementStockLigneRequest", + "EntreeStockRequest", + "SortieStockRequest", + "FamilleCreate", + "PDFGenerationRequest" +] diff --git a/schemas/articles/articles.py b/schemas/articles/articles.py new file mode 100644 index 0000000..1606fdd --- /dev/null +++ b/schemas/articles/articles.py @@ -0,0 +1,111 @@ + +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 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 + + +class MouvementStockLigneRequest(BaseModel): + article_ref: str = Field(..., description="Référence de l'article") + quantite: float = Field(..., gt=0, description="Quantité (>0)") + depot_code: Optional[str] = Field(None, description="Code du dépôt (ex: '01')") + prix_unitaire: Optional[float] = Field( + None, ge=0, description="Prix unitaire (optionnel)" + ) + commentaire: Optional[str] = Field(None, description="Commentaire ligne") + numero_lot: Optional[str] = Field( + None, description="Numéro de lot (pour FIFO/LIFO)" + ) + stock_mini: Optional[float] = Field( + None, + ge=0, + description="""Stock minimum à définir pour cet article. + Si fourni, met à jour AS_QteMini dans F_ARTSTOCK. + Laisser None pour ne pas modifier.""", + ) + stock_maxi: Optional[float] = Field( + None, + ge=0, + description="""Stock maximum à définir pour cet article. + Doit être > stock_mini si les deux sont fournis.""", + ) + + class Config: + schema_extra = { + "example": { + "article_ref": "ARTS-001", + "quantite": 50.0, + "depot_code": "01", + "prix_unitaire": 100.0, + "commentaire": "Réapprovisionnement", + "numero_lot": "LOT20241217", + "stock_mini": 10.0, + "stock_maxi": 200.0, + } + } + + @validator("stock_maxi") + def validate_stock_maxi(cls, v, values): + """Valide que stock_maxi > stock_mini si les deux sont fournis""" + if ( + v is not None + and "stock_mini" in values + and values["stock_mini"] is not None + ): + if v <= values["stock_mini"]: + raise ValueError( + "stock_maxi doit être strictement supérieur à stock_mini" + ) + return v + + +class EntreeStockRequest(BaseModel): + """Création d'un bon d'entrée en stock""" + + date_entree: Optional[date] = Field( + None, description="Date du mouvement (aujourd'hui par défaut)" + ) + reference: Optional[str] = Field(None, description="Référence externe") + depot_code: Optional[str] = Field( + None, description="Dépôt principal (si applicable)" + ) + lignes: List[MouvementStockLigneRequest] = Field( + ..., min_items=1, description="Lignes du mouvement" + ) + commentaire: Optional[str] = Field(None, description="Commentaire général") + + +class SortieStockRequest(BaseModel): + """Création d'un bon de sortie de stock""" + + date_sortie: Optional[date] = Field( + None, description="Date du mouvement (aujourd'hui par défaut)" + ) + reference: Optional[str] = Field(None, description="Référence externe") + depot_code: Optional[str] = Field( + None, description="Dépôt principal (si applicable)" + ) + lignes: List[MouvementStockLigneRequest] = Field( + ..., min_items=1, description="Lignes du mouvement" + ) + commentaire: Optional[str] = Field(None, description="Commentaire général") diff --git a/schemas/articles/famille_d_articles.py b/schemas/articles/famille_d_articles.py new file mode 100644 index 0000000..5a6e60e --- /dev/null +++ b/schemas/articles/famille_d_articles.py @@ -0,0 +1,19 @@ + + +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 FamilleCreate(BaseModel): + """Modèle pour créer une famille d'articles""" + + code: str = Field(..., description="Code famille (max 18 car)", max_length=18) + intitule: str = Field(..., description="Intitulé (max 69 car)", max_length=69) + type: int = Field(0, description="0=Détail, 1=Total") + compte_achat: Optional[str] = Field( + None, description="Compte général d'achat (ex: 607000)" + ) + compte_vente: Optional[str] = Field( + None, description="Compte général de vente (ex: 707000)" + ) diff --git a/schemas/documents/avoirs.py b/schemas/documents/avoirs.py new file mode 100644 index 0000000..48d6066 --- /dev/null +++ b/schemas/documents/avoirs.py @@ -0,0 +1,22 @@ + + +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 AvoirCreateGatewayRequest(BaseModel): + """Création d'un avoir côté gateway""" + + client_id: str + date_avoir: Optional[date] = None + date_livraison: 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 diff --git a/schemas/documents/commandes.py b/schemas/documents/commandes.py new file mode 100644 index 0000000..7024090 --- /dev/null +++ b/schemas/documents/commandes.py @@ -0,0 +1,21 @@ + +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 CommandeCreateRequest(BaseModel): + """Création d'une commande""" + + client_id: str + date_commande: Optional[date] = None + date_livraison: 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 diff --git a/schemas/documents/devis.py b/schemas/documents/devis.py new file mode 100644 index 0000000..ec0d60e --- /dev/null +++ b/schemas/documents/devis.py @@ -0,0 +1,19 @@ + + +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 DevisRequest(BaseModel): + client_id: str + date_devis: Optional[date] = None + date_livraison: Optional[date] = None + 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 diff --git a/schemas/documents/documents.py b/schemas/documents/documents.py new file mode 100644 index 0000000..989ea90 --- /dev/null +++ b/schemas/documents/documents.py @@ -0,0 +1,32 @@ + +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 + BON_LIVRAISON = 1 + BON_RETOUR = 2 + COMMANDE = 3 + PREPARATION = 4 + FACTURE = 5 + + +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 new file mode 100644 index 0000000..9401a68 --- /dev/null +++ b/schemas/documents/factures.py @@ -0,0 +1,22 @@ + + +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 FactureCreateGatewayRequest(BaseModel): + """Création d'une facture côté gateway""" + + client_id: str + date_facture: Optional[date] = None + date_livraison: 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 diff --git a/schemas/documents/livraisons.py b/schemas/documents/livraisons.py new file mode 100644 index 0000000..3125f6c --- /dev/null +++ b/schemas/documents/livraisons.py @@ -0,0 +1,21 @@ + +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 LivraisonCreateGatewayRequest(BaseModel): + """Création d'une livraison côté gateway""" + + client_id: str + date_livraison: Optional[date] = None + date_livraison_prevue: 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 diff --git a/schemas/others/general_schema.py b/schemas/others/general_schema.py new file mode 100644 index 0000000..39ff91f --- /dev/null +++ b/schemas/others/general_schema.py @@ -0,0 +1,21 @@ + +from pydantic import BaseModel, Field, validator, EmailStr, field_validator +from typing import Optional, List, Dict + +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 StatutRequest(BaseModel): + nouveau_statut: int diff --git a/schemas/tiers/clients.py b/schemas/tiers/clients.py new file mode 100644 index 0000000..c80bf63 --- /dev/null +++ b/schemas/tiers/clients.py @@ -0,0 +1,410 @@ +from pydantic import BaseModel, Field, validator, EmailStr, field_validator +from typing import Optional, List, Dict + +class ClientCreateRequest(BaseModel): + intitule: str = Field( + ..., max_length=69, description="Nom du client (CT_Intitule) - OBLIGATOIRE" + ) + + numero: Optional[str] = Field( + None, max_length=17, description="Numéro client CT_Num (auto si None)" + ) + + type_tiers: int = Field( + 0, + ge=0, + le=3, + description="CT_Type: 0=Client, 1=Fournisseur, 2=Salarié, 3=Autre", + ) + + qualite: Optional[str] = Field( + "CLI", max_length=17, description="CT_Qualite: CLI/FOU/SAL/DIV/AUT" + ) + + classement: Optional[str] = Field(None, max_length=17, description="CT_Classement") + + raccourci: Optional[str] = Field( + None, max_length=7, description="CT_Raccourci (7 chars max, unique)" + ) + + siret: Optional[str] = Field( + None, max_length=15, description="CT_Siret (14-15 chars)" + ) + + tva_intra: Optional[str] = Field( + None, max_length=25, description="CT_Identifiant (TVA intracommunautaire)" + ) + + code_naf: Optional[str] = Field( + None, max_length=7, description="CT_Ape (Code NAF/APE)" + ) + + contact: Optional[str] = Field( + None, + max_length=35, + description="CT_Contact (double affectation: client + adresse)", + ) + + adresse: Optional[str] = Field(None, max_length=35, description="Adresse.Adresse") + + complement: Optional[str] = Field( + None, max_length=35, description="Adresse.Complement" + ) + + code_postal: Optional[str] = Field( + None, max_length=9, description="Adresse.CodePostal" + ) + + ville: Optional[str] = Field(None, max_length=35, description="Adresse.Ville") + + region: Optional[str] = Field(None, max_length=25, description="Adresse.CodeRegion") + + pays: Optional[str] = Field(None, max_length=35, description="Adresse.Pays") + + telephone: Optional[str] = Field( + None, max_length=21, description="Telecom.Telephone" + ) + + telecopie: Optional[str] = Field( + None, max_length=21, description="Telecom.Telecopie (fax)" + ) + + email: Optional[str] = Field(None, max_length=69, description="Telecom.EMail") + + site_web: Optional[str] = Field(None, max_length=69, description="Telecom.Site") + + portable: Optional[str] = Field(None, max_length=21, description="Telecom.Portable") + + facebook: Optional[str] = Field( + None, max_length=69, description="Telecom.Facebook ou CT_Facebook" + ) + + linkedin: Optional[str] = Field( + None, max_length=69, description="Telecom.LinkedIn ou CT_LinkedIn" + ) + + compte_general: Optional[str] = Field( + None, + max_length=13, + description="CompteGPrinc (défaut selon type_tiers: 4110000, 4010000, 421, 471)", + ) + + categorie_tarifaire: Optional[str] = Field( + None, description="N_CatTarif (ID catégorie tarifaire, défaut '0' ou '1')" + ) + + categorie_comptable: Optional[str] = Field( + None, description="N_CatCompta (ID catégorie comptable, défaut '0' ou '1')" + ) + + taux01: Optional[float] = Field(None, description="CT_Taux01") + taux02: Optional[float] = Field(None, description="CT_Taux02") + taux03: Optional[float] = Field(None, description="CT_Taux03") + taux04: Optional[float] = Field(None, description="CT_Taux04") + + secteur: Optional[str] = Field( + None, max_length=21, description="Alias de statistique01 (CT_Statistique01)" + ) + + statistique01: Optional[str] = Field( + None, max_length=21, description="CT_Statistique01" + ) + statistique02: Optional[str] = Field( + None, max_length=21, description="CT_Statistique02" + ) + statistique03: Optional[str] = Field( + None, max_length=21, description="CT_Statistique03" + ) + statistique04: Optional[str] = Field( + None, max_length=21, description="CT_Statistique04" + ) + statistique05: Optional[str] = Field( + None, max_length=21, description="CT_Statistique05" + ) + statistique06: Optional[str] = Field( + None, max_length=21, description="CT_Statistique06" + ) + statistique07: Optional[str] = Field( + None, max_length=21, description="CT_Statistique07" + ) + statistique08: Optional[str] = Field( + None, max_length=21, description="CT_Statistique08" + ) + statistique09: Optional[str] = Field( + None, max_length=21, description="CT_Statistique09" + ) + statistique10: Optional[str] = Field( + None, max_length=21, description="CT_Statistique10" + ) + + encours_autorise: Optional[float] = Field( + None, description="CT_Encours (montant max autorisé)" + ) + + assurance_credit: Optional[float] = Field( + None, description="CT_Assurance (montant assurance crédit)" + ) + + langue: Optional[int] = Field( + None, ge=0, description="CT_Langue (0=Français, 1=Anglais, etc.)" + ) + + commercial_code: Optional[int] = Field( + None, description="CO_No (ID du collaborateur commercial)" + ) + + lettrage_auto: Optional[bool] = Field( + True, description="CT_Lettrage (1=oui, 0=non)" + ) + + est_actif: Optional[bool] = Field( + True, description="Inverse de CT_Sommeil (True=actif, False=en sommeil)" + ) + + type_facture: Optional[int] = Field( + 1, ge=0, le=2, description="CT_Facture: 0=aucune, 1=normale, 2=regroupée" + ) + + est_prospect: Optional[bool] = Field( + False, description="CT_Prospect (1=oui, 0=non)" + ) + + bl_en_facture: Optional[int] = Field( + None, ge=0, le=1, description="CT_BLFact (impression BL sur facture)" + ) + + saut_page: Optional[int] = Field( + None, ge=0, le=1, description="CT_Saut (saut de page après impression)" + ) + + validation_echeance: Optional[int] = Field( + None, ge=0, le=1, description="CT_ValidEch" + ) + + controle_encours: Optional[int] = Field( + None, ge=0, le=1, description="CT_ControlEnc" + ) + + exclure_relance: Optional[int] = Field(None, ge=0, le=1, description="CT_NotRappel") + + exclure_penalites: Optional[int] = Field( + None, ge=0, le=1, description="CT_NotPenal" + ) + + bon_a_payer: Optional[int] = Field(None, ge=0, le=1, description="CT_BonAPayer") + + priorite_livraison: Optional[int] = Field( + None, ge=0, le=5, description="CT_PrioriteLivr" + ) + + livraison_partielle: Optional[int] = Field( + None, ge=0, le=1, description="CT_LivrPartielle" + ) + + delai_transport: Optional[int] = Field( + None, ge=0, description="CT_DelaiTransport (jours)" + ) + + delai_appro: Optional[int] = Field(None, ge=0, description="CT_DelaiAppro (jours)") + + commentaire: Optional[str] = Field( + None, max_length=35, description="CT_Commentaire" + ) + + section_analytique: Optional[str] = Field(None, max_length=13, description="CA_Num") + + mode_reglement_code: Optional[int] = Field( + None, description="MR_No (ID du mode de règlement)" + ) + + surveillance_active: Optional[int] = Field( + None, ge=0, le=1, description="CT_Surveillance (DOIT être défini AVANT coface)" + ) + + coface: Optional[str] = Field( + None, max_length=25, description="CT_Coface (code Coface)" + ) + + forme_juridique: Optional[str] = Field( + None, max_length=33, description="CT_SvFormeJuri (SARL, SA, etc.)" + ) + + effectif: Optional[str] = Field(None, max_length=11, description="CT_SvEffectif") + + sv_regularite: Optional[str] = Field(None, max_length=3, description="CT_SvRegul") + + sv_cotation: Optional[str] = Field(None, max_length=5, description="CT_SvCotation") + + sv_objet_maj: Optional[str] = Field( + None, max_length=61, description="CT_SvObjetMaj" + ) + + ca_annuel: Optional[float] = Field( + None, + description="CT_SvCA (Chiffre d'affaires annuel) - alias: sv_chiffre_affaires", + ) + + sv_chiffre_affaires: Optional[float] = Field( + None, description="CT_SvCA (alias de ca_annuel)" + ) + + sv_resultat: Optional[float] = Field(None, description="CT_SvResultat") + + @field_validator("siret") + @classmethod + def validate_siret(cls, v): + """Valide et nettoie le SIRET""" + if v and v.lower() not in ("none", "null", ""): + cleaned = v.replace(" ", "").replace("-", "") + if len(cleaned) not in (14, 15): + raise ValueError("Le SIRET doit contenir 14 ou 15 caractères") + return cleaned + return None + + @field_validator("email") + @classmethod + def validate_email(cls, v): + """Valide le format email""" + if v and v.lower() not in ("none", "null", ""): + v = v.strip() + if "@" not in v: + raise ValueError("Format email invalide") + return v + return None + + @field_validator("raccourci") + @classmethod + def validate_raccourci(cls, v): + """Force le raccourci en majuscules""" + if v and v.lower() not in ("none", "null", ""): + return v.upper().strip()[:7] + return None + + @field_validator( + "adresse", + "code_postal", + "ville", + "pays", + "telephone", + "tva_intra", + "contact", + "complement", + mode="before", + ) + @classmethod + def clean_none_strings(cls, v): + """Convertit les chaînes 'None'/'null'/'' en None""" + if isinstance(v, str) and v.lower() in ("none", "null", ""): + return None + return v + + def to_sage_dict(self) -> dict: + """ + Convertit le modèle en dictionnaire compatible avec creer_client() + Mapping 1:1 avec les paramètres réels de la fonction + """ + stat01 = self.statistique01 or self.secteur + + ca = self.ca_annuel or self.sv_chiffre_affaires + + return { + "intitule": self.intitule, + "numero": self.numero, + "type_tiers": self.type_tiers, + "qualite": self.qualite, + "classement": self.classement, + "raccourci": self.raccourci, + "siret": self.siret, + "tva_intra": self.tva_intra, + "code_naf": self.code_naf, + "contact": self.contact, + "adresse": self.adresse, + "complement": self.complement, + "code_postal": self.code_postal, + "ville": self.ville, + "region": self.region, + "pays": self.pays, + "telephone": self.telephone, + "telecopie": self.telecopie, + "email": self.email, + "site_web": self.site_web, + "portable": self.portable, + "facebook": self.facebook, + "linkedin": self.linkedin, + "compte_general": self.compte_general, + "categorie_tarifaire": self.categorie_tarifaire, + "categorie_comptable": self.categorie_comptable, + "taux01": self.taux01, + "taux02": self.taux02, + "taux03": self.taux03, + "taux04": self.taux04, + "statistique01": stat01, + "statistique02": self.statistique02, + "statistique03": self.statistique03, + "statistique04": self.statistique04, + "statistique05": self.statistique05, + "statistique06": self.statistique06, + "statistique07": self.statistique07, + "statistique08": self.statistique08, + "statistique09": self.statistique09, + "statistique10": self.statistique10, + "secteur": self.secteur, # Gardé pour compatibilité + "encours_autorise": self.encours_autorise, + "assurance_credit": self.assurance_credit, + "langue": self.langue, + "commercial_code": self.commercial_code, + "lettrage_auto": self.lettrage_auto, + "est_actif": self.est_actif, + "type_facture": self.type_facture, + "est_prospect": self.est_prospect, + "bl_en_facture": self.bl_en_facture, + "saut_page": self.saut_page, + "validation_echeance": self.validation_echeance, + "controle_encours": self.controle_encours, + "exclure_relance": self.exclure_relance, + "exclure_penalites": self.exclure_penalites, + "bon_a_payer": self.bon_a_payer, + "priorite_livraison": self.priorite_livraison, + "livraison_partielle": self.livraison_partielle, + "delai_transport": self.delai_transport, + "delai_appro": self.delai_appro, + "commentaire": self.commentaire, + "section_analytique": self.section_analytique, + "mode_reglement_code": self.mode_reglement_code, + "surveillance_active": self.surveillance_active, + "coface": self.coface, + "forme_juridique": self.forme_juridique, + "effectif": self.effectif, + "sv_regularite": self.sv_regularite, + "sv_cotation": self.sv_cotation, + "sv_objet_maj": self.sv_objet_maj, + "ca_annuel": ca, + "sv_chiffre_affaires": self.sv_chiffre_affaires, + "sv_resultat": self.sv_resultat, + } + + class Config: + json_schema_extra = { + "example": { + "intitule": "ENTREPRISE EXEMPLE SARL", + "numero": "CLI00123", + "type_tiers": 0, + "qualite": "CLI", + "compte_general": "411000", + "est_prospect": False, + "est_actif": True, + "email": "contact@exemple.fr", + "telephone": "0123456789", + "adresse": "123 Rue de la Paix", + "code_postal": "75001", + "ville": "Paris", + "pays": "France", + } + } + + +class ClientUpdateGatewayRequest(BaseModel): + """Modèle pour modification client côté gateway""" + + code: str + client_data: Dict diff --git a/schemas/tiers/contact.py b/schemas/tiers/contact.py new file mode 100644 index 0000000..bd6e56b --- /dev/null +++ b/schemas/tiers/contact.py @@ -0,0 +1,43 @@ + +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 + prenom: Optional[str] = None + fonction: Optional[str] = None + service_code: Optional[int] = None + telephone: Optional[str] = None + portable: Optional[str] = None + telecopie: Optional[str] = None + email: Optional[str] = None + facebook: Optional[str] = None + linkedin: Optional[str] = None + skype: Optional[str] = None + + +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 + + +class ContactDeleteRequest(BaseModel): + """Requête de suppression d'un contact""" + numero: str + contact_numero: int \ No newline at end of file diff --git a/schemas/tiers/fournisseurs.py b/schemas/tiers/fournisseurs.py new file mode 100644 index 0000000..7a0776d --- /dev/null +++ b/schemas/tiers/fournisseurs.py @@ -0,0 +1,41 @@ + +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 + + +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 + + diff --git a/schemas/tiers/tiers.py b/schemas/tiers/tiers.py index 9520ffb..f7e6ea2 100644 --- a/schemas/tiers/tiers.py +++ b/schemas/tiers/tiers.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, Field, validator, EmailStr, field_validator from typing import Optional, List, Dict +from enum import Enum, IntEnum class TiersListRequest(BaseModel): """Requête de listage des tiers""" @@ -10,4 +11,12 @@ class TiersListRequest(BaseModel): filtre: str = Field( "", description="Filtre sur code ou intitulé" - ) \ No newline at end of file + ) + +class TypeTiers(IntEnum): + """CT_Type - Type de tiers""" + + CLIENT = 0 + FOURNISSEUR = 1 + SALARIE = 2 + AUTRE = 3 \ No newline at end of file