updated client create pydantic schema

This commit is contained in:
fanilo 2025-12-26 14:40:48 +01:00
parent fde2b4615e
commit c9497adad2

811
main.py
View file

@ -89,153 +89,173 @@ class TypeTiers(IntEnum):
class ClientCreateRequest(BaseModel): class ClientCreateRequest(BaseModel):
"""
Modèle complet pour la création d'un client Sage 100c
Noms alignés sur le frontend + mapping vers champs Sage
"""
# ══════════════════════════════════════════════════════════════ intitule: str = Field(
# IDENTIFICATION PRINCIPALE ...,
# ══════════════════════════════════════════════════════════════ max_length=69,
intitule: str = Field(..., max_length=69, description="Nom du client (CT_Intitule)") description="Nom du client (CT_Intitule) - OBLIGATOIRE"
numero: Optional[str] = Field(None, max_length=17, description="Numéro client CT_Num (auto si vide)")
type_tiers: Optional[int] = Field(0, ge=0, le=3, description="CT_Type: 0=Client, 1=Fournisseur, 2=Salarié, 3=Autre")
qualite: str = Field(None, 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")
# ══════════════════════════════════════════════════════════════
# STATUTS & FLAGS
# ══════════════════════════════════════════════════════════════
est_prospect: bool = Field(False, description="CT_Prospect")
est_actif: bool = Field(True, description="Inverse de CT_Sommeil")
est_en_sommeil: Optional[bool] = Field(None, description="CT_Sommeil (calculé depuis est_actif si None)")
# ══════════════════════════════════════════════════════════════
# INFORMATIONS ENTREPRISE / PERSONNE
# ══════════════════════════════════════════════════════════════
est_entreprise: Optional[bool] = Field(None, description="Flag interne - pas de champ Sage direct")
est_particulier: Optional[bool] = Field(None, description="Flag interne - pas de champ Sage direct")
forme_juridique: Optional[str] = Field(None, max_length=33, description="CT_SvFormeJuri")
civilite: Optional[str] = Field(None, max_length=17, description="Stocké dans CT_Qualite ou champ libre")
nom: Optional[str] = Field(None, max_length=35, description="Pour particuliers - partie de CT_Intitule")
prenom: Optional[str] = Field(None, max_length=35, description="Pour particuliers - partie de CT_Intitule")
nom_complet: Optional[str] = Field(None, max_length=69, description="Calculé ou CT_Intitule")
# ══════════════════════════════════════════════════════════════
# ADRESSE PRINCIPALE
# ══════════════════════════════════════════════════════════════
contact: Optional[str] = Field(None, max_length=35, description="CT_Contact")
adresse: Optional[str] = Field(None, max_length=35, description="CT_Adresse")
complement: Optional[str] = Field(None, max_length=35, description="CT_Complement")
code_postal: Optional[str] = Field(None, max_length=9, description="CT_CodePostal")
ville: Optional[str] = Field(None, max_length=35, description="CT_Ville")
region: Optional[str] = Field(None, max_length=25, description="CT_CodeRegion")
pays: Optional[str] = Field(None, max_length=35, description="CT_Pays")
# ══════════════════════════════════════════════════════════════
# CONTACT & COMMUNICATION
# ══════════════════════════════════════════════════════════════
telephone: Optional[str] = Field(None, max_length=21, description="CT_Telephone")
portable: Optional[str] = Field(None, max_length=21, description="Stocké dans statistiques ou contact")
telecopie: Optional[str] = Field(None, max_length=21, description="CT_Telecopie")
email: Optional[str] = Field(None, max_length=69, description="CT_EMail")
site_web: Optional[str] = Field(None, max_length=69, description="CT_Site")
facebook: Optional[str] = Field(None, max_length=35, description="CT_Facebook")
linkedin: Optional[str] = Field(None, max_length=35, description="CT_LinkedIn")
# ══════════════════════════════════════════════════════════════
# IDENTIFIANTS LÉGAUX & FISCAUX
# ══════════════════════════════════════════════════════════════
siret: Optional[str] = Field(None, max_length=15, description="CT_Siret (14-15 chars)")
siren: Optional[str] = Field(None, max_length=9, description="Extrait du SIRET")
tva_intra: Optional[str] = Field(None, max_length=25, description="CT_Identifiant")
code_naf: Optional[str] = Field(None, max_length=7, description="CT_Ape")
type_nif: Optional[int] = Field(None, ge=0, le=10, description="CT_TypeNIF")
# ══════════════════════════════════════════════════════════════
# BANQUE & DEVISE
# ══════════════════════════════════════════════════════════════
banque_num: Optional[int] = Field(None, description="BT_Num (smallint)")
devise: Optional[int] = Field(0, description="N_Devise (0=EUR)")
# ══════════════════════════════════════════════════════════════
# CATÉGORIES & CLASSIFICATIONS COMMERCIALES
# ══════════════════════════════════════════════════════════════
categorie_tarifaire: Optional[int] = Field(1, ge=0, description="N_CatTarif")
categorie_comptable: Optional[int] = Field(1, ge=0, description="N_CatCompta")
periode_reglement: Optional[int] = Field(1, ge=0, description="N_Period")
mode_expedition: Optional[int] = Field(1, ge=0, description="N_Expedition")
condition_livraison: Optional[int] = Field(1, ge=0, description="N_Condition")
niveau_risque: Optional[int] = Field(1, ge=0, description="N_Risque")
secteur: Optional[str] = Field(None, max_length=21, description="CT_Statistique01 ou champ libre")
# ══════════════════════════════════════════════════════════════
# TAUX PERSONNALISÉS
# ══════════════════════════════════════════════════════════════
taux01: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_Taux01")
taux02: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_Taux02")
taux03: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_Taux03")
taux04: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_Taux04")
# ══════════════════════════════════════════════════════════════
# GESTION COMMERCIALE
# ══════════════════════════════════════════════════════════════
encours_autorise: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_Encours")
assurance_credit: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_Assurance")
num_payeur: Optional[str] = Field(None, max_length=17, description="CT_NumPayeur")
langue: Optional[int] = Field(None, ge=0, description="CT_Langue")
langue_iso2: Optional[str] = Field(None, max_length=3, description="CT_LangueISO2")
commercial_code: Optional[int] = Field(None, description="CO_No (int)")
commercial_nom: Optional[str] = Field(None, description="Résolu depuis CO_No - non stocké")
effectif: Optional[str] = Field(None, max_length=11, description="CT_SvEffectif")
ca_annuel: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_SvCA")
# ══════════════════════════════════════════════════════════════
# COMPTABILITÉ
# ══════════════════════════════════════════════════════════════
compte_general: Optional[str] = Field("411000", max_length=13, description="CG_NumPrinc")
# ══════════════════════════════════════════════════════════════
# PARAMÈTRES FACTURATION
# ══════════════════════════════════════════════════════════════
type_facture: Optional[int] = Field(1, ge=0, le=2, description="CT_Facture: 0=aucune, 1=normale, 2=regroupée")
bl_en_facture: Optional[int] = Field(None, ge=0, le=1, description="CT_BLFact")
saut_page: Optional[int] = Field(None, ge=0, le=1, description="CT_Saut")
lettrage_auto: Optional[bool] = Field(True, description="CT_Lettrage")
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")
# ══════════════════════════════════════════════════════════════
# LIVRAISON & LOGISTIQUE
# ══════════════════════════════════════════════════════════════
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)")
# JOURS DE COMMANDE (0=non, 1=oui) - CT_OrderDay01-07
jours_commande: Optional[dict] = Field(
None,
description="Dict avec clés: lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche"
) )
# JOURS DE LIVRAISON (0=non, 1=oui) - CT_DeliveryDay01-07 numero: Optional[str] = Field(
jours_livraison: Optional[dict] = Field(
None, None,
description="Dict avec clés: lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche" max_length=17,
description="Numéro client CT_Num (auto si None)"
) )
# DATES FERMETURE type_tiers: int = Field(
date_fermeture_debut: Optional[date] = Field(None, description="CT_DateFermeDebut") 0,
date_fermeture_fin: Optional[date] = Field(None, description="CT_DateFermeFin") 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)"
)
# ══════════════════════════════════════════════════════════════
# STATISTIQUES PERSONNALISÉES (10 champs de 21 chars max)
# ══════════════════════════════════════════════════════════════
statistique01: Optional[str] = Field(None, max_length=21, description="CT_Statistique01") statistique01: Optional[str] = Field(None, max_length=21, description="CT_Statistique01")
statistique02: Optional[str] = Field(None, max_length=21, description="CT_Statistique02") statistique02: Optional[str] = Field(None, max_length=21, description="CT_Statistique02")
statistique03: Optional[str] = Field(None, max_length=21, description="CT_Statistique03") statistique03: Optional[str] = Field(None, max_length=21, description="CT_Statistique03")
@ -247,250 +267,289 @@ class ClientCreateRequest(BaseModel):
statistique09: Optional[str] = Field(None, max_length=21, description="CT_Statistique09") statistique09: Optional[str] = Field(None, max_length=21, description="CT_Statistique09")
statistique10: Optional[str] = Field(None, max_length=21, description="CT_Statistique10") statistique10: Optional[str] = Field(None, max_length=21, description="CT_Statistique10")
# ══════════════════════════════════════════════════════════════ encours_autorise: Optional[float] = Field(
# COMMENTAIRE None,
# ══════════════════════════════════════════════════════════════ description="CT_Encours (montant max autorisé)"
commentaire: Optional[str] = Field(None, max_length=35, description="CT_Commentaire") )
# ══════════════════════════════════════════════════════════════ assurance_credit: Optional[float] = Field(
# ANALYTIQUE None,
# ══════════════════════════════════════════════════════════════ description="CT_Assurance (montant assurance crédit)"
section_analytique: Optional[str] = Field(None, max_length=13, description="CA_Num") )
section_analytique_ifrs: Optional[str] = Field(None, max_length=13, description="CA_NumIFRS")
plan_analytique: Optional[int] = Field(None, ge=0, description="N_Analytique")
plan_analytique_ifrs: Optional[int] = Field(None, ge=0, description="N_AnalytiqueIFRS")
# ══════════════════════════════════════════════════════════════ langue: Optional[int] = Field(
# ORGANISATION None,
# ══════════════════════════════════════════════════════════════ ge=0,
depot_code: Optional[int] = Field(None, description="DE_No (int)") description="CT_Langue (0=Français, 1=Anglais, etc.)"
etablissement_code: Optional[int] = Field(None, description="EB_No (int)") )
mode_reglement_code: Optional[int] = Field(None, description="MR_No (int)")
calendrier_code: Optional[int] = Field(None, description="CAL_No (int)")
num_centrale: Optional[str] = Field(None, max_length=17, description="CT_NumCentrale")
# ══════════════════════════════════════════════════════════════ commercial_code: Optional[int] = Field(
# SURVEILLANCE COFACE None,
# ══════════════════════════════════════════════════════════════ description="CO_No (ID du collaborateur commercial)"
coface: Optional[str] = Field(None, max_length=25, description="CT_Coface") )
surveillance_active: Optional[int] = Field(None, ge=0, le=1, description="CT_Surveillance")
sv_date_creation: Optional[datetime] = Field(None, description="CT_SvDateCreate")
sv_chiffre_affaires: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_SvCA")
sv_resultat: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_SvResultat")
sv_incident: Optional[int] = Field(None, ge=0, le=1, description="CT_SvIncident")
sv_date_incident: Optional[datetime] = Field(None, description="CT_SvDateIncid")
sv_privilege: Optional[int] = Field(None, ge=0, le=1, description="CT_SvPrivil")
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_date_maj: Optional[datetime] = Field(None, description="CT_SvDateMaj")
sv_objet_maj: Optional[str] = Field(None, max_length=61, description="CT_SvObjetMaj")
sv_date_bilan: Optional[datetime] = Field(None, description="CT_SvDateBilan")
sv_nb_mois_bilan: Optional[int] = Field(None, ge=0, description="CT_SvNbMoisBilan")
# ══════════════════════════════════════════════════════════════ lettrage_auto: Optional[bool] = Field(
# FACTURATION ÉLECTRONIQUE True,
# ══════════════════════════════════════════════════════════════ description="CT_Lettrage (1=oui, 0=non)"
facture_electronique: Optional[int] = Field(None, ge=0, le=1, description="CT_FactureElec") )
edi_code_type: Optional[int] = Field(None, description="CT_EdiCodeType")
edi_code: Optional[str] = Field(None, max_length=23, description="CT_EdiCode")
edi_code_sage: Optional[str] = Field(None, max_length=9, description="CT_EdiCodeSage")
fe_assujetti: Optional[int] = Field(None, description="CT_FEAssujetti")
fe_autre_identif_type: Optional[int] = Field(None, description="CT_FEAutreIdentifType")
fe_autre_identif_val: Optional[str] = Field(None, max_length=81, description="CT_FEAutreIdentifVal")
fe_entite_type: Optional[int] = Field(None, description="CT_FEEntiteType")
fe_emission: Optional[int] = Field(None, description="CT_FEEmission")
fe_application: Optional[int] = Field(None, description="CT_FEApplication")
fe_date_synchro: Optional[datetime] = Field(None, description="CT_FEDateSynchro")
# ══════════════════════════════════════════════════════════════ est_actif: Optional[bool] = Field(
# ÉCHANGES & INTÉGRATION True,
# ══════════════════════════════════════════════════════════════ description="Inverse de CT_Sommeil (True=actif, False=en sommeil)"
echange_rappro: Optional[int] = Field(None, description="CT_EchangeRappro") )
echange_cr: Optional[int] = Field(None, description="CT_EchangeCR")
pi_no_echange: Optional[int] = Field(None, description="PI_NoEchange")
annulation_cr: Optional[int] = Field(None, description="CT_AnnulationCR")
profil_societe: Optional[int] = Field(None, description="CT_ProfilSoc")
statut_contrat: Optional[int] = Field(None, description="CT_StatutContrat")
# ══════════════════════════════════════════════════════════════ type_facture: Optional[int] = Field(
# RGPD & CONFIDENTIALITÉ 1,
# ══════════════════════════════════════════════════════════════ ge=0,
rgpd_consentement: Optional[int] = Field(None, ge=0, le=1, description="CT_GDPR") le=2,
exclure_traitement: Optional[int] = Field(None, ge=0, le=1, description="CT_ExclureTrait") description="CT_Facture: 0=aucune, 1=normale, 2=regroupée"
)
# ══════════════════════════════════════════════════════════════ est_prospect: Optional[bool] = Field(
# REPRÉSENTANT FISCAL False,
# ══════════════════════════════════════════════════════════════ description="CT_Prospect (1=oui, 0=non)"
representant_intl: Optional[str] = Field(None, max_length=35, description="CT_RepresentInt") )
representant_nif: Optional[str] = Field(None, max_length=25, description="CT_RepresentNIF")
# ══════════════════════════════════════════════════════════════ bl_en_facture: Optional[int] = Field(
# CHAMPS PERSONNALISÉS (Info Libres Sage) None,
# ══════════════════════════════════════════════════════════════ ge=0,
date_creation_societe: Optional[datetime] = Field(None, description="Date création société (Info Libre)") le=1,
capital_social: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="Capital social") description="CT_BLFact (impression BL sur facture)"
actionnaire_principal: Optional[str] = Field(None, max_length=69, description="Actionnaire Pal") )
score_banque_france: Optional[str] = Field(None, max_length=14, description="Score Banque de France")
# FIDÉLITÉ saut_page: Optional[int] = Field(
total_points_fidelite: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6) None,
points_fidelite_restants: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6) ge=0,
date_fin_carte_fidelite: Optional[datetime] = Field(None, description="Fin validité carte fidélité") le=1,
date_negociation_reglement: Optional[datetime] = Field(None, description="Date négociation règlement") description="CT_Saut (saut de page après impression)"
)
# ══════════════════════════════════════════════════════════════ validation_echeance: Optional[int] = Field(
# AUTRES None,
# ══════════════════════════════════════════════════════════════ ge=0,
mode_test: Optional[int] = Field(None, ge=0, le=1, description="CT_ModeTest") le=1,
confiance: Optional[int] = Field(None, description="CT_Confiance") description="CT_ValidEch"
dn_id: Optional[str] = Field(None, max_length=37, description="DN_Id") )
# ══════════════════════════════════════════════════════════════ controle_encours: Optional[int] = Field(
# MÉTADONNÉES (en lecture seule généralement) None,
# ══════════════════════════════════════════════════════════════ ge=0,
date_creation: Optional[datetime] = Field(None, description="cbCreation - géré par Sage") le=1,
date_modification: Optional[datetime] = Field(None, description="cbModification - géré par Sage") description="CT_ControlEnc"
date_maj: Optional[datetime] = Field(None, description="CT_DateMAJ") )
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"
)
# ══════════════════════════════════════════════════════════════
# VALIDATORS
# ══════════════════════════════════════════════════════════════
@field_validator('siret') @field_validator('siret')
@classmethod @classmethod
def validate_siret(cls, v): def validate_siret(cls, v):
if v and v.lower() not in ('none', ''): """Valide et nettoie le SIRET"""
if v and v.lower() not in ('none', 'null', ''):
cleaned = v.replace(' ', '').replace('-', '') cleaned = v.replace(' ', '').replace('-', '')
if len(cleaned) not in (14, 15): if len(cleaned) not in (14, 15):
raise ValueError('Le SIRET doit contenir 14 ou 15 caractères') raise ValueError('Le SIRET doit contenir 14 ou 15 caractères')
return cleaned return cleaned
return None return None
@field_validator('siren')
@classmethod
def validate_siren(cls, v):
if v and v.lower() not in ('none', ''):
cleaned = v.replace(' ', '')
if len(cleaned) != 9:
raise ValueError('Le SIREN doit contenir 9 caractères')
return cleaned
return None
@field_validator('email') @field_validator('email')
@classmethod @classmethod
def validate_email(cls, v): def validate_email(cls, v):
if v and v.lower() not in ('none', ''): """Valide le format email"""
if v and v.lower() not in ('none', 'null', ''):
v = v.strip()
if '@' not in v: if '@' not in v:
raise ValueError('Format email invalide') raise ValueError('Format email invalide')
return v.strip() return v
return None return None
@field_validator('adresse', 'code_postal', 'ville', 'pays', 'telephone', 'tva_intra', mode='before') @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 @classmethod
def clean_none_strings(cls, v): def clean_none_strings(cls, v):
"""Convertit les chaînes 'None' en None""" """Convertit les chaînes 'None'/'null'/'' en None"""
if isinstance(v, str) and v.lower() in ('none', 'null', ''): if isinstance(v, str) and v.lower() in ('none', 'null', ''):
return None return None
return v return v
@field_validator('est_en_sommeil', mode='before')
@classmethod
def compute_sommeil(cls, v, info):
"""Calcule est_en_sommeil depuis est_actif si non fourni"""
if v is None and 'est_actif' in info.data:
return not info.data.get('est_actif', True)
return v
def to_sage_dict(self) -> dict: def to_sage_dict(self) -> dict:
"""Convertit le modèle en dictionnaire compatible avec la méthode creer_client""" """
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 { return {
# Identification
"intitule": self.intitule, "intitule": self.intitule,
"num": self.numero, "numero": self.numero,
"type_tiers": self.type_tiers, "type_tiers": self.type_tiers,
"qualite": self.qualite, "qualite": self.qualite,
"classement": self.classement, "classement": self.classement,
"raccourci": self.raccourci, "raccourci": self.raccourci,
# Statuts "siret": self.siret,
"prospect": self.est_prospect, "tva_intra": self.tva_intra,
"sommeil": not self.est_actif if self.est_en_sommeil is None else self.est_en_sommeil, "code_naf": self.code_naf,
# Adresse
"contact": self.contact, "contact": self.contact,
"adresse": self.adresse, "adresse": self.adresse,
"complement": self.complement, "complement": self.complement,
"code_postal": self.code_postal, "code_postal": self.code_postal,
"ville": self.ville, "ville": self.ville,
"code_region": self.region, "region": self.region,
"pays": self.pays, "pays": self.pays,
# Communication
"telephone": self.telephone, "telephone": self.telephone,
"telecopie": self.telecopie, "telecopie": self.telecopie,
"email": self.email, "email": self.email,
"site": self.site_web, "site_web": self.site_web,
"portable": self.portable,
"facebook": self.facebook, "facebook": self.facebook,
"linkedin": self.linkedin, "linkedin": self.linkedin,
# Identifiants légaux "compte_general": self.compte_general,
"siret": self.siret,
"tva_intra": self.tva_intra,
"ape": self.code_naf,
"type_nif": self.type_nif,
# Banque & devise "categorie_tarifaire": self.categorie_tarifaire,
"banque_num": self.banque_num, "categorie_comptable": self.categorie_comptable,
"devise": self.devise,
# Catégories
"cat_tarif": self.categorie_tarifaire or 1,
"cat_compta": self.categorie_comptable or 1,
"period": self.periode_reglement or 1,
"expedition": self.mode_expedition or 1,
"condition": self.condition_livraison or 1,
"risque": self.niveau_risque or 1,
# Taux
"taux01": self.taux01, "taux01": self.taux01,
"taux02": self.taux02, "taux02": self.taux02,
"taux03": self.taux03, "taux03": self.taux03,
"taux04": self.taux04, "taux04": self.taux04,
# Gestion commerciale "statistique01": stat01,
"encours": self.encours_autorise,
"assurance": self.assurance_credit,
"num_payeur": self.num_payeur,
"langue": self.langue,
"langue_iso2": self.langue_iso2,
"compte_collectif": self.compte_general or "411000",
"collaborateur": self.commercial_code,
# Facturation
"facture": self.type_facture,
"bl_fact": self.bl_en_facture,
"saut": self.saut_page,
"lettrage": self.lettrage_auto,
"valid_ech": self.validation_echeance,
"control_enc": self.controle_encours,
"not_rappel": self.exclure_relance,
"not_penal": self.exclure_penalites,
"bon_a_payer": self.bon_a_payer,
# Livraison
"priorite_livr": self.priorite_livraison,
"livr_partielle": self.livraison_partielle,
"delai_transport": self.delai_transport,
"delai_appro": self.delai_appro,
"date_ferme_debut": self.date_fermeture_debut,
"date_ferme_fin": self.date_fermeture_fin,
# Jours commande/livraison
**(self._expand_jours("order_day", self.jours_commande) if self.jours_commande else {}),
**(self._expand_jours("delivery_day", self.jours_livraison) if self.jours_livraison else {}),
# Statistiques
"statistique01": self.statistique01 or self.secteur,
"statistique02": self.statistique02, "statistique02": self.statistique02,
"statistique03": self.statistique03, "statistique03": self.statistique03,
"statistique04": self.statistique04, "statistique04": self.statistique04,
@ -500,98 +559,68 @@ class ClientCreateRequest(BaseModel):
"statistique08": self.statistique08, "statistique08": self.statistique08,
"statistique09": self.statistique09, "statistique09": self.statistique09,
"statistique10": self.statistique10, "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
"commentaire": self.commentaire, "commentaire": self.commentaire,
# Analytique
"section_analytique": self.section_analytique, "section_analytique": self.section_analytique,
"section_analytique_ifrs": self.section_analytique_ifrs,
"plan_analytique": self.plan_analytique,
"plan_analytique_ifrs": self.plan_analytique_ifrs,
# Organisation "mode_reglement_code": self.mode_reglement_code,
"depot": self.depot_code,
"etablissement": self.etablissement_code,
"mode_regl": self.mode_reglement_code,
"calendrier": self.calendrier_code,
"num_centrale": self.num_centrale,
# Surveillance "surveillance_active": self.surveillance_active,
"coface": self.coface, "coface": self.coface,
"surveillance": self.surveillance_active, "forme_juridique": self.forme_juridique,
"sv_forme_juri": self.forme_juridique, "effectif": self.effectif,
"sv_effectif": self.effectif, "sv_regularite": self.sv_regularite,
"sv_ca": self.sv_chiffre_affaires or self.ca_annuel,
"sv_resultat": self.sv_resultat,
"sv_incident": self.sv_incident,
"sv_date_incid": self.sv_date_incident,
"sv_privil": self.sv_privilege,
"sv_regul": self.sv_regularite,
"sv_cotation": self.sv_cotation, "sv_cotation": self.sv_cotation,
"sv_date_create": self.sv_date_creation,
"sv_date_maj": self.sv_date_maj,
"sv_objet_maj": self.sv_objet_maj, "sv_objet_maj": self.sv_objet_maj,
"sv_date_bilan": self.sv_date_bilan, "ca_annuel": ca,
"sv_nb_mois_bilan": self.sv_nb_mois_bilan, "sv_chiffre_affaires": self.sv_chiffre_affaires,
"sv_resultat": self.sv_resultat,
# Facturation électronique
"facture_elec": self.facture_electronique,
"edi_code_type": self.edi_code_type,
"edi_code": self.edi_code,
"edi_code_sage": self.edi_code_sage,
"fe_assujetti": self.fe_assujetti,
"fe_autre_identif_type": self.fe_autre_identif_type,
"fe_autre_identif_val": self.fe_autre_identif_val,
"fe_entite_type": self.fe_entite_type,
"fe_emission": self.fe_emission,
"fe_application": self.fe_application,
# Échanges
"echange_rappro": self.echange_rappro,
"echange_cr": self.echange_cr,
"annulation_cr": self.annulation_cr,
"profil_soc": self.profil_societe,
"statut_contrat": self.statut_contrat,
# RGPD
"gdpr": self.rgpd_consentement,
"exclure_trait": self.exclure_traitement,
# Représentant
"represent_int": self.representant_intl,
"represent_nif": self.representant_nif,
# Autres
"mode_test": self.mode_test,
"confiance": self.confiance,
} }
def _expand_jours(self, prefix: str, jours: dict) -> dict:
"""Expand les jours en champs individuels"""
mapping = {
"lundi": f"{prefix}_lundi",
"mardi": f"{prefix}_mardi",
"mercredi": f"{prefix}_mercredi",
"jeudi": f"{prefix}_jeudi",
"vendredi": f"{prefix}_vendredi",
"samedi": f"{prefix}_samedi",
"dimanche": f"{prefix}_dimanche",
}
return {v: jours.get(k) for k, v in mapping.items() if jours.get(k) is not None}
class Config: class Config:
json_schema_extra = { json_schema_extra = {
"example": { "example": {
"intitule": "ENTREPRISE EXEMPLE SARL", "intitule": "ENTREPRISE EXEMPLE SARL",
"numero": "CLI00123", "numero": "CLI00123",
"compte_general": "411000", "type_tiers": 0,
"qualite": "CLI", "qualite": "CLI",
"compte_general": "411000",
"est_prospect": False, "est_prospect": False,
"est_actif": True "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): class ClientUpdateGatewayRequest(BaseModel):
"""Modèle pour modification client côté gateway""" """Modèle pour modification client côté gateway"""