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