From c9497adad2c91e30bcdf43edbe6482cea0c02ff5 Mon Sep 17 00:00:00 2001 From: fanilo Date: Fri, 26 Dec 2025 14:40:48 +0100 Subject: [PATCH] updated client create pydantic schema --- main.py | 815 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 422 insertions(+), 393 deletions(-) diff --git a/main.py b/main.py index d8c5b9b..705e81c 100644 --- a/main.py +++ b/main.py @@ -89,153 +89,173 @@ class TypeTiers(IntEnum): 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 - """ - - # ══════════════════════════════════════════════════════════════ - # IDENTIFICATION PRINCIPALE - # ══════════════════════════════════════════════════════════════ - intitule: str = Field(..., max_length=69, description="Nom du client (CT_Intitule)") - 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" + + intitule: str = Field( + ..., + max_length=69, + description="Nom du client (CT_Intitule) - OBLIGATOIRE" ) - # JOURS DE LIVRAISON (0=non, 1=oui) - CT_DeliveryDay01-07 - jours_livraison: Optional[dict] = Field( - None, - description="Dict avec clés: lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche" + numero: Optional[str] = Field( + None, + max_length=17, + description="Numéro client CT_Num (auto si None)" ) - # DATES FERMETURE - date_fermeture_debut: Optional[date] = Field(None, description="CT_DateFermeDebut") - date_fermeture_fin: Optional[date] = Field(None, description="CT_DateFermeFin") + 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)" + ) - # ══════════════════════════════════════════════════════════════ - # STATISTIQUES PERSONNALISÉES (10 champs de 21 chars max) - # ══════════════════════════════════════════════════════════════ 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") @@ -247,250 +267,289 @@ class ClientCreateRequest(BaseModel): statistique09: Optional[str] = Field(None, max_length=21, description="CT_Statistique09") statistique10: Optional[str] = Field(None, max_length=21, description="CT_Statistique10") - # ══════════════════════════════════════════════════════════════ - # COMMENTAIRE - # ══════════════════════════════════════════════════════════════ - commentaire: Optional[str] = Field(None, max_length=35, description="CT_Commentaire") + encours_autorise: Optional[float] = Field( + None, + description="CT_Encours (montant max autorisé)" + ) - # ══════════════════════════════════════════════════════════════ - # ANALYTIQUE - # ══════════════════════════════════════════════════════════════ - 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") + assurance_credit: Optional[float] = Field( + None, + description="CT_Assurance (montant assurance crédit)" + ) - # ══════════════════════════════════════════════════════════════ - # ORGANISATION - # ══════════════════════════════════════════════════════════════ - depot_code: Optional[int] = Field(None, description="DE_No (int)") - 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") + langue: Optional[int] = Field( + None, + ge=0, + description="CT_Langue (0=Français, 1=Anglais, etc.)" + ) - # ══════════════════════════════════════════════════════════════ - # SURVEILLANCE COFACE - # ══════════════════════════════════════════════════════════════ - 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") + commercial_code: Optional[int] = Field( + None, + description="CO_No (ID du collaborateur commercial)" + ) - # ══════════════════════════════════════════════════════════════ - # FACTURATION ÉLECTRONIQUE - # ══════════════════════════════════════════════════════════════ - 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") + lettrage_auto: Optional[bool] = Field( + True, + description="CT_Lettrage (1=oui, 0=non)" + ) - # ══════════════════════════════════════════════════════════════ - # ÉCHANGES & INTÉGRATION - # ══════════════════════════════════════════════════════════════ - 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") + est_actif: Optional[bool] = Field( + True, + description="Inverse de CT_Sommeil (True=actif, False=en sommeil)" + ) - # ══════════════════════════════════════════════════════════════ - # RGPD & CONFIDENTIALITÉ - # ══════════════════════════════════════════════════════════════ - rgpd_consentement: Optional[int] = Field(None, ge=0, le=1, description="CT_GDPR") - exclure_traitement: Optional[int] = Field(None, ge=0, le=1, description="CT_ExclureTrait") + type_facture: Optional[int] = Field( + 1, + ge=0, + le=2, + description="CT_Facture: 0=aucune, 1=normale, 2=regroupée" + ) - # ══════════════════════════════════════════════════════════════ - # REPRÉSENTANT FISCAL - # ══════════════════════════════════════════════════════════════ - representant_intl: Optional[str] = Field(None, max_length=35, description="CT_RepresentInt") - representant_nif: Optional[str] = Field(None, max_length=25, description="CT_RepresentNIF") + est_prospect: Optional[bool] = Field( + False, + description="CT_Prospect (1=oui, 0=non)" + ) - # ══════════════════════════════════════════════════════════════ - # CHAMPS PERSONNALISÉS (Info Libres Sage) - # ══════════════════════════════════════════════════════════════ - date_creation_societe: Optional[datetime] = Field(None, description="Date création société (Info Libre)") - capital_social: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="Capital social") - 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") + bl_en_facture: Optional[int] = Field( + None, + ge=0, + le=1, + description="CT_BLFact (impression BL sur facture)" + ) - # FIDÉLITÉ - total_points_fidelite: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6) - points_fidelite_restants: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6) - date_fin_carte_fidelite: Optional[datetime] = Field(None, description="Fin validité carte fidélité") - date_negociation_reglement: Optional[datetime] = Field(None, description="Date négociation règlement") + saut_page: Optional[int] = Field( + None, + ge=0, + le=1, + description="CT_Saut (saut de page après impression)" + ) - # ══════════════════════════════════════════════════════════════ - # AUTRES - # ══════════════════════════════════════════════════════════════ - mode_test: Optional[int] = Field(None, ge=0, le=1, description="CT_ModeTest") - confiance: Optional[int] = Field(None, description="CT_Confiance") - dn_id: Optional[str] = Field(None, max_length=37, description="DN_Id") + validation_echeance: Optional[int] = Field( + None, + ge=0, + le=1, + description="CT_ValidEch" + ) - # ══════════════════════════════════════════════════════════════ - # MÉTADONNÉES (en lecture seule généralement) - # ══════════════════════════════════════════════════════════════ - date_creation: Optional[datetime] = Field(None, description="cbCreation - géré par Sage") - date_modification: Optional[datetime] = Field(None, description="cbModification - géré par Sage") - date_maj: Optional[datetime] = Field(None, description="CT_DateMAJ") + 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" + ) - # ══════════════════════════════════════════════════════════════ - # VALIDATORS - # ══════════════════════════════════════════════════════════════ @field_validator('siret') @classmethod 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('-', '') if len(cleaned) not in (14, 15): raise ValueError('Le SIRET doit contenir 14 ou 15 caractères') return cleaned 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') @classmethod 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: raise ValueError('Format email invalide') - return v.strip() + return v 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 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', ''): return None 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: - """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 { - # Identification "intitule": self.intitule, - "num": self.numero, + "numero": self.numero, "type_tiers": self.type_tiers, "qualite": self.qualite, "classement": self.classement, "raccourci": self.raccourci, - # Statuts - "prospect": self.est_prospect, - "sommeil": not self.est_actif if self.est_en_sommeil is None else self.est_en_sommeil, + "siret": self.siret, + "tva_intra": self.tva_intra, + "code_naf": self.code_naf, - # Adresse "contact": self.contact, "adresse": self.adresse, "complement": self.complement, "code_postal": self.code_postal, "ville": self.ville, - "code_region": self.region, + "region": self.region, "pays": self.pays, - # Communication "telephone": self.telephone, "telecopie": self.telecopie, "email": self.email, - "site": self.site_web, + "site_web": self.site_web, + "portable": self.portable, "facebook": self.facebook, "linkedin": self.linkedin, - # Identifiants légaux - "siret": self.siret, - "tva_intra": self.tva_intra, - "ape": self.code_naf, - "type_nif": self.type_nif, + "compte_general": self.compte_general, - # Banque & devise - "banque_num": self.banque_num, - "devise": self.devise, + "categorie_tarifaire": self.categorie_tarifaire, + "categorie_comptable": self.categorie_comptable, - # 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, "taux02": self.taux02, "taux03": self.taux03, "taux04": self.taux04, - # Gestion commerciale - "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, + "statistique01": stat01, "statistique02": self.statistique02, "statistique03": self.statistique03, "statistique04": self.statistique04, @@ -500,98 +559,68 @@ class ClientCreateRequest(BaseModel): "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 "commentaire": self.commentaire, - # 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 - "depot": self.depot_code, - "etablissement": self.etablissement_code, - "mode_regl": self.mode_reglement_code, - "calendrier": self.calendrier_code, - "num_centrale": self.num_centrale, + "mode_reglement_code": self.mode_reglement_code, - # Surveillance + "surveillance_active": self.surveillance_active, "coface": self.coface, - "surveillance": self.surveillance_active, - "sv_forme_juri": self.forme_juridique, - "sv_effectif": self.effectif, - "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, + "forme_juridique": self.forme_juridique, + "effectif": self.effectif, + "sv_regularite": self.sv_regularite, "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_date_bilan": self.sv_date_bilan, - "sv_nb_mois_bilan": self.sv_nb_mois_bilan, - - # 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, + "ca_annuel": ca, + "sv_chiffre_affaires": self.sv_chiffre_affaires, + "sv_resultat": self.sv_resultat, } - 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: json_schema_extra = { "example": { "intitule": "ENTREPRISE EXEMPLE SARL", "numero": "CLI00123", - "compte_general": "411000", + "type_tiers": 0, "qualite": "CLI", + "compte_general": "411000", "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): """Modèle pour modification client côté gateway"""