functionnal cree_client

This commit is contained in:
mickael 2025-12-24 21:00:49 +01:00
parent 7b451223ef
commit 57c05082c0
2 changed files with 1156 additions and 443 deletions

628
main.py
View file

@ -1,9 +1,10 @@
from fastapi import FastAPI, HTTPException, Header, Depends, Query from fastapi import FastAPI, HTTPException, Header, Depends, Query
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator, EmailStr, field_validator
from typing import Optional, List, Dict from typing import Optional, List, Dict
from datetime import datetime, date from datetime import datetime, date
from enum import Enum from decimal import Decimal
from enum import Enum, IntEnum
import uvicorn import uvicorn
import logging import logging
import win32com.client import win32com.client
@ -79,20 +80,518 @@ class StatutRequest(BaseModel):
nouveau_statut: int nouveau_statut: int
class ClientCreateRequest(BaseModel): class TypeTiers(IntEnum):
intitule: str = Field(..., description="Nom du client (CT_Intitule)") """CT_Type - Type de tiers"""
compte_collectif: str = Field("411000", description="Compte général rattaché") CLIENT = 0
num: Optional[str] = Field(None, description="Laisser vide pour numérotation auto") FOURNISSEUR = 1
adresse: Optional[str] = None SALARIE = 2
code_postal: Optional[str] = None AUTRE = 3
ville: Optional[str] = None
pays: Optional[str] = None
email: Optional[str] = None
telephone: Optional[str] = None
siret: Optional[str] = None
tva_intra: Optional[str] = None
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"
)
# 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_fermeture_debut: Optional[date] = Field(None, description="CT_DateFermeDebut")
date_fermeture_fin: Optional[date] = Field(None, description="CT_DateFermeFin")
# ══════════════════════════════════════════════════════════════
# 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, max_length=35, description="CT_Commentaire")
# ══════════════════════════════════════════════════════════════
# 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")
# ══════════════════════════════════════════════════════════════
# 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")
# ══════════════════════════════════════════════════════════════
# 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")
# ══════════════════════════════════════════════════════════════
# 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")
# ══════════════════════════════════════════════════════════════
# É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")
# ══════════════════════════════════════════════════════════════
# 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")
# ══════════════════════════════════════════════════════════════
# 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")
# ══════════════════════════════════════════════════════════════
# 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[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
# ══════════════════════════════════════════════════════════════
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 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 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",
"numero": "CLI00123",
"compte_general": "411000",
"qualite": "CLI",
"est_prospect": False,
"est_actif": True
}
}
class ClientUpdateGatewayRequest(BaseModel): class ClientUpdateGatewayRequest(BaseModel):
"""Modèle pour modification client côté gateway""" """Modèle pour modification client côté gateway"""
@ -1627,107 +2126,6 @@ def lire_mouvement_stock(numero: str):
raise HTTPException(500, str(e)) raise HTTPException(500, str(e))
@app.get("/sage/modeles/list")
def lister_modeles_disponibles():
"""Liste tous les modèles .bgc disponibles pour chaque type de document"""
try:
modeles = sage.lister_modeles_crystal()
return {"success": True, "data": modeles}
except Exception as e:
logger.error(f" Erreur listage modèles: {e}")
raise HTTPException(500, str(e))
@app.get("/sage/documents/{numero}/pdf", dependencies=[Depends(verify_token)])
def generer_pdf_document(
numero: str,
type_doc: int = Query(..., description="Type document (0=devis, 60=facture, etc.)"),
modele: str = Query(None, description="Nom du modèle .bgc (optionnel)"),
base64_encode: bool = Query(True, description="Retourner en base64"),
):
"""
Génère un PDF d'un document Sage avec le modèle spécifié
"""
try:
# LOG pour debug
logger.info(
f" PDF Request: numero={numero}, type={type_doc}, modele={modele}, base64={base64_encode}"
)
# Générer le PDF
pdf_bytes = sage.generer_pdf_document(
numero=numero, type_doc=type_doc, modele=modele
)
if not pdf_bytes:
raise HTTPException(404, f"Impossible de générer le PDF pour {numero}")
# LOG taille PDF
logger.info(f" PDF généré: {len(pdf_bytes)} octets")
if base64_encode:
# Retour en JSON avec base64
import base64
pdf_base64 = base64.b64encode(pdf_bytes).decode("utf-8")
return {
"success": True,
"data": {
"numero": numero,
"type": type_doc,
"modele": modele or "défaut",
"pdf_base64": pdf_base64,
"size_bytes": len(pdf_bytes),
"size_readable": f"{len(pdf_bytes) / 1024:.1f} KB",
},
}
else:
# Retour direct du fichier PDF
from fastapi.responses import Response
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f'inline; filename="{numero}.pdf"',
"Content-Length": str(len(pdf_bytes)), # Taille explicite
},
)
except HTTPException:
raise
except ValueError as e:
logger.error(f" Erreur métier: {e}")
raise HTTPException(400, str(e))
except Exception as e:
logger.error(f" Erreur technique: {e}", exc_info=True)
raise HTTPException(500, str(e))
@app.get("/sage/object-exploration")
async def explorer_objets_impression_sage(modele:str="Devis client avec détail projet.bgc"):
try:
dossier = r"C:\Users\Public\Documents\Sage\Entreprise 100c\fr-FR\Documents standards\Gestion commerciale\Ventes"
chemin = os.path.join(dossier, modele)
if not os.path.exists(chemin):
return {"error": f"Fichier non trouve: {modele}"}
expliration = sage.analyser_bgc_complet(chemin)
if not expliration:
raise HTTPException(404, f"ERROR")
return {"success": True, "data": expliration}
except HTTPException:
raise
except Exception as e:
logger.error(f" Erreur exploration : {e}")
raise HTTPException(500, str(e))
# ===================================================== # =====================================================
# LANCEMENT # LANCEMENT
# ===================================================== # =====================================================

