from pydantic import BaseModel, Field, field_validator from typing import List, Optional from schemas.tiers.contact import Contact class ClientResponse(BaseModel): """Modèle de réponse client simplifié (pour listes)""" numero: Optional[str] = None intitule: Optional[str] = None adresse: Optional[str] = None code_postal: Optional[str] = None ville: Optional[str] = None email: Optional[str] = None telephone: Optional[str] = None class ClientDetails(BaseModel): numero: Optional[str] = Field(None, description="Code client (CT_Num)") intitule: Optional[str] = Field( None, description="Raison sociale ou Nom complet (CT_Intitule)" ) type_tiers: Optional[int] = Field( None, description="Type : 0=Client, 1=Fournisseur (CT_Type)" ) qualite: Optional[str] = Field( None, description="Qualité Sage : CLI, FOU, PRO (CT_Qualite)" ) classement: Optional[str] = Field( None, description="Code de classement (CT_Classement)" ) raccourci: Optional[str] = Field( None, description="Code raccourci 7 car. (CT_Raccourci)" ) siret: Optional[str] = Field(None, description="N° SIRET 14 chiffres (CT_Siret)") tva_intra: Optional[str] = Field( None, description="N° TVA intracommunautaire (CT_Identifiant)" ) code_naf: Optional[str] = Field(None, description="Code NAF/APE (CT_Ape)") contact: Optional[str] = Field( None, description="Nom du contact principal (CT_Contact)" ) adresse: Optional[str] = Field(None, description="Adresse ligne 1 (CT_Adresse)") complement: Optional[str] = Field( None, description="Complément d'adresse (CT_Complement)" ) code_postal: Optional[str] = Field(None, description="Code postal (CT_CodePostal)") ville: Optional[str] = Field(None, description="Ville (CT_Ville)") region: Optional[str] = Field(None, description="Région/État (CT_CodeRegion)") pays: Optional[str] = Field(None, description="Pays (CT_Pays)") telephone: Optional[str] = Field(None, description="Téléphone fixe (CT_Telephone)") telecopie: Optional[str] = Field(None, description="Fax (CT_Telecopie)") email: Optional[str] = Field(None, description="Email principal (CT_EMail)") site_web: Optional[str] = Field(None, description="Site web (CT_Site)") facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)") linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)") taux01: Optional[float] = Field(None, description="Taux personnalisé 1 (CT_Taux01)") taux02: Optional[float] = Field(None, description="Taux personnalisé 2 (CT_Taux02)") taux03: Optional[float] = Field(None, description="Taux personnalisé 3 (CT_Taux03)") taux04: Optional[float] = Field(None, description="Taux personnalisé 4 (CT_Taux04)") statistique01: Optional[str] = Field( None, description="Statistique 1 (CT_Statistique01)" ) statistique02: Optional[str] = Field( None, description="Statistique 2 (CT_Statistique02)" ) statistique03: Optional[str] = Field( None, description="Statistique 3 (CT_Statistique03)" ) statistique04: Optional[str] = Field( None, description="Statistique 4 (CT_Statistique04)" ) statistique05: Optional[str] = Field( None, description="Statistique 5 (CT_Statistique05)" ) statistique06: Optional[str] = Field( None, description="Statistique 6 (CT_Statistique06)" ) statistique07: Optional[str] = Field( None, description="Statistique 7 (CT_Statistique07)" ) statistique08: Optional[str] = Field( None, description="Statistique 8 (CT_Statistique08)" ) statistique09: Optional[str] = Field( None, description="Statistique 9 (CT_Statistique09)" ) statistique10: Optional[str] = Field( None, description="Statistique 10 (CT_Statistique10)" ) encours_autorise: Optional[float] = Field( None, description="Encours maximum autorisé (CT_Encours)" ) assurance_credit: Optional[float] = Field( None, description="Montant assurance crédit (CT_Assurance)" ) langue: Optional[int] = Field( None, description="Code langue 0=FR, 1=EN (CT_Langue)" ) commercial_code: Optional[int] = Field( None, description="Code du commercial (CO_No)" ) lettrage_auto: Optional[bool] = Field( None, description="Lettrage automatique (CT_Lettrage)" ) est_actif: Optional[bool] = Field(None, description="True si actif (CT_Sommeil=0)") type_facture: Optional[int] = Field( None, description="Type facture 0=Facture, 1=BL (CT_Facture)" ) est_prospect: Optional[bool] = Field( None, description="True si prospect (CT_Prospect=1)" ) bl_en_facture: Optional[int] = Field( None, description="Imprimer BL en facture (CT_BLFact)" ) saut_page: Optional[int] = Field( None, description="Saut de page sur documents (CT_Saut)" ) validation_echeance: Optional[int] = Field( None, description="Valider les échéances (CT_ValidEch)" ) controle_encours: Optional[int] = Field( None, description="Contrôler l'encours (CT_ControlEnc)" ) exclure_relance: Optional[bool] = Field( None, description="Exclure des relances (CT_NotRappel)" ) exclure_penalites: Optional[bool] = Field( None, description="Exclure des pénalités (CT_NotPenal)" ) bon_a_payer: Optional[int] = Field( None, description="Bon à payer obligatoire (CT_BonAPayer)" ) priorite_livraison: Optional[int] = Field( None, description="Priorité livraison (CT_PrioriteLivr)" ) livraison_partielle: Optional[int] = Field( None, description="Livraison partielle (CT_LivrPartielle)" ) delai_transport: Optional[int] = Field( None, description="Délai transport jours (CT_DelaiTransport)" ) delai_appro: Optional[int] = Field( None, description="Délai appro jours (CT_DelaiAppro)" ) commentaire: Optional[str] = Field( None, description="Commentaire libre (CT_Commentaire)" ) section_analytique: Optional[str] = Field( None, description="Section analytique (CA_Num)" ) mode_reglement_code: Optional[int] = Field( None, description="Code mode règlement (MR_No)" ) surveillance_active: Optional[bool] = Field( None, description="Surveillance financière (CT_Surveillance)" ) coface: Optional[str] = Field(None, description="Code Coface 25 car. (CT_Coface)") forme_juridique: Optional[str] = Field( None, description="Forme juridique SA, SARL (CT_SvFormeJuri)" ) effectif: Optional[str] = Field( None, description="Nombre d'employés (CT_SvEffectif)" ) sv_regularite: Optional[str] = Field( None, description="Régularité paiements (CT_SvRegul)" ) sv_cotation: Optional[str] = Field( None, description="Cotation crédit (CT_SvCotation)" ) sv_objet_maj: Optional[str] = Field( None, description="Objet dernière MAJ (CT_SvObjetMaj)" ) sv_chiffre_affaires: Optional[float] = Field( None, description="Chiffre d'affaires (CT_SvCA)" ) sv_resultat: Optional[float] = Field( None, description="Résultat financier (CT_SvResultat)" ) compte_general: Optional[str] = Field( None, description="Compte général principal (CG_NumPrinc)" ) categorie_tarif: Optional[int] = Field( None, description="Catégorie tarifaire (N_CatTarif)" ) categorie_compta: Optional[int] = Field( None, description="Catégorie comptable (N_CatCompta)" ) contacts: Optional[List[Contact]] = Field( default_factory=list, description="Liste des contacts du client" ) class Config: json_schema_extra = { "example": { "numero": "CLI000001", "intitule": "SARL EXEMPLE", "type_tiers": 0, "qualite": "CLI", "classement": "A", "raccourci": "EXEMPL", "siret": "12345678901234", "tva_intra": "FR12345678901", "code_naf": "6201Z", "contact": "Jean Dupont", "adresse": "123 Rue de la Paix", "complement": "Bâtiment B", "code_postal": "75001", "ville": "Paris", "region": "Île-de-France", "pays": "France", "telephone": "0123456789", "telecopie": "0123456788", "email": "contact@exemple.fr", "site_web": "https://www.exemple.fr", "facebook": "https://facebook.com/exemple", "linkedin": "https://linkedin.com/company/exemple", "taux01": 0.0, "taux02": 0.0, "taux03": 0.0, "taux04": 0.0, "statistique01": "Informatique", "statistique02": "", "statistique03": "", "statistique04": "", "statistique05": "", "statistique06": "", "statistique07": "", "statistique08": "", "statistique09": "", "statistique10": "", "encours_autorise": 50000.0, "assurance_credit": 40000.0, "langue": 0, "commercial_code": 1, "lettrage_auto": True, "est_actif": True, "type_facture": 1, "est_prospect": False, "bl_en_facture": 0, "saut_page": 0, "validation_echeance": 0, "controle_encours": 1, "exclure_relance": False, "exclure_penalites": False, "bon_a_payer": 0, "priorite_livraison": 1, "livraison_partielle": 1, "delai_transport": 2, "delai_appro": 0, "commentaire": "Client important", "section_analytique": "", "mode_reglement_code": 1, "surveillance_active": True, "coface": "COF12345", "forme_juridique": "SARL", "effectif": "50-99", "sv_regularite": "", "sv_cotation": "", "sv_objet_maj": "", "sv_chiffre_affaires": 2500000.0, "sv_resultat": 150000.0, "compte_general": "4110000", "categorie_tarif": 0, "categorie_compta": 0, } } class ClientCreateRequest(BaseModel): intitule: str = Field( ..., max_length=69, description="Nom du client (CT_Intitule) - OBLIGATOIRE" ) numero: str = Field( ..., 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 ClientUpdateRequest(BaseModel): """ Modèle pour modification d'un client existant TOUS les champs de ClientCreateRequest sont modifiables TOUS optionnels (seuls les champs fournis sont modifiés) """ intitule: Optional[str] = Field(None, max_length=69) qualite: Optional[str] = Field(None, max_length=17) classement: Optional[str] = Field(None, max_length=17) raccourci: Optional[str] = Field(None, max_length=7) siret: Optional[str] = Field(None, max_length=15) tva_intra: Optional[str] = Field(None, max_length=25) code_naf: Optional[str] = Field(None, max_length=7) contact: Optional[str] = Field(None, max_length=35) adresse: Optional[str] = Field(None, max_length=35) complement: Optional[str] = Field(None, max_length=35) code_postal: Optional[str] = Field(None, max_length=9) ville: Optional[str] = Field(None, max_length=35) region: Optional[str] = Field(None, max_length=25) pays: Optional[str] = Field(None, max_length=35) telephone: Optional[str] = Field(None, max_length=21) telecopie: Optional[str] = Field(None, max_length=21) email: Optional[str] = Field(None, max_length=69) site_web: Optional[str] = Field(None, max_length=69) portable: Optional[str] = Field(None, max_length=21) facebook: Optional[str] = Field(None, max_length=69) linkedin: Optional[str] = Field(None, max_length=69) compte_general: Optional[str] = Field(None, max_length=13) categorie_tarifaire: Optional[str] = None categorie_comptable: Optional[str] = None taux01: Optional[float] = None taux02: Optional[float] = None taux03: Optional[float] = None taux04: Optional[float] = None secteur: Optional[str] = Field(None, max_length=21) statistique01: Optional[str] = Field(None, max_length=21) statistique02: Optional[str] = Field(None, max_length=21) statistique03: Optional[str] = Field(None, max_length=21) statistique04: Optional[str] = Field(None, max_length=21) statistique05: Optional[str] = Field(None, max_length=21) statistique06: Optional[str] = Field(None, max_length=21) statistique07: Optional[str] = Field(None, max_length=21) statistique08: Optional[str] = Field(None, max_length=21) statistique09: Optional[str] = Field(None, max_length=21) statistique10: Optional[str] = Field(None, max_length=21) encours_autorise: Optional[float] = None assurance_credit: Optional[float] = None langue: Optional[int] = Field(None, ge=0) commercial_code: Optional[int] = None lettrage_auto: Optional[bool] = None est_actif: Optional[bool] = None type_facture: Optional[int] = Field(None, ge=0, le=2) est_prospect: Optional[bool] = None bl_en_facture: Optional[int] = Field(None, ge=0, le=1) saut_page: Optional[int] = Field(None, ge=0, le=1) validation_echeance: Optional[int] = Field(None, ge=0, le=1) controle_encours: Optional[int] = Field(None, ge=0, le=1) exclure_relance: Optional[int] = Field(None, ge=0, le=1) exclure_penalites: Optional[int] = Field(None, ge=0, le=1) bon_a_payer: Optional[int] = Field(None, ge=0, le=1) priorite_livraison: Optional[int] = Field(None, ge=0, le=5) livraison_partielle: Optional[int] = Field(None, ge=0, le=1) delai_transport: Optional[int] = Field(None, ge=0) delai_appro: Optional[int] = Field(None, ge=0) commentaire: Optional[str] = Field(None, max_length=35) section_analytique: Optional[str] = Field(None, max_length=13) mode_reglement_code: Optional[int] = None surveillance_active: Optional[int] = Field(None, ge=0, le=1) coface: Optional[str] = Field(None, max_length=25) forme_juridique: Optional[str] = Field(None, max_length=33) effectif: Optional[str] = Field(None, max_length=11) sv_regularite: Optional[str] = Field(None, max_length=3) sv_cotation: Optional[str] = Field(None, max_length=5) sv_objet_maj: Optional[str] = Field(None, max_length=61) ca_annuel: Optional[float] = None sv_chiffre_affaires: Optional[float] = None sv_resultat: Optional[float] = None @field_validator("siret") @classmethod def validate_siret(cls, v): 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): 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): 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): if isinstance(v, str) and v.lower() in ("none", "null", ""): return None return v class Config: json_schema_extra = { "example": { "email": "nouveau@email.fr", "telephone": "0198765432", "portable": "0687654321", "adresse": "456 Avenue Nouvelle", "ville": "Lyon", } }