diff --git a/api.py b/api.py index f9e0e4d..7d6783a 100644 --- a/api.py +++ b/api.py @@ -4,7 +4,7 @@ from fastapi.responses import StreamingResponse from pydantic import BaseModel, Field, EmailStr, validator, field_validator from typing import List, Optional, Dict from datetime import date, datetime -from enum import Enum +from enum import Enum, IntEnum from decimal import Decimal import uvicorn from contextlib import asynccontextmanager @@ -401,275 +401,518 @@ class BaremeRemiseResponse(BaseModel): message: str +class TypeTiers(IntEnum): + """CT_Type - Type de tiers""" + CLIENT = 0 + FOURNISSEUR = 1 + SALARIE = 2 + AUTRE = 3 + + class ClientCreateAPIRequest(BaseModel): - """Modèle complet pour la création d'un client Sage avec tous les champs disponibles""" + """ + Modèle complet pour la création d'un client Sage 100c + Noms alignés sur le frontend + mapping vers champs Sage + """ - # ======================================== - # CHAMPS OBLIGATOIRES - # ======================================== + # ══════════════════════════════════════════════════════════════ + # 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("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") - # ======================================== - # IDENTIFICATION & CLASSIFICATION - # ======================================== - num: Optional[str] = Field(None, max_length=17, description="Numéro client (auto si vide)") - compte_collectif: str = Field("411000", max_length=13, description="Compte général (CG_NumPrinc)") - qualite: str = Field("CLI", max_length=3, description="CLI/FOU/SAL/DIV/AUT") - classement: Optional[str] = Field(None, max_length=17, description="Code de classement") - raccourci: Optional[str] = Field(None, max_length=17, description="Code abrégé") + # ══════════════════════════════════════════════════════════════ + # 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=69, description="Nom du contact principal") - adresse: Optional[str] = Field(None, max_length=35, description="Adresse ligne 1") - complement: Optional[str] = Field(None, max_length=35, description="Adresse ligne 2") - code_postal: Optional[str] = Field(None, max_length=9, description="Code postal") - ville: Optional[str] = Field(None, max_length=35, description="Ville") - code_region: Optional[str] = Field(None, max_length=25, description="Code région/département") - pays: Optional[str] = Field(None, max_length=35, description="Pays") + # ══════════════════════════════════════════════════════════════ + 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="Téléphone principal") - telecopie: Optional[str] = Field(None, max_length=21, description="Fax") - email: Optional[str] = Field(None, max_length=69, description="Email principal") - site: Optional[str] = Field(None, max_length=69, description="Site web") - facebook: Optional[str] = Field(None, max_length=100, description="URL Facebook") - linkedin: Optional[str] = Field(None, max_length=100, description="URL LinkedIn") + # ══════════════════════════════════════════════════════════════ + 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=14, description="SIRET (14 chiffres)") - tva_intra: Optional[str] = Field(None, max_length=25, description="TVA intracommunautaire (CT_Identifiant)") - ape: Optional[str] = Field(None, max_length=5, description="Code APE/NAF") - type_nif: Optional[int] = Field(None, description="Type de NIF (0-10)") + # ══════════════════════════════════════════════════════════════ + 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[str] = Field(None, max_length=13, description="Code banque (BT_Num)") - devise: Optional[int] = Field(0, description="Code devise (N_Devise, 0=EUR)") + # ══════════════════════════════════════════════════════════════ + banque_num: Optional[int] = Field(None, description="BT_Num (smallint)") + devise: Optional[int] = Field(0, description="N_Devise (0=EUR)") - # ======================================== - # CATÉGORIES & CLASSIFICATIONS - # ======================================== - cat_tarif: int = Field(1, ge=1, description="Catégorie tarifaire (N_CatTarif)") - cat_compta: int = Field(1, ge=1, description="Catégorie comptable (N_CatCompta)") - period: int = Field(1, ge=1, description="Période de règlement (N_Period)") - expedition: int = Field(1, ge=1, description="Mode d'expédition (N_Expedition)") - condition: int = Field(1, ge=1, description="Condition de livraison (N_Condition)") - risque: int = Field(1, ge=1, description="Niveau de risque (N_Risque)") + # ══════════════════════════════════════════════════════════════ + # 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, description="Taux personnalisé 1 (CT_Taux01)") - taux02: Optional[Decimal] = Field(None, description="Taux personnalisé 2 (CT_Taux02)") - taux03: Optional[Decimal] = Field(None, description="Taux personnalisé 3 (CT_Taux03)") - taux04: Optional[Decimal] = Field(None, description="Taux personnalisé 4 (CT_Taux04)") + # ══════════════════════════════════════════════════════════════ + 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: Optional[Decimal] = Field(None, description="Encours autorisé (CT_Encours)") - assurance: Optional[Decimal] = Field(None, description="Plafond assurance (CT_Assurance)") - num_payeur: Optional[str] = Field(None, max_length=17, description="Numéro client payeur") - langue: Optional[int] = Field(None, description="Code langue (0-25)") - langue_iso2: Optional[str] = Field(None, max_length=2, description="Code ISO langue (FR, EN, etc)") + # ══════════════════════════════════════════════════════════════ + 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 - # ======================================== - facture: int = Field(1, description="Type facturation (0=aucune, 1=normale, 2=regroupée)") - bl_fact: Optional[int] = Field(None, description="BL en facture (0/1)") - saut: Optional[int] = Field(None, description="Saut de page (0/1)") - lettrage: bool = Field(True, description="Lettrage auto (CT_Lettrage)") - valid_ech: Optional[int] = Field(None, description="Validation échéance (0/1)") - control_enc: Optional[int] = Field(None, description="Contrôle encours (0/1)") - not_rappel: Optional[int] = Field(None, description="Pas de relance (0/1)") - not_penal: Optional[int] = Field(None, description="Pas de pénalités (0/1)") + # ══════════════════════════════════════════════════════════════ + 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_livr: Optional[int] = Field(None, description="Priorité livraison (0-5)") - livr_partielle: Optional[int] = Field(None, description="Livraison partielle autorisée (0/1)") - delai_transport: Optional[int] = Field(None, description="Délai transport (jours)") - delai_appro: Optional[int] = Field(None, description="Délai approvisionnement (jours)") + # ══════════════════════════════════════════════════════════════ + 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) - order_day_lundi: Optional[int] = Field(None, ge=0, le=1) - order_day_mardi: Optional[int] = Field(None, ge=0, le=1) - order_day_mercredi: Optional[int] = Field(None, ge=0, le=1) - order_day_jeudi: Optional[int] = Field(None, ge=0, le=1) - order_day_vendredi: Optional[int] = Field(None, ge=0, le=1) - order_day_samedi: Optional[int] = Field(None, ge=0, le=1) - order_day_dimanche: Optional[int] = Field(None, ge=0, le=1) + # 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) - delivery_day_lundi: Optional[int] = Field(None, ge=0, le=1) - delivery_day_mardi: Optional[int] = Field(None, ge=0, le=1) - delivery_day_mercredi: Optional[int] = Field(None, ge=0, le=1) - delivery_day_jeudi: Optional[int] = Field(None, ge=0, le=1) - delivery_day_vendredi: Optional[int] = Field(None, ge=0, le=1) - delivery_day_samedi: Optional[int] = Field(None, ge=0, le=1) - delivery_day_dimanche: Optional[int] = Field(None, ge=0, le=1) + # 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" + ) - # ======================================== # DATES FERMETURE - # ======================================== - date_ferme_debut: Optional[date] = Field(None, description="Début période fermeture") - date_ferme_fin: Optional[date] = Field(None, description="Fin période fermeture") + date_fermeture_debut: Optional[date] = Field(None, description="CT_DateFermeDebut") + date_fermeture_fin: Optional[date] = Field(None, description="CT_DateFermeFin") - # ======================================== - # STATISTIQUES PERSONNALISÉES (10 champs) - # ======================================== - statistique01: Optional[str] = Field(None, max_length=69) - statistique02: Optional[str] = Field(None, max_length=69) - statistique03: Optional[str] = Field(None, max_length=69) - statistique04: Optional[str] = Field(None, max_length=69) - statistique05: Optional[str] = Field(None, max_length=69) - statistique06: Optional[str] = Field(None, max_length=69) - statistique07: Optional[str] = Field(None, max_length=69) - statistique08: Optional[str] = Field(None, max_length=69) - statistique09: Optional[str] = Field(None, max_length=69) - statistique10: Optional[str] = Field(None, max_length=69) + # ══════════════════════════════════════════════════════════════ + # 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") + 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") - # ======================================== + # ══════════════════════════════════════════════════════════════ # COMMENTAIRE - # ======================================== - commentaire: Optional[str] = Field(None, description="Commentaire libre (CT_Commentaire, jusqu'à 2Go théorique)") + # ══════════════════════════════════════════════════════════════ + commentaire: Optional[str] = Field(None, max_length=35, description="CT_Commentaire") - # ======================================== - # ÉTAT & STATUT - # ======================================== - sommeil: bool = Field(False, description="Compte en sommeil") - prospect: bool = Field(False, description="Prospect (pas encore client)") - bon_a_payer: Optional[int] = Field(None, description="Bon à payer (0/1)") - - # ======================================== + # ══════════════════════════════════════════════════════════════ # ANALYTIQUE - # ======================================== - section_analytique: Optional[str] = Field(None, max_length=13, description="Section analytique (CA_Num)") - section_analytique_ifrs: Optional[str] = Field(None, max_length=13, description="Section IFRS (CA_NumIFRS)") - plan_analytique: Optional[int] = Field(None, description="Plan analytique (N_Analytique)") - plan_analytique_ifrs: Optional[int] = Field(None, description="Plan IFRS (N_AnalytiqueIFRS)") + # ══════════════════════════════════════════════════════════════ + 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") - # ======================================== - # COLLABORATEUR & DÉPÔT - # ======================================== - collaborateur: Optional[str] = Field(None, max_length=4, description="Code collaborateur (CO_No)") - depot: Optional[str] = Field(None, max_length=13, description="Dépôt par défaut (DE_No)") - etablissement: Optional[str] = Field(None, max_length=13, description="Établissement (EB_No)") - mode_regl: Optional[str] = Field(None, max_length=3, description="Mode règlement (MR_No)") + # ══════════════════════════════════════════════════════════════ + # 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") - # ======================================== - # CENTRALE D'ACHAT - # ======================================== - num_centrale: Optional[str] = Field(None, max_length=17, description="Numéro centrale d'achat") - - # ======================================== + # ══════════════════════════════════════════════════════════════ # SURVEILLANCE COFACE - # ======================================== - coface: Optional[str] = Field(None, max_length=25, description="Code Coface") - surveillance: Optional[int] = Field(None, description="Surveillance activée (0/1)") - sv_date_create: Optional[date] = Field(None, description="Date création entreprise") - sv_forme_juri: Optional[str] = Field(None, max_length=50, description="Forme juridique") - sv_effectif: Optional[int] = Field(None, description="Effectif") - sv_ca: Optional[Decimal] = Field(None, description="Chiffre d'affaires") - sv_resultat: Optional[Decimal] = Field(None, description="Résultat") - sv_incident: Optional[int] = Field(None, description="Incidents (0/1)") - sv_date_incid: Optional[date] = Field(None, description="Date dernier incident") - sv_privil: Optional[int] = Field(None, description="Privilèges (0/1)") - sv_regul: Optional[int] = Field(None, description="Régularité (0/1)") - sv_cotation: Optional[str] = Field(None, max_length=25, description="Cotation") - sv_date_maj: Optional[date] = Field(None, description="Date MAJ surveillance") - sv_objet_maj: Optional[str] = Field(None, max_length=35, description="Objet MAJ") - sv_date_bilan: Optional[date] = Field(None, description="Date dernier bilan") - sv_nb_mois_bilan: Optional[int] = Field(None, description="Nb mois bilan") + # ══════════════════════════════════════════════════════════════ + 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") - # ======================================== + # ══════════════════════════════════════════════════════════════ # FACTURATION ÉLECTRONIQUE - # ======================================== - facture_elec: Optional[int] = Field(None, description="Facturation électronique (0/1)") - edi_code_type: Optional[int] = Field(None, description="Type code EDI") - edi_code: Optional[str] = Field(None, max_length=35, description="Code EDI") - edi_code_sage: Optional[str] = Field(None, max_length=35, description="Code EDI Sage") - fe_assujetti: Optional[int] = Field(None, description="Assujetti facture électronique") - fe_autre_identif_type: Optional[str] = Field(None, max_length=10) - fe_autre_identif_val: Optional[str] = Field(None, max_length=50) - fe_entite_type: Optional[int] = Field(None) - fe_emission: Optional[int] = Field(None) - fe_application: Optional[int] = Field(None) + # ══════════════════════════════════════════════════════════════ + 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") - # ======================================== + # ══════════════════════════════════════════════════════════════ # ÉCHANGES & INTÉGRATION - # ======================================== - echange_rappro: Optional[int] = Field(None, description="Échange rapprochement") - echange_cr: Optional[int] = Field(None, description="Échange compte rendu") - annulation_cr: Optional[int] = Field(None, description="Annulation CR") - profil_soc: Optional[int] = Field(None, description="Profil société") - statut_contrat: Optional[int] = Field(None, description="Statut contrat") + # ══════════════════════════════════════════════════════════════ + 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") - # ======================================== + # ══════════════════════════════════════════════════════════════ # RGPD & CONFIDENTIALITÉ - # ======================================== - gdpr: Optional[int] = Field(None, description="Consentement RGPD (0/1)") - exclure_trait: Optional[int] = Field(None, description="Exclure des traitements (0/1)") + # ══════════════════════════════════════════════════════════════ + 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") - # ======================================== + # ══════════════════════════════════════════════════════════════ # REPRÉSENTANT FISCAL - # ======================================== - represent_int: Optional[int] = Field(None, description="Représentant intracommunautaire") - represent_nif: Optional[str] = Field(None, max_length=25, description="NIF représentant") + # ══════════════════════════════════════════════════════════════ + representant_intl: Optional[str] = Field(None, max_length=35, description="CT_RepresentInt") + representant_nif: Optional[str] = Field(None, max_length=25, description="CT_RepresentNIF") - # ======================================== - # CHAMPS PERSONNALISÉS (exemples) - # ======================================== - date_creation_societe: Optional[date] = Field(None, description="Date création de la société") - capital_social: Optional[Decimal] = Field(None, description="Capital social") - actionnaire_principal: Optional[str] = Field(None, max_length=100, description="Actionnaire principal") - score_banque_france: Optional[str] = Field(None, max_length=10, description="Score BDF") + # ══════════════════════════════════════════════════════════════ + # 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") # FIDÉLITÉ - total_points_fidelite: Optional[int] = Field(None, description="Total points fidélité") - points_fidelite_restants: Optional[int] = Field(None, description="Points restants") - fin_validite_carte: Optional[date] = Field(None, description="Fin validité carte") + 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") - # ======================================== + # ══════════════════════════════════════════════════════════════ # AUTRES - # ======================================== - calendrier: Optional[str] = Field(None, max_length=13, description="Code calendrier (CAL_No)") - mode_test: Optional[int] = Field(None, description="Mode test (0/1)") - confiance: Optional[int] = Field(None, description="Niveau de confiance") + # ══════════════════════════════════════════════════════════════ + 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") + + # ══════════════════════════════════════════════════════════════ + # 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") + + # ══════════════════════════════════════════════════════════════ + # VALIDATORS + # ══════════════════════════════════════════════════════════════ @field_validator('siret') @classmethod def validate_siret(cls, v): - if v and len(v.replace(' ', '')) != 14: - raise ValueError('Le SIRET doit contenir 14 chiffres') - return v.replace(' ', '') if v else v + if v and v.lower() not in ('none', ''): + 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 '@' not in v: - raise ValueError('Format email invalide') + if v and v.lower() not in ('none', ''): + if '@' not in v: + raise ValueError('Format email invalide') + return v.strip() + return None + + @field_validator('adresse', 'code_postal', 'ville', 'pays', 'telephone', 'tva_intra', mode='before') + @classmethod + def clean_none_strings(cls, v): + """Convertit les chaînes 'None' 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""" + return { + # Identification + "intitule": self.intitule, + "num": 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, + + # Adresse + "contact": self.contact, + "adresse": self.adresse, + "complement": self.complement, + "code_postal": self.code_postal, + "ville": self.ville, + "code_region": self.region, + "pays": self.pays, + + # Communication + "telephone": self.telephone, + "telecopie": self.telecopie, + "email": self.email, + "site": self.site_web, + "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, + + # Banque & devise + "banque_num": self.banque_num, + "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, + "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, + "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, + + # 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, + + # Surveillance + "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, + "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, + } + + 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", - "num": "CLI00123", - "compte_collectif": "411000", - "qualite": "CLI" + "numero": "CLI00123", + "compte_general": "411000", + "qualite": "CLI", + "est_prospect": False, + "est_actif": True } } - + class ClientUpdateRequest(BaseModel): """Modèle pour modification d'un client existant"""