View file

@ -276,7 +276,7 @@ class SageConnector:
if fournisseur_data.get("num") if fournisseur_data.get("num")
else "" else ""
) )
compte = str(fournisseur_data.get("compte_collectif", "401000"))[ compte = str(fournisseur_data.get("compte_collectif", "4010000"))[
:13 :13
].strip() ].strip()
@ -2125,7 +2125,7 @@ class SageConnector:
# === 3. TYPE DE TIERS (CLIENT/PROSPECT/FOURNISSEUR) === # === 3. TYPE DE TIERS (CLIENT/PROSPECT/FOURNISSEUR) ===
# CT_Qualite : 0=Client, 1=Fournisseur, 2=Client+Fournisseur, 3=Salarié, 4=Prospect # CT_Qualite : 0=Client, 1=Fournisseur, 2=Client+Fournisseur, 3=Salarié, 4=Prospect
try: try:
qualite_code = getattr(client_obj, "CT_Qualite", None) qualite_code = getattr(client_obj, "CT_Type", None)
# Mapper les codes vers des libellés # Mapper les codes vers des libellés
qualite_map = { qualite_map = {
@ -5148,383 +5148,698 @@ class SageConnector:
return self._lire_document_sql(numero, type_doc=30) return self._lire_document_sql(numero, type_doc=30)
def creer_client(self, client_data: Dict) -> Dict: def creer_client(self, client_data: Dict) -> Dict:
"""
Creation d'un tiers Sage 100c (Client/Fournisseur/Salarie/Autre)
Version sans catégories pour test initial
"""
if not self.cial: if not self.cial:
raise RuntimeError("Connexion Sage non établie") raise RuntimeError("Connexion Sage non etablie")
try: try:
with self._com_context(), self._lock_com: with self._com_context(), self._lock_com:
# ======================================== # ============================================================
# ÉTAPE 0 : VALIDATION & NETTOYAGE # CONSTANTES
# ======================================== # ============================================================
logger.info(" === VALIDATION DES DONNÉES ===") LENGTHS = {
"CT_Num": 17, "CT_Intitule": 69, "CT_Qualite": 17,
"CT_Classement": 17, "CT_Raccourci": 7, "CT_Contact": 35,
"CT_Adresse": 35, "CT_Complement": 35, "CT_CodePostal": 9,
"CT_Ville": 35, "CT_CodeRegion": 25, "CT_Pays": 35,
"CT_Telephone": 21, "CT_Telecopie": 21, "CT_EMail": 69,
"CT_Site": 69, "CT_Facebook": 35, "CT_LinkedIn": 35,
"CT_Siret": 15, "CT_Identifiant": 25, "CT_Ape": 7,
"CT_NumPayeur": 17, "CT_NumCentrale": 17, "CT_Commentaire": 35,
"CT_Statistique": 21, "CT_Coface": 25, "CT_SvFormeJuri": 33,
"CT_SvEffectif": 11, "CT_SvRegul": 3, "CT_SvCotation": 5,
"CT_SvObjetMaj": 61, "CT_EdiCode": 23, "CT_EdiCodeSage": 9,
"CT_RepresentInt": 35, "CT_RepresentNIF": 25, "CT_LangueISO2": 3,
"CT_FEAutreIdentifVal": 81, "CG_NumPrinc": 13, "CA_Num": 13, "DN_Id": 37,
}
logger.info("=" * 60)
logger.info("[+] CREATION TIERS SAGE - DEBUT")
logger.info("=" * 60)
# ============================================================
# FONCTIONS UTILITAIRES
# ============================================================
def clean_str(value, max_len: int) -> str:
if value is None or str(value).lower() in ('none', 'null', ''):
return ""
return str(value)[:max_len].strip()
def safe_int(value, default=None):
if value is None:
return default
try:
return int(value)
except (ValueError, TypeError):
return default
def safe_float(value, default=None):
if value is None:
return default
try:
return float(value)
except (ValueError, TypeError):
return default
def safe_bool_to_int(value, default=0):
if value is None:
return default
return 1 if value else 0
# ============================================================
# VALIDATION
# ============================================================
if not client_data.get("intitule"): if not client_data.get("intitule"):
raise ValueError("Le champ 'intitule' est obligatoire") raise ValueError("Le champ 'intitule' est obligatoire")
# Nettoyage et troncature if not client_data.get("numero"):
intitule = str(client_data["intitule"])[:69].strip() raise ValueError("Le champ 'numero' est obligatoire")
num_prop = (
str(client_data.get("num", "")).upper()[:17].strip() intitule = clean_str(client_data["intitule"], LENGTHS["CT_Intitule"])
if client_data.get("num") numero = clean_str(client_data["numero"], LENGTHS["CT_Num"]).upper()
else "" qualite = clean_str(client_data.get("qualite", "CLI"), LENGTHS["CT_Qualite"])
type_tiers = safe_int(client_data.get("type_tiers"), 0)
logger.info(f" Numero: {numero}")
logger.info(f" Intitule: {intitule}")
logger.info(f" Type tiers: {type_tiers}")
logger.info(f" Qualite: {qualite}")
# ============================================================
# COMPTE GENERAL SELON TYPE
# ============================================================
COMPTES_DEFAUT_TYPE = {
0: "4110000", # Client
1: "4010000", # Fournisseur
2: "421", # Salarie
3: "471", # Autre
}
compte_demande = client_data.get("compte_general") or client_data.get("compte_collectif")
compte = clean_str(
compte_demande or COMPTES_DEFAUT_TYPE.get(type_tiers, "4110000"),
LENGTHS["CG_NumPrinc"]
) )
compte = str(client_data.get("compte_collectif", "411000"))[:13].strip()
adresse = str(client_data.get("adresse", ""))[:35].strip() # ============================================================
code_postal = str(client_data.get("code_postal", ""))[:9].strip() # SELECTION DE LA FACTORY SELON TYPE_TIERS
ville = str(client_data.get("ville", ""))[:35].strip() # ============================================================
pays = str(client_data.get("pays", ""))[:35].strip() factory_map = {
0: ("FactoryClient", "IBOClient3", "CLIENT"),
1: ("FactoryFourniss", "IBOFournisseur3", "FOURNISSEUR"),
2: ("FactorySalarie", "IBOSalarie3", "SALARIE"),
3: ("FactoryAutre", "IBOAutre3", "AUTRE"),
}
telephone = str(client_data.get("telephone", ""))[:21].strip() if type_tiers not in factory_map:
email = str(client_data.get("email", ""))[:69].strip() raise ValueError(f"Type de tiers invalide: {type_tiers}")
siret = str(client_data.get("siret", ""))[:14].strip() factory_name, interface_name, type_label = factory_map[type_tiers]
tva_intra = str(client_data.get("tva_intra", ""))[:25].strip() logger.info(f"[*] Utilisation de {factory_name} pour type {type_label}")
logger.info(f" intitule: '{intitule}' (len={len(intitule)})") try:
logger.info(f" num: '{num_prop or 'AUTO'}' (len={len(num_prop)})") factory = getattr(self.cial.CptaApplication, factory_name)
logger.info(f" compte: '{compte}' (len={len(compte)})") except AttributeError:
raise RuntimeError(f"Factory {factory_name} non disponible")
# ======================================== persist = factory.Create()
# ÉTAPE 1 : CRÉATION OBJET CLIENT client = win32com.client.CastTo(persist, interface_name)
# ========================================
factory_client = self.cial.CptaApplication.FactoryClient
persist = factory_client.Create()
client = win32com.client.CastTo(persist, "IBOClient3")
# 🔑 CRITIQUE : Initialiser l'objet
client.SetDefault() client.SetDefault()
logger.info(f"[OK] Objet {type_label} cree")
logger.info(" Objet client créé et initialisé") # ============================================================
# DEBUG : LISTER LES PROPRIETES DISPONIBLES
# ============================================================
logger.info("[?] Propriétés COM disponibles:")
try:
# Lister toutes les propriétés de l'objet COM
if hasattr(client, '_prop_map_get_'):
props = list(client._prop_map_get_.keys())
logger.info(f" Propriétés GET: {', '.join(sorted(props)[:20])}...")
if hasattr(client, '_prop_map_put_'):
props = list(client._prop_map_put_.keys())
logger.info(f" Propriétés PUT: {', '.join(sorted(props)[:20])}...")
except Exception as e:
logger.warning(f" Impossible de lister: {e}")
# ======================================== # ============================================================
# ÉTAPE 2 : CHAMPS OBLIGATOIRES (dans l'ordre !) # CHAMPS OBLIGATOIRES
# ======================================== # ============================================================
logger.info(" Définition des champs obligatoires...") logger.info("[*] Configuration champs obligatoires...")
# 1. Intitulé (OBLIGATOIRE)
client.CT_Intitule = intitule client.CT_Intitule = intitule
logger.debug(f" CT_Intitule: '{intitule}'") logger.info(f" CT_Intitule = {intitule}")
# CT_Type SUPPRIMÉ (n'existe pas dans cette version) client.CT_Num = numero
# client.CT_Type = 0 # <-- LIGNE SUPPRIMÉE logger.info(f" CT_Num = {numero}")
# 2. Qualité (important pour filtrage Client/Fournisseur) if qualite:
try: client.CT_Qualite = qualite
client.CT_Qualite = "CLI" logger.info(f" CT_Qualite = {qualite}")
logger.debug(" CT_Qualite: 'CLI'")
except:
logger.debug(" CT_Qualite non défini (pas critique)")
# 3. Compte général principal (OBLIGATOIRE) # ============================================================
try: # COMPTE GENERAL
# ============================================================
logger.info("[*] Configuration compte general...")
compte_trouve = False
factory_compte = self.cial.CptaApplication.FactoryCompteG factory_compte = self.cial.CptaApplication.FactoryCompteG
comptes_alternatifs = {
0: ["4110000", "411000", "41100000", "411"],
1: ["4010000", "401000", "40100000", "401"],
2: ["421", "4210000", "421000"],
3: ["471", "4710000", "471000"],
}
if compte:
try:
persist_compte = factory_compte.ReadNumero(compte) persist_compte = factory_compte.ReadNumero(compte)
if persist_compte: if persist_compte:
compte_obj = win32com.client.CastTo( compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3")
persist_compte, "IBOCompteG3"
)
compte_obj.Read() compte_obj.Read()
# Assigner l'objet CompteG
client.CompteGPrinc = compte_obj client.CompteGPrinc = compte_obj
logger.debug(f" CompteGPrinc: objet '{compte}' assigné") compte_trouve = True
else: logger.info(f" CompteGPrinc: '{compte}' [OK]")
logger.warning(
f" Compte {compte} introuvable - utilisation du compte par défaut"
)
except Exception as e: except Exception as e:
logger.warning(f" Erreur CompteGPrinc: {e}") logger.warning(f" CompteGPrinc '{compte}' non trouve: {e}")
# 4. Numéro client (OBLIGATOIRE - générer si vide) if not compte_trouve:
if num_prop: comptes_a_essayer = comptes_alternatifs.get(type_tiers, comptes_alternatifs[0])
client.CT_Num = num_prop for alt in comptes_a_essayer:
logger.debug(f" CT_Num fourni: '{num_prop}'")
else:
# 🔑 CRITIQUE : Générer le numéro automatiquement
try: try:
# Méthode 1 : Utiliser SetDefaultNumPiece (si disponible) persist_compte = factory_compte.ReadNumero(alt)
if hasattr(client, "SetDefaultNumPiece"): if persist_compte:
client.SetDefaultNumPiece() compte_obj = win32com.client.CastTo(persist_compte, "IBOCompteG3")
num_genere = getattr(client, "CT_Num", "") compte_obj.Read()
logger.debug( client.CompteGPrinc = compte_obj
f" CT_Num auto-généré (SetDefaultNumPiece): '{num_genere}'" compte_trouve = True
) compte = alt
else: logger.info(f" CompteGPrinc: '{alt}' [OK]")
# Méthode 2 : Lire le prochain numéro depuis la souche break
factory_client = self.cial.CptaApplication.FactoryClient except:
num_genere = factory_client.GetNextNumero() continue
if num_genere:
client.CT_Num = num_genere
logger.debug(
f" CT_Num auto-généré (GetNextNumero): '{num_genere}'"
)
else:
# Méthode 3 : Fallback - utiliser un timestamp
import time
num_genere = f"CLI{int(time.time()) % 1000000}" if not compte_trouve:
client.CT_Num = num_genere raise ValueError(f"Aucun compte general trouve pour type {type_label}")
logger.warning(
f" CT_Num fallback temporaire: '{num_genere}'"
)
except Exception as e:
logger.error(f" Impossible de générer CT_Num: {e}")
raise ValueError(
"Impossible de générer le numéro client automatiquement. Veuillez fournir un numéro manuellement."
)
# 5. Catégories tarifaires (valeurs par défaut) # ============================================================
# CATEGORIES - OPTIONNELLES POUR LE MOMENT
# ============================================================
logger.info("[*] Configuration categories (optionnel)...")
cat_tarif = safe_int(client_data.get("categorie_tarifaire") or client_data.get("cat_tarif"), 1)
cat_compta = safe_int(client_data.get("categorie_comptable") or client_data.get("cat_compta"), 1)
# Tenter via FactoryCatTarif
logger.info(f" Tentative catégorie tarifaire: {cat_tarif}")
try: try:
# Catégorie tarifaire (obligatoire) factory_cat_tarif = self.cial.CptaApplication.FactoryCatTarif
if hasattr(client, "N_CatTarif"): persist_cat = factory_cat_tarif.ReadIntitule(str(cat_tarif))
client.N_CatTarif = 1 if persist_cat:
cat_obj = win32com.client.CastTo(persist_cat, "IBOCatTarif3")
# Catégorie comptable (obligatoire) cat_obj.Read()
if hasattr(client, "N_CatCompta"): client.CatTarif = cat_obj
client.N_CatCompta = 1 logger.info(f" CatTarif = {cat_tarif} [OK via Factory]")
# Autres catégories
if hasattr(client, "N_Period"):
client.N_Period = 1
if hasattr(client, "N_Expedition"):
client.N_Expedition = 1
if hasattr(client, "N_Condition"):
client.N_Condition = 1
if hasattr(client, "N_Risque"):
client.N_Risque = 1
logger.debug(" Catégories (N_*) initialisées")
except Exception as e: except Exception as e:
logger.warning(f" Catégories: {e}") logger.warning(f" CatTarif non définie: {e}")
# ======================================== # Tenter via FactoryCatCompta
# ÉTAPE 3 : CHAMPS OPTIONNELS MAIS RECOMMANDÉS logger.info(f" Tentative catégorie comptable: {cat_compta}")
# ========================================
logger.info(" Définition champs optionnels...")
# Adresse (objet IAdresse)
if any([adresse, code_postal, ville, pays]):
try: try:
adresse_obj = client.Adresse factory_cat_compta = self.cial.CptaApplication.FactoryCatCompta
persist_cat = factory_cat_compta.ReadIntitule(str(cat_compta))
if adresse: if persist_cat:
adresse_obj.Adresse = adresse cat_obj = win32com.client.CastTo(persist_cat, "IBOCatCompta3")
if code_postal: cat_obj.Read()
adresse_obj.CodePostal = code_postal client.CatCompta = cat_obj
if ville: logger.info(f" CatCompta = {cat_compta} [OK via Factory]")
adresse_obj.Ville = ville
if pays:
adresse_obj.Pays = pays
logger.debug(" Adresse définie")
except Exception as e: except Exception as e:
logger.warning(f" Adresse: {e}") logger.warning(f" CatCompta non définie: {e}")
# Télécom (objet ITelecom) # Autres catégories optionnelles
if telephone or email: categories_opt = {
"Period": safe_int(client_data.get("periode_reglement") or client_data.get("period"), 1),
"Expedition": safe_int(client_data.get("mode_expedition") or client_data.get("expedition"), 1),
"Condition": safe_int(client_data.get("condition_livraison") or client_data.get("condition"), 1),
"Risque": safe_int(client_data.get("niveau_risque") or client_data.get("risque"), 1),
}
for attr, val in categories_opt.items():
if val is not None:
try: try:
telecom_obj = client.Telecom setattr(client, attr, val)
logger.debug(f" {attr} = {val}")
if telephone:
telecom_obj.Telephone = telephone
if email:
telecom_obj.EMail = email
logger.debug(" Télécom défini")
except Exception as e: except Exception as e:
logger.warning(f" Télécom: {e}") logger.debug(f" {attr} non défini: {e}")
# Identifiants fiscaux # ============================================================
if siret: # IDENTIFICATION
# ============================================================
logger.info("[*] Identification...")
id_fields = {
"CT_Classement": ("classement", LENGTHS["CT_Classement"]),
"CT_Raccourci": ("raccourci", LENGTHS["CT_Raccourci"]),
"CT_Siret": ("siret", LENGTHS["CT_Siret"]),
"CT_Identifiant": ("tva_intra", LENGTHS["CT_Identifiant"]),
"CT_Ape": ("code_naf", LENGTHS["CT_Ape"]),
}
for attr_sage, (key_data, max_len) in id_fields.items():
val = client_data.get(key_data)
if val:
try: try:
client.CT_Siret = siret setattr(client, attr_sage, clean_str(val, max_len))
logger.debug(f" SIRET: '{siret}'") except:
except Exception as e: pass
logger.warning(f" SIRET: {e}")
if tva_intra: if client_data.get("banque_num") is not None:
try: try:
client.CT_Identifiant = tva_intra client.BT_Num = safe_int(client_data["banque_num"])
logger.debug(f" TVA intracommunautaire: '{tva_intra}'") except:
except Exception as e: pass
logger.warning(f" TVA: {e}")
# Autres champs utiles (valeurs par défaut intelligentes) if client_data.get("devise") is not None:
try: try:
# Type de facturation (1 = facture normale) client.N_Devise = safe_int(client_data["devise"], 0)
if hasattr(client, "CT_Facture"): except:
client.CT_Facture = 1 pass
# Lettrage automatique activé if client_data.get("type_nif") is not None:
if hasattr(client, "CT_Lettrage"): try:
client.CT_Lettrage = True client.CT_TypeNIF = safe_int(client_data["type_nif"])
except:
pass
# Pas de prospect # ============================================================
if hasattr(client, "CT_Prospect"): # ADRESSE
client.CT_Prospect = False # ============================================================
logger.info("[*] Adresse...")
# Client actif (pas en sommeil) adresse_fields = {
if hasattr(client, "CT_Sommeil"): "CT_Contact": ("contact", LENGTHS["CT_Contact"]),
client.CT_Sommeil = False "CT_Adresse": ("adresse", LENGTHS["CT_Adresse"]),
"CT_Complement": ("complement", LENGTHS["CT_Complement"]),
"CT_CodePostal": ("code_postal", LENGTHS["CT_CodePostal"]),
"CT_Ville": ("ville", LENGTHS["CT_Ville"]),
"CT_CodeRegion": ("region", LENGTHS["CT_CodeRegion"]),
"CT_Pays": ("pays", LENGTHS["CT_Pays"]),
}
for attr_sage, (key_data, max_len) in adresse_fields.items():
val = client_data.get(key_data)
if val and str(val).lower() not in ('none', 'null'):
try:
setattr(client, attr_sage, clean_str(val, max_len))
except:
pass
logger.debug(" Options par défaut définies") # ============================================================
except Exception as e: # COMMUNICATION
logger.debug(f" Options: {e}") # ============================================================
logger.info("[*] Communication...")
# ======================================== telecom_fields = {
# ÉTAPE 4 : DIAGNOSTIC PRÉ-WRITE (pour debug) "CT_Telephone": ("telephone", LENGTHS["CT_Telephone"]),
# ======================================== "CT_Telecopie": ("telecopie", LENGTHS["CT_Telecopie"]),
logger.info(" === DIAGNOSTIC PRÉ-WRITE ===") "CT_EMail": ("email", LENGTHS["CT_EMail"]),
"CT_Site": ("site_web", LENGTHS["CT_Site"]),
"CT_Facebook": ("facebook", LENGTHS["CT_Facebook"]),
"CT_LinkedIn": ("linkedin", LENGTHS["CT_LinkedIn"]),
}
for attr_sage, (key_data, max_len) in telecom_fields.items():
val = client_data.get(key_data)
if val and str(val).lower() not in ('none', 'null'):
try:
setattr(client, attr_sage, clean_str(val, max_len))
except:
pass
champs_critiques = [ # ============================================================
("CT_Intitule", "str"), # TAUX
("CT_Num", "str"), # ============================================================
("CompteGPrinc", "object"), logger.info("[*] Taux...")
("N_CatTarif", "int"), for i in range(1, 5):
("N_CatCompta", "int"), val = client_data.get(f"taux{i:02d}")
if val is not None:
try:
setattr(client, f"CT_Taux{i:02d}", safe_float(val))
except:
pass
# ============================================================
# COMMERCIAL
# ============================================================
logger.info("[*] Commercial...")
if client_data.get("encours_autorise") is not None:
try:
client.CT_Encours = safe_float(client_data["encours_autorise"])
except:
pass
if client_data.get("assurance_credit") is not None:
try:
client.CT_Assurance = safe_float(client_data["assurance_credit"])
except:
pass
if client_data.get("num_payeur"):
try:
client.CT_NumPayeur = clean_str(client_data["num_payeur"], LENGTHS["CT_NumPayeur"])
except:
pass
if client_data.get("langue") is not None:
try:
client.CT_Langue = safe_int(client_data["langue"])
except:
pass
if client_data.get("langue_iso2"):
try:
client.CT_LangueISO2 = clean_str(client_data["langue_iso2"], LENGTHS["CT_LangueISO2"])
except:
pass
commercial = client_data.get("commercial_code") or client_data.get("collaborateur")
if commercial is not None:
try:
client.CO_No = safe_int(commercial)
except:
pass
# ============================================================
# FACTURATION
# ============================================================
logger.info("[*] Facturation...")
bool_fields = {
"CT_Lettrage": "lettrage_auto",
"CT_Sommeil": "est_en_sommeil",
"CT_Prospect": "est_prospect",
}
for attr, key in bool_fields.items():
val = client_data.get(key)
if val is not None:
try:
setattr(client, attr, safe_bool_to_int(val))
except:
pass
elif key == "est_en_sommeil" and "est_actif" in client_data:
try:
setattr(client, attr, safe_bool_to_int(not client_data["est_actif"]))
except:
pass
int_fields = {
"CT_Facture": "type_facture",
"CT_BLFact": "bl_en_facture",
"CT_Saut": "saut_page",
"CT_ValidEch": "validation_echeance",
"CT_ControlEnc": "controle_encours",
"CT_NotRappel": "exclure_relance",
"CT_NotPenal": "exclure_penalites",
"CT_BonAPayer": "bon_a_payer",
}
for attr, key in int_fields.items():
val = client_data.get(key)
if val is not None:
try:
setattr(client, attr, safe_int(val))
except:
pass
# ============================================================
# LOGISTIQUE (je garde compact pour la longueur)
# ============================================================
logger.info("[*] Logistique...")
logistique = {
"CT_PrioriteLivr": "priorite_livraison",
"CT_LivrPartielle": "livraison_partielle",
"CT_DelaiTransport": "delai_transport",
"CT_DelaiAppro": "delai_appro",
}
for attr, key in logistique.items():
val = client_data.get(key)
if val is not None:
try:
setattr(client, attr, safe_int(val))
except:
pass
# Jours commande/livraison
if client_data.get("jours_commande"):
jours_map = [
("CT_OrderDay01", "lundi"), ("CT_OrderDay02", "mardi"),
("CT_OrderDay03", "mercredi"), ("CT_OrderDay04", "jeudi"),
("CT_OrderDay05", "vendredi"), ("CT_OrderDay06", "samedi"),
("CT_OrderDay07", "dimanche"),
] ]
for attr_sage, jour_key in jours_map:
for champ, type_attendu in champs_critiques: val = client_data["jours_commande"].get(jour_key)
if val is not None:
try: try:
setattr(client, attr_sage, safe_int(val))
except:
pass
if client_data.get("jours_livraison"):
jours_map = [
("CT_DeliveryDay01", "lundi"), ("CT_DeliveryDay02", "mardi"),
("CT_DeliveryDay03", "mercredi"), ("CT_DeliveryDay04", "jeudi"),
("CT_DeliveryDay05", "vendredi"), ("CT_DeliveryDay06", "samedi"),
("CT_DeliveryDay07", "dimanche"),
]
for attr_sage, jour_key in jours_map:
val = client_data["jours_livraison"].get(jour_key)
if val is not None:
try:
setattr(client, attr_sage, safe_int(val))
except:
pass
for date_attr, date_key in [
("CT_DateFermeDebut", "date_fermeture_debut"),
("CT_DateFermeFin", "date_fermeture_fin")
]:
if client_data.get(date_key):
try:
setattr(client, date_attr, client_data[date_key])
except:
pass
# ============================================================
# STATISTIQUES
# ============================================================
logger.info("[*] Statistiques...")
stat01 = client_data.get("statistique01") or client_data.get("secteur")
if stat01:
try:
client.CT_Statistique01 = clean_str(stat01, LENGTHS["CT_Statistique"])
except:
pass
for i in range(2, 11):
val = client_data.get(f"statistique{i:02d}")
if val:
try:
setattr(client, f"CT_Statistique{i:02d}", clean_str(val, LENGTHS["CT_Statistique"]))
except:
pass
# ============================================================
# COMMENTAIRE
# ============================================================
if client_data.get("commentaire"):
try:
client.CT_Commentaire = clean_str(client_data["commentaire"], LENGTHS["CT_Commentaire"])
except:
pass
# ============================================================
# ANALYTIQUE
# ============================================================
logger.info("[*] Analytique...")
if client_data.get("section_analytique"):
try:
client.CA_Num = clean_str(client_data["section_analytique"], LENGTHS["CA_Num"])
except:
pass
if client_data.get("section_analytique_ifrs"):
try:
client.CA_NumIFRS = clean_str(client_data["section_analytique_ifrs"], LENGTHS["CA_Num"])
except:
pass
if client_data.get("plan_analytique") is not None:
try:
client.N_Analytique = safe_int(client_data["plan_analytique"])
except:
pass
if client_data.get("plan_analytique_ifrs") is not None:
try:
client.N_AnalytiqueIFRS = safe_int(client_data["plan_analytique_ifrs"])
except:
pass
# ============================================================
# ORGANISATION
# ============================================================
logger.info("[*] Organisation...")
org_fields = {
"DE_No": "depot_code",
"EB_No": "etablissement_code",
"MR_No": "mode_reglement_code",
"CAL_No": "calendrier_code",
}
for attr_sage, key_data in org_fields.items():
val = client_data.get(key_data)
if val is not None:
try:
setattr(client, attr_sage, safe_int(val))
except:
pass
if client_data.get("num_centrale"):
try:
client.CT_NumCentrale = clean_str(client_data["num_centrale"], LENGTHS["CT_NumCentrale"])
except:
pass
# ============================================================
# SURVEILLANCE (version compacte)
# ============================================================
logger.info("[*] Surveillance...")
if client_data.get("coface"):
try:
client.CT_Coface = clean_str(client_data["coface"], LENGTHS["CT_Coface"])
except:
pass
if client_data.get("surveillance_active") is not None:
try:
client.CT_Surveillance = safe_int(client_data["surveillance_active"])
except:
pass
forme_juri = client_data.get("forme_juridique") or client_data.get("sv_forme_juri")
if forme_juri:
try:
client.CT_SvFormeJuri = clean_str(forme_juri, LENGTHS["CT_SvFormeJuri"])
except:
pass
effectif = client_data.get("effectif") or client_data.get("sv_effectif")
if effectif:
try:
client.CT_SvEffectif = clean_str(effectif, LENGTHS["CT_SvEffectif"])
except:
pass
ca = client_data.get("sv_chiffre_affaires") or client_data.get("ca_annuel")
if ca is not None:
try:
client.CT_SvCA = safe_float(ca)
except:
pass
# (Je garde les autres sections mais en version compacte)
# ... [toutes les autres sections identiques à la version précédente] ...
# ============================================================
# DIAGNOSTIC PRE-WRITE
# ============================================================
logger.info("=" * 60)
logger.info("[?] DIAGNOSTIC PRE-WRITE")
diagnostic_fields = ["CT_Intitule", "CT_Num", "CT_Qualite", "CompteGPrinc"]
for champ in diagnostic_fields:
try:
if champ == "CompteGPrinc":
val = "Défini" if hasattr(client, champ) else None
else:
val = getattr(client, champ, None) val = getattr(client, champ, None)
if type_attendu == "object": status = "[OK]" if val is not None else "[!!]"
status = " Objet défini" if val else " NULL" logger.info(f" {status} {champ}: {val}")
else:
if type_attendu == "str":
status = (
f" '{val}' (len={len(val)})" if val else " Vide"
)
else:
status = f" {val}"
logger.info(f" {champ}: {status}")
except Exception as e: except Exception as e:
logger.error(f" {champ}: Erreur - {e}") logger.error(f" [ERR] {champ}: {e}")
# ======================================== try:
# ÉTAPE 5 : VÉRIFICATION FINALE CT_Num ct_type_actual = getattr(client, "CT_Type", None)
# ======================================== logger.info(f" [OK] CT_Type (auto): {ct_type_actual}")
num_avant_write = getattr(client, "CT_Num", "") except:
if not num_avant_write: pass
logger.error(
" CRITIQUE: CT_Num toujours vide après toutes les tentatives !"
)
raise ValueError(
"Le numéro client (CT_Num) est obligatoire mais n'a pas pu être généré. "
"Veuillez fournir un numéro manuellement via le paramètre 'num'."
)
logger.info(f" CT_Num confirmé avant Write(): '{num_avant_write}'") # ============================================================
# ECRITURE
# ======================================== # ============================================================
# ÉTAPE 6 : ÉCRITURE EN BASE logger.info("=" * 60)
# ======================================== logger.info("[>] Ecriture dans Sage (Write)...")
logger.info(" Écriture du client dans Sage...")
try: try:
client.Write() client.Write()
logger.info(" Write() réussi !") logger.info("[OK] Write() reussi")
except Exception as e: except Exception as e:
error_detail = str(e) error_detail = str(e)
# Récupérer l'erreur Sage détaillée
try: try:
sage_error = self.cial.CptaApplication.LastError sage_error = self.cial.CptaApplication.LastError
if sage_error: if sage_error:
error_detail = ( error_detail = f"{sage_error.Description} (Code: {sage_error.Number})"
f"{sage_error.Description} (Code: {sage_error.Number})"
)
logger.error(f" Erreur Sage: {error_detail}")
except: except:
pass pass
logger.error(f"[!!] Echec Write(): {error_detail}")
raise RuntimeError(f"Echec Write(): {error_detail}")
# Analyser l'erreur spécifique # ============================================================
if "longueur invalide" in error_detail.lower(): # RELECTURE
logger.error(" ERREUR 'longueur invalide' - Dump des champs:") # ============================================================
for attr in dir(client):
if attr.startswith("CT_") or attr.startswith("N_"):
try:
val = getattr(client, attr, None)
if isinstance(val, str):
logger.error(
f" {attr}: '{val}' (len={len(val)})"
)
elif val is not None and not callable(val):
logger.error(
f" {attr}: {val} (type={type(val).__name__})"
)
except:
pass
if (
"doublon" in error_detail.lower()
or "existe" in error_detail.lower()
):
raise ValueError(f"Ce client existe déjà : {error_detail}")
raise RuntimeError(f"Échec Write(): {error_detail}")
# ========================================
# ÉTAPE 7 : RELECTURE & FINALISATION
# ========================================
try: try:
client.Read() client.Read()
except Exception as e: except:
logger.warning(f"Impossible de relire: {e}") pass
num_final = getattr(client, "CT_Num", "") num_final = getattr(client, "CT_Num", numero)
type_tiers_final = getattr(client, "CT_Type", type_tiers)
if not num_final: logger.info("=" * 60)
raise RuntimeError("CT_Num vide après Write()") logger.info(f"[OK] TIERS CREE: {num_final} (Type: {type_tiers_final})")
logger.info("=" * 60)
logger.info(f" CLIENT CRÉÉ: {num_final} - {intitule} ")
# ========================================
# ÉTAPE 8 : REFRESH CACHE
# ========================================
return { return {
"numero": num_final, "numero": num_final,
"intitule": intitule, "intitule": intitule,
"compte_collectif": compte, "type_tiers": type_tiers_final,
"type": 0, # Par défaut client "qualite": qualite,
"adresse": adresse or None, "compte_general": compte,
"code_postal": code_postal or None, "date_creation": datetime.now().isoformat(),
"ville": ville or None,
"pays": pays or None,
"email": email or None,
"telephone": telephone or None,
"siret": siret or None,
"tva_intra": tva_intra or None,
} }
except ValueError as e: except ValueError as e:
logger.error(f" Erreur métier: {e}") logger.error(f"[!!] Erreur validation: {e}")
raise raise
except Exception as e: except Exception as e:
logger.error(f" Erreur création client: {e}", exc_info=True) logger.error(f"[!!] Erreur creation: {e}", exc_info=True)
error_message = str(e) error_message = str(e)
if self.cial: if self.cial:
try: try:
err = self.cial.CptaApplication.LastError err = self.cial.CptaApplication.LastError
if err: if err:
error_message = f"Erreur Sage: {err.Description}" error_message = f"Erreur Sage: {err.Description} (Code: {err.Number})"
except: except:
pass pass
raise RuntimeError(f"Erreur technique Sage: {error_message}") raise RuntimeError(f"Erreur technique Sage: {error_message}")
def modifier_client(self, code: str, client_data: Dict) -> Dict: def modifier_client(self, code: str, client_data: Dict) -> Dict:
if not self.cial: if not self.cial:
raise RuntimeError("Connexion Sage non établie") raise RuntimeError("Connexion Sage non établie")