diff --git a/api.py b/api.py index a0072de..a75ae88 100644 --- a/api.py +++ b/api.py @@ -400,54 +400,275 @@ class BaremeRemiseResponse(BaseModel): message: str -class ClientCreateAPIRequest(BaseModel): - """Modèle pour création d'un nouveau client""" - - intitule: str = Field( - ..., min_length=1, max_length=69, description="Raison sociale ou Nom complet" - ) - compte_collectif: str = Field( - "411000", description="Compte comptable (411000 par défaut)" - ) - num: Optional[str] = Field( - None, max_length=17, description="Code client souhaité (auto si vide)" - ) - - # Adresse - adresse: Optional[str] = Field(None, max_length=35) - code_postal: Optional[str] = Field(None, max_length=9) - ville: Optional[str] = Field(None, max_length=35) - pays: Optional[str] = Field(None, max_length=35) - - # Contact - email: Optional[EmailStr] = None - telephone: Optional[str] = Field(None, max_length=21, description="Téléphone fixe") - portable: Optional[str] = Field(None, max_length=21, description="Téléphone mobile") - - # Juridique - forme_juridique: Optional[str] = Field( - None, max_length=50, description="SARL, SA, SAS, EI, etc." - ) - siret: Optional[str] = Field(None, max_length=14) - tva_intra: Optional[str] = Field(None, max_length=25) - +class ClientCreateRequest(BaseModel): + """Modèle complet pour la création d'un client Sage avec tous les champs disponibles""" + + # ======================================== + # CHAMPS OBLIGATOIRES + # ======================================== + intitule: str = Field(..., max_length=69, description="Nom du client (CT_Intitule)") + + # ======================================== + # IDENTIFICATION & CLASSIFICATION + # ======================================== + num: Optional[str] = Field(None, max_length=17, description="Numéro client (auto si vide)") + compte_collectif: str = Field("411000", max_length=13, description="Compte général (CG_NumPrinc)") + qualite: str = Field("CLI", max_length=3, description="CLI/FOU/SAL/DIV/AUT") + classement: Optional[str] = Field(None, max_length=17, description="Code de classement") + raccourci: Optional[str] = Field(None, max_length=17, description="Code abrégé") + + # ======================================== + # ADRESSE PRINCIPALE + # ======================================== + contact: Optional[str] = Field(None, max_length=69, description="Nom du contact principal") + adresse: Optional[str] = Field(None, max_length=35, description="Adresse ligne 1") + complement: Optional[str] = Field(None, max_length=35, description="Adresse ligne 2") + code_postal: Optional[str] = Field(None, max_length=9, description="Code postal") + ville: Optional[str] = Field(None, max_length=35, description="Ville") + code_region: Optional[str] = Field(None, max_length=25, description="Code région/département") + pays: Optional[str] = Field(None, max_length=35, description="Pays") + + # ======================================== + # CONTACT & COMMUNICATION + # ======================================== + telephone: Optional[str] = Field(None, max_length=21, description="Téléphone principal") + telecopie: Optional[str] = Field(None, max_length=21, description="Fax") + email: Optional[str] = Field(None, max_length=69, description="Email principal") + site: Optional[str] = Field(None, max_length=69, description="Site web") + facebook: Optional[str] = Field(None, max_length=100, description="URL Facebook") + linkedin: Optional[str] = Field(None, max_length=100, description="URL LinkedIn") + + # ======================================== + # IDENTIFIANTS LÉGAUX & FISCAUX + # ======================================== + siret: Optional[str] = Field(None, max_length=14, description="SIRET (14 chiffres)") + tva_intra: Optional[str] = Field(None, max_length=25, description="TVA intracommunautaire (CT_Identifiant)") + ape: Optional[str] = Field(None, max_length=5, description="Code APE/NAF") + type_nif: Optional[int] = Field(None, description="Type de NIF (0-10)") + + # ======================================== + # BANQUE & DEVISE + # ======================================== + banque_num: Optional[str] = Field(None, max_length=13, description="Code banque (BT_Num)") + devise: Optional[int] = Field(0, description="Code devise (N_Devise, 0=EUR)") + + # ======================================== + # CATÉGORIES & CLASSIFICATIONS + # ======================================== + cat_tarif: int = Field(1, ge=1, description="Catégorie tarifaire (N_CatTarif)") + cat_compta: int = Field(1, ge=1, description="Catégorie comptable (N_CatCompta)") + period: int = Field(1, ge=1, description="Période de règlement (N_Period)") + expedition: int = Field(1, ge=1, description="Mode d'expédition (N_Expedition)") + condition: int = Field(1, ge=1, description="Condition de livraison (N_Condition)") + risque: int = Field(1, ge=1, description="Niveau de risque (N_Risque)") + + # ======================================== + # TAUX PERSONNALISÉS + # ======================================== + taux01: Optional[Decimal] = Field(None, description="Taux personnalisé 1 (CT_Taux01)") + taux02: Optional[Decimal] = Field(None, description="Taux personnalisé 2 (CT_Taux02)") + taux03: Optional[Decimal] = Field(None, description="Taux personnalisé 3 (CT_Taux03)") + taux04: Optional[Decimal] = Field(None, description="Taux personnalisé 4 (CT_Taux04)") + + # ======================================== + # GESTION COMMERCIALE + # ======================================== + encours: Optional[Decimal] = Field(None, description="Encours autorisé (CT_Encours)") + assurance: Optional[Decimal] = Field(None, description="Plafond assurance (CT_Assurance)") + num_payeur: Optional[str] = Field(None, max_length=17, description="Numéro client payeur") + langue: Optional[int] = Field(None, description="Code langue (0-25)") + langue_iso2: Optional[str] = Field(None, max_length=2, description="Code ISO langue (FR, EN, etc)") + + # ======================================== + # PARAMÈTRES FACTURATION + # ======================================== + facture: int = Field(1, description="Type facturation (0=aucune, 1=normale, 2=regroupée)") + bl_fact: Optional[int] = Field(None, description="BL en facture (0/1)") + saut: Optional[int] = Field(None, description="Saut de page (0/1)") + lettrage: bool = Field(True, description="Lettrage auto (CT_Lettrage)") + valid_ech: Optional[int] = Field(None, description="Validation échéance (0/1)") + control_enc: Optional[int] = Field(None, description="Contrôle encours (0/1)") + not_rappel: Optional[int] = Field(None, description="Pas de relance (0/1)") + not_penal: Optional[int] = Field(None, description="Pas de pénalités (0/1)") + + # ======================================== + # LIVRAISON & LOGISTIQUE + # ======================================== + priorite_livr: Optional[int] = Field(None, description="Priorité livraison (0-5)") + livr_partielle: Optional[int] = Field(None, description="Livraison partielle autorisée (0/1)") + delai_transport: Optional[int] = Field(None, description="Délai transport (jours)") + delai_appro: Optional[int] = Field(None, description="Délai approvisionnement (jours)") + + # JOURS DE COMMANDE (0=non, 1=oui) + order_day_lundi: Optional[int] = Field(None, ge=0, le=1) + order_day_mardi: Optional[int] = Field(None, ge=0, le=1) + order_day_mercredi: Optional[int] = Field(None, ge=0, le=1) + order_day_jeudi: Optional[int] = Field(None, ge=0, le=1) + order_day_vendredi: Optional[int] = Field(None, ge=0, le=1) + order_day_samedi: Optional[int] = Field(None, ge=0, le=1) + order_day_dimanche: Optional[int] = Field(None, ge=0, le=1) + + # JOURS DE LIVRAISON (0=non, 1=oui) + delivery_day_lundi: Optional[int] = Field(None, ge=0, le=1) + delivery_day_mardi: Optional[int] = Field(None, ge=0, le=1) + delivery_day_mercredi: Optional[int] = Field(None, ge=0, le=1) + delivery_day_jeudi: Optional[int] = Field(None, ge=0, le=1) + delivery_day_vendredi: Optional[int] = Field(None, ge=0, le=1) + delivery_day_samedi: Optional[int] = Field(None, ge=0, le=1) + delivery_day_dimanche: Optional[int] = Field(None, ge=0, le=1) + + # ======================================== + # DATES FERMETURE + # ======================================== + date_ferme_debut: Optional[date] = Field(None, description="Début période fermeture") + date_ferme_fin: Optional[date] = Field(None, description="Fin période fermeture") + + # ======================================== + # STATISTIQUES PERSONNALISÉES (10 champs) + # ======================================== + statistique01: Optional[str] = Field(None, max_length=69) + statistique02: Optional[str] = Field(None, max_length=69) + statistique03: Optional[str] = Field(None, max_length=69) + statistique04: Optional[str] = Field(None, max_length=69) + statistique05: Optional[str] = Field(None, max_length=69) + statistique06: Optional[str] = Field(None, max_length=69) + statistique07: Optional[str] = Field(None, max_length=69) + statistique08: Optional[str] = Field(None, max_length=69) + statistique09: Optional[str] = Field(None, max_length=69) + statistique10: Optional[str] = Field(None, max_length=69) + + # ======================================== + # COMMENTAIRE + # ======================================== + commentaire: Optional[str] = Field(None, description="Commentaire libre (CT_Commentaire, jusqu'à 2Go théorique)") + + # ======================================== + # ÉTAT & STATUT + # ======================================== + sommeil: bool = Field(False, description="Compte en sommeil") + prospect: bool = Field(False, description="Prospect (pas encore client)") + bon_a_payer: Optional[int] = Field(None, description="Bon à payer (0/1)") + + # ======================================== + # ANALYTIQUE + # ======================================== + section_analytique: Optional[str] = Field(None, max_length=13, description="Section analytique (CA_Num)") + section_analytique_ifrs: Optional[str] = Field(None, max_length=13, description="Section IFRS (CA_NumIFRS)") + plan_analytique: Optional[int] = Field(None, description="Plan analytique (N_Analytique)") + plan_analytique_ifrs: Optional[int] = Field(None, description="Plan IFRS (N_AnalytiqueIFRS)") + + # ======================================== + # COLLABORATEUR & DÉPÔT + # ======================================== + collaborateur: Optional[str] = Field(None, max_length=4, description="Code collaborateur (CO_No)") + depot: Optional[str] = Field(None, max_length=13, description="Dépôt par défaut (DE_No)") + etablissement: Optional[str] = Field(None, max_length=13, description="Établissement (EB_No)") + mode_regl: Optional[str] = Field(None, max_length=3, description="Mode règlement (MR_No)") + + # ======================================== + # CENTRALE D'ACHAT + # ======================================== + num_centrale: Optional[str] = Field(None, max_length=17, description="Numéro centrale d'achat") + + # ======================================== + # SURVEILLANCE COFACE + # ======================================== + coface: Optional[str] = Field(None, max_length=25, description="Code Coface") + surveillance: Optional[int] = Field(None, description="Surveillance activée (0/1)") + sv_date_create: Optional[date] = Field(None, description="Date création entreprise") + sv_forme_juri: Optional[str] = Field(None, max_length=50, description="Forme juridique") + sv_effectif: Optional[int] = Field(None, description="Effectif") + sv_ca: Optional[Decimal] = Field(None, description="Chiffre d'affaires") + sv_resultat: Optional[Decimal] = Field(None, description="Résultat") + sv_incident: Optional[int] = Field(None, description="Incidents (0/1)") + sv_date_incid: Optional[date] = Field(None, description="Date dernier incident") + sv_privil: Optional[int] = Field(None, description="Privilèges (0/1)") + sv_regul: Optional[int] = Field(None, description="Régularité (0/1)") + sv_cotation: Optional[str] = Field(None, max_length=25, description="Cotation") + sv_date_maj: Optional[date] = Field(None, description="Date MAJ surveillance") + sv_objet_maj: Optional[str] = Field(None, max_length=35, description="Objet MAJ") + sv_date_bilan: Optional[date] = Field(None, description="Date dernier bilan") + sv_nb_mois_bilan: Optional[int] = Field(None, description="Nb mois bilan") + + # ======================================== + # FACTURATION ÉLECTRONIQUE + # ======================================== + facture_elec: Optional[int] = Field(None, description="Facturation électronique (0/1)") + edi_code_type: Optional[int] = Field(None, description="Type code EDI") + edi_code: Optional[str] = Field(None, max_length=35, description="Code EDI") + edi_code_sage: Optional[str] = Field(None, max_length=35, description="Code EDI Sage") + fe_assujetti: Optional[int] = Field(None, description="Assujetti facture électronique") + fe_autre_identif_type: Optional[str] = Field(None, max_length=10) + fe_autre_identif_val: Optional[str] = Field(None, max_length=50) + fe_entite_type: Optional[int] = Field(None) + fe_emission: Optional[int] = Field(None) + fe_application: Optional[int] = Field(None) + + # ======================================== + # ÉCHANGES & INTÉGRATION + # ======================================== + echange_rappro: Optional[int] = Field(None, description="Échange rapprochement") + echange_cr: Optional[int] = Field(None, description="Échange compte rendu") + annulation_cr: Optional[int] = Field(None, description="Annulation CR") + profil_soc: Optional[int] = Field(None, description="Profil société") + statut_contrat: Optional[int] = Field(None, description="Statut contrat") + + # ======================================== + # RGPD & CONFIDENTIALITÉ + # ======================================== + gdpr: Optional[int] = Field(None, description="Consentement RGPD (0/1)") + exclure_trait: Optional[int] = Field(None, description="Exclure des traitements (0/1)") + + # ======================================== + # REPRÉSENTANT FISCAL + # ======================================== + represent_int: Optional[int] = Field(None, description="Représentant intracommunautaire") + represent_nif: Optional[str] = Field(None, max_length=25, description="NIF représentant") + + # ======================================== + # CHAMPS PERSONNALISÉS (exemples) + # ======================================== + date_creation_societe: Optional[date] = Field(None, description="Date création de la société") + capital_social: Optional[Decimal] = Field(None, description="Capital social") + actionnaire_principal: Optional[str] = Field(None, max_length=100, description="Actionnaire principal") + score_banque_france: Optional[str] = Field(None, max_length=10, description="Score BDF") + + # FIDÉLITÉ + total_points_fidelite: Optional[int] = Field(None, description="Total points fidélité") + points_fidelite_restants: Optional[int] = Field(None, description="Points restants") + fin_validite_carte: Optional[date] = Field(None, description="Fin validité carte") + + # ======================================== + # AUTRES + # ======================================== + calendrier: Optional[str] = Field(None, max_length=13, description="Code calendrier (CAL_No)") + mode_test: Optional[int] = Field(None, description="Mode test (0/1)") + confiance: Optional[int] = Field(None, description="Niveau de confiance") + + @field_validator('siret') + @classmethod + def validate_siret(cls, v): + if v and len(v.replace(' ', '')) != 14: + raise ValueError('Le SIRET doit contenir 14 chiffres') + return v.replace(' ', '') if v else v + + @field_validator('email') + @classmethod + def validate_email(cls, v): + if v and '@' not in v: + raise ValueError('Format email invalide') + return v + class Config: json_schema_extra = { "example": { - "intitule": "SARL NOUVELLE ENTREPRISE", - "forme_juridique": "SARL", - "adresse": "10 Avenue des Champs", - "code_postal": "75008", - "ville": "Paris", - "telephone": "0123456789", - "portable": "0612345678", - "email": "contact@nouvelle-entreprise.fr", - "siret": "12345678901234", - "tva_intra": "FR12345678901", + "intitule": "ENTREPRISE EXEMPLE SARL", + "num": "CLI00123", + "compte_collectif": "411000", + "qualite": "CLI" } } - class ClientUpdateRequest(BaseModel): """Modèle pour modification d'un client existant""" diff --git a/email_queue.py b/email_queue.py index 53d65af..05bd286 100644 --- a/email_queue.py +++ b/email_queue.py @@ -1,9 +1,3 @@ -# -*- coding: utf-8 -*- -""" -Queue d'envoi d'emails avec threading et génération PDF -Version VPS Linux - utilise sage_client pour récupérer les données -""" - import threading import queue import time @@ -17,7 +11,11 @@ from email.mime.text import MIMEText from email.mime.application import MIMEApplication from config import settings import logging - +from reportlab.lib.pagesizes import A4 +from reportlab.pdfgen import canvas +from reportlab.lib.units import cm +from io import BytesIO + logger = logging.getLogger(__name__) @@ -202,15 +200,6 @@ class EmailQueue: await asyncio.to_thread(self._send_smtp, msg) def _generate_pdf(self, doc_id: str, type_doc: int) -> bytes: - """ - Génération PDF via ReportLab + sage_client - - ⚠️ Cette méthode est appelée depuis un thread worker - """ - from reportlab.lib.pagesizes import A4 - from reportlab.pdfgen import canvas - from reportlab.lib.units import cm - from io import BytesIO if not self.sage_client: logger.error("❌ sage_client non configuré")