Refactored Create Client Object

This commit is contained in:
Fanilo-Nantenaina 2025-12-24 15:49:37 +03:00
parent a4827c0534
commit 3809d3403b

657
api.py
View file

@ -4,7 +4,7 @@ from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field, EmailStr, validator, field_validator from pydantic import BaseModel, Field, EmailStr, validator, field_validator
from typing import List, Optional, Dict from typing import List, Optional, Dict
from datetime import date, datetime from datetime import date, datetime
from enum import Enum from enum import Enum, IntEnum
from decimal import Decimal from decimal import Decimal
import uvicorn import uvicorn
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@ -401,272 +401,515 @@ class BaremeRemiseResponse(BaseModel):
message: str message: str
class TypeTiers(IntEnum):
"""CT_Type - Type de tiers"""
CLIENT = 0
FOURNISSEUR = 1
SALARIE = 2
AUTRE = 3
class ClientCreateAPIRequest(BaseModel): 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)") 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 # STATUTS & FLAGS
# ======================================== # ══════════════════════════════════════════════════════════════
num: Optional[str] = Field(None, max_length=17, description="Numéro client (auto si vide)") est_prospect: bool = Field(False, description="CT_Prospect")
compte_collectif: str = Field("411000", max_length=13, description="Compte général (CG_NumPrinc)") est_actif: bool = Field(True, description="Inverse de CT_Sommeil")
qualite: str = Field("CLI", max_length=3, description="CLI/FOU/SAL/DIV/AUT") est_en_sommeil: Optional[bool] = Field(None, description="CT_Sommeil (calculé depuis est_actif si None)")
classement: Optional[str] = Field(None, max_length=17, description="Code de classement")
raccourci: Optional[str] = Field(None, max_length=17, description="Code abrégé")
# ======================================== # ══════════════════════════════════════════════════════════════
# 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 # ADRESSE PRINCIPALE
# ======================================== # ══════════════════════════════════════════════════════════════
contact: Optional[str] = Field(None, max_length=69, description="Nom du contact principal") contact: Optional[str] = Field(None, max_length=35, description="CT_Contact")
adresse: Optional[str] = Field(None, max_length=35, description="Adresse ligne 1") adresse: Optional[str] = Field(None, max_length=35, description="CT_Adresse")
complement: Optional[str] = Field(None, max_length=35, description="Adresse ligne 2") complement: Optional[str] = Field(None, max_length=35, description="CT_Complement")
code_postal: Optional[str] = Field(None, max_length=9, description="Code postal") code_postal: Optional[str] = Field(None, max_length=9, description="CT_CodePostal")
ville: Optional[str] = Field(None, max_length=35, description="Ville") ville: Optional[str] = Field(None, max_length=35, description="CT_Ville")
code_region: Optional[str] = Field(None, max_length=25, description="Code région/département") region: Optional[str] = Field(None, max_length=25, description="CT_CodeRegion")
pays: Optional[str] = Field(None, max_length=35, description="Pays") pays: Optional[str] = Field(None, max_length=35, description="CT_Pays")
# ======================================== # ══════════════════════════════════════════════════════════════
# CONTACT & COMMUNICATION # CONTACT & COMMUNICATION
# ======================================== # ══════════════════════════════════════════════════════════════
telephone: Optional[str] = Field(None, max_length=21, description="Téléphone principal") telephone: Optional[str] = Field(None, max_length=21, description="CT_Telephone")
telecopie: Optional[str] = Field(None, max_length=21, description="Fax") portable: Optional[str] = Field(None, max_length=21, description="Stocké dans statistiques ou contact")
email: Optional[str] = Field(None, max_length=69, description="Email principal") telecopie: Optional[str] = Field(None, max_length=21, description="CT_Telecopie")
site: Optional[str] = Field(None, max_length=69, description="Site web") email: Optional[str] = Field(None, max_length=69, description="CT_EMail")
facebook: Optional[str] = Field(None, max_length=100, description="URL Facebook") site_web: Optional[str] = Field(None, max_length=69, description="CT_Site")
linkedin: Optional[str] = Field(None, max_length=100, description="URL LinkedIn") 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 # IDENTIFIANTS LÉGAUX & FISCAUX
# ======================================== # ══════════════════════════════════════════════════════════════
siret: Optional[str] = Field(None, max_length=14, description="SIRET (14 chiffres)") siret: Optional[str] = Field(None, max_length=15, description="CT_Siret (14-15 chars)")
tva_intra: Optional[str] = Field(None, max_length=25, description="TVA intracommunautaire (CT_Identifiant)") siren: Optional[str] = Field(None, max_length=9, description="Extrait du SIRET")
ape: Optional[str] = Field(None, max_length=5, description="Code APE/NAF") tva_intra: Optional[str] = Field(None, max_length=25, description="CT_Identifiant")
type_nif: Optional[int] = Field(None, description="Type de NIF (0-10)") 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 & DEVISE
# ======================================== # ══════════════════════════════════════════════════════════════
banque_num: Optional[str] = Field(None, max_length=13, description="Code banque (BT_Num)") banque_num: Optional[int] = Field(None, description="BT_Num (smallint)")
devise: Optional[int] = Field(0, description="Code devise (N_Devise, 0=EUR)") devise: Optional[int] = Field(0, description="N_Devise (0=EUR)")
# ======================================== # ══════════════════════════════════════════════════════════════
# CATÉGORIES & CLASSIFICATIONS # CATÉGORIES & CLASSIFICATIONS COMMERCIALES
# ======================================== # ══════════════════════════════════════════════════════════════
cat_tarif: int = Field(1, ge=1, description="Catégorie tarifaire (N_CatTarif)") categorie_tarifaire: Optional[int] = Field(1, ge=0, description="N_CatTarif")
cat_compta: int = Field(1, ge=1, description="Catégorie comptable (N_CatCompta)") categorie_comptable: Optional[int] = Field(1, ge=0, description="N_CatCompta")
period: int = Field(1, ge=1, description="Période de règlement (N_Period)") periode_reglement: Optional[int] = Field(1, ge=0, description="N_Period")
expedition: int = Field(1, ge=1, description="Mode d'expédition (N_Expedition)") mode_expedition: Optional[int] = Field(1, ge=0, description="N_Expedition")
condition: int = Field(1, ge=1, description="Condition de livraison (N_Condition)") condition_livraison: Optional[int] = Field(1, ge=0, description="N_Condition")
risque: int = Field(1, ge=1, description="Niveau de risque (N_Risque)") 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 # TAUX PERSONNALISÉS
# ======================================== # ══════════════════════════════════════════════════════════════
taux01: Optional[Decimal] = Field(None, description="Taux personnalisé 1 (CT_Taux01)") taux01: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_Taux01")
taux02: Optional[Decimal] = Field(None, description="Taux personnalisé 2 (CT_Taux02)") taux02: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_Taux02")
taux03: Optional[Decimal] = Field(None, description="Taux personnalisé 3 (CT_Taux03)") taux03: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_Taux03")
taux04: Optional[Decimal] = Field(None, description="Taux personnalisé 4 (CT_Taux04)") taux04: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_Taux04")
# ======================================== # ══════════════════════════════════════════════════════════════
# GESTION COMMERCIALE # GESTION COMMERCIALE
# ======================================== # ══════════════════════════════════════════════════════════════
encours: Optional[Decimal] = Field(None, description="Encours autorisé (CT_Encours)") encours_autorise: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_Encours")
assurance: Optional[Decimal] = Field(None, description="Plafond assurance (CT_Assurance)") 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="Numéro client payeur") num_payeur: Optional[str] = Field(None, max_length=17, description="CT_NumPayeur")
langue: Optional[int] = Field(None, description="Code langue (0-25)") langue: Optional[int] = Field(None, ge=0, description="CT_Langue")
langue_iso2: Optional[str] = Field(None, max_length=2, description="Code ISO langue (FR, EN, etc)") 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 # PARAMÈTRES FACTURATION
# ======================================== # ══════════════════════════════════════════════════════════════
facture: int = Field(1, description="Type facturation (0=aucune, 1=normale, 2=regroupée)") type_facture: Optional[int] = Field(1, ge=0, le=2, description="CT_Facture: 0=aucune, 1=normale, 2=regroupée")
bl_fact: Optional[int] = Field(None, description="BL en facture (0/1)") bl_en_facture: Optional[int] = Field(None, ge=0, le=1, description="CT_BLFact")
saut: Optional[int] = Field(None, description="Saut de page (0/1)") saut_page: Optional[int] = Field(None, ge=0, le=1, description="CT_Saut")
lettrage: bool = Field(True, description="Lettrage auto (CT_Lettrage)") lettrage_auto: Optional[bool] = Field(True, description="CT_Lettrage")
valid_ech: Optional[int] = Field(None, description="Validation échéance (0/1)") validation_echeance: Optional[int] = Field(None, ge=0, le=1, description="CT_ValidEch")
control_enc: Optional[int] = Field(None, description="Contrôle encours (0/1)") controle_encours: Optional[int] = Field(None, ge=0, le=1, description="CT_ControlEnc")
not_rappel: Optional[int] = Field(None, description="Pas de relance (0/1)") exclure_relance: Optional[int] = Field(None, ge=0, le=1, description="CT_NotRappel")
not_penal: Optional[int] = Field(None, description="Pas de pénalités (0/1)") 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 # LIVRAISON & LOGISTIQUE
# ======================================== # ══════════════════════════════════════════════════════════════
priorite_livr: Optional[int] = Field(None, description="Priorité livraison (0-5)") priorite_livraison: Optional[int] = Field(None, ge=0, le=5, description="CT_PrioriteLivr")
livr_partielle: Optional[int] = Field(None, description="Livraison partielle autorisée (0/1)") livraison_partielle: Optional[int] = Field(None, ge=0, le=1, description="CT_LivrPartielle")
delai_transport: Optional[int] = Field(None, description="Délai transport (jours)") delai_transport: Optional[int] = Field(None, ge=0, description="CT_DelaiTransport (jours)")
delai_appro: Optional[int] = Field(None, description="Délai approvisionnement (jours)") delai_appro: Optional[int] = Field(None, ge=0, description="CT_DelaiAppro (jours)")
# JOURS DE COMMANDE (0=non, 1=oui) # JOURS DE COMMANDE (0=non, 1=oui) - CT_OrderDay01-07
order_day_lundi: Optional[int] = Field(None, ge=0, le=1) jours_commande: Optional[dict] = Field(
order_day_mardi: Optional[int] = Field(None, ge=0, le=1) None,
order_day_mercredi: Optional[int] = Field(None, ge=0, le=1) description="Dict avec clés: lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche"
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 LIVRAISON (0=non, 1=oui) # JOURS DE LIVRAISON (0=non, 1=oui) - CT_DeliveryDay01-07
delivery_day_lundi: Optional[int] = Field(None, ge=0, le=1) jours_livraison: Optional[dict] = Field(
delivery_day_mardi: Optional[int] = Field(None, ge=0, le=1) None,
delivery_day_mercredi: Optional[int] = Field(None, ge=0, le=1) description="Dict avec clés: lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche"
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)
# ========================================
# DATES FERMETURE # DATES FERMETURE
# ======================================== date_fermeture_debut: Optional[date] = Field(None, description="CT_DateFermeDebut")
date_ferme_debut: Optional[date] = Field(None, description="Début période fermeture") date_fermeture_fin: Optional[date] = Field(None, description="CT_DateFermeFin")
date_ferme_fin: Optional[date] = Field(None, description="Fin période fermeture")
# ======================================== # ══════════════════════════════════════════════════════════════
# STATISTIQUES PERSONNALISÉES (10 champs) # STATISTIQUES PERSONNALISÉES (10 champs de 21 chars max)
# ======================================== # ══════════════════════════════════════════════════════════════
statistique01: Optional[str] = Field(None, max_length=69) statistique01: Optional[str] = Field(None, max_length=21, description="CT_Statistique01")
statistique02: Optional[str] = Field(None, max_length=69) statistique02: Optional[str] = Field(None, max_length=21, description="CT_Statistique02")
statistique03: Optional[str] = Field(None, max_length=69) statistique03: Optional[str] = Field(None, max_length=21, description="CT_Statistique03")
statistique04: Optional[str] = Field(None, max_length=69) statistique04: Optional[str] = Field(None, max_length=21, description="CT_Statistique04")
statistique05: Optional[str] = Field(None, max_length=69) statistique05: Optional[str] = Field(None, max_length=21, description="CT_Statistique05")
statistique06: Optional[str] = Field(None, max_length=69) statistique06: Optional[str] = Field(None, max_length=21, description="CT_Statistique06")
statistique07: Optional[str] = Field(None, max_length=69) statistique07: Optional[str] = Field(None, max_length=21, description="CT_Statistique07")
statistique08: Optional[str] = Field(None, max_length=69) statistique08: Optional[str] = Field(None, max_length=21, description="CT_Statistique08")
statistique09: Optional[str] = Field(None, max_length=69) statistique09: Optional[str] = Field(None, max_length=21, description="CT_Statistique09")
statistique10: Optional[str] = Field(None, max_length=69) statistique10: Optional[str] = Field(None, max_length=21, description="CT_Statistique10")
# ======================================== # ══════════════════════════════════════════════════════════════
# COMMENTAIRE # 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 # ANALYTIQUE
# ======================================== # ══════════════════════════════════════════════════════════════
section_analytique: Optional[str] = Field(None, max_length=13, description="Section analytique (CA_Num)") section_analytique: Optional[str] = Field(None, max_length=13, description="CA_Num")
section_analytique_ifrs: Optional[str] = Field(None, max_length=13, description="Section IFRS (CA_NumIFRS)") section_analytique_ifrs: Optional[str] = Field(None, max_length=13, description="CA_NumIFRS")
plan_analytique: Optional[int] = Field(None, description="Plan analytique (N_Analytique)") plan_analytique: Optional[int] = Field(None, ge=0, description="N_Analytique")
plan_analytique_ifrs: Optional[int] = Field(None, description="Plan IFRS (N_AnalytiqueIFRS)") plan_analytique_ifrs: Optional[int] = Field(None, ge=0, description="N_AnalytiqueIFRS")
# ======================================== # ══════════════════════════════════════════════════════════════
# COLLABORATEUR & DÉPÔT # ORGANISATION
# ======================================== # ══════════════════════════════════════════════════════════════
collaborateur: Optional[str] = Field(None, max_length=4, description="Code collaborateur (CO_No)") depot_code: Optional[int] = Field(None, description="DE_No (int)")
depot: Optional[str] = Field(None, max_length=13, description="Dépôt par défaut (DE_No)") etablissement_code: Optional[int] = Field(None, description="EB_No (int)")
etablissement: Optional[str] = Field(None, max_length=13, description="Établissement (EB_No)") mode_reglement_code: Optional[int] = Field(None, description="MR_No (int)")
mode_regl: Optional[str] = Field(None, max_length=3, description="Mode règlement (MR_No)") 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 # SURVEILLANCE COFACE
# ======================================== # ══════════════════════════════════════════════════════════════
coface: Optional[str] = Field(None, max_length=25, description="Code Coface") coface: Optional[str] = Field(None, max_length=25, description="CT_Coface")
surveillance: Optional[int] = Field(None, description="Surveillance activée (0/1)") surveillance_active: Optional[int] = Field(None, ge=0, le=1, description="CT_Surveillance")
sv_date_create: Optional[date] = Field(None, description="Date création entreprise") sv_date_creation: Optional[datetime] = Field(None, description="CT_SvDateCreate")
sv_forme_juri: Optional[str] = Field(None, max_length=50, description="Forme juridique") sv_chiffre_affaires: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_SvCA")
sv_effectif: Optional[int] = Field(None, description="Effectif") sv_resultat: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="CT_SvResultat")
sv_ca: Optional[Decimal] = Field(None, description="Chiffre d'affaires") sv_incident: Optional[int] = Field(None, ge=0, le=1, description="CT_SvIncident")
sv_resultat: Optional[Decimal] = Field(None, description="Résultat") sv_date_incident: Optional[datetime] = Field(None, description="CT_SvDateIncid")
sv_incident: Optional[int] = Field(None, description="Incidents (0/1)") sv_privilege: Optional[int] = Field(None, ge=0, le=1, description="CT_SvPrivil")
sv_date_incid: Optional[date] = Field(None, description="Date dernier incident") sv_regularite: Optional[str] = Field(None, max_length=3, description="CT_SvRegul")
sv_privil: Optional[int] = Field(None, description="Privilèges (0/1)") sv_cotation: Optional[str] = Field(None, max_length=5, description="CT_SvCotation")
sv_regul: Optional[int] = Field(None, description="Régularité (0/1)") sv_date_maj: Optional[datetime] = Field(None, description="CT_SvDateMaj")
sv_cotation: Optional[str] = Field(None, max_length=25, description="Cotation") sv_objet_maj: Optional[str] = Field(None, max_length=61, description="CT_SvObjetMaj")
sv_date_maj: Optional[date] = Field(None, description="Date MAJ surveillance") sv_date_bilan: Optional[datetime] = Field(None, description="CT_SvDateBilan")
sv_objet_maj: Optional[str] = Field(None, max_length=35, description="Objet MAJ") sv_nb_mois_bilan: Optional[int] = Field(None, ge=0, description="CT_SvNbMoisBilan")
sv_date_bilan: Optional[date] = Field(None, description="Date dernier bilan")
sv_nb_mois_bilan: Optional[int] = Field(None, description="Nb mois bilan")
# ======================================== # ══════════════════════════════════════════════════════════════
# FACTURATION ÉLECTRONIQUE # FACTURATION ÉLECTRONIQUE
# ======================================== # ══════════════════════════════════════════════════════════════
facture_elec: Optional[int] = Field(None, description="Facturation électronique (0/1)") facture_electronique: Optional[int] = Field(None, ge=0, le=1, description="CT_FactureElec")
edi_code_type: Optional[int] = Field(None, description="Type code EDI") edi_code_type: Optional[int] = Field(None, description="CT_EdiCodeType")
edi_code: Optional[str] = Field(None, max_length=35, description="Code EDI") edi_code: Optional[str] = Field(None, max_length=23, description="CT_EdiCode")
edi_code_sage: Optional[str] = Field(None, max_length=35, description="Code EDI Sage") edi_code_sage: Optional[str] = Field(None, max_length=9, description="CT_EdiCodeSage")
fe_assujetti: Optional[int] = Field(None, description="Assujetti facture électronique") fe_assujetti: Optional[int] = Field(None, description="CT_FEAssujetti")
fe_autre_identif_type: Optional[str] = Field(None, max_length=10) fe_autre_identif_type: Optional[int] = Field(None, description="CT_FEAutreIdentifType")
fe_autre_identif_val: Optional[str] = Field(None, max_length=50) fe_autre_identif_val: Optional[str] = Field(None, max_length=81, description="CT_FEAutreIdentifVal")
fe_entite_type: Optional[int] = Field(None) fe_entite_type: Optional[int] = Field(None, description="CT_FEEntiteType")
fe_emission: Optional[int] = Field(None) fe_emission: Optional[int] = Field(None, description="CT_FEEmission")
fe_application: Optional[int] = Field(None) fe_application: Optional[int] = Field(None, description="CT_FEApplication")
fe_date_synchro: Optional[datetime] = Field(None, description="CT_FEDateSynchro")
# ======================================== # ══════════════════════════════════════════════════════════════
# ÉCHANGES & INTÉGRATION # ÉCHANGES & INTÉGRATION
# ======================================== # ══════════════════════════════════════════════════════════════
echange_rappro: Optional[int] = Field(None, description="Échange rapprochement") echange_rappro: Optional[int] = Field(None, description="CT_EchangeRappro")
echange_cr: Optional[int] = Field(None, description="Échange compte rendu") echange_cr: Optional[int] = Field(None, description="CT_EchangeCR")
annulation_cr: Optional[int] = Field(None, description="Annulation CR") pi_no_echange: Optional[int] = Field(None, description="PI_NoEchange")
profil_soc: Optional[int] = Field(None, description="Profil société") annulation_cr: Optional[int] = Field(None, description="CT_AnnulationCR")
statut_contrat: Optional[int] = Field(None, description="Statut contrat") profil_societe: Optional[int] = Field(None, description="CT_ProfilSoc")
statut_contrat: Optional[int] = Field(None, description="CT_StatutContrat")
# ======================================== # ══════════════════════════════════════════════════════════════
# RGPD & CONFIDENTIALITÉ # RGPD & CONFIDENTIALITÉ
# ======================================== # ══════════════════════════════════════════════════════════════
gdpr: Optional[int] = Field(None, description="Consentement RGPD (0/1)") rgpd_consentement: Optional[int] = Field(None, ge=0, le=1, description="CT_GDPR")
exclure_trait: Optional[int] = Field(None, description="Exclure des traitements (0/1)") exclure_traitement: Optional[int] = Field(None, ge=0, le=1, description="CT_ExclureTrait")
# ======================================== # ══════════════════════════════════════════════════════════════
# REPRÉSENTANT FISCAL # REPRÉSENTANT FISCAL
# ======================================== # ══════════════════════════════════════════════════════════════
represent_int: Optional[int] = Field(None, description="Représentant intracommunautaire") representant_intl: Optional[str] = Field(None, max_length=35, description="CT_RepresentInt")
represent_nif: Optional[str] = Field(None, max_length=25, description="NIF représentant") representant_nif: Optional[str] = Field(None, max_length=25, description="CT_RepresentNIF")
# ======================================== # ══════════════════════════════════════════════════════════════
# CHAMPS PERSONNALISÉS (exemples) # CHAMPS PERSONNALISÉS (Info Libres Sage)
# ======================================== # ══════════════════════════════════════════════════════════════
date_creation_societe: Optional[date] = Field(None, description="Date création de la société") date_creation_societe: Optional[datetime] = Field(None, description="Date création société (Info Libre)")
capital_social: Optional[Decimal] = Field(None, description="Capital social") capital_social: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6, description="Capital social")
actionnaire_principal: Optional[str] = Field(None, max_length=100, description="Actionnaire principal") actionnaire_principal: Optional[str] = Field(None, max_length=69, description="Actionnaire Pal")
score_banque_france: Optional[str] = Field(None, max_length=10, description="Score BDF") score_banque_france: Optional[str] = Field(None, max_length=14, description="Score Banque de France")
# FIDÉLITÉ # FIDÉLITÉ
total_points_fidelite: Optional[int] = Field(None, description="Total points fidélité") total_points_fidelite: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6)
points_fidelite_restants: Optional[int] = Field(None, description="Points restants") points_fidelite_restants: Optional[Decimal] = Field(None, max_digits=24, decimal_places=6)
fin_validite_carte: Optional[date] = Field(None, description="Fin validité carte") 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 # AUTRES
# ======================================== # ══════════════════════════════════════════════════════════════
calendrier: Optional[str] = Field(None, max_length=13, description="Code calendrier (CAL_No)") mode_test: Optional[int] = Field(None, ge=0, le=1, description="CT_ModeTest")
mode_test: Optional[int] = Field(None, description="Mode test (0/1)") confiance: Optional[int] = Field(None, description="CT_Confiance")
confiance: Optional[int] = Field(None, description="Niveau de 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') @field_validator('siret')
@classmethod @classmethod
def validate_siret(cls, v): def validate_siret(cls, v):
if v and len(v.replace(' ', '')) != 14: if v and v.lower() not in ('none', ''):
raise ValueError('Le SIRET doit contenir 14 chiffres') cleaned = v.replace(' ', '').replace('-', '')
return v.replace(' ', '') if v else v 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') @field_validator('email')
@classmethod @classmethod
def validate_email(cls, v): def validate_email(cls, v):
if v and '@' not in v: if v and v.lower() not in ('none', ''):
raise ValueError('Format email invalide') 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 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: class Config:
json_schema_extra = { json_schema_extra = {
"example": { "example": {
"intitule": "ENTREPRISE EXEMPLE SARL", "intitule": "ENTREPRISE EXEMPLE SARL",
"num": "CLI00123", "numero": "CLI00123",
"compte_collectif": "411000", "compte_general": "411000",
"qualite": "CLI" "qualite": "CLI",
"est_prospect": False,
"est_actif": True
} }
} }