diff --git a/api.py b/api.py index 4bed8f6..d08fd3c 100644 --- a/api.py +++ b/api.py @@ -2,10 +2,9 @@ from fastapi import FastAPI, HTTPException, Path, Query, Depends, status, Body, from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse from fastapi.encoders import jsonable_encoder -from pydantic import BaseModel, Field, EmailStr, validator, field_validator -from typing import List, Optional, Dict, ClassVar, Any -from datetime import date, datetime -from enum import Enum, IntEnum +from pydantic import BaseModel, Field, EmailStr +from typing import List, Optional +from datetime import datetime import uvicorn from contextlib import asynccontextmanager import uuid @@ -15,6 +14,7 @@ import logging from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select +from data.data import TAGS_METADATA, templates_signature_email from routes.auth import router as auth_router from config import settings from database import ( @@ -30,7 +30,51 @@ from database import ( from email_queue import email_queue from sage_client import sage_client -from schemas import TiersDetails, TypeTiers +from schemas import ( + TiersDetails, + TypeTiers, + BaremeRemiseResponse, + UserResponse, + ClientCreateRequest, + ClientDetails, + ClientResponse, + ClientUpdateRequest, + FournisseurCreateAPIRequest, + FournisseurDetails, + FournisseurUpdateRequest, + Contact, + AvoirCreateRequest, + AvoirUpdateRequest, + CommandeCreateRequest, + CommandeUpdateRequest, + DevisRequest, + DevisResponse, + DevisUpdateRequest, + TypeDocument, + TypeDocumentSQL, + StatutEmail, + EmailEnvoiRequest, + FactureCreateRequest, + FactureUpdateRequest, + LivraisonCreateRequest, + LivraisonUpdateRequest, + SignatureRequest, + StatutSignature, + TypeTiersInt, + ArticleCreateRequest, + ArticleResponse, + ArticleUpdateRequest, + ArticleListResponse, + EntreeStockRequest, + SortieStockRequest, + MouvementStockResponse, + RelanceDevisRequest, + FamilleResponse, + FamilleCreateRequest, + FamilleListResponse, + ContactCreate, + ContactUpdate +) from utils.normalization import normaliser_type_tiers logging.basicConfig( @@ -38,3214 +82,10 @@ logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[logging.FileHandler("sage_api.log"), logging.StreamHandler()], ) + logger = logging.getLogger(__name__) -TAGS_METADATA = [ - { - "name": "Clients", - "description": "Gestion des clients (recherche, création, modification)", - }, - {"name": "Articles", "description": "Gestion des articles et produits"}, - {"name": "Devis", "description": "Création, consultation et gestion des devis"}, - { - "name": "Commandes", - "description": "Création, consultation et gestion des commandes", - }, - { - "name": "Livraisons", - "description": "Création, consultation et gestion des bons de livraison", - }, - { - "name": "Factures", - "description": "Création, consultation et gestion des factures", - }, - {"name": "Avoirs", "description": "Création, consultation et gestion des avoirs"}, - {"name": "Fournisseurs", "description": "Gestion des fournisseurs"}, - {"name": "Prospects", "description": "Gestion des prospects"}, - { - "name": "Workflows", - "description": "Transformations de documents (devis→commande, commande→facture, etc.)", - }, - {"name": "Signatures", "description": "Signature électronique via Universign"}, - {"name": "Emails", "description": "Envoi d'emails, templates et logs d'envoi"}, - {"name": "Validation", "description": "Validation de données (remises, etc.)"}, - {"name": "Admin", "description": "🔧 Administration système (cache, queue)"}, - {"name": "System", "description": "🏥 Health checks et informations système"}, - {"name": "Debug", "description": "🐛 Routes de debug et diagnostics"}, -] - - - -class TypeDocument(int, Enum): - DEVIS = settings.SAGE_TYPE_DEVIS - BON_COMMANDE = settings.SAGE_TYPE_BON_COMMANDE - PREPARATION = settings.SAGE_TYPE_PREPARATION - BON_LIVRAISON = settings.SAGE_TYPE_BON_LIVRAISON - BON_RETOUR = settings.SAGE_TYPE_BON_RETOUR - BON_AVOIR = settings.SAGE_TYPE_BON_AVOIR - FACTURE = settings.SAGE_TYPE_FACTURE - - -class TypeDocumentSQL(int, Enum): - DEVIS = settings.SAGE_TYPE_DEVIS - BON_COMMANDE = 1 - PREPARATION = 2 - BON_LIVRAISON = 3 - BON_RETOUR = 4 - BON_AVOIR = 5 - FACTURE = 6 - - -class StatutSignature(str, Enum): - EN_ATTENTE = "EN_ATTENTE" - ENVOYE = "ENVOYE" - SIGNE = "SIGNE" - REFUSE = "REFUSE" - EXPIRE = "EXPIRE" - - -class StatutEmail(str, Enum): - EN_ATTENTE = "EN_ATTENTE" - EN_COURS = "EN_COURS" - ENVOYE = "ENVOYE" - OUVERT = "OUVERT" - ERREUR = "ERREUR" - BOUNCE = "BOUNCE" - - - -class ClientResponse(BaseModel): - """Modèle de réponse client simplifié (pour listes)""" - - numero: Optional[str] = None - intitule: Optional[str] = None - adresse: Optional[str] = None - code_postal: Optional[str] = None - ville: Optional[str] = None - email: Optional[str] = None - telephone: Optional[str] = None # Téléphone principal (fixe ou mobile) - - -class Contact(BaseModel): - """ - Contact associé à un tiers (client/fournisseur) - Tous les champs de F_CONTACTT - """ - - numero: Optional[str] = Field(None, description="Code du tiers parent (CT_Num)") - contact_numero: Optional[int] = Field(None, description="Numéro unique du contact (CT_No)") - n_contact: Optional[int] = Field(None, description="Numéro de référence contact (N_Contact)") - - civilite: Optional[str] = Field(None, description="Civilité : M., Mme, Mlle (CT_Civilite)") - nom: Optional[str] = Field(None, description="Nom de famille (CT_Nom)") - prenom: Optional[str] = Field(None, description="Prénom (CT_Prenom)") - fonction: Optional[str] = Field(None, description="Fonction/Titre (CT_Fonction)") - - service_code: Optional[int] = Field(None, description="Code du service (N_Service)") - - telephone: Optional[str] = Field(None, description="Téléphone fixe (CT_Telephone)") - portable: Optional[str] = Field(None, description="Téléphone mobile (CT_TelPortable)") - telecopie: Optional[str] = Field(None, description="Fax (CT_Telecopie)") - email: Optional[str] = Field(None, description="Adresse email (CT_EMail)") - - facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)") - linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)") - skype: Optional[str] = Field(None, description="Identifiant Skype (CT_Skype)") - - est_defaut: Optional[bool] = Field( - False, - description="True si ce contact est le contact par défaut du client" - ) - - civilite_map: ClassVar[dict] = { - 0: "M.", - 1: "Mme", - 2: "Mlle", - 3: "Société", - } - - @validator("civilite", pre=True, always=True) - def convert_civilite(cls, v): - if v is None: - return v - if isinstance(v, int): - return cls.civilite_map.get(v, str(v)) - return v - - class Config: - json_schema_extra = { - "example": { - "numero": "CLI000001", - "contact_numero": 1, - "n_contact": 1, - "civilite": "M.", - "nom": "Dupont", - "prenom": "Jean", - "fonction": "Directeur Commercial", - "service_code": 1, - "telephone": "0123456789", - "portable": "0612345678", - "telecopie": "0123456788", - "email": "j.dupont@exemple.fr", - "facebook": "https://facebook.com/jean.dupont", - "linkedin": "https://linkedin.com/in/jeandupont", - "skype": "jean.dupont.pro" - } - } - - -class ClientDetails(BaseModel): - """ - Modèle de réponse client complet (GET /clients/{code}) - Strictement aligné avec les champs retournés par lister_tous_clients - """ - - numero: Optional[str] = Field(None, description="Code client (CT_Num)") - intitule: Optional[str] = Field(None, description="Raison sociale ou Nom complet (CT_Intitule)") - type_tiers: Optional[int] = Field(None, description="Type : 0=Client, 1=Fournisseur (CT_Type)") - qualite: Optional[str] = Field(None, description="Qualité Sage : CLI, FOU, PRO (CT_Qualite)") - classement: Optional[str] = Field(None, description="Code de classement (CT_Classement)") - raccourci: Optional[str] = Field(None, description="Code raccourci 7 car. (CT_Raccourci)") - siret: Optional[str] = Field(None, description="N° SIRET 14 chiffres (CT_Siret)") - tva_intra: Optional[str] = Field(None, description="N° TVA intracommunautaire (CT_Identifiant)") - code_naf: Optional[str] = Field(None, description="Code NAF/APE (CT_Ape)") - - contact: Optional[str] = Field(None, description="Nom du contact principal (CT_Contact)") - adresse: Optional[str] = Field(None, description="Adresse ligne 1 (CT_Adresse)") - complement: Optional[str] = Field(None, description="Complément d'adresse (CT_Complement)") - code_postal: Optional[str] = Field(None, description="Code postal (CT_CodePostal)") - ville: Optional[str] = Field(None, description="Ville (CT_Ville)") - region: Optional[str] = Field(None, description="Région/État (CT_CodeRegion)") - pays: Optional[str] = Field(None, description="Pays (CT_Pays)") - - telephone: Optional[str] = Field(None, description="Téléphone fixe (CT_Telephone)") - telecopie: Optional[str] = Field(None, description="Fax (CT_Telecopie)") - email: Optional[str] = Field(None, description="Email principal (CT_EMail)") - site_web: Optional[str] = Field(None, description="Site web (CT_Site)") - facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)") - linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)") - - taux01: Optional[float] = Field(None, description="Taux personnalisé 1 (CT_Taux01)") - taux02: Optional[float] = Field(None, description="Taux personnalisé 2 (CT_Taux02)") - taux03: Optional[float] = Field(None, description="Taux personnalisé 3 (CT_Taux03)") - taux04: Optional[float] = Field(None, description="Taux personnalisé 4 (CT_Taux04)") - - statistique01: Optional[str] = Field(None, description="Statistique 1 (CT_Statistique01)") - statistique02: Optional[str] = Field(None, description="Statistique 2 (CT_Statistique02)") - statistique03: Optional[str] = Field(None, description="Statistique 3 (CT_Statistique03)") - statistique04: Optional[str] = Field(None, description="Statistique 4 (CT_Statistique04)") - statistique05: Optional[str] = Field(None, description="Statistique 5 (CT_Statistique05)") - statistique06: Optional[str] = Field(None, description="Statistique 6 (CT_Statistique06)") - statistique07: Optional[str] = Field(None, description="Statistique 7 (CT_Statistique07)") - statistique08: Optional[str] = Field(None, description="Statistique 8 (CT_Statistique08)") - statistique09: Optional[str] = Field(None, description="Statistique 9 (CT_Statistique09)") - statistique10: Optional[str] = Field(None, description="Statistique 10 (CT_Statistique10)") - - encours_autorise: Optional[float] = Field(None, description="Encours maximum autorisé (CT_Encours)") - assurance_credit: Optional[float] = Field(None, description="Montant assurance crédit (CT_Assurance)") - langue: Optional[int] = Field(None, description="Code langue 0=FR, 1=EN (CT_Langue)") - commercial_code: Optional[int] = Field(None, description="Code du commercial (CO_No)") - - lettrage_auto: Optional[bool] = Field(None, description="Lettrage automatique (CT_Lettrage)") - est_actif: Optional[bool] = Field(None, description="True si actif (CT_Sommeil=0)") - type_facture: Optional[int] = Field(None, description="Type facture 0=Facture, 1=BL (CT_Facture)") - est_prospect: Optional[bool] = Field(None, description="True si prospect (CT_Prospect=1)") - bl_en_facture: Optional[int] = Field(None, description="Imprimer BL en facture (CT_BLFact)") - saut_page: Optional[int] = Field(None, description="Saut de page sur documents (CT_Saut)") - validation_echeance: Optional[int] = Field(None, description="Valider les échéances (CT_ValidEch)") - controle_encours: Optional[int] = Field(None, description="Contrôler l'encours (CT_ControlEnc)") - exclure_relance: Optional[bool] = Field(None, description="Exclure des relances (CT_NotRappel)") - exclure_penalites: Optional[bool] = Field(None, description="Exclure des pénalités (CT_NotPenal)") - bon_a_payer: Optional[int] = Field(None, description="Bon à payer obligatoire (CT_BonAPayer)") - - priorite_livraison: Optional[int] = Field(None, description="Priorité livraison (CT_PrioriteLivr)") - livraison_partielle: Optional[int] = Field(None, description="Livraison partielle (CT_LivrPartielle)") - delai_transport: Optional[int] = Field(None, description="Délai transport jours (CT_DelaiTransport)") - delai_appro: Optional[int] = Field(None, description="Délai appro jours (CT_DelaiAppro)") - - commentaire: Optional[str] = Field(None, description="Commentaire libre (CT_Commentaire)") - - section_analytique: Optional[str] = Field(None, description="Section analytique (CA_Num)") - - mode_reglement_code: Optional[int] = Field(None, description="Code mode règlement (MR_No)") - surveillance_active: Optional[bool] = Field(None, description="Surveillance financière (CT_Surveillance)") - coface: Optional[str] = Field(None, description="Code Coface 25 car. (CT_Coface)") - forme_juridique: Optional[str] = Field(None, description="Forme juridique SA, SARL (CT_SvFormeJuri)") - effectif: Optional[str] = Field(None, description="Nombre d'employés (CT_SvEffectif)") - sv_regularite: Optional[str] = Field(None, description="Régularité paiements (CT_SvRegul)") - sv_cotation: Optional[str] = Field(None, description="Cotation crédit (CT_SvCotation)") - sv_objet_maj: Optional[str] = Field(None, description="Objet dernière MAJ (CT_SvObjetMaj)") - sv_chiffre_affaires: Optional[float] = Field(None, description="Chiffre d'affaires (CT_SvCA)") - sv_resultat: Optional[float] = Field(None, description="Résultat financier (CT_SvResultat)") - - compte_general: Optional[str] = Field(None, description="Compte général principal (CG_NumPrinc)") - categorie_tarif: Optional[int] = Field(None, description="Catégorie tarifaire (N_CatTarif)") - categorie_compta: Optional[int] = Field(None, description="Catégorie comptable (N_CatCompta)") - - contacts: Optional[List[Contact]] = Field( - default_factory=list, - description="Liste des contacts du client" - ) - - class Config: - json_schema_extra = { - "example": { - "numero": "CLI000001", - "intitule": "SARL EXEMPLE", - "type_tiers": 0, - "qualite": "CLI", - "classement": "A", - "raccourci": "EXEMPL", - "siret": "12345678901234", - "tva_intra": "FR12345678901", - "code_naf": "6201Z", - "contact": "Jean Dupont", - "adresse": "123 Rue de la Paix", - "complement": "Bâtiment B", - "code_postal": "75001", - "ville": "Paris", - "region": "Île-de-France", - "pays": "France", - "telephone": "0123456789", - "telecopie": "0123456788", - "email": "contact@exemple.fr", - "site_web": "https://www.exemple.fr", - "facebook": "https://facebook.com/exemple", - "linkedin": "https://linkedin.com/company/exemple", - "taux01": 0.0, - "taux02": 0.0, - "taux03": 0.0, - "taux04": 0.0, - "statistique01": "Informatique", - "statistique02": "", - "statistique03": "", - "statistique04": "", - "statistique05": "", - "statistique06": "", - "statistique07": "", - "statistique08": "", - "statistique09": "", - "statistique10": "", - "encours_autorise": 50000.0, - "assurance_credit": 40000.0, - "langue": 0, - "commercial_code": 1, - "lettrage_auto": True, - "est_actif": True, - "type_facture": 1, - "est_prospect": False, - "bl_en_facture": 0, - "saut_page": 0, - "validation_echeance": 0, - "controle_encours": 1, - "exclure_relance": False, - "exclure_penalites": False, - "bon_a_payer": 0, - "priorite_livraison": 1, - "livraison_partielle": 1, - "delai_transport": 2, - "delai_appro": 0, - "commentaire": "Client important", - "section_analytique": "", - "mode_reglement_code": 1, - "surveillance_active": True, - "coface": "COF12345", - "forme_juridique": "SARL", - "effectif": "50-99", - "sv_regularite": "", - "sv_cotation": "", - "sv_objet_maj": "", - "sv_chiffre_affaires": 2500000.0, - "sv_resultat": 150000.0, - "compte_general": "4110000", - "categorie_tarif": 0, - "categorie_compta": 0, - } - } - - -class FournisseurDetails(BaseModel): - """ - Modèle de réponse fournisseur complet (GET /fournisseurs/{code}) - Strictement aligné avec ClientDetails - """ - - numero: Optional[str] = Field(None, description="Code fournisseur (CT_Num)") - intitule: Optional[str] = Field(None, description="Raison sociale ou Nom complet (CT_Intitule)") - type_tiers: Optional[int] = Field(None, description="Type : 0=Client, 1=Fournisseur (CT_Type)") - qualite: Optional[str] = Field(None, description="Qualité Sage : CLI, FOU, PRO (CT_Qualite)") - classement: Optional[str] = Field(None, description="Code de classement (CT_Classement)") - raccourci: Optional[str] = Field(None, description="Code raccourci 7 car. (CT_Raccourci)") - siret: Optional[str] = Field(None, description="N° SIRET 14 chiffres (CT_Siret)") - tva_intra: Optional[str] = Field(None, description="N° TVA intracommunautaire (CT_Identifiant)") - code_naf: Optional[str] = Field(None, description="Code NAF/APE (CT_Ape)") - - contact: Optional[str] = Field(None, description="Nom du contact principal (CT_Contact)") - adresse: Optional[str] = Field(None, description="Adresse ligne 1 (CT_Adresse)") - complement: Optional[str] = Field(None, description="Complément d'adresse (CT_Complement)") - code_postal: Optional[str] = Field(None, description="Code postal (CT_CodePostal)") - ville: Optional[str] = Field(None, description="Ville (CT_Ville)") - region: Optional[str] = Field(None, description="Région/État (CT_CodeRegion)") - pays: Optional[str] = Field(None, description="Pays (CT_Pays)") - - telephone: Optional[str] = Field(None, description="Téléphone fixe (CT_Telephone)") - telecopie: Optional[str] = Field(None, description="Fax (CT_Telecopie)") - email: Optional[str] = Field(None, description="Email principal (CT_EMail)") - site_web: Optional[str] = Field(None, description="Site web (CT_Site)") - facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)") - linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)") - - taux01: Optional[float] = Field(None, description="Taux personnalisé 1 (CT_Taux01)") - taux02: Optional[float] = Field(None, description="Taux personnalisé 2 (CT_Taux02)") - taux03: Optional[float] = Field(None, description="Taux personnalisé 3 (CT_Taux03)") - taux04: Optional[float] = Field(None, description="Taux personnalisé 4 (CT_Taux04)") - - statistique01: Optional[str] = Field(None, description="Statistique 1 (CT_Statistique01)") - statistique02: Optional[str] = Field(None, description="Statistique 2 (CT_Statistique02)") - statistique03: Optional[str] = Field(None, description="Statistique 3 (CT_Statistique03)") - statistique04: Optional[str] = Field(None, description="Statistique 4 (CT_Statistique04)") - statistique05: Optional[str] = Field(None, description="Statistique 5 (CT_Statistique05)") - statistique06: Optional[str] = Field(None, description="Statistique 6 (CT_Statistique06)") - statistique07: Optional[str] = Field(None, description="Statistique 7 (CT_Statistique07)") - statistique08: Optional[str] = Field(None, description="Statistique 8 (CT_Statistique08)") - statistique09: Optional[str] = Field(None, description="Statistique 9 (CT_Statistique09)") - statistique10: Optional[str] = Field(None, description="Statistique 10 (CT_Statistique10)") - - encours_autorise: Optional[float] = Field(None, description="Encours maximum autorisé (CT_Encours)") - assurance_credit: Optional[float] = Field(None, description="Montant assurance crédit (CT_Assurance)") - langue: Optional[int] = Field(None, description="Code langue 0=FR, 1=EN (CT_Langue)") - commercial_code: Optional[int] = Field(None, description="Code du commercial (CO_No)") - - lettrage_auto: Optional[bool] = Field(None, description="Lettrage automatique (CT_Lettrage)") - est_actif: Optional[bool] = Field(None, description="True si actif (CT_Sommeil=0)") - type_facture: Optional[int] = Field(None, description="Type facture 0=Facture, 1=BL (CT_Facture)") - est_prospect: Optional[bool] = Field(None, description="True si prospect (CT_Prospect=1)") - bl_en_facture: Optional[int] = Field(None, description="Imprimer BL en facture (CT_BLFact)") - saut_page: Optional[int] = Field(None, description="Saut de page sur documents (CT_Saut)") - validation_echeance: Optional[int] = Field(None, description="Valider les échéances (CT_ValidEch)") - controle_encours: Optional[int] = Field(None, description="Contrôler l'encours (CT_ControlEnc)") - exclure_relance: Optional[bool] = Field(None, description="Exclure des relances (CT_NotRappel)") - exclure_penalites: Optional[bool] = Field(None, description="Exclure des pénalités (CT_NotPenal)") - bon_a_payer: Optional[int] = Field(None, description="Bon à payer obligatoire (CT_BonAPayer)") - - priorite_livraison: Optional[int] = Field(None, description="Priorité livraison (CT_PrioriteLivr)") - livraison_partielle: Optional[int] = Field(None, description="Livraison partielle (CT_LivrPartielle)") - delai_transport: Optional[int] = Field(None, description="Délai transport jours (CT_DelaiTransport)") - delai_appro: Optional[int] = Field(None, description="Délai appro jours (CT_DelaiAppro)") - - commentaire: Optional[str] = Field(None, description="Commentaire libre (CT_Commentaire)") - - section_analytique: Optional[str] = Field(None, description="Section analytique (CA_Num)") - - mode_reglement_code: Optional[int] = Field(None, description="Code mode règlement (MR_No)") - surveillance_active: Optional[bool] = Field(None, description="Surveillance financière (CT_Surveillance)") - coface: Optional[str] = Field(None, description="Code Coface 25 car. (CT_Coface)") - forme_juridique: Optional[str] = Field(None, description="Forme juridique SA, SARL (CT_SvFormeJuri)") - effectif: Optional[str] = Field(None, description="Nombre d'employés (CT_SvEffectif)") - sv_regularite: Optional[str] = Field(None, description="Régularité paiements (CT_SvRegul)") - sv_cotation: Optional[str] = Field(None, description="Cotation crédit (CT_SvCotation)") - sv_objet_maj: Optional[str] = Field(None, description="Objet dernière MAJ (CT_SvObjetMaj)") - sv_chiffre_affaires: Optional[float] = Field(None, description="Chiffre d'affaires (CT_SvCA)") - sv_resultat: Optional[float] = Field(None, description="Résultat financier (CT_SvResultat)") - - compte_general: Optional[str] = Field(None, description="Compte général principal (CG_NumPrinc)") - categorie_tarif: Optional[int] = Field(None, description="Catégorie tarifaire (N_CatTarif)") - categorie_compta: Optional[int] = Field(None, description="Catégorie comptable (N_CatCompta)") - - contacts: Optional[List[Contact]] = Field( - default_factory=list, - description="Liste des contacts du fournisseur" - ) - - class Config: - json_schema_extra = { - "example": { - "numero": "FOU000001", - "intitule": "SARL FOURNISSEUR EXEMPLE", - "type_tiers": 1, - "qualite": "FOU", - "classement": "A", - "raccourci": "EXEMPL", - "siret": "12345678901234", - "tva_intra": "FR12345678901", - "code_naf": "6201Z", - "contact": "Jean Dupont", - "adresse": "123 Rue de la Paix", - "complement": "Bâtiment B", - "code_postal": "75001", - "ville": "Paris", - "region": "Île-de-France", - "pays": "France", - "telephone": "0123456789", - "telecopie": "0123456788", - "email": "contact@exemple.fr", - "site_web": "https://www.exemple.fr", - "facebook": "https://facebook.com/exemple", - "linkedin": "https://linkedin.com/company/exemple", - "taux01": 0.0, - "taux02": 0.0, - "taux03": 0.0, - "taux04": 0.0, - "statistique01": "Informatique", - "statistique02": "", - "statistique03": "", - "statistique04": "", - "statistique05": "", - "statistique06": "", - "statistique07": "", - "statistique08": "", - "statistique09": "", - "statistique10": "", - "encours_autorise": 50000.0, - "assurance_credit": 40000.0, - "langue": 0, - "commercial_code": 1, - "lettrage_auto": True, - "est_actif": True, - "type_facture": 1, - "est_prospect": False, - "bl_en_facture": 0, - "saut_page": 0, - "validation_echeance": 0, - "controle_encours": 1, - "exclure_relance": False, - "exclure_penalites": False, - "bon_a_payer": 0, - "priorite_livraison": 1, - "livraison_partielle": 1, - "delai_transport": 2, - "delai_appro": 0, - "commentaire": "Client important", - "section_analytique": "", - "mode_reglement_code": 1, - "surveillance_active": True, - "coface": "COF12345", - "forme_juridique": "SARL", - "effectif": "50-99", - "sv_regularite": "", - "sv_cotation": "", - "sv_objet_maj": "", - "sv_chiffre_affaires": 2500000.0, - "sv_resultat": 150000.0, - "compte_general": "4110000", - "categorie_tarif": 0, - "categorie_compta": 0, - } - } - - - - -class EmplacementStockModel(BaseModel): - """Détail du stock dans un emplacement spécifique""" - - depot: str = Field(..., description="Numéro du dépôt (DE_No)") - emplacement: str = Field(..., description="Code emplacement (DP_No)") - - qte_stockee: float = Field(0.0, description="Quantité stockée (AE_QteSto)") - qte_preparee: float = Field(0.0, description="Quantité préparée (AE_QtePrepa)") - qte_a_controler: float = Field(0.0, description="Quantité à contrôler (AE_QteAControler)") - - date_creation: Optional[datetime] = Field(None, description="Date création") - date_modification: Optional[datetime] = Field(None, description="Date modification") - - depot_num: Optional[str] = Field(None, description="Numéro dépôt") - depot_nom: Optional[str] = Field(None, description="Nom du dépôt (DE_Intitule)") - depot_code: Optional[str] = Field(None, description="Code dépôt (DE_Code)") - depot_adresse: Optional[str] = Field(None, description="Adresse (DE_Adresse)") - depot_complement: Optional[str] = Field(None, description="Complément adresse") - depot_code_postal: Optional[str] = Field(None, description="Code postal") - depot_ville: Optional[str] = Field(None, description="Ville") - depot_contact: Optional[str] = Field(None, description="Contact") - depot_est_principal: Optional[bool] = Field(None, description="Dépôt principal (DE_Principal)") - depot_categorie_compta: Optional[int] = Field(None, description="Catégorie comptable") - depot_region: Optional[str] = Field(None, description="Région") - depot_pays: Optional[str] = Field(None, description="Pays") - depot_email: Optional[str] = Field(None, description="Email") - depot_telephone: Optional[str] = Field(None, description="Téléphone") - depot_fax: Optional[str] = Field(None, description="Fax") - depot_emplacement_defaut: Optional[str] = Field(None, description="Emplacement par défaut") - depot_exclu: Optional[bool] = Field(None, description="Dépôt exclu") - - emplacement_code: Optional[str] = Field(None, description="Code emplacement (DP_Code)") - emplacement_libelle: Optional[str] = Field(None, description="Libellé emplacement (DP_Intitule)") - emplacement_zone: Optional[str] = Field(None, description="Zone (DP_Zone)") - emplacement_type: Optional[int] = Field(None, description="Type emplacement (DP_Type)") - - class Config: - json_schema_extra = { - "example": { - "depot": "01", - "emplacement": "A1-01", - "qte_stockee": 100.0, - "qte_preparee": 5.0, - "depot_nom": "Dépôt principal", - "depot_ville": "Paris", - "emplacement_libelle": "Allée A, Niveau 1, Case 01", - "emplacement_zone": "Zone A" - } - } - - -class GammeArticleModel(BaseModel): - """Gamme d'un article (taille, couleur, etc.)""" - - numero_gamme: int = Field(..., description="Numéro de gamme (AG_No)") - enumere: str = Field(..., description="Code énuméré (EG_Enumere)") - type_gamme: int = Field(0, description="Type de gamme (AG_Type)") - - date_creation: Optional[datetime] = Field(None, description="Date création") - date_modification: Optional[datetime] = Field(None, description="Date modification") - - ligne: Optional[int] = Field(None, description="Ligne énuméré (EG_Ligne)") - borne_sup: Optional[float] = Field(None, description="Borne supérieure (EG_BorneSup)") - gamme_nom: Optional[str] = Field(None, description="Nom de la gamme (P_GAMME.G_Intitule)") - - class Config: - json_schema_extra = { - "example": { - "numero_gamme": 1, - "enumere": "001", - "type_gamme": 0, - "ligne": 1, - "gamme_nom": "Taille" - } - } - - -class TarifClientModel(BaseModel): - """Tarif spécifique pour un client ou catégorie tarifaire""" - - categorie: int = Field(..., description="Catégorie tarifaire (AC_Categorie)") - client_num: Optional[str] = Field(None, description="Numéro client (CT_Num)") - - prix_vente: float = Field(0.0, description="Prix de vente HT (AC_PrixVen)") - coefficient: float = Field(0.0, description="Coefficient (AC_Coef)") - prix_ttc: float = Field(0.0, description="Prix TTC (AC_PrixTTC)") - arrondi: float = Field(0.0, description="Arrondi (AC_Arrondi)") - qte_montant: float = Field(0.0, description="Quantité montant (AC_QteMont)") - - enumere_gamme: int = Field(0, description="Énuméré gamme (EG_Champ)") - prix_devise: float = Field(0.0, description="Prix en devise (AC_PrixDev)") - devise: int = Field(0, description="Code devise (AC_Devise)") - - remise: float = Field(0.0, description="Remise (AC_Remise)") - mode_calcul: int = Field(0, description="Mode de calcul (AC_Calcul)") - type_remise: int = Field(0, description="Type de remise (AC_TypeRem)") - ref_client: Optional[str] = Field(None, description="Référence client (AC_RefClient)") - - coef_nouveau: float = Field(0.0, description="Nouveau coefficient (AC_CoefNouv)") - prix_vente_nouveau: float = Field(0.0, description="Nouveau prix vente (AC_PrixVenNouv)") - prix_devise_nouveau: float = Field(0.0, description="Nouveau prix devise (AC_PrixDevNouv)") - remise_nouvelle: float = Field(0.0, description="Nouvelle remise (AC_RemiseNouv)") - date_application: Optional[datetime] = Field(None, description="Date application (AC_DateApplication)") - - date_creation: Optional[datetime] = Field(None, description="Date création") - date_modification: Optional[datetime] = Field(None, description="Date modification") - - class Config: - json_schema_extra = { - "example": { - "categorie": 1, - "client_num": "CLI001", - "prix_vente": 110.00, - "coefficient": 1.294, - "remise": 12.0 - } - } - - -class ComposantModel(BaseModel): - """Composant/Opération de nomenclature""" - - operation: str = Field(..., description="Code opération (AT_Operation)") - code_ressource: Optional[str] = Field(None, description="Code ressource (RP_Code)") - - temps: float = Field(0.0, description="Temps nécessaire (AT_Temps)") - type: int = Field(0, description="Type composant (AT_Type)") - description: Optional[str] = Field(None, description="Description (AT_Description)") - ordre: int = Field(0, description="Ordre d'exécution (AT_Ordre)") - - gamme_1_comp: int = Field(0, description="Gamme 1 composant (AG_No1Comp)") - gamme_2_comp: int = Field(0, description="Gamme 2 composant (AG_No2Comp)") - - type_ressource: int = Field(0, description="Type ressource (AT_TypeRessource)") - chevauche: int = Field(0, description="Chevauchement (AT_Chevauche)") - demarre: int = Field(0, description="Démarrage (AT_Demarre)") - operation_chevauche: Optional[str] = Field(None, description="Opération chevauchée (AT_OperationChevauche)") - valeur_chevauche: float = Field(0.0, description="Valeur chevauchement (AT_ValeurChevauche)") - type_chevauche: int = Field(0, description="Type chevauchement (AT_TypeChevauche)") - - date_creation: Optional[datetime] = Field(None, description="Date création") - date_modification: Optional[datetime] = Field(None, description="Date modification") - - class Config: - json_schema_extra = { - "example": { - "operation": "OP010", - "code_ressource": "RES01", - "temps": 15.5, - "description": "Montage pièce A", - "ordre": 10 - } - } - - -class ComptaArticleModel(BaseModel): - """Comptabilité spécifique d'un article""" - - champ: int = Field(..., description="Champ (ACP_Champ)") - compte_general: Optional[str] = Field(None, description="Compte général (ACP_ComptaCPT_CompteG)") - compte_auxiliaire: Optional[str] = Field(None, description="Compte auxiliaire (ACP_ComptaCPT_CompteA)") - - taxe_1: Optional[str] = Field(None, description="Taxe 1 (ACP_ComptaCPT_Taxe1)") - taxe_2: Optional[str] = Field(None, description="Taxe 2 (ACP_ComptaCPT_Taxe2)") - taxe_3: Optional[str] = Field(None, description="Taxe 3 (ACP_ComptaCPT_Taxe3)") - - taxe_date_1: Optional[datetime] = Field(None, description="Date taxe 1") - taxe_date_2: Optional[datetime] = Field(None, description="Date taxe 2") - taxe_date_3: Optional[datetime] = Field(None, description="Date taxe 3") - - taxe_anc_1: Optional[str] = Field(None, description="Ancienne taxe 1") - taxe_anc_2: Optional[str] = Field(None, description="Ancienne taxe 2") - taxe_anc_3: Optional[str] = Field(None, description="Ancienne taxe 3") - - type_facture: int = Field(0, description="Type de facture (ACP_TypeFacture)") - - date_creation: Optional[datetime] = Field(None, description="Date création") - date_modification: Optional[datetime] = Field(None, description="Date modification") - - class Config: - json_schema_extra = { - "example": { - "champ": 1, - "compte_general": "707100", - "taxe_1": "TVA20", - "type_facture": 0 - } - } - - -class FournisseurArticleModel(BaseModel): - """Fournisseur d'un article""" - - fournisseur_num: str = Field(..., description="Numéro fournisseur (CT_Num)") - ref_fournisseur: Optional[str] = Field(None, description="Référence fournisseur (AF_RefFourniss)") - - prix_achat: float = Field(0.0, description="Prix d'achat (AF_PrixAch)") - unite: Optional[str] = Field(None, description="Unité (AF_Unite)") - conversion: float = Field(0.0, description="Conversion (AF_Conversion)") - - delai_appro: int = Field(0, description="Délai approvisionnement (AF_DelaiAppro)") - garantie: int = Field(0, description="Garantie (AF_Garantie)") - colisage: int = Field(0, description="Colisage (AF_Colisage)") - qte_mini: float = Field(0.0, description="Quantité minimum (AF_QteMini)") - qte_montant: float = Field(0.0, description="Quantité montant (AF_QteMont)") - - enumere_gamme: int = Field(0, description="Énuméré gamme (EG_Champ)") - est_principal: bool = Field(False, description="Fournisseur principal (AF_Principal)") - - prix_devise: float = Field(0.0, description="Prix devise (AF_PrixDev)") - devise: int = Field(0, description="Code devise (AF_Devise)") - remise: float = Field(0.0, description="Remise (AF_Remise)") - conversion_devise: float = Field(0.0, description="Conversion devise (AF_ConvDiv)") - type_remise: int = Field(0, description="Type remise (AF_TypeRem)") - - code_barre_fournisseur: Optional[str] = Field(None, description="Code-barres fournisseur (AF_CodeBarre)") - - prix_achat_nouveau: float = Field(0.0, description="Nouveau prix achat (AF_PrixAchNouv)") - prix_devise_nouveau: float = Field(0.0, description="Nouveau prix devise (AF_PrixDevNouv)") - remise_nouvelle: float = Field(0.0, description="Nouvelle remise (AF_RemiseNouv)") - date_application: Optional[datetime] = Field(None, description="Date application (AF_DateApplication)") - - date_creation: Optional[datetime] = Field(None, description="Date création") - date_modification: Optional[datetime] = Field(None, description="Date modification") - - class Config: - json_schema_extra = { - "example": { - "fournisseur_num": "F001", - "ref_fournisseur": "REF-FOURN-001", - "prix_achat": 85.00, - "delai_appro": 15, - "est_principal": True - } - } - - -class ReferenceEnumereeModel(BaseModel): - """Référence énumérée (article avec gammes)""" - - gamme_1: int = Field(0, description="Gamme 1 (AG_No1)") - gamme_2: int = Field(0, description="Gamme 2 (AG_No2)") - reference_enumeree: str = Field(..., description="Référence énumérée (AE_Ref)") - - prix_achat: float = Field(0.0, description="Prix achat (AE_PrixAch)") - code_barre: Optional[str] = Field(None, description="Code-barres (AE_CodeBarre)") - prix_achat_nouveau: float = Field(0.0, description="Nouveau prix achat (AE_PrixAchNouv)") - edi_code: Optional[str] = Field(None, description="Code EDI (AE_EdiCode)") - en_sommeil: bool = Field(False, description="En sommeil (AE_Sommeil)") - - date_creation: Optional[datetime] = Field(None, description="Date création") - date_modification: Optional[datetime] = Field(None, description="Date modification") - - class Config: - json_schema_extra = { - "example": { - "gamme_1": 1, - "gamme_2": 3, - "reference_enumeree": "ART001-T1-C3", - "prix_achat": 85.00 - } - } - - -class MediaArticleModel(BaseModel): - """Média attaché à un article (photo, document, etc.)""" - - commentaire: Optional[str] = Field(None, description="Commentaire (ME_Commentaire)") - fichier: Optional[str] = Field(None, description="Nom fichier (ME_Fichier)") - type_mime: Optional[str] = Field(None, description="Type MIME (ME_TypeMIME)") - origine: int = Field(0, description="Origine (ME_Origine)") - ged_id: Optional[str] = Field(None, description="ID GED (ME_GedId)") - - date_creation: Optional[datetime] = Field(None, description="Date création") - date_modification: Optional[datetime] = Field(None, description="Date modification") - - class Config: - json_schema_extra = { - "example": { - "commentaire": "Photo produit principale", - "fichier": "ART001_photo1.jpg", - "type_mime": "image/jpeg" - } - } - - -class PrixGammeModel(BaseModel): - """Prix spécifique par combinaison de gammes""" - - gamme_1: int = Field(0, description="Gamme 1 (AG_No1)") - gamme_2: int = Field(0, description="Gamme 2 (AG_No2)") - prix_net: float = Field(0.0, description="Prix net (AR_PUNet)") - cout_standard: float = Field(0.0, description="Coût standard (AR_CoutStd)") - - date_creation: Optional[datetime] = Field(None, description="Date création") - date_modification: Optional[datetime] = Field(None, description="Date modification") - - class Config: - json_schema_extra = { - "example": { - "gamme_1": 1, - "gamme_2": 3, - "prix_net": 125.50, - "cout_standard": 82.30 - } - } - - - -class ArticleResponse(BaseModel): - """Article complet avec tous les enrichissements disponibles""" - - reference: str = Field(..., description="Référence article (AR_Ref)") - designation: str = Field(..., description="Désignation principale (AR_Design)") - - code_ean: Optional[str] = Field(None, description="Code EAN / Code-barres principal (AR_CodeBarre)") - code_barre: Optional[str] = Field(None, description="Code-barres (alias de code_ean)") - edi_code: Optional[str] = Field(None, description="Code EDI (AR_EdiCode)") - raccourci: Optional[str] = Field(None, description="Code raccourci (AR_Raccourci)") - - prix_vente: float = Field(..., description="Prix de vente HT unitaire (AR_PrixVen)") - prix_achat: Optional[float] = Field(None, description="Prix d'achat HT (AR_PrixAch)") - coef: Optional[float] = Field(None, description="Coefficient multiplicateur (AR_Coef)") - prix_net: Optional[float] = Field(None, description="Prix unitaire net (AR_PUNet)") - - prix_achat_nouveau: Optional[float] = Field(None, description="Nouveau prix d'achat à venir (AR_PrixAchNouv)") - coef_nouveau: Optional[float] = Field(None, description="Nouveau coefficient à venir (AR_CoefNouv)") - prix_vente_nouveau: Optional[float] = Field(None, description="Nouveau prix de vente à venir (AR_PrixVenNouv)") - date_application_prix: Optional[str] = Field(None, description="Date d'application des nouveaux prix (AR_DateApplication)") - - cout_standard: Optional[float] = Field(None, description="Coût standard (AR_CoutStd)") - - stock_reel: float = Field(default=0.0, description="Stock réel total (F_ARTSTOCK.AS_QteSto)") - stock_mini: Optional[float] = Field(None, description="Stock minimum (F_ARTSTOCK.AS_QteMini)") - stock_maxi: Optional[float] = Field(None, description="Stock maximum (F_ARTSTOCK.AS_QteMaxi)") - stock_reserve: Optional[float] = Field(None, description="Stock réservé / en commande client (F_ARTSTOCK.AS_QteRes)") - stock_commande: Optional[float] = Field(None, description="Stock en commande fournisseur (F_ARTSTOCK.AS_QteCom)") - stock_disponible: Optional[float] = Field(None, description="Stock disponible = réel - réservé") - - emplacements: List[EmplacementStockModel] = Field( - default_factory=list, - description="Détail du stock par emplacement (F_ARTSTOCKEMPL + F_DEPOT + F_DEPOTEMPL)" - ) - nb_emplacements: int = Field(0, description="Nombre d'emplacements") - - suivi_stock: Optional[bool] = Field(None, description="Suivi de stock activé (AR_SuiviStock)") - nomenclature: Optional[bool] = Field(None, description="Article avec nomenclature (AR_Nomencl)") - qte_composant: Optional[float] = Field(None, description="Quantité de composant (AR_QteComp)") - qte_operatoire: Optional[float] = Field(None, description="Quantité opératoire (AR_QteOperatoire)") - - unite_vente: Optional[str] = Field(None, max_length=10, description="Unité de vente (AR_UniteVen)") - unite_poids: Optional[str] = Field(None, max_length=10, description="Unité de poids (AR_UnitePoids)") - poids_net: Optional[float] = Field(None, description="Poids net unitaire en kg (AR_PoidsNet)") - poids_brut: Optional[float] = Field(None, description="Poids brut unitaire en kg (AR_PoidsBrut)") - - gamme_1: Optional[str] = Field(None, description="Énumération gamme 1 (AR_Gamme1)") - gamme_2: Optional[str] = Field(None, description="Énumération gamme 2 (AR_Gamme2)") - - gammes: List[GammeArticleModel] = Field( - default_factory=list, - description="Détail des gammes (F_ARTGAMME + F_ENUMGAMME + P_GAMME)" - ) - nb_gammes: int = Field(0, description="Nombre de gammes") - - tarifs_clients: List[TarifClientModel] = Field( - default_factory=list, - description="Tarifs spécifiques par client/catégorie (F_ARTCLIENT)" - ) - nb_tarifs_clients: int = Field(0, description="Nombre de tarifs clients") - - composants: List[ComposantModel] = Field( - default_factory=list, - description="Composants/Opérations de production (F_ARTCOMPO)" - ) - nb_composants: int = Field(0, description="Nombre de composants") - - compta_vente: List[ComptaArticleModel] = Field( - default_factory=list, - description="Comptabilité vente (F_ARTCOMPTA type 0)" - ) - compta_achat: List[ComptaArticleModel] = Field( - default_factory=list, - description="Comptabilité achat (F_ARTCOMPTA type 1)" - ) - compta_stock: List[ComptaArticleModel] = Field( - default_factory=list, - description="Comptabilité stock (F_ARTCOMPTA type 2)" - ) - - fournisseurs: List[FournisseurArticleModel] = Field( - default_factory=list, - description="Tous les fournisseurs de l'article (F_ARTFOURNISS)" - ) - nb_fournisseurs: int = Field(0, description="Nombre de fournisseurs") - - refs_enumerees: List[ReferenceEnumereeModel] = Field( - default_factory=list, - description="Références énumérées (F_ARTENUMREF)" - ) - nb_refs_enumerees: int = Field(0, description="Nombre de références énumérées") - - medias: List[MediaArticleModel] = Field( - default_factory=list, - description="Médias attachés (F_ARTICLEMEDIA)" - ) - nb_medias: int = Field(0, description="Nombre de médias") - - prix_gammes: List[PrixGammeModel] = Field( - default_factory=list, - description="Prix par combinaison de gammes (F_ARTPRIX)" - ) - nb_prix_gammes: int = Field(0, description="Nombre de prix par gammes") - - type_article: Optional[int] = Field( - None, ge=0, le=3, - description="Type : 0=Article, 1=Prestation, 2=Divers, 3=Nomenclature (AR_Type)" - ) - type_article_libelle: Optional[str] = Field(None, description="Libellé du type d'article") - - famille_code: Optional[str] = Field(None, max_length=20, description="Code famille (FA_CodeFamille)") - famille_libelle: Optional[str] = Field(None, description="Libellé de la famille (F_FAMILLE.FA_Intitule)") - famille_type: Optional[int] = Field(None, description="Type de famille : 0=Détail, 1=Total (F_FAMILLE.FA_Type)") - famille_unite_vente: Optional[str] = Field(None, description="Unité de vente de la famille (F_FAMILLE.FA_UniteVen)") - famille_coef: Optional[float] = Field(None, description="Coefficient de la famille (F_FAMILLE.FA_Coef)") - famille_suivi_stock: Optional[bool] = Field(None, description="Suivi stock de la famille (F_FAMILLE.FA_SuiviStock)") - famille_garantie: Optional[int] = Field(None, description="Garantie de la famille (F_FAMILLE.FA_Garantie)") - famille_unite_poids: Optional[str] = Field(None, description="Unité de poids de la famille (F_FAMILLE.FA_UnitePoids)") - famille_delai: Optional[int] = Field(None, description="Délai de la famille (F_FAMILLE.FA_Delai)") - famille_nb_colis: Optional[int] = Field(None, description="Nombre de colis de la famille (F_FAMILLE.FA_NbColis)") - famille_code_fiscal: Optional[str] = Field(None, description="Code fiscal de la famille (F_FAMILLE.FA_CodeFiscal)") - famille_escompte: Optional[bool] = Field(None, description="Escompte de la famille (F_FAMILLE.FA_Escompte)") - famille_centrale: Optional[bool] = Field(None, description="Famille centrale (F_FAMILLE.FA_Central)") - famille_nature: Optional[int] = Field(None, description="Nature de la famille (F_FAMILLE.FA_Nature)") - famille_hors_stat: Optional[bool] = Field(None, description="Hors statistique famille (F_FAMILLE.FA_HorsStat)") - famille_pays: Optional[str] = Field(None, description="Pays de la famille (F_FAMILLE.FA_Pays)") - - nature: Optional[int] = Field(None, description="Nature de l'article (AR_Nature)") - garantie: Optional[int] = Field(None, description="Durée de garantie en mois (AR_Garantie)") - code_fiscal: Optional[str] = Field(None, max_length=10, description="Code fiscal/TVA (AR_CodeFiscal)") - pays: Optional[str] = Field(None, description="Pays d'origine (AR_Pays)") - - fournisseur_principal: Optional[int] = Field(None, description="N° compte du fournisseur principal (CO_No → F_COMPTET.CT_Num)") - fournisseur_nom: Optional[str] = Field(None, description="Nom du fournisseur principal (F_COMPTET.CT_Intitule)") - - conditionnement: Optional[str] = Field(None, description="Conditionnement d'achat (AR_Condition)") - conditionnement_qte: Optional[float] = Field(None, description="Quantité conditionnement (F_ENUMCOND.EC_Quantite)") - conditionnement_edi: Optional[str] = Field(None, description="Code EDI conditionnement (F_ENUMCOND.EC_EdiCode)") - - nb_colis: Optional[int] = Field(None, description="Nombre de colis par unité (AR_NbColis)") - prevision: Optional[bool] = Field(None, description="Gestion en prévision (AR_Prevision)") - - est_actif: bool = Field(default=True, description="Article actif (AR_Sommeil = 0)") - en_sommeil: bool = Field(default=False, description="Article en sommeil (AR_Sommeil = 1)") - article_substitut: Optional[str] = Field(None, description="Référence article de substitution (AR_Substitut)") - soumis_escompte: Optional[bool] = Field(None, description="Soumis à escompte (AR_Escompte)") - delai: Optional[int] = Field(None, description="Délai de livraison en jours (AR_Delai)") - - publie: Optional[bool] = Field(None, description="Publié sur web/catalogue (AR_Publie)") - hors_statistique: Optional[bool] = Field(None, description="Exclus des statistiques (AR_HorsStat)") - vente_debit: Optional[bool] = Field(None, description="Vente au débit (AR_VteDebit)") - non_imprimable: Optional[bool] = Field(None, description="Non imprimable sur documents (AR_NotImp)") - transfere: Optional[bool] = Field(None, description="Article transféré (AR_Transfere)") - contremarque: Optional[bool] = Field(None, description="Article en contremarque (AR_Contremarque)") - fact_poids: Optional[bool] = Field(None, description="Facturation au poids (AR_FactPoids)") - fact_forfait: Optional[bool] = Field(None, description="Facturation au forfait (AR_FactForfait)") - saisie_variable: Optional[bool] = Field(None, description="Saisie variable (AR_SaisieVar)") - fictif: Optional[bool] = Field(None, description="Article fictif (AR_Fictif)") - sous_traitance: Optional[bool] = Field(None, description="Article en sous-traitance (AR_SousTraitance)") - criticite: Optional[int] = Field(None, description="Niveau de criticité (AR_Criticite)") - - reprise_code_defaut: Optional[str] = Field(None, description="Code reprise par défaut (RP_CodeDefaut)") - delai_fabrication: Optional[int] = Field(None, description="Délai de fabrication (AR_DelaiFabrication)") - delai_peremption: Optional[int] = Field(None, description="Délai de péremption (AR_DelaiPeremption)") - delai_securite: Optional[int] = Field(None, description="Délai de sécurité (AR_DelaiSecurite)") - type_lancement: Optional[int] = Field(None, description="Type de lancement production (AR_TypeLancement)") - cycle: Optional[int] = Field(None, description="Cycle de production (AR_Cycle)") - - photo: Optional[str] = Field(None, description="Chemin/nom du fichier photo (AR_Photo)") - langue_1: Optional[str] = Field(None, description="Texte en langue 1 (AR_Langue1)") - langue_2: Optional[str] = Field(None, description="Texte en langue 2 (AR_Langue2)") - - frais_01_denomination: Optional[str] = Field(None, description="Dénomination frais 1 (AR_Frais01FR_Denomination)") - frais_02_denomination: Optional[str] = Field(None, description="Dénomination frais 2 (AR_Frais02FR_Denomination)") - frais_03_denomination: Optional[str] = Field(None, description="Dénomination frais 3 (AR_Frais03FR_Denomination)") - - tva_code: Optional[str] = Field(None, description="Code TVA (F_TAXE.TA_Code)") - tva_taux: Optional[float] = Field(None, description="Taux de TVA en % (F_TAXE.TA_Taux)") - - stat_01: Optional[str] = Field(None, description="Statistique 1 (AR_Stat01)") - stat_02: Optional[str] = Field(None, description="Statistique 2 (AR_Stat02)") - stat_03: Optional[str] = Field(None, description="Statistique 3 (AR_Stat03)") - stat_04: Optional[str] = Field(None, description="Statistique 4 (AR_Stat04)") - stat_05: Optional[str] = Field(None, description="Statistique 5 (AR_Stat05)") - - categorie_1: Optional[int] = Field(None, description="Catégorie comptable 1 (CL_No1)") - categorie_2: Optional[int] = Field(None, description="Catégorie comptable 2 (CL_No2)") - categorie_3: Optional[int] = Field(None, description="Catégorie comptable 3 (CL_No3)") - categorie_4: Optional[int] = Field(None, description="Catégorie comptable 4 (CL_No4)") - - date_modification: Optional[str] = Field(None, description="Date de dernière modification (AR_DateModif)") - - marque_commerciale: Optional[str] = Field(None, description="Marque commerciale (champ personnalisé)") - objectif_qtes_vendues: Optional[str] = Field(None, description="Objectif / Quantités vendues (champ personnalisé)") - pourcentage_or: Optional[str] = Field(None, description="Pourcentage teneur en or (champ personnalisé)") - premiere_commercialisation: Optional[str] = Field(None, description="Date de 1ère commercialisation (champ personnalisé)") - interdire_commande: Optional[bool] = Field(None, description="Interdire la commande (champ personnalisé)") - exclure: Optional[bool] = Field(None, description="Exclure de certains traitements (champ personnalisé)") - - class Config: - json_schema_extra = { - "example": { - "reference": "BAGUE-001", - "designation": "Bague Or 18K Diamant", - "prix_vente": 1299.00, - "stock_reel": 15.0, - "stock_disponible": 13.0, - "nb_emplacements": 2, - "nb_gammes": 2, - "nb_tarifs_clients": 3, - "nb_fournisseurs": 2, - "nb_medias": 2, - "emplacements": [ - { - "depot": "01", - "emplacement": "A1-01", - "qte_stockee": 10.0, - "depot_nom": "Dépôt principal" - } - ], - "gammes": [ - { - "numero_gamme": 1, - "enumere": "001", - "gamme_nom": "Taille" - } - ] - } - } - - -class ArticleListResponse(BaseModel): - """Réponse pour une liste d'articles""" - - total: int = Field(..., description="Nombre total d'articles") - articles: List[ArticleResponse] = Field(..., description="Liste des articles") - filtre_applique: Optional[str] = Field(None, description="Filtre de recherche appliqué") - avec_stock: bool = Field(True, description="Indique si les stocks ont été chargés") - avec_famille: bool = Field(True, description="Indique si les familles ont été enrichies") - avec_enrichissements_complets: bool = Field( - False, - description="Indique si tous les enrichissements sont activés" - ) - - class Config: - json_schema_extra = { - "example": { - "total": 1250, - "filtre_applique": "bague", - "avec_stock": True, - "avec_famille": True, - "avec_enrichissements_complets": True, - "articles": [] - } - } - - -class LigneDevis(BaseModel): - article_code: str - quantite: float - remise_pourcentage: Optional[float] = 0.0 - - @field_validator("article_code", mode="before") - def strip_insecables(cls, v): - return v.replace("\xa0", "").strip() - - -class DevisRequest(BaseModel): - client_id: str - date_devis: Optional[date] = None - date_livraison: Optional[date] = None - reference: Optional[str] = None - lignes: List[LigneDevis] - - -class DevisResponse(BaseModel): - id: str - client_id: str - date_devis: str - montant_total_ht: float - montant_total_ttc: float - nb_lignes: int - - -class SignatureRequest(BaseModel): - doc_id: str - type_doc: TypeDocument - email_signataire: EmailStr - nom_signataire: str - - -class EmailEnvoiRequest(BaseModel): - destinataire: EmailStr - cc: Optional[List[EmailStr]] = [] - cci: Optional[List[EmailStr]] = [] - sujet: str - corps_html: str - document_ids: Optional[List[str]] = None - type_document: Optional[TypeDocument] = None - - -class RelanceDevisRequest(BaseModel): - doc_id: str - message_personnalise: Optional[str] = None - - -class BaremeRemiseResponse(BaseModel): - client_id: str - remise_max_autorisee: float - remise_demandee: float - autorisee: bool - message: str - - -class TypeTiers(IntEnum): - """CT_Type - Type de tiers""" - CLIENT = 0 - FOURNISSEUR = 1 - SALARIE = 2 - AUTRE = 3 - - -class ClientCreateRequest(BaseModel): - - intitule: str = Field( - ..., - max_length=69, - description="Nom du client (CT_Intitule) - OBLIGATOIRE" - ) - - numero: str = Field( - ..., - max_length=17, - description="Numéro client CT_Num (auto si None)" - ) - - type_tiers: int = Field( - 0, - ge=0, - le=3, - description="CT_Type: 0=Client, 1=Fournisseur, 2=Salarié, 3=Autre" - ) - - qualite: Optional[str] = Field( - "CLI", - max_length=17, - description="CT_Qualite: CLI/FOU/SAL/DIV/AUT" - ) - - classement: Optional[str] = Field( - None, - max_length=17, - description="CT_Classement" - ) - - raccourci: Optional[str] = Field( - None, - max_length=7, - description="CT_Raccourci (7 chars max, unique)" - ) - - siret: Optional[str] = Field( - None, - max_length=15, - description="CT_Siret (14-15 chars)" - ) - - tva_intra: Optional[str] = Field( - None, - max_length=25, - description="CT_Identifiant (TVA intracommunautaire)" - ) - - code_naf: Optional[str] = Field( - None, - max_length=7, - description="CT_Ape (Code NAF/APE)" - ) - - contact: Optional[str] = Field( - None, - max_length=35, - description="CT_Contact (double affectation: client + adresse)" - ) - - adresse: Optional[str] = Field( - None, - max_length=35, - description="Adresse.Adresse" - ) - - complement: Optional[str] = Field( - None, - max_length=35, - description="Adresse.Complement" - ) - - code_postal: Optional[str] = Field( - None, - max_length=9, - description="Adresse.CodePostal" - ) - - ville: Optional[str] = Field( - None, - max_length=35, - description="Adresse.Ville" - ) - - region: Optional[str] = Field( - None, - max_length=25, - description="Adresse.CodeRegion" - ) - - pays: Optional[str] = Field( - None, - max_length=35, - description="Adresse.Pays" - ) - - telephone: Optional[str] = Field( - None, - max_length=21, - description="Telecom.Telephone" - ) - - telecopie: Optional[str] = Field( - None, - max_length=21, - description="Telecom.Telecopie (fax)" - ) - - email: Optional[str] = Field( - None, - max_length=69, - description="Telecom.EMail" - ) - - site_web: Optional[str] = Field( - None, - max_length=69, - description="Telecom.Site" - ) - - portable: Optional[str] = Field( - None, - max_length=21, - description="Telecom.Portable" - ) - - facebook: Optional[str] = Field( - None, - max_length=69, - description="Telecom.Facebook ou CT_Facebook" - ) - - linkedin: Optional[str] = Field( - None, - max_length=69, - description="Telecom.LinkedIn ou CT_LinkedIn" - ) - - compte_general: Optional[str] = Field( - None, - max_length=13, - description="CompteGPrinc (défaut selon type_tiers: 4110000, 4010000, 421, 471)" - ) - - categorie_tarifaire: Optional[str] = Field( - None, - description="N_CatTarif (ID catégorie tarifaire, défaut '0' ou '1')" - ) - - categorie_comptable: Optional[str] = Field( - None, - description="N_CatCompta (ID catégorie comptable, défaut '0' ou '1')" - ) - - taux01: Optional[float] = Field(None, description="CT_Taux01") - taux02: Optional[float] = Field(None, description="CT_Taux02") - taux03: Optional[float] = Field(None, description="CT_Taux03") - taux04: Optional[float] = Field(None, description="CT_Taux04") - - secteur: Optional[str] = Field( - None, - max_length=21, - description="Alias de statistique01 (CT_Statistique01)" - ) - - 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") - - encours_autorise: Optional[float] = Field( - None, - description="CT_Encours (montant max autorisé)" - ) - - assurance_credit: Optional[float] = Field( - None, - description="CT_Assurance (montant assurance crédit)" - ) - - langue: Optional[int] = Field( - None, - ge=0, - description="CT_Langue (0=Français, 1=Anglais, etc.)" - ) - - commercial_code: Optional[int] = Field( - None, - description="CO_No (ID du collaborateur commercial)" - ) - - lettrage_auto: Optional[bool] = Field( - True, - description="CT_Lettrage (1=oui, 0=non)" - ) - - est_actif: Optional[bool] = Field( - True, - description="Inverse de CT_Sommeil (True=actif, False=en sommeil)" - ) - - type_facture: Optional[int] = Field( - 1, - ge=0, - le=2, - description="CT_Facture: 0=aucune, 1=normale, 2=regroupée" - ) - - est_prospect: Optional[bool] = Field( - False, - description="CT_Prospect (1=oui, 0=non)" - ) - - bl_en_facture: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_BLFact (impression BL sur facture)" - ) - - saut_page: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_Saut (saut de page après impression)" - ) - - 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" - ) - - priorite_livraison: Optional[int] = Field( - None, - ge=0, - le=5, - description="CT_PrioriteLivr" - ) - - livraison_partielle: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_LivrPartielle" - ) - - delai_transport: Optional[int] = Field( - None, - ge=0, - description="CT_DelaiTransport (jours)" - ) - - delai_appro: Optional[int] = Field( - None, - ge=0, - description="CT_DelaiAppro (jours)" - ) - - commentaire: Optional[str] = Field( - None, - max_length=35, - description="CT_Commentaire" - ) - - section_analytique: Optional[str] = Field( - None, - max_length=13, - description="CA_Num" - ) - - mode_reglement_code: Optional[int] = Field( - None, - description="MR_No (ID du mode de règlement)" - ) - - surveillance_active: Optional[int] = Field( - None, - ge=0, - le=1, - description="CT_Surveillance (DOIT être défini AVANT coface)" - ) - - coface: Optional[str] = Field( - None, - max_length=25, - description="CT_Coface (code Coface)" - ) - - forme_juridique: Optional[str] = Field( - None, - max_length=33, - description="CT_SvFormeJuri (SARL, SA, etc.)" - ) - - effectif: Optional[str] = Field( - None, - max_length=11, - description="CT_SvEffectif" - ) - - sv_regularite: Optional[str] = Field( - None, - max_length=3, - description="CT_SvRegul" - ) - - sv_cotation: Optional[str] = Field( - None, - max_length=5, - description="CT_SvCotation" - ) - - sv_objet_maj: Optional[str] = Field( - None, - max_length=61, - description="CT_SvObjetMaj" - ) - - ca_annuel: Optional[float] = Field( - None, - description="CT_SvCA (Chiffre d'affaires annuel) - alias: sv_chiffre_affaires" - ) - - sv_chiffre_affaires: Optional[float] = Field( - None, - description="CT_SvCA (alias de ca_annuel)" - ) - - sv_resultat: Optional[float] = Field( - None, - description="CT_SvResultat" - ) - - - @field_validator('siret') - @classmethod - def validate_siret(cls, v): - """Valide et nettoie le SIRET""" - if v and v.lower() not in ('none', 'null', ''): - cleaned = v.replace(' ', '').replace('-', '') - if len(cleaned) not in (14, 15): - raise ValueError('Le SIRET doit contenir 14 ou 15 caractères') - return cleaned - return None - - @field_validator('email') - @classmethod - def validate_email(cls, v): - """Valide le format email""" - if v and v.lower() not in ('none', 'null', ''): - v = v.strip() - if '@' not in v: - raise ValueError('Format email invalide') - return v - return None - - @field_validator('raccourci') - @classmethod - def validate_raccourci(cls, v): - """Force le raccourci en majuscules""" - if v and v.lower() not in ('none', 'null', ''): - return v.upper().strip()[:7] - return None - - @field_validator( - 'adresse', 'code_postal', 'ville', 'pays', 'telephone', - 'tva_intra', 'contact', 'complement', mode='before' - ) - @classmethod - def clean_none_strings(cls, v): - """Convertit les chaînes 'None'/'null'/'' en None""" - if isinstance(v, str) and v.lower() in ('none', 'null', ''): - return None - return v - - def to_sage_dict(self) -> dict: - """ - Convertit le modèle en dictionnaire compatible avec creer_client() - Mapping 1:1 avec les paramètres réels de la fonction - """ - stat01 = self.statistique01 or self.secteur - - ca = self.ca_annuel or self.sv_chiffre_affaires - - return { - "intitule": self.intitule, - "numero": self.numero, - "type_tiers": self.type_tiers, - "qualite": self.qualite, - "classement": self.classement, - "raccourci": self.raccourci, - - "siret": self.siret, - "tva_intra": self.tva_intra, - "code_naf": self.code_naf, - - "contact": self.contact, - "adresse": self.adresse, - "complement": self.complement, - "code_postal": self.code_postal, - "ville": self.ville, - "region": self.region, - "pays": self.pays, - - "telephone": self.telephone, - "telecopie": self.telecopie, - "email": self.email, - "site_web": self.site_web, - "portable": self.portable, - "facebook": self.facebook, - "linkedin": self.linkedin, - - "compte_general": self.compte_general, - - "categorie_tarifaire": self.categorie_tarifaire, - "categorie_comptable": self.categorie_comptable, - - "taux01": self.taux01, - "taux02": self.taux02, - "taux03": self.taux03, - "taux04": self.taux04, - - "statistique01": stat01, - "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, - "secteur": self.secteur, # Gardé pour compatibilité - - "encours_autorise": self.encours_autorise, - "assurance_credit": self.assurance_credit, - "langue": self.langue, - "commercial_code": self.commercial_code, - - "lettrage_auto": self.lettrage_auto, - "est_actif": self.est_actif, - "type_facture": self.type_facture, - "est_prospect": self.est_prospect, - "bl_en_facture": self.bl_en_facture, - "saut_page": self.saut_page, - "validation_echeance": self.validation_echeance, - "controle_encours": self.controle_encours, - "exclure_relance": self.exclure_relance, - "exclure_penalites": self.exclure_penalites, - "bon_a_payer": self.bon_a_payer, - - "priorite_livraison": self.priorite_livraison, - "livraison_partielle": self.livraison_partielle, - "delai_transport": self.delai_transport, - "delai_appro": self.delai_appro, - - "commentaire": self.commentaire, - - "section_analytique": self.section_analytique, - - "mode_reglement_code": self.mode_reglement_code, - - "surveillance_active": self.surveillance_active, - "coface": self.coface, - "forme_juridique": self.forme_juridique, - "effectif": self.effectif, - "sv_regularite": self.sv_regularite, - "sv_cotation": self.sv_cotation, - "sv_objet_maj": self.sv_objet_maj, - "ca_annuel": ca, - "sv_chiffre_affaires": self.sv_chiffre_affaires, - "sv_resultat": self.sv_resultat, - } - - class Config: - json_schema_extra = { - "example": { - "intitule": "ENTREPRISE EXEMPLE SARL", - "numero": "CLI00123", - "type_tiers": 0, - "qualite": "CLI", - "compte_general": "411000", - "est_prospect": False, - "est_actif": True, - "email": "contact@exemple.fr", - "telephone": "0123456789", - "adresse": "123 Rue de la Paix", - "code_postal": "75001", - "ville": "Paris", - "pays": "France" - } - } - - -class ClientUpdateRequest(BaseModel): - """ - Modèle pour modification d'un client existant - TOUS les champs de ClientCreateRequest sont modifiables - TOUS optionnels (seuls les champs fournis sont modifiés) - """ - - intitule: Optional[str] = Field(None, max_length=69) - qualite: Optional[str] = Field(None, max_length=17) - classement: Optional[str] = Field(None, max_length=17) - raccourci: Optional[str] = Field(None, max_length=7) - - siret: Optional[str] = Field(None, max_length=15) - tva_intra: Optional[str] = Field(None, max_length=25) - code_naf: Optional[str] = Field(None, max_length=7) - - contact: Optional[str] = Field(None, max_length=35) - adresse: Optional[str] = Field(None, max_length=35) - complement: Optional[str] = Field(None, max_length=35) - code_postal: Optional[str] = Field(None, max_length=9) - ville: Optional[str] = Field(None, max_length=35) - region: Optional[str] = Field(None, max_length=25) - pays: Optional[str] = Field(None, max_length=35) - - telephone: Optional[str] = Field(None, max_length=21) - telecopie: Optional[str] = Field(None, max_length=21) - email: Optional[str] = Field(None, max_length=69) - site_web: Optional[str] = Field(None, max_length=69) - portable: Optional[str] = Field(None, max_length=21) - facebook: Optional[str] = Field(None, max_length=69) - linkedin: Optional[str] = Field(None, max_length=69) - - compte_general: Optional[str] = Field(None, max_length=13) - - categorie_tarifaire: Optional[str] = None - categorie_comptable: Optional[str] = None - - taux01: Optional[float] = None - taux02: Optional[float] = None - taux03: Optional[float] = None - taux04: Optional[float] = None - - secteur: Optional[str] = Field(None, max_length=21) - statistique01: Optional[str] = Field(None, max_length=21) - statistique02: Optional[str] = Field(None, max_length=21) - statistique03: Optional[str] = Field(None, max_length=21) - statistique04: Optional[str] = Field(None, max_length=21) - statistique05: Optional[str] = Field(None, max_length=21) - statistique06: Optional[str] = Field(None, max_length=21) - statistique07: Optional[str] = Field(None, max_length=21) - statistique08: Optional[str] = Field(None, max_length=21) - statistique09: Optional[str] = Field(None, max_length=21) - statistique10: Optional[str] = Field(None, max_length=21) - - encours_autorise: Optional[float] = None - assurance_credit: Optional[float] = None - langue: Optional[int] = Field(None, ge=0) - commercial_code: Optional[int] = None - - lettrage_auto: Optional[bool] = None - est_actif: Optional[bool] = None - type_facture: Optional[int] = Field(None, ge=0, le=2) - est_prospect: Optional[bool] = None - bl_en_facture: Optional[int] = Field(None, ge=0, le=1) - saut_page: Optional[int] = Field(None, ge=0, le=1) - validation_echeance: Optional[int] = Field(None, ge=0, le=1) - controle_encours: Optional[int] = Field(None, ge=0, le=1) - exclure_relance: Optional[int] = Field(None, ge=0, le=1) - exclure_penalites: Optional[int] = Field(None, ge=0, le=1) - bon_a_payer: Optional[int] = Field(None, ge=0, le=1) - - priorite_livraison: Optional[int] = Field(None, ge=0, le=5) - livraison_partielle: Optional[int] = Field(None, ge=0, le=1) - delai_transport: Optional[int] = Field(None, ge=0) - delai_appro: Optional[int] = Field(None, ge=0) - - commentaire: Optional[str] = Field(None, max_length=35) - - section_analytique: Optional[str] = Field(None, max_length=13) - - mode_reglement_code: Optional[int] = None - - surveillance_active: Optional[int] = Field(None, ge=0, le=1) - coface: Optional[str] = Field(None, max_length=25) - forme_juridique: Optional[str] = Field(None, max_length=33) - effectif: Optional[str] = Field(None, max_length=11) - sv_regularite: Optional[str] = Field(None, max_length=3) - sv_cotation: Optional[str] = Field(None, max_length=5) - sv_objet_maj: Optional[str] = Field(None, max_length=61) - ca_annuel: Optional[float] = None - sv_chiffre_affaires: Optional[float] = None - sv_resultat: Optional[float] = None - - @field_validator('siret') - @classmethod - def validate_siret(cls, v): - if v and v.lower() not in ('none', 'null', ''): - cleaned = v.replace(' ', '').replace('-', '') - if len(cleaned) not in (14, 15): - raise ValueError('Le SIRET doit contenir 14 ou 15 caractères') - return cleaned - return None - - @field_validator('email') - @classmethod - def validate_email(cls, v): - if v and v.lower() not in ('none', 'null', ''): - v = v.strip() - if '@' not in v: - raise ValueError('Format email invalide') - return v - return None - - @field_validator('raccourci') - @classmethod - def validate_raccourci(cls, v): - if v and v.lower() not in ('none', 'null', ''): - return v.upper().strip()[:7] - return None - - @field_validator( - 'adresse', 'code_postal', 'ville', 'pays', 'telephone', - 'tva_intra', 'contact', 'complement', mode='before' - ) - @classmethod - def clean_none_strings(cls, v): - if isinstance(v, str) and v.lower() in ('none', 'null', ''): - return None - return v - - class Config: - json_schema_extra = { - "example": { - "email": "nouveau@email.fr", - "telephone": "0198765432", - "portable": "0687654321", - "adresse": "456 Avenue Nouvelle", - "ville": "Lyon" - } - } - - - - -class UserResponse(BaseModel): - """Modèle de réponse pour un utilisateur""" - - id: str - email: str - nom: str - prenom: str - role: str - is_verified: bool - is_active: bool - created_at: str - last_login: Optional[str] = None - failed_login_attempts: int = 0 - - class Config: - from_attributes = True - - -class FournisseurCreateAPIRequest(BaseModel): - intitule: str = Field( - ..., min_length=1, max_length=69, description="Raison sociale du fournisseur" - ) - compte_collectif: str = Field( - "401000", description="Compte comptable fournisseur (ex: 401000)" - ) - num: Optional[str] = Field( - None, max_length=17, description="Code fournisseur souhaité (optionnel)" - ) - 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) - email: Optional[EmailStr] = None - telephone: Optional[str] = Field(None, max_length=21) - siret: Optional[str] = Field(None, max_length=14) - tva_intra: Optional[str] = Field(None, max_length=25) - - class Config: - json_schema_extra = { - "example": { - "intitule": "ACME SUPPLIES SARL", - "compte_collectif": "401000", - "num": "FOUR001", - "adresse": "15 Rue du Commerce", - "code_postal": "75001", - "ville": "Paris", - "pays": "France", - "email": "contact@acmesupplies.fr", - "telephone": "0145678901", - "siret": "12345678901234", - "tva_intra": "FR12345678901", - } - } - - -class FournisseurUpdateRequest(BaseModel): - """Modèle pour modification d'un fournisseur existant""" - - intitule: Optional[str] = Field(None, min_length=1, max_length=69) - 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) - email: Optional[EmailStr] = None - telephone: Optional[str] = Field(None, max_length=21) - siret: Optional[str] = Field(None, max_length=14) - tva_intra: Optional[str] = Field(None, max_length=25) - - class Config: - json_schema_extra = { - "example": { - "intitule": "ACME SUPPLIES MODIFIÉ", - "email": "nouveau@acme.fr", - "telephone": "0198765432", - } - } - - -class DevisUpdateRequest(BaseModel): - """Modèle pour modification d'un devis existant""" - - date_devis: Optional[date] = None - date_livraison: Optional[date] = None - reference: Optional[str] = None - lignes: Optional[List[LigneDevis]] = None - statut: Optional[int] = Field(None, ge=0, le=6) - - class Config: - json_schema_extra = { - "example": { - "date_devis": "2024-01-15", - "date_livraison": "2024-01-15", - "reference": "DEV-001", - "lignes": [ - { - "article_code": "ART001", - "quantite": 5.0, - "prix_unitaire_ht": 100.0, - "remise_pourcentage": 10.0, - } - ], - "statut": 2, - } - } - - -class LigneCommande(BaseModel): - """Ligne de commande""" - - article_code: str - quantite: float - remise_pourcentage: Optional[float] = 0.0 - - @field_validator("article_code", mode="before") - def strip_insecables(cls, v): - return v.replace("\xa0", "").strip() - - -class CommandeCreateRequest(BaseModel): - """Création d'une commande""" - - client_id: str - date_commande: Optional[date] = None - date_livraison: Optional[date] = None - lignes: List[LigneCommande] - reference: Optional[str] = None - - class Config: - json_schema_extra = { - "example": { - "client_id": "CLI000001", - "date_commande": "2024-01-15", - "reference": "CMD-EXT-001", - "lignes": [ - { - "article_code": "ART001", - "quantite": 10.0, - "prix_unitaire_ht": 50.0, - "remise_pourcentage": 5.0, - } - ], - } - } - - -class CommandeUpdateRequest(BaseModel): - """Modification d'une commande existante""" - - date_commande: Optional[date] = None - date_livraison: Optional[date] = None - lignes: Optional[List[LigneCommande]] = None - statut: Optional[int] = Field(None, ge=0, le=6) - reference: Optional[str] = None - - class Config: - json_schema_extra = { - "example": { - "date_commande": "2024-01-15", - "date_livraison": "2024-01-15", - "reference": "CMD-EXT-001", - "lignes": [ - { - "article_code": "ART001", - "quantite": 15.0, - "prix_unitaire_ht": 45.0, - } - ], - "statut": 2, - } - } - - -class LigneLivraison(BaseModel): - """Ligne de livraison""" - - article_code: str - quantite: float - remise_pourcentage: Optional[float] = 0.0 - - @field_validator("article_code", mode="before") - def strip_insecables(cls, v): - return v.replace("\xa0", "").strip() - - -class LivraisonCreateRequest(BaseModel): - """Création d'une livraison""" - - client_id: str - date_livraison: Optional[date] = None - date_livraison_prevue: Optional[date] = None - lignes: List[LigneLivraison] - reference: Optional[str] = None - - class Config: - json_schema_extra = { - "example": { - "client_id": "CLI000001", - "date_livraison": "2024-01-15", - "reference": "BL-EXT-001", - "lignes": [ - { - "article_code": "ART001", - "quantite": 10.0, - "prix_unitaire_ht": 50.0, - "remise_pourcentage": 5.0, - } - ], - } - } - - -class LivraisonUpdateRequest(BaseModel): - """Modification d'une livraison existante""" - - date_livraison: Optional[date] = None - date_livraison_prevue: Optional[date] = None - lignes: Optional[List[LigneLivraison]] = None - statut: Optional[int] = Field(None, ge=0, le=6) - reference: Optional[str] = None - - class Config: - json_schema_extra = { - "example": { - "date_livraison": "2024-01-15", - "date_livraison_prevue": "2024-01-15", - "reference": "BL-EXT-001", - "lignes": [ - { - "article_code": "ART001", - "quantite": 15.0, - "prix_unitaire_ht": 45.0, - } - ], - "statut": 2, - } - } - - -class LigneAvoir(BaseModel): - """Ligne d'avoir""" - - article_code: str - quantite: float - remise_pourcentage: Optional[float] = 0.0 - - @field_validator("article_code", mode="before") - def strip_insecables(cls, v): - return v.replace("\xa0", "").strip() - - -class AvoirCreateRequest(BaseModel): - """Création d'un avoir""" - - client_id: str - date_avoir: Optional[date] = None - date_livraison: Optional[date] = None - lignes: List[LigneAvoir] - reference: Optional[str] = None - - class Config: - json_schema_extra = { - "example": { - "client_id": "CLI000001", - "date_avoir": "2024-01-15", - "reference": "AV-EXT-001", - "lignes": [ - { - "article_code": "ART001", - "quantite": 5.0, - "prix_unitaire_ht": 50.0, - "remise_pourcentage": 0.0, - } - ], - } - } - - -class AvoirUpdateRequest(BaseModel): - """Modification d'un avoir existant""" - - date_avoir: Optional[date] = None - date_livraison: Optional[date] = None - lignes: Optional[List[LigneAvoir]] = None - statut: Optional[int] = Field(None, ge=0, le=6) - reference: Optional[str] = None - - class Config: - json_schema_extra = { - "example": { - "date_avoir": "2024-01-15", - "date_livraison": "2024-01-15", - "reference": "AV-EXT-001", - "lignes": [ - { - "article_code": "ART001", - "quantite": 10.0, - "prix_unitaire_ht": 45.0, - } - ], - "statut": 2, - } - } - - -class LigneFacture(BaseModel): - """Ligne de facture""" - - article_code: str - quantite: float - remise_pourcentage: Optional[float] = 0.0 - - @field_validator("article_code", mode="before") - def strip_insecables(cls, v): - return v.replace("\xa0", "").strip() - - -class FactureCreateRequest(BaseModel): - """Création d'une facture""" - - client_id: str - date_facture: Optional[date] = None - date_livraison: Optional[date] = None - lignes: List[LigneFacture] - reference: Optional[str] = None - - class Config: - json_schema_extra = { - "example": { - "client_id": "CLI000001", - "date_facture": "2024-01-15", - "reference": "FA-EXT-001", - "lignes": [ - { - "article_code": "ART001", - "quantite": 10.0, - "prix_unitaire_ht": 50.0, - "remise_pourcentage": 5.0, - } - ], - } - } - - -class FactureUpdateRequest(BaseModel): - """Modification d'une facture existante""" - - date_facture: Optional[date] = None - date_livraison: Optional[date] = None - lignes: Optional[List[LigneFacture]] = None - statut: Optional[int] = Field(None, ge=0, le=6) - reference: Optional[str] = None - - class Config: - json_schema_extra = { - "example": { - "date_facture": "2024-01-15", - "date_livraison": "2024-01-15", - "lignes": [ - { - "article_code": "ART001", - "quantite": 15.0, - "prix_unitaire_ht": 45.0, - } - ], - "statut": 2, - } - } - - -class ArticleCreateRequest(BaseModel): - """Schéma pour création d'article""" - - reference: str = Field(..., max_length=18, description="Référence article") - designation: str = Field(..., max_length=69, description="Désignation") - famille: Optional[str] = Field(None, max_length=18, description="Code famille") - prix_vente: Optional[float] = Field(None, ge=0, description="Prix vente HT") - prix_achat: Optional[float] = Field(None, ge=0, description="Prix achat HT") - stock_reel: Optional[float] = Field(None, ge=0, description="Stock initial") - stock_mini: Optional[float] = Field(None, ge=0, description="Stock minimum") - code_ean: Optional[str] = Field(None, max_length=13, description="Code-barres") - unite_vente: Optional[str] = Field("UN", max_length=4, description="Unité") - tva_code: Optional[str] = Field(None, max_length=5, description="Code TVA") - description: Optional[str] = Field(None, description="Description") - - -class ArticleUpdateRequest(BaseModel): - """Schéma pour modification d'article""" - - designation: Optional[str] = Field(None, max_length=69) - prix_vente: Optional[float] = Field(None, ge=0) - prix_achat: Optional[float] = Field(None, ge=0) - stock_reel: Optional[float] = Field( - None, ge=0, description="Critique pour erreur 2881" - ) - stock_mini: Optional[float] = Field(None, ge=0) - code_ean: Optional[str] = Field(None, max_length=13) - description: Optional[str] = Field(None) - - -class FamilleCreateRequest(BaseModel): - """Schéma pour création de famille d'articles""" - - code: str = Field(..., max_length=18, description="Code famille (max 18 car)") - intitule: str = Field(..., max_length=69, description="Intitulé (max 69 car)") - type: int = Field(0, ge=0, le=1, description="0=Détail, 1=Total") - compte_achat: Optional[str] = Field( - None, max_length=13, description="Compte général achat (ex: 607000)" - ) - compte_vente: Optional[str] = Field( - None, max_length=13, description="Compte général vente (ex: 707000)" - ) - - class Config: - json_schema_extra = { - "example": { - "code": "PRODLAIT", - "intitule": "Produits laitiers", - "type": 0, - "compte_achat": "607000", - "compte_vente": "707000", - } - } - - - -class FamilleResponse(BaseModel): - """Modèle complet d'une famille avec données comptables et fournisseur""" - - code: str = Field(..., description="Code famille") - intitule: str = Field(..., description="Intitulé") - type: int = Field(..., description="Type (0=Détail, 1=Total)") - type_libelle: str = Field(..., description="Libellé du type") - est_total: bool = Field(..., description="True si type Total") - est_detail: bool = Field(..., description="True si type Détail") - - unite_vente: Optional[str] = Field(None, description="Unité de vente par défaut") - unite_poids: Optional[str] = Field(None, description="Unité de poids") - coef: Optional[float] = Field(None, description="Coefficient multiplicateur") - - suivi_stock: Optional[bool] = Field(None, description="Suivi du stock activé") - garantie: Optional[int] = Field(None, description="Durée de garantie (mois)") - delai: Optional[int] = Field(None, description="Délai de livraison (jours)") - nb_colis: Optional[int] = Field(None, description="Nombre de colis") - - code_fiscal: Optional[str] = Field(None, description="Code fiscal par défaut") - escompte: Optional[bool] = Field(None, description="Escompte autorisé") - - est_centrale: Optional[bool] = Field(None, description="Famille d'achat centralisé") - nature: Optional[int] = Field(None, description="Nature de la famille") - pays: Optional[str] = Field(None, description="Pays d'origine") - - categorie_1: Optional[int] = Field(None, description="Catégorie comptable 1 (CL_No1)") - categorie_2: Optional[int] = Field(None, description="Catégorie comptable 2 (CL_No2)") - categorie_3: Optional[int] = Field(None, description="Catégorie comptable 3 (CL_No3)") - categorie_4: Optional[int] = Field(None, description="Catégorie comptable 4 (CL_No4)") - - stat_01: Optional[str] = Field(None, description="Statistique libre 1") - stat_02: Optional[str] = Field(None, description="Statistique libre 2") - stat_03: Optional[str] = Field(None, description="Statistique libre 3") - stat_04: Optional[str] = Field(None, description="Statistique libre 4") - stat_05: Optional[str] = Field(None, description="Statistique libre 5") - hors_statistique: Optional[bool] = Field(None, description="Exclue des statistiques") - - vente_debit: Optional[bool] = Field(None, description="Vente au débit") - non_imprimable: Optional[bool] = Field(None, description="Non imprimable sur documents") - contremarque: Optional[bool] = Field(None, description="Article en contremarque") - fact_poids: Optional[bool] = Field(None, description="Facturation au poids") - fact_forfait: Optional[bool] = Field(None, description="Facturation forfaitaire") - publie: Optional[bool] = Field(None, description="Publié (e-commerce)") - - racine_reference: Optional[str] = Field(None, description="Racine pour génération auto de références") - racine_code_barre: Optional[str] = Field(None, description="Racine pour génération auto de codes-barres") - raccourci: Optional[str] = Field(None, description="Raccourci clavier") - - sous_traitance: Optional[bool] = Field(None, description="Famille en sous-traitance") - fictif: Optional[bool] = Field(None, description="Famille fictive (nomenclature)") - criticite: Optional[int] = Field(None, description="Niveau de criticité (0-5)") - - compte_vente: Optional[str] = Field(None, description="Compte général de vente") - compte_auxiliaire_vente: Optional[str] = Field(None, description="Compte auxiliaire de vente") - tva_vente_1: Optional[str] = Field(None, description="Code TVA vente principal") - tva_vente_2: Optional[str] = Field(None, description="Code TVA vente secondaire") - tva_vente_3: Optional[str] = Field(None, description="Code TVA vente tertiaire") - type_facture_vente: Optional[int] = Field(None, description="Type de facture vente") - - compte_achat: Optional[str] = Field(None, description="Compte général d'achat") - compte_auxiliaire_achat: Optional[str] = Field(None, description="Compte auxiliaire d'achat") - tva_achat_1: Optional[str] = Field(None, description="Code TVA achat principal") - tva_achat_2: Optional[str] = Field(None, description="Code TVA achat secondaire") - tva_achat_3: Optional[str] = Field(None, description="Code TVA achat tertiaire") - type_facture_achat: Optional[int] = Field(None, description="Type de facture achat") - - compte_stock: Optional[str] = Field(None, description="Compte de stock") - compte_auxiliaire_stock: Optional[str] = Field(None, description="Compte auxiliaire de stock") - - fournisseur_principal: Optional[str] = Field(None, description="N° compte fournisseur principal") - fournisseur_unite: Optional[str] = Field(None, description="Unité d'achat fournisseur") - fournisseur_conversion: Optional[float] = Field(None, description="Coefficient de conversion") - fournisseur_delai_appro: Optional[int] = Field(None, description="Délai d'approvisionnement (jours)") - fournisseur_garantie: Optional[int] = Field(None, description="Garantie fournisseur (mois)") - fournisseur_colisage: Optional[int] = Field(None, description="Colisage fournisseur") - fournisseur_qte_mini: Optional[float] = Field(None, description="Quantité minimum de commande") - fournisseur_qte_mont: Optional[float] = Field(None, description="Quantité montant") - fournisseur_devise: Optional[int] = Field(None, description="Devise fournisseur (0=Euro)") - fournisseur_remise: Optional[float] = Field(None, description="Remise fournisseur (%)") - fournisseur_type_remise: Optional[int] = Field(None, description="Type de remise (0=%, 1=Montant)") - - nb_articles: Optional[int] = Field(None, description="Nombre d'articles dans la famille") - - FA_CodeFamille: Optional[str] = Field(None, description="[Legacy] Code famille") - FA_Intitule: Optional[str] = Field(None, description="[Legacy] Intitulé") - FA_Type: Optional[int] = Field(None, description="[Legacy] Type") - CG_NumVte: Optional[str] = Field(None, description="[Legacy] Compte vente") - CG_NumAch: Optional[str] = Field(None, description="[Legacy] Compte achat") - - class Config: - json_schema_extra = { - "example": { - "code": "ELECT", - "intitule": "Électronique et Informatique", - "type": 0, - "type_libelle": "Détail", - "est_total": False, - "est_detail": True, - "unite_vente": "U", - "unite_poids": "KG", - "coef": 2.5, - "suivi_stock": True, - "garantie": 24, - "delai": 5, - "nb_colis": 1, - "code_fiscal": "C19", - "escompte": True, - "est_centrale": False, - "nature": 0, - "pays": "FR", - "categorie_1": 1, - "categorie_2": 0, - "categorie_3": 0, - "categorie_4": 0, - "stat_01": "HIGH_TECH", - "stat_02": "", - "stat_03": "", - "stat_04": "", - "stat_05": "", - "hors_statistique": False, - "vente_debit": False, - "non_imprimable": False, - "contremarque": False, - "fact_poids": False, - "fact_forfait": False, - "publie": True, - "racine_reference": "ELEC", - "racine_code_barre": "339", - "raccourci": "F5", - "sous_traitance": False, - "fictif": False, - "criticite": 2, - "compte_vente": "707100", - "compte_auxiliaire_vente": "", - "tva_vente_1": "C19", - "tva_vente_2": "", - "tva_vente_3": "", - "type_facture_vente": 0, - "compte_achat": "607100", - "compte_auxiliaire_achat": "", - "tva_achat_1": "C19", - "tva_achat_2": "", - "tva_achat_3": "", - "type_facture_achat": 0, - "compte_stock": "350000", - "compte_auxiliaire_stock": "", - "fournisseur_principal": "FTECH001", - "fournisseur_unite": "U", - "fournisseur_conversion": 1.0, - "fournisseur_delai_appro": 7, - "fournisseur_garantie": 12, - "fournisseur_colisage": 10, - "fournisseur_qte_mini": 5.0, - "fournisseur_qte_mont": 100.0, - "fournisseur_devise": 0, - "fournisseur_remise": 5.0, - "fournisseur_type_remise": 0, - "nb_articles": 156 - } - } - - -class FamilleListResponse(BaseModel): - """Réponse pour la liste des familles""" - familles: list[FamilleResponse] - total: int - filtre: Optional[str] = None - inclure_totaux: bool = True - - class Config: - json_schema_extra = { - "example": { - "familles": [], - "total": 42, - "filtre": "ELECT", - "inclure_totaux": False - } - } - - -class MouvementStockLigneRequest(BaseModel): - article_ref: str = Field(..., description="Référence de l'article") - quantite: float = Field(..., gt=0, description="Quantité (>0)") - depot_code: Optional[str] = Field(None, description="Code du dépôt (ex: '01')") - prix_unitaire: Optional[float] = Field( - None, ge=0, description="Prix unitaire (optionnel)" - ) - commentaire: Optional[str] = Field(None, description="Commentaire ligne") - numero_lot: Optional[str] = Field( - None, description="Numéro de lot (pour FIFO/LIFO)" - ) - stock_mini: Optional[float] = Field( - None, - ge=0, - description="""Stock minimum à définir pour cet article. - Si fourni, met à jour AS_QteMini dans F_ARTSTOCK. - Laisser None pour ne pas modifier.""", - ) - stock_maxi: Optional[float] = Field( - None, - ge=0, - description="""Stock maximum à définir pour cet article. - Doit être > stock_mini si les deux sont fournis.""", - ) - - class Config: - schema_extra = { - "example": { - "article_ref": "ARTS-001", - "quantite": 50.0, - "depot_code": "01", - "prix_unitaire": 100.0, - "commentaire": "Réapprovisionnement", - "numero_lot": "LOT20241217", - "stock_mini": 10.0, - "stock_maxi": 200.0, - } - } - - @validator("stock_maxi") - def validate_stock_maxi(cls, v, values): - """Valide que stock_maxi > stock_mini si les deux sont fournis""" - if ( - v is not None - and "stock_mini" in values - and values["stock_mini"] is not None - ): - if v <= values["stock_mini"]: - raise ValueError( - "stock_maxi doit être strictement supérieur à stock_mini" - ) - return v - - -class EntreeStockRequest(BaseModel): - """Création d'un bon d'entrée en stock""" - - date_entree: Optional[date] = Field( - None, description="Date du mouvement (aujourd'hui par défaut)" - ) - reference: Optional[str] = Field(None, description="Référence externe") - depot_code: Optional[str] = Field( - None, description="Dépôt principal (si applicable)" - ) - lignes: List[MouvementStockLigneRequest] = Field( - ..., min_items=1, description="Lignes du mouvement" - ) - commentaire: Optional[str] = Field(None, description="Commentaire général") - - class Config: - json_schema_extra = { - "example": { - "date_entree": "2025-01-15", - "reference": "REC-2025-001", - "depot_code": "01", - "lignes": [ - { - "article_ref": "ART001", - "quantite": 50, - "depot_code": "01", - "prix_unitaire": 10.50, - "commentaire": "Réception fournisseur", - } - ], - "commentaire": "Réception livraison fournisseur XYZ", - } - } - - -class SortieStockRequest(BaseModel): - """Création d'un bon de sortie de stock""" - - date_sortie: Optional[date] = Field( - None, description="Date du mouvement (aujourd'hui par défaut)" - ) - reference: Optional[str] = Field(None, description="Référence externe") - depot_code: Optional[str] = Field( - None, description="Dépôt principal (si applicable)" - ) - lignes: List[MouvementStockLigneRequest] = Field( - ..., min_items=1, description="Lignes du mouvement" - ) - commentaire: Optional[str] = Field(None, description="Commentaire général") - - class Config: - json_schema_extra = { - "example": { - "date_sortie": "2025-01-15", - "reference": "SOR-2025-001", - "depot_code": "01", - "lignes": [ - { - "article_ref": "ART001", - "quantite": 10, - "depot_code": "01", - "commentaire": "Utilisation interne", - } - ], - "commentaire": "Consommation atelier", - } - } - - -class MouvementStockResponse(BaseModel): - """Réponse pour un mouvement de stock""" - - article_ref: str = Field(..., description="Numéro d'article") - numero: str = Field(..., description="Numéro du mouvement") - type: int = Field(..., description="Type (0=Entrée, 1=Sortie)") - type_libelle: str = Field(..., description="Libellé du type") - date: str = Field(..., description="Date du mouvement") - reference: Optional[str] = Field(None, description="Référence externe") - nb_lignes: int = Field(..., description="Nombre de lignes") - - -class ContactCreate(BaseModel): - """Données pour créer ou modifier un contact""" - numero: str = Field(..., description="Code du client parent (obligatoire)") - - civilite: Optional[str] = Field(None, description="M., Mme, Mlle, Société") - nom: str = Field(..., description="Nom de famille (obligatoire)") - prenom: Optional[str] = Field(None, description="Prénom") - fonction: Optional[str] = Field(None, description="Fonction/Titre") - - est_defaut: Optional[bool] = Field(False, description="Définir comme contact par défaut du client") - - service_code: Optional[int] = Field(None, description="Code du service") - - telephone: Optional[str] = Field(None, description="Téléphone fixe") - portable: Optional[str] = Field(None, description="Téléphone mobile") - telecopie: Optional[str] = Field(None, description="Fax") - email: Optional[str] = Field(None, description="Email") - - facebook: Optional[str] = Field(None, description="URL Facebook") - linkedin: Optional[str] = Field(None, description="URL LinkedIn") - skype: Optional[str] = Field(None, description="Identifiant Skype") - - @validator("civilite") - def validate_civilite(cls, v): - if v and v not in ["M.", "Mme", "Mlle", "Société"]: - raise ValueError("Civilité doit être: M., Mme, Mlle ou Société") - return v - - class Config: - json_schema_extra = { - "example": { - "numero": "CLI000001", - "civilite": "M.", - "nom": "Dupont", - "prenom": "Jean", - "fonction": "Directeur Commercial", - "telephone": "0123456789", - "portable": "0612345678", - "email": "j.dupont@exemple.fr", - "linkedin": "https://linkedin.com/in/jeandupont", - "est_defaut": True - } - } - - -class ContactUpdate(BaseModel): - """Données pour modifier un contact (tous champs optionnels)""" - civilite: Optional[str] = None - nom: Optional[str] = None - prenom: Optional[str] = None - fonction: Optional[str] = None - service_code: Optional[int] = None - telephone: Optional[str] = None - portable: Optional[str] = None - telecopie: Optional[str] = None - email: Optional[str] = None - facebook: Optional[str] = None - linkedin: Optional[str] = None - skype: Optional[str] = None - est_defaut: Optional[bool] = None - -templates_signature_email = { - "demande_signature": { - "id": "demande_signature", - "nom": "Demande de Signature Électronique", - "sujet": "Signature requise - {{TYPE_DOC}} {{NUMERO}}", - "corps_html": """ - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - -
-

- Signature Électronique Requise -

-
-

- Bonjour {{NOM_SIGNATAIRE}}, -

- -

- Nous vous invitons à signer électroniquement le document suivant : -

- - - - - - -
- - - - - - - - - - - - - - - - - -
Type de document{{TYPE_DOC}}
Numéro{{NUMERO}}
Date{{DATE}}
Montant TTC{{MONTANT_TTC}} €
-
- -

- Cliquez sur le bouton ci-dessous pour accéder au document et apposer votre signature électronique sécurisée : -

- - - - - - -
- - ✍️ Signer le document - -
- - - - - - -
-

- ⏰ Important : Ce lien de signature est valable pendant 30 jours. - Nous vous recommandons de signer ce document dès que possible. -

-
- -

- 🔒 Signature électronique sécurisée
- Votre signature est protégée par notre partenaire de confiance Universign, - certifié eIDAS et conforme au RGPD. Votre identité sera vérifiée et le document sera - horodaté de manière infalsifiable. -

-
-

- Vous avez des questions ? Contactez-nous à {{CONTACT_EMAIL}} -

-

- Cet email a été envoyé automatiquement par le système Sage 100c Dataven.
- Si vous avez reçu cet email par erreur, veuillez nous en informer. -

-
-
- - - """, - "variables_disponibles": [ - "NOM_SIGNATAIRE", - "TYPE_DOC", - "NUMERO", - "DATE", - "MONTANT_TTC", - "SIGNER_URL", - "CONTACT_EMAIL", - ], - }, - "signature_confirmee": { - "id": "signature_confirmee", - "nom": "Confirmation de Signature", - "sujet": "Document signé - {{TYPE_DOC}} {{NUMERO}}", - "corps_html": """ - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - -
-

- Document Signé avec Succès -

-
-

- Bonjour {{NOM_SIGNATAIRE}}, -

- -

- Nous confirmons la signature électronique du document suivant : -

- - - - - - -
- - - - - - - - - - - - - -
Document{{TYPE_DOC}} {{NUMERO}}
Signé le{{DATE_SIGNATURE}}
ID Transaction{{TRANSACTION_ID}}
-
- -

- Le document signé a été automatiquement archivé et est disponible dans votre espace client. - Un certificat de signature électronique conforme eIDAS a été généré. -

- - - - - -
-

- Signature certifiée : Ce document a été signé avec une signature - électronique qualifiée, ayant la même valeur juridique qu'une signature manuscrite - conformément au règlement eIDAS. -

-
- -

- Merci pour votre confiance. Notre équipe reste à votre disposition pour toute question. -

-
-

- Contact : {{CONTACT_EMAIL}} -

-

- Sage 100c Dataven - Système de signature électronique sécurisée -

-
-
- - - """, - "variables_disponibles": [ - "NOM_SIGNATAIRE", - "TYPE_DOC", - "NUMERO", - "DATE_SIGNATURE", - "TRANSACTION_ID", - "CONTACT_EMAIL", - ], - }, - "relance_signature": { - "id": "relance_signature", - "nom": "Relance Signature en Attente", - "sujet": "⏰ Rappel - Signature en attente {{TYPE_DOC}} {{NUMERO}}", - "corps_html": """ - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - -
-

- ⏰ Signature en Attente -

-
-

- Bonjour {{NOM_SIGNATAIRE}}, -

- -

- Nous vous avons envoyé il y a {{NB_JOURS}} jours un document à signer électroniquement. - Nous constatons que celui-ci n'a pas encore été signé. -

- - - - - - -
-

- Document en attente : {{TYPE_DOC}} {{NUMERO}} -

-

- ⏳ Le lien de signature expirera dans {{JOURS_RESTANTS}} jours -

-
- -

- Pour éviter tout retard dans le traitement de votre dossier, nous vous invitons à signer ce document dès maintenant : -

- - - - - - -
- - ✍️ Signer maintenant - -
- -

- Si vous rencontrez des difficultés ou avez des questions, n'hésitez pas à nous contacter. -

-
-

- Contact : {{CONTACT_EMAIL}} -

-

- Sage 100c Dataven - Relance automatique -

-
-
- - - """, - "variables_disponibles": [ - "NOM_SIGNATAIRE", - "TYPE_DOC", - "NUMERO", - "NB_JOURS", - "JOURS_RESTANTS", - "SIGNER_URL", - "CONTACT_EMAIL", - ], - }, -} - - -async def universign_envoyer( - doc_id: str, - pdf_bytes: bytes, - email: str, - nom: str, - doc_data: Dict, - session: AsyncSession, -) -> Dict: - import requests - - try: - api_key = settings.universign_api_key - api_url = settings.universign_api_url - auth = (api_key, "") - - logger.info(f" Démarrage processus Universign pour {email}") - logger.info(f"📌 Document: {doc_id} ({doc_data.get('type_label')})") - - if not pdf_bytes or len(pdf_bytes) == 0: - raise Exception("Le PDF généré est vide") - - logger.info(f"PDF valide : {len(pdf_bytes)} octets") - - logger.info("ÉTAPE 1/6 : Création transaction") - - response = requests.post( - f"{api_url}/transactions", - auth=auth, - json={ - "name": f"{doc_data.get('type_label', 'Document')} {doc_id}", - "language": "fr", - }, - timeout=30, - ) - - if response.status_code != 200: - logger.error(f"Erreur création transaction: {response.text}") - raise Exception(f"Erreur création transaction: {response.status_code}") - - transaction_id = response.json().get("id") - logger.info(f"Transaction créée: {transaction_id}") - - logger.info("ÉTAPE 2/6 : Upload PDF") - - files = { - "file": ( - f"{doc_data.get('type_label', 'Document')}_{doc_id}.pdf", - pdf_bytes, - "application/pdf", - ) - } - - response = requests.post( - f"{api_url}/files", - auth=auth, - files=files, - timeout=60, - ) - - if response.status_code not in [200, 201]: - logger.error(f"Erreur upload: {response.text}") - raise Exception(f"Erreur upload fichier: {response.status_code}") - - file_id = response.json().get("id") - logger.info(f"Fichier uploadé: {file_id}") - - logger.info("📋 ÉTAPE 3/6 : Ajout document à transaction") - - response = requests.post( - f"{api_url}/transactions/{transaction_id}/documents", - auth=auth, - data={"document": file_id}, - timeout=30, - ) - - if response.status_code not in [200, 201]: - logger.error(f"Erreur ajout document: {response.text}") - raise Exception(f"Erreur ajout document: {response.status_code}") - - document_id = response.json().get("id") - logger.info(f"Document ajouté: {document_id}") - - logger.info("✍️ ÉTAPE 4/6 : Création champ signature") - - response = requests.post( - f"{api_url}/transactions/{transaction_id}/documents/{document_id}/fields", - auth=auth, - data={ - "type": "signature", - }, - timeout=30, - ) - - if response.status_code not in [200, 201]: - logger.error(f"Erreur création champ: {response.text}") - raise Exception(f"Erreur création champ: {response.status_code}") - - field_id = response.json().get("id") - logger.info(f"Champ créé: {field_id}") - - logger.info(" ÉTAPE 5/6 : Liaison signataire au champ") - - response = requests.post( - f"{api_url}/transactions/{transaction_id}/signatures", # /signatures pas /signers - auth=auth, - data={ - "signer": email, - "field": field_id, - }, - timeout=30, - ) - - if response.status_code not in [200, 201]: - logger.error(f"Erreur liaison signataire: {response.text}") - raise Exception(f"Erreur liaison signataire: {response.status_code}") - - logger.info(f"Signataire lié: {email}") - - logger.info("🚀 ÉTAPE 6/6 : Démarrage transaction") - - response = requests.post( - f"{api_url}/transactions/{transaction_id}/start", - auth=auth, - timeout=30 - ) - - if response.status_code not in [200, 201]: - logger.error(f"Erreur démarrage: {response.text}") - raise Exception(f"Erreur démarrage: {response.status_code}") - - final_data = response.json() - logger.info("Transaction démarrée") - - logger.info("🔗 Récupération URL de signature") - - signer_url = "" - - if final_data.get("actions"): - for action in final_data["actions"]: - if action.get("url"): - signer_url = action["url"] - break - - if not signer_url and final_data.get("signers"): - for signer in final_data["signers"]: - if signer.get("email") == email: - signer_url = signer.get("url", "") - break - - if not signer_url: - logger.error(f"URL introuvable dans: {final_data}") - raise ValueError("URL de signature non retournée par Universign") - - logger.info("URL récupérée") - - logger.info(" Préparation email") - - template = templates_signature_email["demande_signature"] - - type_labels = { - 0: "Devis", - 10: "Commande", - 30: "Bon de Livraison", - 60: "Facture", - 50: "Avoir", - } - - variables = { - "NOM_SIGNATAIRE": nom, - "TYPE_DOC": type_labels.get(doc_data.get("type_doc", 0), "Document"), - "NUMERO": doc_id, - "DATE": doc_data.get("date", datetime.now().strftime("%d/%m/%Y")), - "MONTANT_TTC": f"{doc_data.get('montant_ttc', 0):.2f}", - "SIGNER_URL": signer_url, - "CONTACT_EMAIL": settings.smtp_from, - } - - sujet = template["sujet"] - corps = template["corps_html"] - - for var, valeur in variables.items(): - sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur)) - corps = corps.replace(f"{{{{{var}}}}}", str(valeur)) - - email_log = EmailLog( - id=str(uuid.uuid4()), - destinataire=email, - sujet=sujet, - corps_html=corps, - document_ids=doc_id, - type_document=doc_data.get("type_doc"), - statut=StatutEmailEnum.EN_ATTENTE, - date_creation=datetime.now(), - nb_tentatives=0, - ) - - session.add(email_log) - await session.flush() - - email_queue.enqueue(email_log.id) - - logger.info(f"Email mis en file pour {email}") - logger.info("🎉 Processus terminé avec succès") - - return { - "transaction_id": transaction_id, - "signer_url": signer_url, - "statut": "ENVOYE", - "email_log_id": email_log.id, - "email_sent": True, - } - - except Exception as e: - logger.error(f"Erreur Universign: {e}", exc_info=True) - return { - "error": str(e), - "statut": "ERREUR", - "email_sent": False, - } - - -async def universign_statut(transaction_id: str) -> Dict: - """Récupération statut signature""" - import requests - - try: - response = requests.get( - f"{settings.universign_api_url}/transactions/{transaction_id}", - auth=(settings.universign_api_key, ""), - timeout=10, - ) - - if response.status_code == 200: - data = response.json() - statut_map = { - "draft": "EN_ATTENTE", - "started": "EN_ATTENTE", - "completed": "SIGNE", - "refused": "REFUSE", - "expired": "EXPIRE", - "canceled": "REFUSE", - } - return { - "statut": statut_map.get(data.get("state"), "EN_ATTENTE"), - "date_signature": data.get("completed_at"), - } - else: - return {"statut": "ERREUR"} - - except Exception as e: - logger.error(f"Erreur statut Universign: {e}") - return {"statut": "ERREUR", "error": str(e)} - - @asynccontextmanager async def lifespan(app: FastAPI): await init_db() @@ -3262,8 +102,7 @@ async def lifespan(app: FastAPI): yield email_queue.stop() - logger.info("👋 Services arrêtés") - + logger.info("Services arrêtés") app = FastAPI( @@ -3296,7 +135,7 @@ async def obtenir_clients(query: Optional[str] = Query(None)): raise HTTPException(500, str(e)) -@app.get("/clients/{code}", response_model=ClientDetails , tags=["Clients"]) +@app.get("/clients/{code}", response_model=ClientDetails, tags=["Clients"]) async def lire_client_detail(code: str): try: client = sage_client.lire_client(code) @@ -3345,15 +184,17 @@ async def ajouter_client( client: ClientCreateRequest, session: AsyncSession = Depends(get_session) ): try: - nouveau_client = sage_client.creer_client(client.model_dump(mode='json')) - + nouveau_client = sage_client.creer_client(client.model_dump(mode="json")) + logger.info(f"Client créé via API: {nouveau_client.get('numero')}") - return jsonable_encoder({ - "success": True, - "message": "Client créé avec succès", - "data": nouveau_client, - }) + return jsonable_encoder( + { + "success": True, + "message": "Client créé avec succès", + "data": nouveau_client, + } + ) except Exception as e: logger.error(f"Erreur lors de la création du client: {e}") @@ -3457,7 +298,7 @@ async def modifier_article( @app.get("/articles/{reference}", response_model=ArticleResponse, tags=["Articles"]) async def lire_article( - reference: str = Path(..., description="Référence de l'article") + reference: str = Path(..., description="Référence de l'article"), ): try: article = sage_client.lire_article(reference) @@ -4091,7 +932,6 @@ async def webhook_universign( logger.warning(f"Transaction {transaction_id} introuvable en DB") return {"status": "not_found"} - if event_type == "transaction.completed": signature_log.statut = StatutSignatureEnum.SIGNE signature_log.date_signature = datetime.now() @@ -4651,9 +1491,6 @@ async def recuperer_contact_devis(id: str): raise HTTPException(500, str(e)) - - - @app.get("/factures", tags=["Factures"]) async def lister_factures( limit: int = Query(100, le=1000), statut: Optional[int] = Query(None) @@ -5086,7 +1923,6 @@ async def previsualiser_email(preview: TemplatePreviewRequest): } - @app.get("/health", tags=["System"]) async def health_check(): gateway_health = sage_client.health() @@ -5113,9 +1949,6 @@ async def root(): } - - - @app.get("/admin/cache/info", tags=["Admin"]) async def info_cache(): try: @@ -5136,7 +1969,6 @@ async def statut_queue(): } - @app.get("/prospects", tags=["Prospects"]) async def rechercher_prospects(query: Optional[str] = Query(None)): try: @@ -5161,8 +1993,9 @@ async def lire_prospect(code: str): raise HTTPException(500, str(e)) - -@app.get("/fournisseurs", response_model=List[FournisseurDetails], tags=["Fournisseurs"]) +@app.get( + "/fournisseurs", response_model=List[FournisseurDetails], tags=["Fournisseurs"] +) async def rechercher_fournisseurs(query: Optional[str] = Query(None)): try: fournisseurs = sage_client.lister_fournisseurs(filtre=query or "") @@ -5204,7 +2037,9 @@ async def ajouter_fournisseur( raise HTTPException(500, str(e)) -@app.put("/fournisseurs/{code}", response_model=FournisseurDetails, tags=["Fournisseurs"]) +@app.put( + "/fournisseurs/{code}", response_model=FournisseurDetails, tags=["Fournisseurs"] +) async def modifier_fournisseur( code: str, fournisseur_update: FournisseurUpdateRequest, @@ -5241,7 +2076,6 @@ async def lire_fournisseur(code: str): raise HTTPException(500, str(e)) - @app.get("/avoirs", tags=["Avoirs"]) async def lister_avoirs( limit: int = Query(100, le=1000), statut: Optional[int] = Query(None) @@ -5360,7 +2194,6 @@ async def modifier_avoir( raise HTTPException(500, str(e)) - @app.get("/livraisons", tags=["Livraisons"]) async def lister_livraisons( limit: int = Query(100, le=1000), statut: Optional[int] = Query(None) @@ -5654,7 +2487,7 @@ async def commande_vers_livraison( summary="Liste toutes les familles d'articles", ) async def lister_familles( - filtre: Optional[str] = Query(None, description="Filtre sur code ou intitulé") + filtre: Optional[str] = Query(None, description="Filtre sur code ou intitulé"), ): try: familles = sage_client.lister_familles(filtre or "") @@ -5678,7 +2511,7 @@ async def lister_familles( summary="Lecture d'une famille par son code", ) async def lire_famille( - code: str = Path(..., description="Code de la famille (ex: ZDIVERS)") + code: str = Path(..., description="Code de la famille (ex: ZDIVERS)"), ): try: famille = sage_client.lire_famille(code) @@ -5817,7 +2650,7 @@ async def creer_sortie_stock(sortie: SortieStockRequest): summary="Lecture d'un mouvement de stock", ) async def lire_mouvement_stock( - numero: str = Path(..., description="Numéro du mouvement (ex: ME00123 ou MS00124)") + numero: str = Path(..., description="Numéro du mouvement (ex: ME00123 ou MS00124)"), ): try: mouvement = sage_client.lire_mouvement_stock(numero) @@ -5942,7 +2775,9 @@ async def statistiques_utilisateurs(session: AsyncSession = Depends(get_session) "utilisateurs_actifs": active, "utilisateurs_non_verifies": total - verified, "repartition_roles": roles_stats, - "taux_verification": f"{(verified/total*100):.1f}%" if total > 0 else "0%", + "taux_verification": f"{(verified / total * 100):.1f}%" + if total > 0 + else "0%", } except Exception as e: @@ -5994,7 +2829,6 @@ async def get_document_pdf( raise HTTPException(500, str(e)) - @app.post("/tiers/{numero}/contacts", response_model=Contact, tags=["Contacts"]) async def creer_contact(numero: str, contact: ContactCreate): try: @@ -6002,13 +2836,13 @@ async def creer_contact(numero: str, contact: ContactCreate): sage_client.lire_tiers(numero) except: raise HTTPException(404, f"Tiers {numero} non trouvé") - + if contact.numero != numero: contact.numero = numero - + resultat = sage_client.creer_contact(contact.dict()) return Contact(**resultat) - + except HTTPException: raise except Exception as e: @@ -6026,12 +2860,18 @@ async def lister_contacts(numero: str): raise HTTPException(500, str(e)) -@app.get("/tiers/{numero}/contacts/{contact_numero}", response_model=Contact, tags=["Contacts"]) +@app.get( + "/tiers/{numero}/contacts/{contact_numero}", + response_model=Contact, + tags=["Contacts"], +) async def obtenir_contact(numero: str, contact_numero: int): try: contact = sage_client.obtenir_contact(numero, contact_numero) if not contact: - raise HTTPException(404, f"Contact {contact_numero} non trouvé pour client {numero}") + raise HTTPException( + 404, f"Contact {contact_numero} non trouvé pour client {numero}" + ) return Contact(**contact) except HTTPException: raise @@ -6040,21 +2880,25 @@ async def obtenir_contact(numero: str, contact_numero: int): raise HTTPException(500, str(e)) -@app.put("/tiers/{numero}/contacts/{contact_numero}", response_model=Contact, tags=["Contacts"]) +@app.put( + "/tiers/{numero}/contacts/{contact_numero}", + response_model=Contact, + tags=["Contacts"], +) async def modifier_contact(numero: str, contact_numero: int, contact: ContactUpdate): try: contact_existant = sage_client.obtenir_contact(numero, contact_numero) if not contact_existant: raise HTTPException(404, f"Contact {contact_numero} non trouvé") - + updates = {k: v for k, v in contact.dict().items() if v is not None} - + if not updates: raise HTTPException(400, "Aucune modification fournie") - + resultat = sage_client.modifier_contact(numero, contact_numero, updates) return Contact(**resultat) - + except HTTPException: raise except Exception as e: @@ -6077,9 +2921,9 @@ async def definir_contact_defaut(numero: str, contact_numero: int): try: resultat = sage_client.definir_contact_defaut(numero, contact_numero) return { - "success": True, + "success": True, "message": f"Contact {contact_numero} défini comme contact par défaut", - "data": resultat + "data": resultat, } except Exception as e: logger.error(f"Erreur définition contact par défaut: {e}") @@ -6089,20 +2933,14 @@ async def definir_contact_defaut(numero: str, contact_numero: int): @app.get("/tiers", response_model=List[TiersDetails], tags=["Tiers"]) async def obtenir_tiers( type_tiers: Optional[str] = Query( - None, - description="Filtre par type: 0/client, 1/fournisseur, 2/prospect, 3/all ou strings" + None, + description="Filtre par type: 0/client, 1/fournisseur, 2/prospect, 3/all ou strings", ), - query: Optional[str] = Query( - None, - description="Recherche sur code ou intitulé" - ) + query: Optional[str] = Query(None, description="Recherche sur code ou intitulé"), ): try: type_normalise = normaliser_type_tiers(type_tiers) - tiers = sage_client.lister_tiers( - type_tiers=type_normalise, - filtre=query or "" - ) + tiers = sage_client.lister_tiers(type_tiers=type_normalise, filtre=query or "") return [TiersDetails(**t) for t in tiers] except Exception as e: logger.error(f"Erreur recherche tiers: {e}") @@ -6111,7 +2949,6 @@ async def obtenir_tiers( @app.get("/tiers/{code}", response_model=TiersDetails, tags=["Tiers"]) async def lire_tiers_detail(code: str): - try: tiers = sage_client.lire_tiers(code) if not tiers: @@ -6122,12 +2959,12 @@ async def lire_tiers_detail(code: str): except Exception as e: logger.error(f"Erreur lecture tiers {code}: {e}") raise HTTPException(500, str(e)) - - + + if __name__ == "__main__": uvicorn.run( "api:app", host=settings.api_host, port=settings.api_port, reload=settings.api_reload, - ) \ No newline at end of file + ) diff --git a/data/data.py b/data/data.py new file mode 100644 index 0000000..a39ea55 --- /dev/null +++ b/data/data.py @@ -0,0 +1,377 @@ +TAGS_METADATA = [ + { + "name": "Clients", + "description": "Gestion des clients (recherche, création, modification)", + }, + {"name": "Articles", "description": "Gestion des articles et produits"}, + {"name": "Devis", "description": "Création, consultation et gestion des devis"}, + { + "name": "Commandes", + "description": "Création, consultation et gestion des commandes", + }, + { + "name": "Livraisons", + "description": "Création, consultation et gestion des bons de livraison", + }, + { + "name": "Factures", + "description": "Création, consultation et gestion des factures", + }, + {"name": "Avoirs", "description": "Création, consultation et gestion des avoirs"}, + {"name": "Fournisseurs", "description": "Gestion des fournisseurs"}, + {"name": "Prospects", "description": "Gestion des prospects"}, + { + "name": "Workflows", + "description": "Transformations de documents (devis→commande, commande→facture, etc.)", + }, + {"name": "Signatures", "description": "Signature électronique via Universign"}, + {"name": "Emails", "description": "Envoi d'emails, templates et logs d'envoi"}, + {"name": "Validation", "description": "Validation de données (remises, etc.)"}, + {"name": "Admin", "description": "🔧 Administration système (cache, queue)"}, + {"name": "System", "description": "🏥 Health checks et informations système"}, + {"name": "Debug", "description": "🐛 Routes de debug et diagnostics"}, +] + +templates_signature_email = { + "demande_signature": { + "id": "demande_signature", + "nom": "Demande de Signature Électronique", + "sujet": "Signature requise - {{TYPE_DOC}} {{NUMERO}}", + "corps_html": """ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+

+ Signature Électronique Requise +

+
+

+ Bonjour {{NOM_SIGNATAIRE}}, +

+ +

+ Nous vous invitons à signer électroniquement le document suivant : +

+ + + + + + +
+ + + + + + + + + + + + + + + + + +
Type de document{{TYPE_DOC}}
Numéro{{NUMERO}}
Date{{DATE}}
Montant TTC{{MONTANT_TTC}} €
+
+ +

+ Cliquez sur le bouton ci-dessous pour accéder au document et apposer votre signature électronique sécurisée : +

+ + + + + + +
+ + ✍️ Signer le document + +
+ + + + + + +
+

+ ⏰ Important : Ce lien de signature est valable pendant 30 jours. + Nous vous recommandons de signer ce document dès que possible. +

+
+ +

+ 🔒 Signature électronique sécurisée
+ Votre signature est protégée par notre partenaire de confiance Universign, + certifié eIDAS et conforme au RGPD. Votre identité sera vérifiée et le document sera + horodaté de manière infalsifiable. +

+
+

+ Vous avez des questions ? Contactez-nous à {{CONTACT_EMAIL}} +

+

+ Cet email a été envoyé automatiquement par le système Sage 100c Dataven.
+ Si vous avez reçu cet email par erreur, veuillez nous en informer. +

+
+
+ + + """, + "variables_disponibles": [ + "NOM_SIGNATAIRE", + "TYPE_DOC", + "NUMERO", + "DATE", + "MONTANT_TTC", + "SIGNER_URL", + "CONTACT_EMAIL", + ], + }, + "signature_confirmee": { + "id": "signature_confirmee", + "nom": "Confirmation de Signature", + "sujet": "Document signé - {{TYPE_DOC}} {{NUMERO}}", + "corps_html": """ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+

+ Document Signé avec Succès +

+
+

+ Bonjour {{NOM_SIGNATAIRE}}, +

+ +

+ Nous confirmons la signature électronique du document suivant : +

+ + + + + + +
+ + + + + + + + + + + + + +
Document{{TYPE_DOC}} {{NUMERO}}
Signé le{{DATE_SIGNATURE}}
ID Transaction{{TRANSACTION_ID}}
+
+ +

+ Le document signé a été automatiquement archivé et est disponible dans votre espace client. + Un certificat de signature électronique conforme eIDAS a été généré. +

+ + + + + +
+

+ Signature certifiée : Ce document a été signé avec une signature + électronique qualifiée, ayant la même valeur juridique qu'une signature manuscrite + conformément au règlement eIDAS. +

+
+ +

+ Merci pour votre confiance. Notre équipe reste à votre disposition pour toute question. +

+
+

+ Contact : {{CONTACT_EMAIL}} +

+

+ Sage 100c Dataven - Système de signature électronique sécurisée +

+
+
+ + + """, + "variables_disponibles": [ + "NOM_SIGNATAIRE", + "TYPE_DOC", + "NUMERO", + "DATE_SIGNATURE", + "TRANSACTION_ID", + "CONTACT_EMAIL", + ], + }, + "relance_signature": { + "id": "relance_signature", + "nom": "Relance Signature en Attente", + "sujet": "⏰ Rappel - Signature en attente {{TYPE_DOC}} {{NUMERO}}", + "corps_html": """ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+

+ ⏰ Signature en Attente +

+
+

+ Bonjour {{NOM_SIGNATAIRE}}, +

+ +

+ Nous vous avons envoyé il y a {{NB_JOURS}} jours un document à signer électroniquement. + Nous constatons que celui-ci n'a pas encore été signé. +

+ + + + + + +
+

+ Document en attente : {{TYPE_DOC}} {{NUMERO}} +

+

+ ⏳ Le lien de signature expirera dans {{JOURS_RESTANTS}} jours +

+
+ +

+ Pour éviter tout retard dans le traitement de votre dossier, nous vous invitons à signer ce document dès maintenant : +

+ + + + + + +
+ + ✍️ Signer maintenant + +
+ +

+ Si vous rencontrez des difficultés ou avez des questions, n'hésitez pas à nous contacter. +

+
+

+ Contact : {{CONTACT_EMAIL}} +

+

+ Sage 100c Dataven - Relance automatique +

+
+
+ + + """, + "variables_disponibles": [ + "NOM_SIGNATAIRE", + "TYPE_DOC", + "NUMERO", + "NB_JOURS", + "JOURS_RESTANTS", + "SIGNER_URL", + "CONTACT_EMAIL", + ], + }, +} diff --git a/schemas/__init__.py b/schemas/__init__.py index b46d066..006d7b8 100644 --- a/schemas/__init__.py +++ b/schemas/__init__.py @@ -1,7 +1,118 @@ -from schemas.tiers.tiers import (TiersDetails,) -from schemas.tiers.type_tiers import (TypeTiers,) +from schemas.tiers.tiers import ( + TiersDetails, + TypeTiersInt +) +from schemas.tiers.type_tiers import TypeTiers +from schemas.schema_mixte import BaremeRemiseResponse +from schemas.user import UserResponse +from schemas.tiers.clients import ( + ClientCreateRequest, + ClientDetails, + ClientResponse, + ClientUpdateRequest +) +from schemas.tiers.contact import ( + Contact, + ContactCreate, + ContactUpdate + ) +from schemas.tiers.fournisseurs import ( + FournisseurCreateAPIRequest, + FournisseurDetails, + FournisseurUpdateRequest +) +from schemas.documents.avoirs import ( + AvoirCreateRequest, + AvoirUpdateRequest +) +from schemas.documents.commandes import ( + CommandeCreateRequest, + CommandeUpdateRequest +) +from schemas.documents.devis import ( + DevisRequest, + DevisResponse, + DevisUpdateRequest, + RelanceDevisRequest +) +from schemas.documents.documents import ( + TypeDocument, + TypeDocumentSQL +) +from schemas.documents.email import ( + StatutEmail, + EmailEnvoiRequest +) +from schemas.documents.factures import ( + FactureCreateRequest, + FactureUpdateRequest +) +from schemas.documents.livraisons import ( + LivraisonCreateRequest, + LivraisonUpdateRequest +) +from schemas.documents.universign import ( + SignatureRequest, + StatutSignature +) +from schemas.articles.articles import ( + ArticleCreateRequest, + ArticleResponse, + ArticleUpdateRequest, + ArticleListResponse, + EntreeStockRequest, + SortieStockRequest, + MouvementStockResponse +) +from schemas.articles.famille_article import ( + FamilleResponse, + FamilleCreateRequest, + FamilleListResponse +) + __all__ = [ "TiersDetails", "TypeTiers", + "BaremeRemiseResponse", + "UserResponse", + "ClientCreateRequest", + "ClientDetails", + "ClientResponse", + "ClientUpdateRequest", + "FournisseurCreateAPIRequest", + "FournisseurDetails", + "FournisseurUpdateRequest", + "Contact", + "AvoirCreateRequest", + "AvoirUpdateRequest", + "CommandeCreateRequest", + "CommandeUpdateRequest", + "DevisRequest", + "DevisResponse", + "DevisUpdateRequest", + "TypeDocument", + "TypeDocumentSQL", + "StatutEmail", + "EmailEnvoiRequest", + "FactureCreateRequest", + "FactureUpdateRequest", + "LivraisonCreateRequest", + "LivraisonUpdateRequest", + "SignatureRequest", + "StatutSignature", + "TypeTiersInt", + "ArticleCreateRequest", + "ArticleResponse", + "ArticleUpdateRequest", + "ArticleListResponse", + "EntreeStockRequest", + "SortieStockRequest", + "MouvementStockResponse", + "RelanceDevisRequest", + "FamilleResponse", + "FamilleCreateRequest", + "FamilleListResponse", + "ContactCreate", + "ContactUpdate" ] \ No newline at end of file diff --git a/schemas/articles/articles.py b/schemas/articles/articles.py new file mode 100644 index 0000000..0c98d21 --- /dev/null +++ b/schemas/articles/articles.py @@ -0,0 +1,948 @@ +from pydantic import BaseModel, Field, EmailStr, validator, field_validator +from typing import List, Optional, Dict, ClassVar, Any +from datetime import date, datetime +from enum import Enum, IntEnum + + +class EmplacementStockModel(BaseModel): + """Détail du stock dans un emplacement spécifique""" + + depot: str = Field(..., description="Numéro du dépôt (DE_No)") + emplacement: str = Field(..., description="Code emplacement (DP_No)") + + qte_stockee: float = Field(0.0, description="Quantité stockée (AE_QteSto)") + qte_preparee: float = Field(0.0, description="Quantité préparée (AE_QtePrepa)") + qte_a_controler: float = Field( + 0.0, description="Quantité à contrôler (AE_QteAControler)" + ) + + date_creation: Optional[datetime] = Field(None, description="Date création") + date_modification: Optional[datetime] = Field(None, description="Date modification") + + depot_num: Optional[str] = Field(None, description="Numéro dépôt") + depot_nom: Optional[str] = Field(None, description="Nom du dépôt (DE_Intitule)") + depot_code: Optional[str] = Field(None, description="Code dépôt (DE_Code)") + depot_adresse: Optional[str] = Field(None, description="Adresse (DE_Adresse)") + depot_complement: Optional[str] = Field(None, description="Complément adresse") + depot_code_postal: Optional[str] = Field(None, description="Code postal") + depot_ville: Optional[str] = Field(None, description="Ville") + depot_contact: Optional[str] = Field(None, description="Contact") + depot_est_principal: Optional[bool] = Field( + None, description="Dépôt principal (DE_Principal)" + ) + depot_categorie_compta: Optional[int] = Field( + None, description="Catégorie comptable" + ) + depot_region: Optional[str] = Field(None, description="Région") + depot_pays: Optional[str] = Field(None, description="Pays") + depot_email: Optional[str] = Field(None, description="Email") + depot_telephone: Optional[str] = Field(None, description="Téléphone") + depot_fax: Optional[str] = Field(None, description="Fax") + depot_emplacement_defaut: Optional[str] = Field( + None, description="Emplacement par défaut" + ) + depot_exclu: Optional[bool] = Field(None, description="Dépôt exclu") + + emplacement_code: Optional[str] = Field( + None, description="Code emplacement (DP_Code)" + ) + emplacement_libelle: Optional[str] = Field( + None, description="Libellé emplacement (DP_Intitule)" + ) + emplacement_zone: Optional[str] = Field(None, description="Zone (DP_Zone)") + emplacement_type: Optional[int] = Field( + None, description="Type emplacement (DP_Type)" + ) + + class Config: + json_schema_extra = { + "example": { + "depot": "01", + "emplacement": "A1-01", + "qte_stockee": 100.0, + "qte_preparee": 5.0, + "depot_nom": "Dépôt principal", + "depot_ville": "Paris", + "emplacement_libelle": "Allée A, Niveau 1, Case 01", + "emplacement_zone": "Zone A", + } + } + + +class GammeArticleModel(BaseModel): + """Gamme d'un article (taille, couleur, etc.)""" + + numero_gamme: int = Field(..., description="Numéro de gamme (AG_No)") + enumere: str = Field(..., description="Code énuméré (EG_Enumere)") + type_gamme: int = Field(0, description="Type de gamme (AG_Type)") + + date_creation: Optional[datetime] = Field(None, description="Date création") + date_modification: Optional[datetime] = Field(None, description="Date modification") + + ligne: Optional[int] = Field(None, description="Ligne énuméré (EG_Ligne)") + borne_sup: Optional[float] = Field( + None, description="Borne supérieure (EG_BorneSup)" + ) + gamme_nom: Optional[str] = Field( + None, description="Nom de la gamme (P_GAMME.G_Intitule)" + ) + + class Config: + json_schema_extra = { + "example": { + "numero_gamme": 1, + "enumere": "001", + "type_gamme": 0, + "ligne": 1, + "gamme_nom": "Taille", + } + } + + +class TarifClientModel(BaseModel): + """Tarif spécifique pour un client ou catégorie tarifaire""" + + categorie: int = Field(..., description="Catégorie tarifaire (AC_Categorie)") + client_num: Optional[str] = Field(None, description="Numéro client (CT_Num)") + + prix_vente: float = Field(0.0, description="Prix de vente HT (AC_PrixVen)") + coefficient: float = Field(0.0, description="Coefficient (AC_Coef)") + prix_ttc: float = Field(0.0, description="Prix TTC (AC_PrixTTC)") + arrondi: float = Field(0.0, description="Arrondi (AC_Arrondi)") + qte_montant: float = Field(0.0, description="Quantité montant (AC_QteMont)") + + enumere_gamme: int = Field(0, description="Énuméré gamme (EG_Champ)") + prix_devise: float = Field(0.0, description="Prix en devise (AC_PrixDev)") + devise: int = Field(0, description="Code devise (AC_Devise)") + + remise: float = Field(0.0, description="Remise (AC_Remise)") + mode_calcul: int = Field(0, description="Mode de calcul (AC_Calcul)") + type_remise: int = Field(0, description="Type de remise (AC_TypeRem)") + ref_client: Optional[str] = Field( + None, description="Référence client (AC_RefClient)" + ) + + coef_nouveau: float = Field(0.0, description="Nouveau coefficient (AC_CoefNouv)") + prix_vente_nouveau: float = Field( + 0.0, description="Nouveau prix vente (AC_PrixVenNouv)" + ) + prix_devise_nouveau: float = Field( + 0.0, description="Nouveau prix devise (AC_PrixDevNouv)" + ) + remise_nouvelle: float = Field(0.0, description="Nouvelle remise (AC_RemiseNouv)") + date_application: Optional[datetime] = Field( + None, description="Date application (AC_DateApplication)" + ) + + date_creation: Optional[datetime] = Field(None, description="Date création") + date_modification: Optional[datetime] = Field(None, description="Date modification") + + class Config: + json_schema_extra = { + "example": { + "categorie": 1, + "client_num": "CLI001", + "prix_vente": 110.00, + "coefficient": 1.294, + "remise": 12.0, + } + } + + +class ComposantModel(BaseModel): + """Composant/Opération de nomenclature""" + + operation: str = Field(..., description="Code opération (AT_Operation)") + code_ressource: Optional[str] = Field(None, description="Code ressource (RP_Code)") + + temps: float = Field(0.0, description="Temps nécessaire (AT_Temps)") + type: int = Field(0, description="Type composant (AT_Type)") + description: Optional[str] = Field(None, description="Description (AT_Description)") + ordre: int = Field(0, description="Ordre d'exécution (AT_Ordre)") + + gamme_1_comp: int = Field(0, description="Gamme 1 composant (AG_No1Comp)") + gamme_2_comp: int = Field(0, description="Gamme 2 composant (AG_No2Comp)") + + type_ressource: int = Field(0, description="Type ressource (AT_TypeRessource)") + chevauche: int = Field(0, description="Chevauchement (AT_Chevauche)") + demarre: int = Field(0, description="Démarrage (AT_Demarre)") + operation_chevauche: Optional[str] = Field( + None, description="Opération chevauchée (AT_OperationChevauche)" + ) + valeur_chevauche: float = Field( + 0.0, description="Valeur chevauchement (AT_ValeurChevauche)" + ) + type_chevauche: int = Field(0, description="Type chevauchement (AT_TypeChevauche)") + + date_creation: Optional[datetime] = Field(None, description="Date création") + date_modification: Optional[datetime] = Field(None, description="Date modification") + + class Config: + json_schema_extra = { + "example": { + "operation": "OP010", + "code_ressource": "RES01", + "temps": 15.5, + "description": "Montage pièce A", + "ordre": 10, + } + } + + +class ComptaArticleModel(BaseModel): + """Comptabilité spécifique d'un article""" + + champ: int = Field(..., description="Champ (ACP_Champ)") + compte_general: Optional[str] = Field( + None, description="Compte général (ACP_ComptaCPT_CompteG)" + ) + compte_auxiliaire: Optional[str] = Field( + None, description="Compte auxiliaire (ACP_ComptaCPT_CompteA)" + ) + + taxe_1: Optional[str] = Field(None, description="Taxe 1 (ACP_ComptaCPT_Taxe1)") + taxe_2: Optional[str] = Field(None, description="Taxe 2 (ACP_ComptaCPT_Taxe2)") + taxe_3: Optional[str] = Field(None, description="Taxe 3 (ACP_ComptaCPT_Taxe3)") + + taxe_date_1: Optional[datetime] = Field(None, description="Date taxe 1") + taxe_date_2: Optional[datetime] = Field(None, description="Date taxe 2") + taxe_date_3: Optional[datetime] = Field(None, description="Date taxe 3") + + taxe_anc_1: Optional[str] = Field(None, description="Ancienne taxe 1") + taxe_anc_2: Optional[str] = Field(None, description="Ancienne taxe 2") + taxe_anc_3: Optional[str] = Field(None, description="Ancienne taxe 3") + + type_facture: int = Field(0, description="Type de facture (ACP_TypeFacture)") + + date_creation: Optional[datetime] = Field(None, description="Date création") + date_modification: Optional[datetime] = Field(None, description="Date modification") + + class Config: + json_schema_extra = { + "example": { + "champ": 1, + "compte_general": "707100", + "taxe_1": "TVA20", + "type_facture": 0, + } + } + + +class FournisseurArticleModel(BaseModel): + """Fournisseur d'un article""" + + fournisseur_num: str = Field(..., description="Numéro fournisseur (CT_Num)") + ref_fournisseur: Optional[str] = Field( + None, description="Référence fournisseur (AF_RefFourniss)" + ) + + prix_achat: float = Field(0.0, description="Prix d'achat (AF_PrixAch)") + unite: Optional[str] = Field(None, description="Unité (AF_Unite)") + conversion: float = Field(0.0, description="Conversion (AF_Conversion)") + + delai_appro: int = Field(0, description="Délai approvisionnement (AF_DelaiAppro)") + garantie: int = Field(0, description="Garantie (AF_Garantie)") + colisage: int = Field(0, description="Colisage (AF_Colisage)") + qte_mini: float = Field(0.0, description="Quantité minimum (AF_QteMini)") + qte_montant: float = Field(0.0, description="Quantité montant (AF_QteMont)") + + enumere_gamme: int = Field(0, description="Énuméré gamme (EG_Champ)") + est_principal: bool = Field( + False, description="Fournisseur principal (AF_Principal)" + ) + + prix_devise: float = Field(0.0, description="Prix devise (AF_PrixDev)") + devise: int = Field(0, description="Code devise (AF_Devise)") + remise: float = Field(0.0, description="Remise (AF_Remise)") + conversion_devise: float = Field(0.0, description="Conversion devise (AF_ConvDiv)") + type_remise: int = Field(0, description="Type remise (AF_TypeRem)") + + code_barre_fournisseur: Optional[str] = Field( + None, description="Code-barres fournisseur (AF_CodeBarre)" + ) + + prix_achat_nouveau: float = Field( + 0.0, description="Nouveau prix achat (AF_PrixAchNouv)" + ) + prix_devise_nouveau: float = Field( + 0.0, description="Nouveau prix devise (AF_PrixDevNouv)" + ) + remise_nouvelle: float = Field(0.0, description="Nouvelle remise (AF_RemiseNouv)") + date_application: Optional[datetime] = Field( + None, description="Date application (AF_DateApplication)" + ) + + date_creation: Optional[datetime] = Field(None, description="Date création") + date_modification: Optional[datetime] = Field(None, description="Date modification") + + class Config: + json_schema_extra = { + "example": { + "fournisseur_num": "F001", + "ref_fournisseur": "REF-FOURN-001", + "prix_achat": 85.00, + "delai_appro": 15, + "est_principal": True, + } + } + + +class ReferenceEnumereeModel(BaseModel): + """Référence énumérée (article avec gammes)""" + + gamme_1: int = Field(0, description="Gamme 1 (AG_No1)") + gamme_2: int = Field(0, description="Gamme 2 (AG_No2)") + reference_enumeree: str = Field(..., description="Référence énumérée (AE_Ref)") + + prix_achat: float = Field(0.0, description="Prix achat (AE_PrixAch)") + code_barre: Optional[str] = Field(None, description="Code-barres (AE_CodeBarre)") + prix_achat_nouveau: float = Field( + 0.0, description="Nouveau prix achat (AE_PrixAchNouv)" + ) + edi_code: Optional[str] = Field(None, description="Code EDI (AE_EdiCode)") + en_sommeil: bool = Field(False, description="En sommeil (AE_Sommeil)") + + date_creation: Optional[datetime] = Field(None, description="Date création") + date_modification: Optional[datetime] = Field(None, description="Date modification") + + class Config: + json_schema_extra = { + "example": { + "gamme_1": 1, + "gamme_2": 3, + "reference_enumeree": "ART001-T1-C3", + "prix_achat": 85.00, + } + } + + +class MediaArticleModel(BaseModel): + """Média attaché à un article (photo, document, etc.)""" + + commentaire: Optional[str] = Field(None, description="Commentaire (ME_Commentaire)") + fichier: Optional[str] = Field(None, description="Nom fichier (ME_Fichier)") + type_mime: Optional[str] = Field(None, description="Type MIME (ME_TypeMIME)") + origine: int = Field(0, description="Origine (ME_Origine)") + ged_id: Optional[str] = Field(None, description="ID GED (ME_GedId)") + + date_creation: Optional[datetime] = Field(None, description="Date création") + date_modification: Optional[datetime] = Field(None, description="Date modification") + + class Config: + json_schema_extra = { + "example": { + "commentaire": "Photo produit principale", + "fichier": "ART001_photo1.jpg", + "type_mime": "image/jpeg", + } + } + + +class PrixGammeModel(BaseModel): + """Prix spécifique par combinaison de gammes""" + + gamme_1: int = Field(0, description="Gamme 1 (AG_No1)") + gamme_2: int = Field(0, description="Gamme 2 (AG_No2)") + prix_net: float = Field(0.0, description="Prix net (AR_PUNet)") + cout_standard: float = Field(0.0, description="Coût standard (AR_CoutStd)") + + date_creation: Optional[datetime] = Field(None, description="Date création") + date_modification: Optional[datetime] = Field(None, description="Date modification") + + class Config: + json_schema_extra = { + "example": { + "gamme_1": 1, + "gamme_2": 3, + "prix_net": 125.50, + "cout_standard": 82.30, + } + } + + +class ArticleResponse(BaseModel): + """Article complet avec tous les enrichissements disponibles""" + + reference: str = Field(..., description="Référence article (AR_Ref)") + designation: str = Field(..., description="Désignation principale (AR_Design)") + + code_ean: Optional[str] = Field( + None, description="Code EAN / Code-barres principal (AR_CodeBarre)" + ) + code_barre: Optional[str] = Field( + None, description="Code-barres (alias de code_ean)" + ) + edi_code: Optional[str] = Field(None, description="Code EDI (AR_EdiCode)") + raccourci: Optional[str] = Field(None, description="Code raccourci (AR_Raccourci)") + + prix_vente: float = Field(..., description="Prix de vente HT unitaire (AR_PrixVen)") + prix_achat: Optional[float] = Field( + None, description="Prix d'achat HT (AR_PrixAch)" + ) + coef: Optional[float] = Field( + None, description="Coefficient multiplicateur (AR_Coef)" + ) + prix_net: Optional[float] = Field(None, description="Prix unitaire net (AR_PUNet)") + + prix_achat_nouveau: Optional[float] = Field( + None, description="Nouveau prix d'achat à venir (AR_PrixAchNouv)" + ) + coef_nouveau: Optional[float] = Field( + None, description="Nouveau coefficient à venir (AR_CoefNouv)" + ) + prix_vente_nouveau: Optional[float] = Field( + None, description="Nouveau prix de vente à venir (AR_PrixVenNouv)" + ) + date_application_prix: Optional[str] = Field( + None, description="Date d'application des nouveaux prix (AR_DateApplication)" + ) + + cout_standard: Optional[float] = Field( + None, description="Coût standard (AR_CoutStd)" + ) + + stock_reel: float = Field( + default=0.0, description="Stock réel total (F_ARTSTOCK.AS_QteSto)" + ) + stock_mini: Optional[float] = Field( + None, description="Stock minimum (F_ARTSTOCK.AS_QteMini)" + ) + stock_maxi: Optional[float] = Field( + None, description="Stock maximum (F_ARTSTOCK.AS_QteMaxi)" + ) + stock_reserve: Optional[float] = Field( + None, description="Stock réservé / en commande client (F_ARTSTOCK.AS_QteRes)" + ) + stock_commande: Optional[float] = Field( + None, description="Stock en commande fournisseur (F_ARTSTOCK.AS_QteCom)" + ) + stock_disponible: Optional[float] = Field( + None, description="Stock disponible = réel - réservé" + ) + + emplacements: List[EmplacementStockModel] = Field( + default_factory=list, + description="Détail du stock par emplacement (F_ARTSTOCKEMPL + F_DEPOT + F_DEPOTEMPL)", + ) + nb_emplacements: int = Field(0, description="Nombre d'emplacements") + + suivi_stock: Optional[bool] = Field( + None, description="Suivi de stock activé (AR_SuiviStock)" + ) + nomenclature: Optional[bool] = Field( + None, description="Article avec nomenclature (AR_Nomencl)" + ) + qte_composant: Optional[float] = Field( + None, description="Quantité de composant (AR_QteComp)" + ) + qte_operatoire: Optional[float] = Field( + None, description="Quantité opératoire (AR_QteOperatoire)" + ) + + unite_vente: Optional[str] = Field( + None, max_length=10, description="Unité de vente (AR_UniteVen)" + ) + unite_poids: Optional[str] = Field( + None, max_length=10, description="Unité de poids (AR_UnitePoids)" + ) + poids_net: Optional[float] = Field( + None, description="Poids net unitaire en kg (AR_PoidsNet)" + ) + poids_brut: Optional[float] = Field( + None, description="Poids brut unitaire en kg (AR_PoidsBrut)" + ) + + gamme_1: Optional[str] = Field(None, description="Énumération gamme 1 (AR_Gamme1)") + gamme_2: Optional[str] = Field(None, description="Énumération gamme 2 (AR_Gamme2)") + + gammes: List[GammeArticleModel] = Field( + default_factory=list, + description="Détail des gammes (F_ARTGAMME + F_ENUMGAMME + P_GAMME)", + ) + nb_gammes: int = Field(0, description="Nombre de gammes") + + tarifs_clients: List[TarifClientModel] = Field( + default_factory=list, + description="Tarifs spécifiques par client/catégorie (F_ARTCLIENT)", + ) + nb_tarifs_clients: int = Field(0, description="Nombre de tarifs clients") + + composants: List[ComposantModel] = Field( + default_factory=list, + description="Composants/Opérations de production (F_ARTCOMPO)", + ) + nb_composants: int = Field(0, description="Nombre de composants") + + compta_vente: List[ComptaArticleModel] = Field( + default_factory=list, description="Comptabilité vente (F_ARTCOMPTA type 0)" + ) + compta_achat: List[ComptaArticleModel] = Field( + default_factory=list, description="Comptabilité achat (F_ARTCOMPTA type 1)" + ) + compta_stock: List[ComptaArticleModel] = Field( + default_factory=list, description="Comptabilité stock (F_ARTCOMPTA type 2)" + ) + + fournisseurs: List[FournisseurArticleModel] = Field( + default_factory=list, + description="Tous les fournisseurs de l'article (F_ARTFOURNISS)", + ) + nb_fournisseurs: int = Field(0, description="Nombre de fournisseurs") + + refs_enumerees: List[ReferenceEnumereeModel] = Field( + default_factory=list, description="Références énumérées (F_ARTENUMREF)" + ) + nb_refs_enumerees: int = Field(0, description="Nombre de références énumérées") + + medias: List[MediaArticleModel] = Field( + default_factory=list, description="Médias attachés (F_ARTICLEMEDIA)" + ) + nb_medias: int = Field(0, description="Nombre de médias") + + prix_gammes: List[PrixGammeModel] = Field( + default_factory=list, description="Prix par combinaison de gammes (F_ARTPRIX)" + ) + nb_prix_gammes: int = Field(0, description="Nombre de prix par gammes") + + type_article: Optional[int] = Field( + None, + ge=0, + le=3, + description="Type : 0=Article, 1=Prestation, 2=Divers, 3=Nomenclature (AR_Type)", + ) + type_article_libelle: Optional[str] = Field( + None, description="Libellé du type d'article" + ) + + famille_code: Optional[str] = Field( + None, max_length=20, description="Code famille (FA_CodeFamille)" + ) + famille_libelle: Optional[str] = Field( + None, description="Libellé de la famille (F_FAMILLE.FA_Intitule)" + ) + famille_type: Optional[int] = Field( + None, description="Type de famille : 0=Détail, 1=Total (F_FAMILLE.FA_Type)" + ) + famille_unite_vente: Optional[str] = Field( + None, description="Unité de vente de la famille (F_FAMILLE.FA_UniteVen)" + ) + famille_coef: Optional[float] = Field( + None, description="Coefficient de la famille (F_FAMILLE.FA_Coef)" + ) + famille_suivi_stock: Optional[bool] = Field( + None, description="Suivi stock de la famille (F_FAMILLE.FA_SuiviStock)" + ) + famille_garantie: Optional[int] = Field( + None, description="Garantie de la famille (F_FAMILLE.FA_Garantie)" + ) + famille_unite_poids: Optional[str] = Field( + None, description="Unité de poids de la famille (F_FAMILLE.FA_UnitePoids)" + ) + famille_delai: Optional[int] = Field( + None, description="Délai de la famille (F_FAMILLE.FA_Delai)" + ) + famille_nb_colis: Optional[int] = Field( + None, description="Nombre de colis de la famille (F_FAMILLE.FA_NbColis)" + ) + famille_code_fiscal: Optional[str] = Field( + None, description="Code fiscal de la famille (F_FAMILLE.FA_CodeFiscal)" + ) + famille_escompte: Optional[bool] = Field( + None, description="Escompte de la famille (F_FAMILLE.FA_Escompte)" + ) + famille_centrale: Optional[bool] = Field( + None, description="Famille centrale (F_FAMILLE.FA_Central)" + ) + famille_nature: Optional[int] = Field( + None, description="Nature de la famille (F_FAMILLE.FA_Nature)" + ) + famille_hors_stat: Optional[bool] = Field( + None, description="Hors statistique famille (F_FAMILLE.FA_HorsStat)" + ) + famille_pays: Optional[str] = Field( + None, description="Pays de la famille (F_FAMILLE.FA_Pays)" + ) + + nature: Optional[int] = Field(None, description="Nature de l'article (AR_Nature)") + garantie: Optional[int] = Field( + None, description="Durée de garantie en mois (AR_Garantie)" + ) + code_fiscal: Optional[str] = Field( + None, max_length=10, description="Code fiscal/TVA (AR_CodeFiscal)" + ) + pays: Optional[str] = Field(None, description="Pays d'origine (AR_Pays)") + + fournisseur_principal: Optional[int] = Field( + None, + description="N° compte du fournisseur principal (CO_No → F_COMPTET.CT_Num)", + ) + fournisseur_nom: Optional[str] = Field( + None, description="Nom du fournisseur principal (F_COMPTET.CT_Intitule)" + ) + + conditionnement: Optional[str] = Field( + None, description="Conditionnement d'achat (AR_Condition)" + ) + conditionnement_qte: Optional[float] = Field( + None, description="Quantité conditionnement (F_ENUMCOND.EC_Quantite)" + ) + conditionnement_edi: Optional[str] = Field( + None, description="Code EDI conditionnement (F_ENUMCOND.EC_EdiCode)" + ) + + nb_colis: Optional[int] = Field( + None, description="Nombre de colis par unité (AR_NbColis)" + ) + prevision: Optional[bool] = Field( + None, description="Gestion en prévision (AR_Prevision)" + ) + + est_actif: bool = Field(default=True, description="Article actif (AR_Sommeil = 0)") + en_sommeil: bool = Field( + default=False, description="Article en sommeil (AR_Sommeil = 1)" + ) + article_substitut: Optional[str] = Field( + None, description="Référence article de substitution (AR_Substitut)" + ) + soumis_escompte: Optional[bool] = Field( + None, description="Soumis à escompte (AR_Escompte)" + ) + delai: Optional[int] = Field( + None, description="Délai de livraison en jours (AR_Delai)" + ) + + publie: Optional[bool] = Field( + None, description="Publié sur web/catalogue (AR_Publie)" + ) + hors_statistique: Optional[bool] = Field( + None, description="Exclus des statistiques (AR_HorsStat)" + ) + vente_debit: Optional[bool] = Field( + None, description="Vente au débit (AR_VteDebit)" + ) + non_imprimable: Optional[bool] = Field( + None, description="Non imprimable sur documents (AR_NotImp)" + ) + transfere: Optional[bool] = Field( + None, description="Article transféré (AR_Transfere)" + ) + contremarque: Optional[bool] = Field( + None, description="Article en contremarque (AR_Contremarque)" + ) + fact_poids: Optional[bool] = Field( + None, description="Facturation au poids (AR_FactPoids)" + ) + fact_forfait: Optional[bool] = Field( + None, description="Facturation au forfait (AR_FactForfait)" + ) + saisie_variable: Optional[bool] = Field( + None, description="Saisie variable (AR_SaisieVar)" + ) + fictif: Optional[bool] = Field(None, description="Article fictif (AR_Fictif)") + sous_traitance: Optional[bool] = Field( + None, description="Article en sous-traitance (AR_SousTraitance)" + ) + criticite: Optional[int] = Field( + None, description="Niveau de criticité (AR_Criticite)" + ) + + reprise_code_defaut: Optional[str] = Field( + None, description="Code reprise par défaut (RP_CodeDefaut)" + ) + delai_fabrication: Optional[int] = Field( + None, description="Délai de fabrication (AR_DelaiFabrication)" + ) + delai_peremption: Optional[int] = Field( + None, description="Délai de péremption (AR_DelaiPeremption)" + ) + delai_securite: Optional[int] = Field( + None, description="Délai de sécurité (AR_DelaiSecurite)" + ) + type_lancement: Optional[int] = Field( + None, description="Type de lancement production (AR_TypeLancement)" + ) + cycle: Optional[int] = Field(None, description="Cycle de production (AR_Cycle)") + + photo: Optional[str] = Field( + None, description="Chemin/nom du fichier photo (AR_Photo)" + ) + langue_1: Optional[str] = Field(None, description="Texte en langue 1 (AR_Langue1)") + langue_2: Optional[str] = Field(None, description="Texte en langue 2 (AR_Langue2)") + + frais_01_denomination: Optional[str] = Field( + None, description="Dénomination frais 1 (AR_Frais01FR_Denomination)" + ) + frais_02_denomination: Optional[str] = Field( + None, description="Dénomination frais 2 (AR_Frais02FR_Denomination)" + ) + frais_03_denomination: Optional[str] = Field( + None, description="Dénomination frais 3 (AR_Frais03FR_Denomination)" + ) + + tva_code: Optional[str] = Field(None, description="Code TVA (F_TAXE.TA_Code)") + tva_taux: Optional[float] = Field( + None, description="Taux de TVA en % (F_TAXE.TA_Taux)" + ) + + stat_01: Optional[str] = Field(None, description="Statistique 1 (AR_Stat01)") + stat_02: Optional[str] = Field(None, description="Statistique 2 (AR_Stat02)") + stat_03: Optional[str] = Field(None, description="Statistique 3 (AR_Stat03)") + stat_04: Optional[str] = Field(None, description="Statistique 4 (AR_Stat04)") + stat_05: Optional[str] = Field(None, description="Statistique 5 (AR_Stat05)") + + categorie_1: Optional[int] = Field( + None, description="Catégorie comptable 1 (CL_No1)" + ) + categorie_2: Optional[int] = Field( + None, description="Catégorie comptable 2 (CL_No2)" + ) + categorie_3: Optional[int] = Field( + None, description="Catégorie comptable 3 (CL_No3)" + ) + categorie_4: Optional[int] = Field( + None, description="Catégorie comptable 4 (CL_No4)" + ) + + date_modification: Optional[str] = Field( + None, description="Date de dernière modification (AR_DateModif)" + ) + + marque_commerciale: Optional[str] = Field( + None, description="Marque commerciale (champ personnalisé)" + ) + objectif_qtes_vendues: Optional[str] = Field( + None, description="Objectif / Quantités vendues (champ personnalisé)" + ) + pourcentage_or: Optional[str] = Field( + None, description="Pourcentage teneur en or (champ personnalisé)" + ) + premiere_commercialisation: Optional[str] = Field( + None, description="Date de 1ère commercialisation (champ personnalisé)" + ) + interdire_commande: Optional[bool] = Field( + None, description="Interdire la commande (champ personnalisé)" + ) + exclure: Optional[bool] = Field( + None, description="Exclure de certains traitements (champ personnalisé)" + ) + + class Config: + json_schema_extra = { + "example": { + "reference": "BAGUE-001", + "designation": "Bague Or 18K Diamant", + "prix_vente": 1299.00, + "stock_reel": 15.0, + "stock_disponible": 13.0, + "nb_emplacements": 2, + "nb_gammes": 2, + "nb_tarifs_clients": 3, + "nb_fournisseurs": 2, + "nb_medias": 2, + "emplacements": [ + { + "depot": "01", + "emplacement": "A1-01", + "qte_stockee": 10.0, + "depot_nom": "Dépôt principal", + } + ], + "gammes": [ + {"numero_gamme": 1, "enumere": "001", "gamme_nom": "Taille"} + ], + } + } + + +class ArticleListResponse(BaseModel): + """Réponse pour une liste d'articles""" + + total: int = Field(..., description="Nombre total d'articles") + articles: List[ArticleResponse] = Field(..., description="Liste des articles") + filtre_applique: Optional[str] = Field( + None, description="Filtre de recherche appliqué" + ) + avec_stock: bool = Field(True, description="Indique si les stocks ont été chargés") + avec_famille: bool = Field( + True, description="Indique si les familles ont été enrichies" + ) + avec_enrichissements_complets: bool = Field( + False, description="Indique si tous les enrichissements sont activés" + ) + + class Config: + json_schema_extra = { + "example": { + "total": 1250, + "filtre_applique": "bague", + "avec_stock": True, + "avec_famille": True, + "avec_enrichissements_complets": True, + "articles": [], + } + } + + +class ArticleCreateRequest(BaseModel): + """Schéma pour création d'article""" + + reference: str = Field(..., max_length=18, description="Référence article") + designation: str = Field(..., max_length=69, description="Désignation") + famille: Optional[str] = Field(None, max_length=18, description="Code famille") + prix_vente: Optional[float] = Field(None, ge=0, description="Prix vente HT") + prix_achat: Optional[float] = Field(None, ge=0, description="Prix achat HT") + stock_reel: Optional[float] = Field(None, ge=0, description="Stock initial") + stock_mini: Optional[float] = Field(None, ge=0, description="Stock minimum") + code_ean: Optional[str] = Field(None, max_length=13, description="Code-barres") + unite_vente: Optional[str] = Field("UN", max_length=4, description="Unité") + tva_code: Optional[str] = Field(None, max_length=5, description="Code TVA") + description: Optional[str] = Field(None, description="Description") + + +class ArticleUpdateRequest(BaseModel): + """Schéma pour modification d'article""" + + designation: Optional[str] = Field(None, max_length=69) + prix_vente: Optional[float] = Field(None, ge=0) + prix_achat: Optional[float] = Field(None, ge=0) + stock_reel: Optional[float] = Field( + None, ge=0, description="Critique pour erreur 2881" + ) + stock_mini: Optional[float] = Field(None, ge=0) + code_ean: Optional[str] = Field(None, max_length=13) + description: Optional[str] = Field(None) + + +class MouvementStockLigneRequest(BaseModel): + article_ref: str = Field(..., description="Référence de l'article") + quantite: float = Field(..., gt=0, description="Quantité (>0)") + depot_code: Optional[str] = Field(None, description="Code du dépôt (ex: '01')") + prix_unitaire: Optional[float] = Field( + None, ge=0, description="Prix unitaire (optionnel)" + ) + commentaire: Optional[str] = Field(None, description="Commentaire ligne") + numero_lot: Optional[str] = Field( + None, description="Numéro de lot (pour FIFO/LIFO)" + ) + stock_mini: Optional[float] = Field( + None, + ge=0, + description="""Stock minimum à définir pour cet article. + Si fourni, met à jour AS_QteMini dans F_ARTSTOCK. + Laisser None pour ne pas modifier.""", + ) + stock_maxi: Optional[float] = Field( + None, + ge=0, + description="""Stock maximum à définir pour cet article. + Doit être > stock_mini si les deux sont fournis.""", + ) + + class Config: + schema_extra = { + "example": { + "article_ref": "ARTS-001", + "quantite": 50.0, + "depot_code": "01", + "prix_unitaire": 100.0, + "commentaire": "Réapprovisionnement", + "numero_lot": "LOT20241217", + "stock_mini": 10.0, + "stock_maxi": 200.0, + } + } + + @validator("stock_maxi") + def validate_stock_maxi(cls, v, values): + """Valide que stock_maxi > stock_mini si les deux sont fournis""" + if ( + v is not None + and "stock_mini" in values + and values["stock_mini"] is not None + ): + if v <= values["stock_mini"]: + raise ValueError( + "stock_maxi doit être strictement supérieur à stock_mini" + ) + return v + + +class EntreeStockRequest(BaseModel): + """Création d'un bon d'entrée en stock""" + + date_entree: Optional[date] = Field( + None, description="Date du mouvement (aujourd'hui par défaut)" + ) + reference: Optional[str] = Field(None, description="Référence externe") + depot_code: Optional[str] = Field( + None, description="Dépôt principal (si applicable)" + ) + lignes: List[MouvementStockLigneRequest] = Field( + ..., min_items=1, description="Lignes du mouvement" + ) + commentaire: Optional[str] = Field(None, description="Commentaire général") + + class Config: + json_schema_extra = { + "example": { + "date_entree": "2025-01-15", + "reference": "REC-2025-001", + "depot_code": "01", + "lignes": [ + { + "article_ref": "ART001", + "quantite": 50, + "depot_code": "01", + "prix_unitaire": 10.50, + "commentaire": "Réception fournisseur", + } + ], + "commentaire": "Réception livraison fournisseur XYZ", + } + } + + +class SortieStockRequest(BaseModel): + """Création d'un bon de sortie de stock""" + + date_sortie: Optional[date] = Field( + None, description="Date du mouvement (aujourd'hui par défaut)" + ) + reference: Optional[str] = Field(None, description="Référence externe") + depot_code: Optional[str] = Field( + None, description="Dépôt principal (si applicable)" + ) + lignes: List[MouvementStockLigneRequest] = Field( + ..., min_items=1, description="Lignes du mouvement" + ) + commentaire: Optional[str] = Field(None, description="Commentaire général") + + class Config: + json_schema_extra = { + "example": { + "date_sortie": "2025-01-15", + "reference": "SOR-2025-001", + "depot_code": "01", + "lignes": [ + { + "article_ref": "ART001", + "quantite": 10, + "depot_code": "01", + "commentaire": "Utilisation interne", + } + ], + "commentaire": "Consommation atelier", + } + } + + +class MouvementStockResponse(BaseModel): + """Réponse pour un mouvement de stock""" + + article_ref: str = Field(..., description="Numéro d'article") + numero: str = Field(..., description="Numéro du mouvement") + type: int = Field(..., description="Type (0=Entrée, 1=Sortie)") + type_libelle: str = Field(..., description="Libellé du type") + date: str = Field(..., description="Date du mouvement") + reference: Optional[str] = Field(None, description="Référence externe") + nb_lignes: int = Field(..., description="Nombre de lignes") diff --git a/schemas/articles/famille_article.py b/schemas/articles/famille_article.py new file mode 100644 index 0000000..90fd7ca --- /dev/null +++ b/schemas/articles/famille_article.py @@ -0,0 +1,213 @@ + + +from pydantic import BaseModel, Field, EmailStr, validator, field_validator +from typing import List, Optional, Dict, ClassVar, Any +from datetime import date, datetime +from enum import Enum, IntEnum + +class FamilleCreateRequest(BaseModel): + """Schéma pour création de famille d'articles""" + + code: str = Field(..., max_length=18, description="Code famille (max 18 car)") + intitule: str = Field(..., max_length=69, description="Intitulé (max 69 car)") + type: int = Field(0, ge=0, le=1, description="0=Détail, 1=Total") + compte_achat: Optional[str] = Field( + None, max_length=13, description="Compte général achat (ex: 607000)" + ) + compte_vente: Optional[str] = Field( + None, max_length=13, description="Compte général vente (ex: 707000)" + ) + + class Config: + json_schema_extra = { + "example": { + "code": "PRODLAIT", + "intitule": "Produits laitiers", + "type": 0, + "compte_achat": "607000", + "compte_vente": "707000", + } + } + + + +class FamilleResponse(BaseModel): + """Modèle complet d'une famille avec données comptables et fournisseur""" + + code: str = Field(..., description="Code famille") + intitule: str = Field(..., description="Intitulé") + type: int = Field(..., description="Type (0=Détail, 1=Total)") + type_libelle: str = Field(..., description="Libellé du type") + est_total: bool = Field(..., description="True si type Total") + est_detail: bool = Field(..., description="True si type Détail") + + unite_vente: Optional[str] = Field(None, description="Unité de vente par défaut") + unite_poids: Optional[str] = Field(None, description="Unité de poids") + coef: Optional[float] = Field(None, description="Coefficient multiplicateur") + + suivi_stock: Optional[bool] = Field(None, description="Suivi du stock activé") + garantie: Optional[int] = Field(None, description="Durée de garantie (mois)") + delai: Optional[int] = Field(None, description="Délai de livraison (jours)") + nb_colis: Optional[int] = Field(None, description="Nombre de colis") + + code_fiscal: Optional[str] = Field(None, description="Code fiscal par défaut") + escompte: Optional[bool] = Field(None, description="Escompte autorisé") + + est_centrale: Optional[bool] = Field(None, description="Famille d'achat centralisé") + nature: Optional[int] = Field(None, description="Nature de la famille") + pays: Optional[str] = Field(None, description="Pays d'origine") + + categorie_1: Optional[int] = Field(None, description="Catégorie comptable 1 (CL_No1)") + categorie_2: Optional[int] = Field(None, description="Catégorie comptable 2 (CL_No2)") + categorie_3: Optional[int] = Field(None, description="Catégorie comptable 3 (CL_No3)") + categorie_4: Optional[int] = Field(None, description="Catégorie comptable 4 (CL_No4)") + + stat_01: Optional[str] = Field(None, description="Statistique libre 1") + stat_02: Optional[str] = Field(None, description="Statistique libre 2") + stat_03: Optional[str] = Field(None, description="Statistique libre 3") + stat_04: Optional[str] = Field(None, description="Statistique libre 4") + stat_05: Optional[str] = Field(None, description="Statistique libre 5") + hors_statistique: Optional[bool] = Field(None, description="Exclue des statistiques") + + vente_debit: Optional[bool] = Field(None, description="Vente au débit") + non_imprimable: Optional[bool] = Field(None, description="Non imprimable sur documents") + contremarque: Optional[bool] = Field(None, description="Article en contremarque") + fact_poids: Optional[bool] = Field(None, description="Facturation au poids") + fact_forfait: Optional[bool] = Field(None, description="Facturation forfaitaire") + publie: Optional[bool] = Field(None, description="Publié (e-commerce)") + + racine_reference: Optional[str] = Field(None, description="Racine pour génération auto de références") + racine_code_barre: Optional[str] = Field(None, description="Racine pour génération auto de codes-barres") + raccourci: Optional[str] = Field(None, description="Raccourci clavier") + + sous_traitance: Optional[bool] = Field(None, description="Famille en sous-traitance") + fictif: Optional[bool] = Field(None, description="Famille fictive (nomenclature)") + criticite: Optional[int] = Field(None, description="Niveau de criticité (0-5)") + + compte_vente: Optional[str] = Field(None, description="Compte général de vente") + compte_auxiliaire_vente: Optional[str] = Field(None, description="Compte auxiliaire de vente") + tva_vente_1: Optional[str] = Field(None, description="Code TVA vente principal") + tva_vente_2: Optional[str] = Field(None, description="Code TVA vente secondaire") + tva_vente_3: Optional[str] = Field(None, description="Code TVA vente tertiaire") + type_facture_vente: Optional[int] = Field(None, description="Type de facture vente") + + compte_achat: Optional[str] = Field(None, description="Compte général d'achat") + compte_auxiliaire_achat: Optional[str] = Field(None, description="Compte auxiliaire d'achat") + tva_achat_1: Optional[str] = Field(None, description="Code TVA achat principal") + tva_achat_2: Optional[str] = Field(None, description="Code TVA achat secondaire") + tva_achat_3: Optional[str] = Field(None, description="Code TVA achat tertiaire") + type_facture_achat: Optional[int] = Field(None, description="Type de facture achat") + + compte_stock: Optional[str] = Field(None, description="Compte de stock") + compte_auxiliaire_stock: Optional[str] = Field(None, description="Compte auxiliaire de stock") + + fournisseur_principal: Optional[str] = Field(None, description="N° compte fournisseur principal") + fournisseur_unite: Optional[str] = Field(None, description="Unité d'achat fournisseur") + fournisseur_conversion: Optional[float] = Field(None, description="Coefficient de conversion") + fournisseur_delai_appro: Optional[int] = Field(None, description="Délai d'approvisionnement (jours)") + fournisseur_garantie: Optional[int] = Field(None, description="Garantie fournisseur (mois)") + fournisseur_colisage: Optional[int] = Field(None, description="Colisage fournisseur") + fournisseur_qte_mini: Optional[float] = Field(None, description="Quantité minimum de commande") + fournisseur_qte_mont: Optional[float] = Field(None, description="Quantité montant") + fournisseur_devise: Optional[int] = Field(None, description="Devise fournisseur (0=Euro)") + fournisseur_remise: Optional[float] = Field(None, description="Remise fournisseur (%)") + fournisseur_type_remise: Optional[int] = Field(None, description="Type de remise (0=%, 1=Montant)") + + nb_articles: Optional[int] = Field(None, description="Nombre d'articles dans la famille") + + FA_CodeFamille: Optional[str] = Field(None, description="[Legacy] Code famille") + FA_Intitule: Optional[str] = Field(None, description="[Legacy] Intitulé") + FA_Type: Optional[int] = Field(None, description="[Legacy] Type") + CG_NumVte: Optional[str] = Field(None, description="[Legacy] Compte vente") + CG_NumAch: Optional[str] = Field(None, description="[Legacy] Compte achat") + + class Config: + json_schema_extra = { + "example": { + "code": "ELECT", + "intitule": "Électronique et Informatique", + "type": 0, + "type_libelle": "Détail", + "est_total": False, + "est_detail": True, + "unite_vente": "U", + "unite_poids": "KG", + "coef": 2.5, + "suivi_stock": True, + "garantie": 24, + "delai": 5, + "nb_colis": 1, + "code_fiscal": "C19", + "escompte": True, + "est_centrale": False, + "nature": 0, + "pays": "FR", + "categorie_1": 1, + "categorie_2": 0, + "categorie_3": 0, + "categorie_4": 0, + "stat_01": "HIGH_TECH", + "stat_02": "", + "stat_03": "", + "stat_04": "", + "stat_05": "", + "hors_statistique": False, + "vente_debit": False, + "non_imprimable": False, + "contremarque": False, + "fact_poids": False, + "fact_forfait": False, + "publie": True, + "racine_reference": "ELEC", + "racine_code_barre": "339", + "raccourci": "F5", + "sous_traitance": False, + "fictif": False, + "criticite": 2, + "compte_vente": "707100", + "compte_auxiliaire_vente": "", + "tva_vente_1": "C19", + "tva_vente_2": "", + "tva_vente_3": "", + "type_facture_vente": 0, + "compte_achat": "607100", + "compte_auxiliaire_achat": "", + "tva_achat_1": "C19", + "tva_achat_2": "", + "tva_achat_3": "", + "type_facture_achat": 0, + "compte_stock": "350000", + "compte_auxiliaire_stock": "", + "fournisseur_principal": "FTECH001", + "fournisseur_unite": "U", + "fournisseur_conversion": 1.0, + "fournisseur_delai_appro": 7, + "fournisseur_garantie": 12, + "fournisseur_colisage": 10, + "fournisseur_qte_mini": 5.0, + "fournisseur_qte_mont": 100.0, + "fournisseur_devise": 0, + "fournisseur_remise": 5.0, + "fournisseur_type_remise": 0, + "nb_articles": 156 + } + } + + +class FamilleListResponse(BaseModel): + """Réponse pour la liste des familles""" + familles: list[FamilleResponse] + total: int + filtre: Optional[str] = None + inclure_totaux: bool = True + + class Config: + json_schema_extra = { + "example": { + "familles": [], + "total": 42, + "filtre": "ELECT", + "inclure_totaux": False + } + } + \ No newline at end of file diff --git a/schemas/documents/avoirs.py b/schemas/documents/avoirs.py new file mode 100644 index 0000000..75c7786 --- /dev/null +++ b/schemas/documents/avoirs.py @@ -0,0 +1,71 @@ + + +from pydantic import BaseModel, Field, EmailStr, validator, field_validator +from typing import List, Optional, Dict, ClassVar, Any +from datetime import date, datetime +from enum import Enum, IntEnum + +class LigneAvoir(BaseModel): + """Ligne d'avoir""" + + article_code: str + quantite: float + remise_pourcentage: Optional[float] = 0.0 + + @field_validator("article_code", mode="before") + def strip_insecables(cls, v): + return v.replace("\xa0", "").strip() + + +class AvoirCreateRequest(BaseModel): + """Création d'un avoir""" + + client_id: str + date_avoir: Optional[date] = None + date_livraison: Optional[date] = None + lignes: List[LigneAvoir] + reference: Optional[str] = None + + class Config: + json_schema_extra = { + "example": { + "client_id": "CLI000001", + "date_avoir": "2024-01-15", + "reference": "AV-EXT-001", + "lignes": [ + { + "article_code": "ART001", + "quantite": 5.0, + "prix_unitaire_ht": 50.0, + "remise_pourcentage": 0.0, + } + ], + } + } + + +class AvoirUpdateRequest(BaseModel): + """Modification d'un avoir existant""" + + date_avoir: Optional[date] = None + date_livraison: Optional[date] = None + lignes: Optional[List[LigneAvoir]] = None + statut: Optional[int] = Field(None, ge=0, le=6) + reference: Optional[str] = None + + class Config: + json_schema_extra = { + "example": { + "date_avoir": "2024-01-15", + "date_livraison": "2024-01-15", + "reference": "AV-EXT-001", + "lignes": [ + { + "article_code": "ART001", + "quantite": 10.0, + "prix_unitaire_ht": 45.0, + } + ], + "statut": 2, + } + } diff --git a/schemas/documents/commandes.py b/schemas/documents/commandes.py new file mode 100644 index 0000000..4dc8b15 --- /dev/null +++ b/schemas/documents/commandes.py @@ -0,0 +1,71 @@ + + +from pydantic import BaseModel, Field, EmailStr, validator, field_validator +from typing import List, Optional, Dict, ClassVar, Any +from datetime import date, datetime +from enum import Enum, IntEnum + +class LigneCommande(BaseModel): + """Ligne de commande""" + + article_code: str + quantite: float + remise_pourcentage: Optional[float] = 0.0 + + @field_validator("article_code", mode="before") + def strip_insecables(cls, v): + return v.replace("\xa0", "").strip() + + +class CommandeCreateRequest(BaseModel): + """Création d'une commande""" + + client_id: str + date_commande: Optional[date] = None + date_livraison: Optional[date] = None + lignes: List[LigneCommande] + reference: Optional[str] = None + + class Config: + json_schema_extra = { + "example": { + "client_id": "CLI000001", + "date_commande": "2024-01-15", + "reference": "CMD-EXT-001", + "lignes": [ + { + "article_code": "ART001", + "quantite": 10.0, + "prix_unitaire_ht": 50.0, + "remise_pourcentage": 5.0, + } + ], + } + } + + +class CommandeUpdateRequest(BaseModel): + """Modification d'une commande existante""" + + date_commande: Optional[date] = None + date_livraison: Optional[date] = None + lignes: Optional[List[LigneCommande]] = None + statut: Optional[int] = Field(None, ge=0, le=6) + reference: Optional[str] = None + + class Config: + json_schema_extra = { + "example": { + "date_commande": "2024-01-15", + "date_livraison": "2024-01-15", + "reference": "CMD-EXT-001", + "lignes": [ + { + "article_code": "ART001", + "quantite": 15.0, + "prix_unitaire_ht": 45.0, + } + ], + "statut": 2, + } + } diff --git a/schemas/documents/devis.py b/schemas/documents/devis.py new file mode 100644 index 0000000..f4c938a --- /dev/null +++ b/schemas/documents/devis.py @@ -0,0 +1,66 @@ + +from pydantic import BaseModel, Field, EmailStr, validator, field_validator +from typing import List, Optional, Dict, ClassVar, Any +from datetime import date, datetime +from enum import Enum, IntEnum + +class LigneDevis(BaseModel): + article_code: str + quantite: float + remise_pourcentage: Optional[float] = 0.0 + + @field_validator("article_code", mode="before") + def strip_insecables(cls, v): + return v.replace("\xa0", "").strip() + + +class DevisRequest(BaseModel): + client_id: str + date_devis: Optional[date] = None + date_livraison: Optional[date] = None + reference: Optional[str] = None + lignes: List[LigneDevis] + + +class DevisResponse(BaseModel): + id: str + client_id: str + date_devis: str + montant_total_ht: float + montant_total_ttc: float + nb_lignes: int + + + +class DevisUpdateRequest(BaseModel): + """Modèle pour modification d'un devis existant""" + + date_devis: Optional[date] = None + date_livraison: Optional[date] = None + reference: Optional[str] = None + lignes: Optional[List[LigneDevis]] = None + statut: Optional[int] = Field(None, ge=0, le=6) + + class Config: + json_schema_extra = { + "example": { + "date_devis": "2024-01-15", + "date_livraison": "2024-01-15", + "reference": "DEV-001", + "lignes": [ + { + "article_code": "ART001", + "quantite": 5.0, + "prix_unitaire_ht": 100.0, + "remise_pourcentage": 10.0, + } + ], + "statut": 2, + } + } + + + +class RelanceDevisRequest(BaseModel): + doc_id: str + message_personnalise: Optional[str] = None \ No newline at end of file diff --git a/schemas/documents/documents.py b/schemas/documents/documents.py new file mode 100644 index 0000000..41b978b --- /dev/null +++ b/schemas/documents/documents.py @@ -0,0 +1,22 @@ + +from config import settings +from enum import Enum, IntEnum + +class TypeDocument(int, Enum): + DEVIS = settings.SAGE_TYPE_DEVIS + BON_COMMANDE = settings.SAGE_TYPE_BON_COMMANDE + PREPARATION = settings.SAGE_TYPE_PREPARATION + BON_LIVRAISON = settings.SAGE_TYPE_BON_LIVRAISON + BON_RETOUR = settings.SAGE_TYPE_BON_RETOUR + BON_AVOIR = settings.SAGE_TYPE_BON_AVOIR + FACTURE = settings.SAGE_TYPE_FACTURE + + +class TypeDocumentSQL(int, Enum): + DEVIS = settings.SAGE_TYPE_DEVIS + BON_COMMANDE = 1 + PREPARATION = 2 + BON_LIVRAISON = 3 + BON_RETOUR = 4 + BON_AVOIR = 5 + FACTURE = 6 diff --git a/schemas/documents/email.py b/schemas/documents/email.py new file mode 100644 index 0000000..8fd425c --- /dev/null +++ b/schemas/documents/email.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel, Field, EmailStr, validator, field_validator +from typing import List, Optional, Dict, ClassVar, Any +from datetime import date, datetime +from enum import Enum, IntEnum +from schemas.documents.documents import TypeDocument + +class StatutEmail(str, Enum): + EN_ATTENTE = "EN_ATTENTE" + EN_COURS = "EN_COURS" + ENVOYE = "ENVOYE" + OUVERT = "OUVERT" + ERREUR = "ERREUR" + BOUNCE = "BOUNCE" + +class EmailEnvoiRequest(BaseModel): + destinataire: EmailStr + cc: Optional[List[EmailStr]] = [] + cci: Optional[List[EmailStr]] = [] + sujet: str + corps_html: str + document_ids: Optional[List[str]] = None + type_document: Optional[TypeDocument] = None \ No newline at end of file diff --git a/schemas/documents/factures.py b/schemas/documents/factures.py new file mode 100644 index 0000000..fb3e927 --- /dev/null +++ b/schemas/documents/factures.py @@ -0,0 +1,70 @@ + + +from pydantic import BaseModel, Field, EmailStr, validator, field_validator +from typing import List, Optional, Dict, ClassVar, Any +from datetime import date, datetime +from enum import Enum, IntEnum + +class LigneFacture(BaseModel): + """Ligne de facture""" + + article_code: str + quantite: float + remise_pourcentage: Optional[float] = 0.0 + + @field_validator("article_code", mode="before") + def strip_insecables(cls, v): + return v.replace("\xa0", "").strip() + + +class FactureCreateRequest(BaseModel): + """Création d'une facture""" + + client_id: str + date_facture: Optional[date] = None + date_livraison: Optional[date] = None + lignes: List[LigneFacture] + reference: Optional[str] = None + + class Config: + json_schema_extra = { + "example": { + "client_id": "CLI000001", + "date_facture": "2024-01-15", + "reference": "FA-EXT-001", + "lignes": [ + { + "article_code": "ART001", + "quantite": 10.0, + "prix_unitaire_ht": 50.0, + "remise_pourcentage": 5.0, + } + ], + } + } + + +class FactureUpdateRequest(BaseModel): + """Modification d'une facture existante""" + + date_facture: Optional[date] = None + date_livraison: Optional[date] = None + lignes: Optional[List[LigneFacture]] = None + statut: Optional[int] = Field(None, ge=0, le=6) + reference: Optional[str] = None + + class Config: + json_schema_extra = { + "example": { + "date_facture": "2024-01-15", + "date_livraison": "2024-01-15", + "lignes": [ + { + "article_code": "ART001", + "quantite": 15.0, + "prix_unitaire_ht": 45.0, + } + ], + "statut": 2, + } + } diff --git a/schemas/documents/livraisons.py b/schemas/documents/livraisons.py new file mode 100644 index 0000000..7d77001 --- /dev/null +++ b/schemas/documents/livraisons.py @@ -0,0 +1,70 @@ + +from pydantic import BaseModel, Field, EmailStr, validator, field_validator +from typing import List, Optional, Dict, ClassVar, Any +from datetime import date, datetime +from enum import Enum, IntEnum + +class LigneLivraison(BaseModel): + """Ligne de livraison""" + + article_code: str + quantite: float + remise_pourcentage: Optional[float] = 0.0 + + @field_validator("article_code", mode="before") + def strip_insecables(cls, v): + return v.replace("\xa0", "").strip() + + +class LivraisonCreateRequest(BaseModel): + """Création d'une livraison""" + + client_id: str + date_livraison: Optional[date] = None + date_livraison_prevue: Optional[date] = None + lignes: List[LigneLivraison] + reference: Optional[str] = None + + class Config: + json_schema_extra = { + "example": { + "client_id": "CLI000001", + "date_livraison": "2024-01-15", + "reference": "BL-EXT-001", + "lignes": [ + { + "article_code": "ART001", + "quantite": 10.0, + "prix_unitaire_ht": 50.0, + "remise_pourcentage": 5.0, + } + ], + } + } + + +class LivraisonUpdateRequest(BaseModel): + """Modification d'une livraison existante""" + + date_livraison: Optional[date] = None + date_livraison_prevue: Optional[date] = None + lignes: Optional[List[LigneLivraison]] = None + statut: Optional[int] = Field(None, ge=0, le=6) + reference: Optional[str] = None + + class Config: + json_schema_extra = { + "example": { + "date_livraison": "2024-01-15", + "date_livraison_prevue": "2024-01-15", + "reference": "BL-EXT-001", + "lignes": [ + { + "article_code": "ART001", + "quantite": 15.0, + "prix_unitaire_ht": 45.0, + } + ], + "statut": 2, + } + } diff --git a/schemas/documents/universign.py b/schemas/documents/universign.py new file mode 100644 index 0000000..fd3d852 --- /dev/null +++ b/schemas/documents/universign.py @@ -0,0 +1,19 @@ + +from pydantic import BaseModel, Field, EmailStr, validator, field_validator +from typing import List, Optional, Dict, ClassVar, Any +from datetime import date, datetime +from enum import Enum, IntEnum +from schemas.documents.documents import TypeDocument + +class StatutSignature(str, Enum): + EN_ATTENTE = "EN_ATTENTE" + ENVOYE = "ENVOYE" + SIGNE = "SIGNE" + REFUSE = "REFUSE" + EXPIRE = "EXPIRE" + +class SignatureRequest(BaseModel): + doc_id: str + type_doc: TypeDocument + email_signataire: EmailStr + nom_signataire: str diff --git a/schemas/schema_mixte.py b/schemas/schema_mixte.py new file mode 100644 index 0000000..8a69976 --- /dev/null +++ b/schemas/schema_mixte.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +class BaremeRemiseResponse(BaseModel): + client_id: str + remise_max_autorisee: float + remise_demandee: float + autorisee: bool + message: str diff --git a/schemas/tiers/clients.py b/schemas/tiers/clients.py new file mode 100644 index 0000000..570f8d2 --- /dev/null +++ b/schemas/tiers/clients.py @@ -0,0 +1,829 @@ +from pydantic import BaseModel, Field, field_validator +from typing import List, Optional +from schemas.tiers.contact import Contact + +class ClientResponse(BaseModel): + """Modèle de réponse client simplifié (pour listes)""" + + numero: Optional[str] = None + intitule: Optional[str] = None + adresse: Optional[str] = None + code_postal: Optional[str] = None + ville: Optional[str] = None + email: Optional[str] = None + telephone: Optional[str] = None + + +class ClientDetails(BaseModel): + numero: Optional[str] = Field(None, description="Code client (CT_Num)") + intitule: Optional[str] = Field( + None, description="Raison sociale ou Nom complet (CT_Intitule)" + ) + type_tiers: Optional[int] = Field( + None, description="Type : 0=Client, 1=Fournisseur (CT_Type)" + ) + qualite: Optional[str] = Field( + None, description="Qualité Sage : CLI, FOU, PRO (CT_Qualite)" + ) + classement: Optional[str] = Field( + None, description="Code de classement (CT_Classement)" + ) + raccourci: Optional[str] = Field( + None, description="Code raccourci 7 car. (CT_Raccourci)" + ) + siret: Optional[str] = Field(None, description="N° SIRET 14 chiffres (CT_Siret)") + tva_intra: Optional[str] = Field( + None, description="N° TVA intracommunautaire (CT_Identifiant)" + ) + code_naf: Optional[str] = Field(None, description="Code NAF/APE (CT_Ape)") + + contact: Optional[str] = Field( + None, description="Nom du contact principal (CT_Contact)" + ) + adresse: Optional[str] = Field(None, description="Adresse ligne 1 (CT_Adresse)") + complement: Optional[str] = Field( + None, description="Complément d'adresse (CT_Complement)" + ) + code_postal: Optional[str] = Field(None, description="Code postal (CT_CodePostal)") + ville: Optional[str] = Field(None, description="Ville (CT_Ville)") + region: Optional[str] = Field(None, description="Région/État (CT_CodeRegion)") + pays: Optional[str] = Field(None, description="Pays (CT_Pays)") + + telephone: Optional[str] = Field(None, description="Téléphone fixe (CT_Telephone)") + telecopie: Optional[str] = Field(None, description="Fax (CT_Telecopie)") + email: Optional[str] = Field(None, description="Email principal (CT_EMail)") + site_web: Optional[str] = Field(None, description="Site web (CT_Site)") + facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)") + linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)") + + taux01: Optional[float] = Field(None, description="Taux personnalisé 1 (CT_Taux01)") + taux02: Optional[float] = Field(None, description="Taux personnalisé 2 (CT_Taux02)") + taux03: Optional[float] = Field(None, description="Taux personnalisé 3 (CT_Taux03)") + taux04: Optional[float] = Field(None, description="Taux personnalisé 4 (CT_Taux04)") + + statistique01: Optional[str] = Field( + None, description="Statistique 1 (CT_Statistique01)" + ) + statistique02: Optional[str] = Field( + None, description="Statistique 2 (CT_Statistique02)" + ) + statistique03: Optional[str] = Field( + None, description="Statistique 3 (CT_Statistique03)" + ) + statistique04: Optional[str] = Field( + None, description="Statistique 4 (CT_Statistique04)" + ) + statistique05: Optional[str] = Field( + None, description="Statistique 5 (CT_Statistique05)" + ) + statistique06: Optional[str] = Field( + None, description="Statistique 6 (CT_Statistique06)" + ) + statistique07: Optional[str] = Field( + None, description="Statistique 7 (CT_Statistique07)" + ) + statistique08: Optional[str] = Field( + None, description="Statistique 8 (CT_Statistique08)" + ) + statistique09: Optional[str] = Field( + None, description="Statistique 9 (CT_Statistique09)" + ) + statistique10: Optional[str] = Field( + None, description="Statistique 10 (CT_Statistique10)" + ) + + encours_autorise: Optional[float] = Field( + None, description="Encours maximum autorisé (CT_Encours)" + ) + assurance_credit: Optional[float] = Field( + None, description="Montant assurance crédit (CT_Assurance)" + ) + langue: Optional[int] = Field( + None, description="Code langue 0=FR, 1=EN (CT_Langue)" + ) + commercial_code: Optional[int] = Field( + None, description="Code du commercial (CO_No)" + ) + + lettrage_auto: Optional[bool] = Field( + None, description="Lettrage automatique (CT_Lettrage)" + ) + est_actif: Optional[bool] = Field(None, description="True si actif (CT_Sommeil=0)") + type_facture: Optional[int] = Field( + None, description="Type facture 0=Facture, 1=BL (CT_Facture)" + ) + est_prospect: Optional[bool] = Field( + None, description="True si prospect (CT_Prospect=1)" + ) + bl_en_facture: Optional[int] = Field( + None, description="Imprimer BL en facture (CT_BLFact)" + ) + saut_page: Optional[int] = Field( + None, description="Saut de page sur documents (CT_Saut)" + ) + validation_echeance: Optional[int] = Field( + None, description="Valider les échéances (CT_ValidEch)" + ) + controle_encours: Optional[int] = Field( + None, description="Contrôler l'encours (CT_ControlEnc)" + ) + exclure_relance: Optional[bool] = Field( + None, description="Exclure des relances (CT_NotRappel)" + ) + exclure_penalites: Optional[bool] = Field( + None, description="Exclure des pénalités (CT_NotPenal)" + ) + bon_a_payer: Optional[int] = Field( + None, description="Bon à payer obligatoire (CT_BonAPayer)" + ) + + priorite_livraison: Optional[int] = Field( + None, description="Priorité livraison (CT_PrioriteLivr)" + ) + livraison_partielle: Optional[int] = Field( + None, description="Livraison partielle (CT_LivrPartielle)" + ) + delai_transport: Optional[int] = Field( + None, description="Délai transport jours (CT_DelaiTransport)" + ) + delai_appro: Optional[int] = Field( + None, description="Délai appro jours (CT_DelaiAppro)" + ) + + commentaire: Optional[str] = Field( + None, description="Commentaire libre (CT_Commentaire)" + ) + + section_analytique: Optional[str] = Field( + None, description="Section analytique (CA_Num)" + ) + + mode_reglement_code: Optional[int] = Field( + None, description="Code mode règlement (MR_No)" + ) + surveillance_active: Optional[bool] = Field( + None, description="Surveillance financière (CT_Surveillance)" + ) + coface: Optional[str] = Field(None, description="Code Coface 25 car. (CT_Coface)") + forme_juridique: Optional[str] = Field( + None, description="Forme juridique SA, SARL (CT_SvFormeJuri)" + ) + effectif: Optional[str] = Field( + None, description="Nombre d'employés (CT_SvEffectif)" + ) + sv_regularite: Optional[str] = Field( + None, description="Régularité paiements (CT_SvRegul)" + ) + sv_cotation: Optional[str] = Field( + None, description="Cotation crédit (CT_SvCotation)" + ) + sv_objet_maj: Optional[str] = Field( + None, description="Objet dernière MAJ (CT_SvObjetMaj)" + ) + sv_chiffre_affaires: Optional[float] = Field( + None, description="Chiffre d'affaires (CT_SvCA)" + ) + sv_resultat: Optional[float] = Field( + None, description="Résultat financier (CT_SvResultat)" + ) + + compte_general: Optional[str] = Field( + None, description="Compte général principal (CG_NumPrinc)" + ) + categorie_tarif: Optional[int] = Field( + None, description="Catégorie tarifaire (N_CatTarif)" + ) + categorie_compta: Optional[int] = Field( + None, description="Catégorie comptable (N_CatCompta)" + ) + + contacts: Optional[List[Contact]] = Field( + default_factory=list, description="Liste des contacts du client" + ) + + class Config: + json_schema_extra = { + "example": { + "numero": "CLI000001", + "intitule": "SARL EXEMPLE", + "type_tiers": 0, + "qualite": "CLI", + "classement": "A", + "raccourci": "EXEMPL", + "siret": "12345678901234", + "tva_intra": "FR12345678901", + "code_naf": "6201Z", + "contact": "Jean Dupont", + "adresse": "123 Rue de la Paix", + "complement": "Bâtiment B", + "code_postal": "75001", + "ville": "Paris", + "region": "Île-de-France", + "pays": "France", + "telephone": "0123456789", + "telecopie": "0123456788", + "email": "contact@exemple.fr", + "site_web": "https://www.exemple.fr", + "facebook": "https://facebook.com/exemple", + "linkedin": "https://linkedin.com/company/exemple", + "taux01": 0.0, + "taux02": 0.0, + "taux03": 0.0, + "taux04": 0.0, + "statistique01": "Informatique", + "statistique02": "", + "statistique03": "", + "statistique04": "", + "statistique05": "", + "statistique06": "", + "statistique07": "", + "statistique08": "", + "statistique09": "", + "statistique10": "", + "encours_autorise": 50000.0, + "assurance_credit": 40000.0, + "langue": 0, + "commercial_code": 1, + "lettrage_auto": True, + "est_actif": True, + "type_facture": 1, + "est_prospect": False, + "bl_en_facture": 0, + "saut_page": 0, + "validation_echeance": 0, + "controle_encours": 1, + "exclure_relance": False, + "exclure_penalites": False, + "bon_a_payer": 0, + "priorite_livraison": 1, + "livraison_partielle": 1, + "delai_transport": 2, + "delai_appro": 0, + "commentaire": "Client important", + "section_analytique": "", + "mode_reglement_code": 1, + "surveillance_active": True, + "coface": "COF12345", + "forme_juridique": "SARL", + "effectif": "50-99", + "sv_regularite": "", + "sv_cotation": "", + "sv_objet_maj": "", + "sv_chiffre_affaires": 2500000.0, + "sv_resultat": 150000.0, + "compte_general": "4110000", + "categorie_tarif": 0, + "categorie_compta": 0, + } + } + + +class ClientCreateRequest(BaseModel): + intitule: str = Field( + ..., max_length=69, description="Nom du client (CT_Intitule) - OBLIGATOIRE" + ) + + numero: str = Field( + ..., max_length=17, description="Numéro client CT_Num (auto si None)" + ) + + type_tiers: int = Field( + 0, + ge=0, + le=3, + description="CT_Type: 0=Client, 1=Fournisseur, 2=Salarié, 3=Autre", + ) + + qualite: Optional[str] = Field( + "CLI", max_length=17, description="CT_Qualite: CLI/FOU/SAL/DIV/AUT" + ) + + classement: Optional[str] = Field(None, max_length=17, description="CT_Classement") + + raccourci: Optional[str] = Field( + None, max_length=7, description="CT_Raccourci (7 chars max, unique)" + ) + + siret: Optional[str] = Field( + None, max_length=15, description="CT_Siret (14-15 chars)" + ) + + tva_intra: Optional[str] = Field( + None, max_length=25, description="CT_Identifiant (TVA intracommunautaire)" + ) + + code_naf: Optional[str] = Field( + None, max_length=7, description="CT_Ape (Code NAF/APE)" + ) + + contact: Optional[str] = Field( + None, + max_length=35, + description="CT_Contact (double affectation: client + adresse)", + ) + + adresse: Optional[str] = Field(None, max_length=35, description="Adresse.Adresse") + + complement: Optional[str] = Field( + None, max_length=35, description="Adresse.Complement" + ) + + code_postal: Optional[str] = Field( + None, max_length=9, description="Adresse.CodePostal" + ) + + ville: Optional[str] = Field(None, max_length=35, description="Adresse.Ville") + + region: Optional[str] = Field(None, max_length=25, description="Adresse.CodeRegion") + + pays: Optional[str] = Field(None, max_length=35, description="Adresse.Pays") + + telephone: Optional[str] = Field( + None, max_length=21, description="Telecom.Telephone" + ) + + telecopie: Optional[str] = Field( + None, max_length=21, description="Telecom.Telecopie (fax)" + ) + + email: Optional[str] = Field(None, max_length=69, description="Telecom.EMail") + + site_web: Optional[str] = Field(None, max_length=69, description="Telecom.Site") + + portable: Optional[str] = Field(None, max_length=21, description="Telecom.Portable") + + facebook: Optional[str] = Field( + None, max_length=69, description="Telecom.Facebook ou CT_Facebook" + ) + + linkedin: Optional[str] = Field( + None, max_length=69, description="Telecom.LinkedIn ou CT_LinkedIn" + ) + + compte_general: Optional[str] = Field( + None, + max_length=13, + description="CompteGPrinc (défaut selon type_tiers: 4110000, 4010000, 421, 471)", + ) + + categorie_tarifaire: Optional[str] = Field( + None, description="N_CatTarif (ID catégorie tarifaire, défaut '0' ou '1')" + ) + + categorie_comptable: Optional[str] = Field( + None, description="N_CatCompta (ID catégorie comptable, défaut '0' ou '1')" + ) + + taux01: Optional[float] = Field(None, description="CT_Taux01") + taux02: Optional[float] = Field(None, description="CT_Taux02") + taux03: Optional[float] = Field(None, description="CT_Taux03") + taux04: Optional[float] = Field(None, description="CT_Taux04") + + secteur: Optional[str] = Field( + None, max_length=21, description="Alias de statistique01 (CT_Statistique01)" + ) + + 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" + ) + + encours_autorise: Optional[float] = Field( + None, description="CT_Encours (montant max autorisé)" + ) + + assurance_credit: Optional[float] = Field( + None, description="CT_Assurance (montant assurance crédit)" + ) + + langue: Optional[int] = Field( + None, ge=0, description="CT_Langue (0=Français, 1=Anglais, etc.)" + ) + + commercial_code: Optional[int] = Field( + None, description="CO_No (ID du collaborateur commercial)" + ) + + lettrage_auto: Optional[bool] = Field( + True, description="CT_Lettrage (1=oui, 0=non)" + ) + + est_actif: Optional[bool] = Field( + True, description="Inverse de CT_Sommeil (True=actif, False=en sommeil)" + ) + + type_facture: Optional[int] = Field( + 1, ge=0, le=2, description="CT_Facture: 0=aucune, 1=normale, 2=regroupée" + ) + + est_prospect: Optional[bool] = Field( + False, description="CT_Prospect (1=oui, 0=non)" + ) + + bl_en_facture: Optional[int] = Field( + None, ge=0, le=1, description="CT_BLFact (impression BL sur facture)" + ) + + saut_page: Optional[int] = Field( + None, ge=0, le=1, description="CT_Saut (saut de page après impression)" + ) + + 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") + + priorite_livraison: Optional[int] = Field( + None, ge=0, le=5, description="CT_PrioriteLivr" + ) + + livraison_partielle: Optional[int] = Field( + None, ge=0, le=1, description="CT_LivrPartielle" + ) + + delai_transport: Optional[int] = Field( + None, ge=0, description="CT_DelaiTransport (jours)" + ) + + delai_appro: Optional[int] = Field(None, ge=0, description="CT_DelaiAppro (jours)") + + commentaire: Optional[str] = Field( + None, max_length=35, description="CT_Commentaire" + ) + + section_analytique: Optional[str] = Field(None, max_length=13, description="CA_Num") + + mode_reglement_code: Optional[int] = Field( + None, description="MR_No (ID du mode de règlement)" + ) + + surveillance_active: Optional[int] = Field( + None, ge=0, le=1, description="CT_Surveillance (DOIT être défini AVANT coface)" + ) + + coface: Optional[str] = Field( + None, max_length=25, description="CT_Coface (code Coface)" + ) + + forme_juridique: Optional[str] = Field( + None, max_length=33, description="CT_SvFormeJuri (SARL, SA, etc.)" + ) + + effectif: Optional[str] = Field(None, max_length=11, description="CT_SvEffectif") + + sv_regularite: Optional[str] = Field(None, max_length=3, description="CT_SvRegul") + + sv_cotation: Optional[str] = Field(None, max_length=5, description="CT_SvCotation") + + sv_objet_maj: Optional[str] = Field( + None, max_length=61, description="CT_SvObjetMaj" + ) + + ca_annuel: Optional[float] = Field( + None, + description="CT_SvCA (Chiffre d'affaires annuel) - alias: sv_chiffre_affaires", + ) + + sv_chiffre_affaires: Optional[float] = Field( + None, description="CT_SvCA (alias de ca_annuel)" + ) + + sv_resultat: Optional[float] = Field(None, description="CT_SvResultat") + + @field_validator("siret") + @classmethod + def validate_siret(cls, v): + """Valide et nettoie le SIRET""" + if v and v.lower() not in ("none", "null", ""): + cleaned = v.replace(" ", "").replace("-", "") + if len(cleaned) not in (14, 15): + raise ValueError("Le SIRET doit contenir 14 ou 15 caractères") + return cleaned + return None + + @field_validator("email") + @classmethod + def validate_email(cls, v): + """Valide le format email""" + if v and v.lower() not in ("none", "null", ""): + v = v.strip() + if "@" not in v: + raise ValueError("Format email invalide") + return v + return None + + @field_validator("raccourci") + @classmethod + def validate_raccourci(cls, v): + """Force le raccourci en majuscules""" + if v and v.lower() not in ("none", "null", ""): + return v.upper().strip()[:7] + return None + + @field_validator( + "adresse", + "code_postal", + "ville", + "pays", + "telephone", + "tva_intra", + "contact", + "complement", + mode="before", + ) + @classmethod + def clean_none_strings(cls, v): + """Convertit les chaînes 'None'/'null'/'' en None""" + if isinstance(v, str) and v.lower() in ("none", "null", ""): + return None + return v + + def to_sage_dict(self) -> dict: + """ + Convertit le modèle en dictionnaire compatible avec creer_client() + Mapping 1:1 avec les paramètres réels de la fonction + """ + stat01 = self.statistique01 or self.secteur + + ca = self.ca_annuel or self.sv_chiffre_affaires + + return { + "intitule": self.intitule, + "numero": self.numero, + "type_tiers": self.type_tiers, + "qualite": self.qualite, + "classement": self.classement, + "raccourci": self.raccourci, + "siret": self.siret, + "tva_intra": self.tva_intra, + "code_naf": self.code_naf, + "contact": self.contact, + "adresse": self.adresse, + "complement": self.complement, + "code_postal": self.code_postal, + "ville": self.ville, + "region": self.region, + "pays": self.pays, + "telephone": self.telephone, + "telecopie": self.telecopie, + "email": self.email, + "site_web": self.site_web, + "portable": self.portable, + "facebook": self.facebook, + "linkedin": self.linkedin, + "compte_general": self.compte_general, + "categorie_tarifaire": self.categorie_tarifaire, + "categorie_comptable": self.categorie_comptable, + "taux01": self.taux01, + "taux02": self.taux02, + "taux03": self.taux03, + "taux04": self.taux04, + "statistique01": stat01, + "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, + "secteur": self.secteur, # Gardé pour compatibilité + "encours_autorise": self.encours_autorise, + "assurance_credit": self.assurance_credit, + "langue": self.langue, + "commercial_code": self.commercial_code, + "lettrage_auto": self.lettrage_auto, + "est_actif": self.est_actif, + "type_facture": self.type_facture, + "est_prospect": self.est_prospect, + "bl_en_facture": self.bl_en_facture, + "saut_page": self.saut_page, + "validation_echeance": self.validation_echeance, + "controle_encours": self.controle_encours, + "exclure_relance": self.exclure_relance, + "exclure_penalites": self.exclure_penalites, + "bon_a_payer": self.bon_a_payer, + "priorite_livraison": self.priorite_livraison, + "livraison_partielle": self.livraison_partielle, + "delai_transport": self.delai_transport, + "delai_appro": self.delai_appro, + "commentaire": self.commentaire, + "section_analytique": self.section_analytique, + "mode_reglement_code": self.mode_reglement_code, + "surveillance_active": self.surveillance_active, + "coface": self.coface, + "forme_juridique": self.forme_juridique, + "effectif": self.effectif, + "sv_regularite": self.sv_regularite, + "sv_cotation": self.sv_cotation, + "sv_objet_maj": self.sv_objet_maj, + "ca_annuel": ca, + "sv_chiffre_affaires": self.sv_chiffre_affaires, + "sv_resultat": self.sv_resultat, + } + + class Config: + json_schema_extra = { + "example": { + "intitule": "ENTREPRISE EXEMPLE SARL", + "numero": "CLI00123", + "type_tiers": 0, + "qualite": "CLI", + "compte_general": "411000", + "est_prospect": False, + "est_actif": True, + "email": "contact@exemple.fr", + "telephone": "0123456789", + "adresse": "123 Rue de la Paix", + "code_postal": "75001", + "ville": "Paris", + "pays": "France", + } + } + + +class ClientUpdateRequest(BaseModel): + """ + Modèle pour modification d'un client existant + TOUS les champs de ClientCreateRequest sont modifiables + TOUS optionnels (seuls les champs fournis sont modifiés) + """ + + intitule: Optional[str] = Field(None, max_length=69) + qualite: Optional[str] = Field(None, max_length=17) + classement: Optional[str] = Field(None, max_length=17) + raccourci: Optional[str] = Field(None, max_length=7) + + siret: Optional[str] = Field(None, max_length=15) + tva_intra: Optional[str] = Field(None, max_length=25) + code_naf: Optional[str] = Field(None, max_length=7) + + contact: Optional[str] = Field(None, max_length=35) + adresse: Optional[str] = Field(None, max_length=35) + complement: Optional[str] = Field(None, max_length=35) + code_postal: Optional[str] = Field(None, max_length=9) + ville: Optional[str] = Field(None, max_length=35) + region: Optional[str] = Field(None, max_length=25) + pays: Optional[str] = Field(None, max_length=35) + + telephone: Optional[str] = Field(None, max_length=21) + telecopie: Optional[str] = Field(None, max_length=21) + email: Optional[str] = Field(None, max_length=69) + site_web: Optional[str] = Field(None, max_length=69) + portable: Optional[str] = Field(None, max_length=21) + facebook: Optional[str] = Field(None, max_length=69) + linkedin: Optional[str] = Field(None, max_length=69) + + compte_general: Optional[str] = Field(None, max_length=13) + + categorie_tarifaire: Optional[str] = None + categorie_comptable: Optional[str] = None + + taux01: Optional[float] = None + taux02: Optional[float] = None + taux03: Optional[float] = None + taux04: Optional[float] = None + + secteur: Optional[str] = Field(None, max_length=21) + statistique01: Optional[str] = Field(None, max_length=21) + statistique02: Optional[str] = Field(None, max_length=21) + statistique03: Optional[str] = Field(None, max_length=21) + statistique04: Optional[str] = Field(None, max_length=21) + statistique05: Optional[str] = Field(None, max_length=21) + statistique06: Optional[str] = Field(None, max_length=21) + statistique07: Optional[str] = Field(None, max_length=21) + statistique08: Optional[str] = Field(None, max_length=21) + statistique09: Optional[str] = Field(None, max_length=21) + statistique10: Optional[str] = Field(None, max_length=21) + + encours_autorise: Optional[float] = None + assurance_credit: Optional[float] = None + langue: Optional[int] = Field(None, ge=0) + commercial_code: Optional[int] = None + + lettrage_auto: Optional[bool] = None + est_actif: Optional[bool] = None + type_facture: Optional[int] = Field(None, ge=0, le=2) + est_prospect: Optional[bool] = None + bl_en_facture: Optional[int] = Field(None, ge=0, le=1) + saut_page: Optional[int] = Field(None, ge=0, le=1) + validation_echeance: Optional[int] = Field(None, ge=0, le=1) + controle_encours: Optional[int] = Field(None, ge=0, le=1) + exclure_relance: Optional[int] = Field(None, ge=0, le=1) + exclure_penalites: Optional[int] = Field(None, ge=0, le=1) + bon_a_payer: Optional[int] = Field(None, ge=0, le=1) + + priorite_livraison: Optional[int] = Field(None, ge=0, le=5) + livraison_partielle: Optional[int] = Field(None, ge=0, le=1) + delai_transport: Optional[int] = Field(None, ge=0) + delai_appro: Optional[int] = Field(None, ge=0) + + commentaire: Optional[str] = Field(None, max_length=35) + + section_analytique: Optional[str] = Field(None, max_length=13) + + mode_reglement_code: Optional[int] = None + + surveillance_active: Optional[int] = Field(None, ge=0, le=1) + coface: Optional[str] = Field(None, max_length=25) + forme_juridique: Optional[str] = Field(None, max_length=33) + effectif: Optional[str] = Field(None, max_length=11) + sv_regularite: Optional[str] = Field(None, max_length=3) + sv_cotation: Optional[str] = Field(None, max_length=5) + sv_objet_maj: Optional[str] = Field(None, max_length=61) + ca_annuel: Optional[float] = None + sv_chiffre_affaires: Optional[float] = None + sv_resultat: Optional[float] = None + + @field_validator("siret") + @classmethod + def validate_siret(cls, v): + if v and v.lower() not in ("none", "null", ""): + cleaned = v.replace(" ", "").replace("-", "") + if len(cleaned) not in (14, 15): + raise ValueError("Le SIRET doit contenir 14 ou 15 caractères") + return cleaned + return None + + @field_validator("email") + @classmethod + def validate_email(cls, v): + if v and v.lower() not in ("none", "null", ""): + v = v.strip() + if "@" not in v: + raise ValueError("Format email invalide") + return v + return None + + @field_validator("raccourci") + @classmethod + def validate_raccourci(cls, v): + if v and v.lower() not in ("none", "null", ""): + return v.upper().strip()[:7] + return None + + @field_validator( + "adresse", + "code_postal", + "ville", + "pays", + "telephone", + "tva_intra", + "contact", + "complement", + mode="before", + ) + @classmethod + def clean_none_strings(cls, v): + if isinstance(v, str) and v.lower() in ("none", "null", ""): + return None + return v + + class Config: + json_schema_extra = { + "example": { + "email": "nouveau@email.fr", + "telephone": "0198765432", + "portable": "0687654321", + "adresse": "456 Avenue Nouvelle", + "ville": "Lyon", + } + } diff --git a/schemas/tiers/contact.py b/schemas/tiers/contact.py index 8c8bcc1..3676872 100644 --- a/schemas/tiers/contact.py +++ b/schemas/tiers/contact.py @@ -1,30 +1,40 @@ -from typing import Optional, ClassVar from pydantic import BaseModel, Field, validator +from typing import Optional, ClassVar + class Contact(BaseModel): """Contact associé à un tiers""" + numero: Optional[str] = Field(None, description="Code du tiers parent (CT_Num)") - contact_numero: Optional[int] = Field(None, description="Numéro unique du contact (CT_No)") - n_contact: Optional[int] = Field(None, description="Numéro de référence contact (N_Contact)") - - civilite: Optional[str] = Field(None, description="Civilité : M., Mme, Mlle (CT_Civilite)") + contact_numero: Optional[int] = Field( + None, description="Numéro unique du contact (CT_No)" + ) + n_contact: Optional[int] = Field( + None, description="Numéro de référence contact (N_Contact)" + ) + + civilite: Optional[str] = Field( + None, description="Civilité : M., Mme, Mlle (CT_Civilite)" + ) nom: Optional[str] = Field(None, description="Nom de famille (CT_Nom)") prenom: Optional[str] = Field(None, description="Prénom (CT_Prenom)") fonction: Optional[str] = Field(None, description="Fonction/Titre (CT_Fonction)") - + service_code: Optional[int] = Field(None, description="Code du service (N_Service)") - + telephone: Optional[str] = Field(None, description="Téléphone fixe (CT_Telephone)") - portable: Optional[str] = Field(None, description="Téléphone mobile (CT_TelPortable)") + portable: Optional[str] = Field( + None, description="Téléphone mobile (CT_TelPortable)" + ) telecopie: Optional[str] = Field(None, description="Fax (CT_Telecopie)") email: Optional[str] = Field(None, description="Adresse email (CT_EMail)") - + facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)") linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)") skype: Optional[str] = Field(None, description="Identifiant Skype (CT_Skype)") est_defaut: Optional[bool] = Field(False, description="Contact par défaut") - + civilite_map: ClassVar[dict] = { 0: "M.", 1: "Mme", @@ -38,4 +48,70 @@ class Contact(BaseModel): return v if isinstance(v, int): return cls.civilite_map.get(v, str(v)) - return v \ No newline at end of file + return v + + +class ContactCreate(BaseModel): + """Données pour créer ou modifier un contact""" + + numero: str = Field(..., description="Code du client parent (obligatoire)") + + civilite: Optional[str] = Field(None, description="M., Mme, Mlle, Société") + nom: str = Field(..., description="Nom de famille (obligatoire)") + prenom: Optional[str] = Field(None, description="Prénom") + fonction: Optional[str] = Field(None, description="Fonction/Titre") + + est_defaut: Optional[bool] = Field( + False, description="Définir comme contact par défaut du client" + ) + + service_code: Optional[int] = Field(None, description="Code du service") + + telephone: Optional[str] = Field(None, description="Téléphone fixe") + portable: Optional[str] = Field(None, description="Téléphone mobile") + telecopie: Optional[str] = Field(None, description="Fax") + email: Optional[str] = Field(None, description="Email") + + facebook: Optional[str] = Field(None, description="URL Facebook") + linkedin: Optional[str] = Field(None, description="URL LinkedIn") + skype: Optional[str] = Field(None, description="Identifiant Skype") + + @validator("civilite") + def validate_civilite(cls, v): + if v and v not in ["M.", "Mme", "Mlle", "Société"]: + raise ValueError("Civilité doit être: M., Mme, Mlle ou Société") + return v + + class Config: + json_schema_extra = { + "example": { + "numero": "CLI000001", + "civilite": "M.", + "nom": "Dupont", + "prenom": "Jean", + "fonction": "Directeur Commercial", + "telephone": "0123456789", + "portable": "0612345678", + "email": "j.dupont@exemple.fr", + "linkedin": "https://linkedin.com/in/jeandupont", + "est_defaut": True, + } + } + + +class ContactUpdate(BaseModel): + """Données pour modifier un contact (tous champs optionnels)""" + + civilite: Optional[str] = None + nom: Optional[str] = None + prenom: Optional[str] = None + fonction: Optional[str] = None + service_code: Optional[int] = None + telephone: Optional[str] = None + portable: Optional[str] = None + telecopie: Optional[str] = None + email: Optional[str] = None + facebook: Optional[str] = None + linkedin: Optional[str] = None + skype: Optional[str] = None + est_defaut: Optional[bool] = None diff --git a/schemas/tiers/fournisseurs.py b/schemas/tiers/fournisseurs.py new file mode 100644 index 0000000..af880dc --- /dev/null +++ b/schemas/tiers/fournisseurs.py @@ -0,0 +1,327 @@ +from pydantic import BaseModel, Field, EmailStr +from typing import List, Optional +from schemas.tiers.contact import Contact + + +class FournisseurDetails(BaseModel): + numero: Optional[str] = Field(None, description="Code fournisseur (CT_Num)") + intitule: Optional[str] = Field( + None, description="Raison sociale ou Nom complet (CT_Intitule)" + ) + type_tiers: Optional[int] = Field( + None, description="Type : 0=Client, 1=Fournisseur (CT_Type)" + ) + qualite: Optional[str] = Field( + None, description="Qualité Sage : CLI, FOU, PRO (CT_Qualite)" + ) + classement: Optional[str] = Field( + None, description="Code de classement (CT_Classement)" + ) + raccourci: Optional[str] = Field( + None, description="Code raccourci 7 car. (CT_Raccourci)" + ) + siret: Optional[str] = Field(None, description="N° SIRET 14 chiffres (CT_Siret)") + tva_intra: Optional[str] = Field( + None, description="N° TVA intracommunautaire (CT_Identifiant)" + ) + code_naf: Optional[str] = Field(None, description="Code NAF/APE (CT_Ape)") + + contact: Optional[str] = Field( + None, description="Nom du contact principal (CT_Contact)" + ) + adresse: Optional[str] = Field(None, description="Adresse ligne 1 (CT_Adresse)") + complement: Optional[str] = Field( + None, description="Complément d'adresse (CT_Complement)" + ) + code_postal: Optional[str] = Field(None, description="Code postal (CT_CodePostal)") + ville: Optional[str] = Field(None, description="Ville (CT_Ville)") + region: Optional[str] = Field(None, description="Région/État (CT_CodeRegion)") + pays: Optional[str] = Field(None, description="Pays (CT_Pays)") + + telephone: Optional[str] = Field(None, description="Téléphone fixe (CT_Telephone)") + telecopie: Optional[str] = Field(None, description="Fax (CT_Telecopie)") + email: Optional[str] = Field(None, description="Email principal (CT_EMail)") + site_web: Optional[str] = Field(None, description="Site web (CT_Site)") + facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)") + linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)") + + taux01: Optional[float] = Field(None, description="Taux personnalisé 1 (CT_Taux01)") + taux02: Optional[float] = Field(None, description="Taux personnalisé 2 (CT_Taux02)") + taux03: Optional[float] = Field(None, description="Taux personnalisé 3 (CT_Taux03)") + taux04: Optional[float] = Field(None, description="Taux personnalisé 4 (CT_Taux04)") + + statistique01: Optional[str] = Field( + None, description="Statistique 1 (CT_Statistique01)" + ) + statistique02: Optional[str] = Field( + None, description="Statistique 2 (CT_Statistique02)" + ) + statistique03: Optional[str] = Field( + None, description="Statistique 3 (CT_Statistique03)" + ) + statistique04: Optional[str] = Field( + None, description="Statistique 4 (CT_Statistique04)" + ) + statistique05: Optional[str] = Field( + None, description="Statistique 5 (CT_Statistique05)" + ) + statistique06: Optional[str] = Field( + None, description="Statistique 6 (CT_Statistique06)" + ) + statistique07: Optional[str] = Field( + None, description="Statistique 7 (CT_Statistique07)" + ) + statistique08: Optional[str] = Field( + None, description="Statistique 8 (CT_Statistique08)" + ) + statistique09: Optional[str] = Field( + None, description="Statistique 9 (CT_Statistique09)" + ) + statistique10: Optional[str] = Field( + None, description="Statistique 10 (CT_Statistique10)" + ) + + encours_autorise: Optional[float] = Field( + None, description="Encours maximum autorisé (CT_Encours)" + ) + assurance_credit: Optional[float] = Field( + None, description="Montant assurance crédit (CT_Assurance)" + ) + langue: Optional[int] = Field( + None, description="Code langue 0=FR, 1=EN (CT_Langue)" + ) + commercial_code: Optional[int] = Field( + None, description="Code du commercial (CO_No)" + ) + + lettrage_auto: Optional[bool] = Field( + None, description="Lettrage automatique (CT_Lettrage)" + ) + est_actif: Optional[bool] = Field(None, description="True si actif (CT_Sommeil=0)") + type_facture: Optional[int] = Field( + None, description="Type facture 0=Facture, 1=BL (CT_Facture)" + ) + est_prospect: Optional[bool] = Field( + None, description="True si prospect (CT_Prospect=1)" + ) + bl_en_facture: Optional[int] = Field( + None, description="Imprimer BL en facture (CT_BLFact)" + ) + saut_page: Optional[int] = Field( + None, description="Saut de page sur documents (CT_Saut)" + ) + validation_echeance: Optional[int] = Field( + None, description="Valider les échéances (CT_ValidEch)" + ) + controle_encours: Optional[int] = Field( + None, description="Contrôler l'encours (CT_ControlEnc)" + ) + exclure_relance: Optional[bool] = Field( + None, description="Exclure des relances (CT_NotRappel)" + ) + exclure_penalites: Optional[bool] = Field( + None, description="Exclure des pénalités (CT_NotPenal)" + ) + bon_a_payer: Optional[int] = Field( + None, description="Bon à payer obligatoire (CT_BonAPayer)" + ) + + priorite_livraison: Optional[int] = Field( + None, description="Priorité livraison (CT_PrioriteLivr)" + ) + livraison_partielle: Optional[int] = Field( + None, description="Livraison partielle (CT_LivrPartielle)" + ) + delai_transport: Optional[int] = Field( + None, description="Délai transport jours (CT_DelaiTransport)" + ) + delai_appro: Optional[int] = Field( + None, description="Délai appro jours (CT_DelaiAppro)" + ) + + commentaire: Optional[str] = Field( + None, description="Commentaire libre (CT_Commentaire)" + ) + + section_analytique: Optional[str] = Field( + None, description="Section analytique (CA_Num)" + ) + + mode_reglement_code: Optional[int] = Field( + None, description="Code mode règlement (MR_No)" + ) + surveillance_active: Optional[bool] = Field( + None, description="Surveillance financière (CT_Surveillance)" + ) + coface: Optional[str] = Field(None, description="Code Coface 25 car. (CT_Coface)") + forme_juridique: Optional[str] = Field( + None, description="Forme juridique SA, SARL (CT_SvFormeJuri)" + ) + effectif: Optional[str] = Field( + None, description="Nombre d'employés (CT_SvEffectif)" + ) + sv_regularite: Optional[str] = Field( + None, description="Régularité paiements (CT_SvRegul)" + ) + sv_cotation: Optional[str] = Field( + None, description="Cotation crédit (CT_SvCotation)" + ) + sv_objet_maj: Optional[str] = Field( + None, description="Objet dernière MAJ (CT_SvObjetMaj)" + ) + sv_chiffre_affaires: Optional[float] = Field( + None, description="Chiffre d'affaires (CT_SvCA)" + ) + sv_resultat: Optional[float] = Field( + None, description="Résultat financier (CT_SvResultat)" + ) + + compte_general: Optional[str] = Field( + None, description="Compte général principal (CG_NumPrinc)" + ) + categorie_tarif: Optional[int] = Field( + None, description="Catégorie tarifaire (N_CatTarif)" + ) + categorie_compta: Optional[int] = Field( + None, description="Catégorie comptable (N_CatCompta)" + ) + + contacts: Optional[List[Contact]] = Field( + default_factory=list, description="Liste des contacts du fournisseur" + ) + + class Config: + json_schema_extra = { + "example": { + "numero": "FOU000001", + "intitule": "SARL FOURNISSEUR EXEMPLE", + "type_tiers": 1, + "qualite": "FOU", + "classement": "A", + "raccourci": "EXEMPL", + "siret": "12345678901234", + "tva_intra": "FR12345678901", + "code_naf": "6201Z", + "contact": "Jean Dupont", + "adresse": "123 Rue de la Paix", + "complement": "Bâtiment B", + "code_postal": "75001", + "ville": "Paris", + "region": "Île-de-France", + "pays": "France", + "telephone": "0123456789", + "telecopie": "0123456788", + "email": "contact@exemple.fr", + "site_web": "https://www.exemple.fr", + "facebook": "https://facebook.com/exemple", + "linkedin": "https://linkedin.com/company/exemple", + "taux01": 0.0, + "taux02": 0.0, + "taux03": 0.0, + "taux04": 0.0, + "statistique01": "Informatique", + "statistique02": "", + "statistique03": "", + "statistique04": "", + "statistique05": "", + "statistique06": "", + "statistique07": "", + "statistique08": "", + "statistique09": "", + "statistique10": "", + "encours_autorise": 50000.0, + "assurance_credit": 40000.0, + "langue": 0, + "commercial_code": 1, + "lettrage_auto": True, + "est_actif": True, + "type_facture": 1, + "est_prospect": False, + "bl_en_facture": 0, + "saut_page": 0, + "validation_echeance": 0, + "controle_encours": 1, + "exclure_relance": False, + "exclure_penalites": False, + "bon_a_payer": 0, + "priorite_livraison": 1, + "livraison_partielle": 1, + "delai_transport": 2, + "delai_appro": 0, + "commentaire": "Client important", + "section_analytique": "", + "mode_reglement_code": 1, + "surveillance_active": True, + "coface": "COF12345", + "forme_juridique": "SARL", + "effectif": "50-99", + "sv_regularite": "", + "sv_cotation": "", + "sv_objet_maj": "", + "sv_chiffre_affaires": 2500000.0, + "sv_resultat": 150000.0, + "compte_general": "4110000", + "categorie_tarif": 0, + "categorie_compta": 0, + } + } + + +class FournisseurCreateAPIRequest(BaseModel): + intitule: str = Field( + ..., min_length=1, max_length=69, description="Raison sociale du fournisseur" + ) + compte_collectif: str = Field( + "401000", description="Compte comptable fournisseur (ex: 401000)" + ) + num: Optional[str] = Field( + None, max_length=17, description="Code fournisseur souhaité (optionnel)" + ) + 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) + email: Optional[EmailStr] = None + telephone: Optional[str] = Field(None, max_length=21) + siret: Optional[str] = Field(None, max_length=14) + tva_intra: Optional[str] = Field(None, max_length=25) + + class Config: + json_schema_extra = { + "example": { + "intitule": "ACME SUPPLIES SARL", + "compte_collectif": "401000", + "num": "FOUR001", + "adresse": "15 Rue du Commerce", + "code_postal": "75001", + "ville": "Paris", + "pays": "France", + "email": "contact@acmesupplies.fr", + "telephone": "0145678901", + "siret": "12345678901234", + "tva_intra": "FR12345678901", + } + } + + +class FournisseurUpdateRequest(BaseModel): + """Modèle pour modification d'un fournisseur existant""" + + intitule: Optional[str] = Field(None, min_length=1, max_length=69) + 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) + email: Optional[EmailStr] = None + telephone: Optional[str] = Field(None, max_length=21) + siret: Optional[str] = Field(None, max_length=14) + tva_intra: Optional[str] = Field(None, max_length=25) + + class Config: + json_schema_extra = { + "example": { + "intitule": "ACME SUPPLIES MODIFIÉ", + "email": "nouveau@acme.fr", + "telephone": "0198765432", + } + } diff --git a/schemas/tiers/tiers.py b/schemas/tiers/tiers.py index 89e48e1..badfe78 100644 --- a/schemas/tiers/tiers.py +++ b/schemas/tiers/tiers.py @@ -1,23 +1,50 @@ from typing import List, Optional from pydantic import BaseModel, Field from schemas.tiers.contact import Contact +from enum import Enum, IntEnum + + +class TypeTiersInt(IntEnum): + """CT_Type - Type de tiers""" + + CLIENT = 0 + FOURNISSEUR = 1 + SALARIE = 2 + AUTRE = 3 + class TiersDetails(BaseModel): # IDENTIFICATION numero: Optional[str] = Field(None, description="Code tiers (CT_Num)") - intitule: Optional[str] = Field(None, description="Raison sociale ou Nom complet (CT_Intitule)") - type_tiers: Optional[int] = Field(None, description="Type : 0=Client, 1=Fournisseur (CT_Type)") - qualite: Optional[str] = Field(None, description="Qualité Sage : CLI, FOU, PRO (CT_Qualite)") - classement: Optional[str] = Field(None, description="Code de classement (CT_Classement)") - raccourci: Optional[str] = Field(None, description="Code raccourci 7 car. (CT_Raccourci)") + intitule: Optional[str] = Field( + None, description="Raison sociale ou Nom complet (CT_Intitule)" + ) + type_tiers: Optional[int] = Field( + None, description="Type : 0=Client, 1=Fournisseur (CT_Type)" + ) + qualite: Optional[str] = Field( + None, description="Qualité Sage : CLI, FOU, PRO (CT_Qualite)" + ) + classement: Optional[str] = Field( + None, description="Code de classement (CT_Classement)" + ) + raccourci: Optional[str] = Field( + None, description="Code raccourci 7 car. (CT_Raccourci)" + ) siret: Optional[str] = Field(None, description="N° SIRET 14 chiffres (CT_Siret)") - tva_intra: Optional[str] = Field(None, description="N° TVA intracommunautaire (CT_Identifiant)") + tva_intra: Optional[str] = Field( + None, description="N° TVA intracommunautaire (CT_Identifiant)" + ) code_naf: Optional[str] = Field(None, description="Code NAF/APE (CT_Ape)") # ADRESSE - contact: Optional[str] = Field(None, description="Nom du contact principal (CT_Contact)") + contact: Optional[str] = Field( + None, description="Nom du contact principal (CT_Contact)" + ) adresse: Optional[str] = Field(None, description="Adresse ligne 1 (CT_Adresse)") - complement: Optional[str] = Field(None, description="Complément d'adresse (CT_Complement)") + complement: Optional[str] = Field( + None, description="Complément d'adresse (CT_Complement)" + ) code_postal: Optional[str] = Field(None, description="Code postal (CT_CodePostal)") ville: Optional[str] = Field(None, description="Ville (CT_Ville)") region: Optional[str] = Field(None, description="Région/État (CT_CodeRegion)") @@ -38,67 +65,150 @@ class TiersDetails(BaseModel): taux04: Optional[float] = Field(None, description="Taux personnalisé 4 (CT_Taux04)") # STATISTIQUES - statistique01: Optional[str] = Field(None, description="Statistique 1 (CT_Statistique01)") - statistique02: Optional[str] = Field(None, description="Statistique 2 (CT_Statistique02)") - statistique03: Optional[str] = Field(None, description="Statistique 3 (CT_Statistique03)") - statistique04: Optional[str] = Field(None, description="Statistique 4 (CT_Statistique04)") - statistique05: Optional[str] = Field(None, description="Statistique 5 (CT_Statistique05)") - statistique06: Optional[str] = Field(None, description="Statistique 6 (CT_Statistique06)") - statistique07: Optional[str] = Field(None, description="Statistique 7 (CT_Statistique07)") - statistique08: Optional[str] = Field(None, description="Statistique 8 (CT_Statistique08)") - statistique09: Optional[str] = Field(None, description="Statistique 9 (CT_Statistique09)") - statistique10: Optional[str] = Field(None, description="Statistique 10 (CT_Statistique10)") + statistique01: Optional[str] = Field( + None, description="Statistique 1 (CT_Statistique01)" + ) + statistique02: Optional[str] = Field( + None, description="Statistique 2 (CT_Statistique02)" + ) + statistique03: Optional[str] = Field( + None, description="Statistique 3 (CT_Statistique03)" + ) + statistique04: Optional[str] = Field( + None, description="Statistique 4 (CT_Statistique04)" + ) + statistique05: Optional[str] = Field( + None, description="Statistique 5 (CT_Statistique05)" + ) + statistique06: Optional[str] = Field( + None, description="Statistique 6 (CT_Statistique06)" + ) + statistique07: Optional[str] = Field( + None, description="Statistique 7 (CT_Statistique07)" + ) + statistique08: Optional[str] = Field( + None, description="Statistique 8 (CT_Statistique08)" + ) + statistique09: Optional[str] = Field( + None, description="Statistique 9 (CT_Statistique09)" + ) + statistique10: Optional[str] = Field( + None, description="Statistique 10 (CT_Statistique10)" + ) # COMMERCIAL - encours_autorise: Optional[float] = Field(None, description="Encours maximum autorisé (CT_Encours)") - assurance_credit: Optional[float] = Field(None, description="Montant assurance crédit (CT_Assurance)") - langue: Optional[int] = Field(None, description="Code langue 0=FR, 1=EN (CT_Langue)") - commercial_code: Optional[int] = Field(None, description="Code du commercial (CO_No)") + encours_autorise: Optional[float] = Field( + None, description="Encours maximum autorisé (CT_Encours)" + ) + assurance_credit: Optional[float] = Field( + None, description="Montant assurance crédit (CT_Assurance)" + ) + langue: Optional[int] = Field( + None, description="Code langue 0=FR, 1=EN (CT_Langue)" + ) + commercial_code: Optional[int] = Field( + None, description="Code du commercial (CO_No)" + ) # FACTURATION - lettrage_auto: Optional[bool] = Field(None, description="Lettrage automatique (CT_Lettrage)") + lettrage_auto: Optional[bool] = Field( + None, description="Lettrage automatique (CT_Lettrage)" + ) est_actif: Optional[bool] = Field(None, description="True si actif (CT_Sommeil=0)") - type_facture: Optional[int] = Field(None, description="Type facture 0=Facture, 1=BL (CT_Facture)") - est_prospect: Optional[bool] = Field(None, description="True si prospect (CT_Prospect=1)") - bl_en_facture: Optional[int] = Field(None, description="Imprimer BL en facture (CT_BLFact)") - saut_page: Optional[int] = Field(None, description="Saut de page sur documents (CT_Saut)") - validation_echeance: Optional[int] = Field(None, description="Valider les échéances (CT_ValidEch)") - controle_encours: Optional[int] = Field(None, description="Contrôler l'encours (CT_ControlEnc)") - exclure_relance: Optional[bool] = Field(None, description="Exclure des relances (CT_NotRappel)") - exclure_penalites: Optional[bool] = Field(None, description="Exclure des pénalités (CT_NotPenal)") - bon_a_payer: Optional[int] = Field(None, description="Bon à payer obligatoire (CT_BonAPayer)") + type_facture: Optional[int] = Field( + None, description="Type facture 0=Facture, 1=BL (CT_Facture)" + ) + est_prospect: Optional[bool] = Field( + None, description="True si prospect (CT_Prospect=1)" + ) + bl_en_facture: Optional[int] = Field( + None, description="Imprimer BL en facture (CT_BLFact)" + ) + saut_page: Optional[int] = Field( + None, description="Saut de page sur documents (CT_Saut)" + ) + validation_echeance: Optional[int] = Field( + None, description="Valider les échéances (CT_ValidEch)" + ) + controle_encours: Optional[int] = Field( + None, description="Contrôler l'encours (CT_ControlEnc)" + ) + exclure_relance: Optional[bool] = Field( + None, description="Exclure des relances (CT_NotRappel)" + ) + exclure_penalites: Optional[bool] = Field( + None, description="Exclure des pénalités (CT_NotPenal)" + ) + bon_a_payer: Optional[int] = Field( + None, description="Bon à payer obligatoire (CT_BonAPayer)" + ) # LOGISTIQUE - priorite_livraison: Optional[int] = Field(None, description="Priorité livraison (CT_PrioriteLivr)") - livraison_partielle: Optional[int] = Field(None, description="Livraison partielle (CT_LivrPartielle)") - delai_transport: Optional[int] = Field(None, description="Délai transport jours (CT_DelaiTransport)") - delai_appro: Optional[int] = Field(None, description="Délai appro jours (CT_DelaiAppro)") + priorite_livraison: Optional[int] = Field( + None, description="Priorité livraison (CT_PrioriteLivr)" + ) + livraison_partielle: Optional[int] = Field( + None, description="Livraison partielle (CT_LivrPartielle)" + ) + delai_transport: Optional[int] = Field( + None, description="Délai transport jours (CT_DelaiTransport)" + ) + delai_appro: Optional[int] = Field( + None, description="Délai appro jours (CT_DelaiAppro)" + ) # COMMENTAIRE - commentaire: Optional[str] = Field(None, description="Commentaire libre (CT_Commentaire)") + commentaire: Optional[str] = Field( + None, description="Commentaire libre (CT_Commentaire)" + ) # ANALYTIQUE - section_analytique: Optional[str] = Field(None, description="Section analytique (CA_Num)") + section_analytique: Optional[str] = Field( + None, description="Section analytique (CA_Num)" + ) # ORGANISATION / SURVEILLANCE - mode_reglement_code: Optional[int] = Field(None, description="Code mode règlement (MR_No)") - surveillance_active: Optional[bool] = Field(None, description="Surveillance financière (CT_Surveillance)") + mode_reglement_code: Optional[int] = Field( + None, description="Code mode règlement (MR_No)" + ) + surveillance_active: Optional[bool] = Field( + None, description="Surveillance financière (CT_Surveillance)" + ) coface: Optional[str] = Field(None, description="Code Coface 25 car. (CT_Coface)") - forme_juridique: Optional[str] = Field(None, description="Forme juridique SA, SARL (CT_SvFormeJuri)") - effectif: Optional[str] = Field(None, description="Nombre d'employés (CT_SvEffectif)") - sv_regularite: Optional[str] = Field(None, description="Régularité paiements (CT_SvRegul)") - sv_cotation: Optional[str] = Field(None, description="Cotation crédit (CT_SvCotation)") - sv_objet_maj: Optional[str] = Field(None, description="Objet dernière MAJ (CT_SvObjetMaj)") - sv_chiffre_affaires: Optional[float] = Field(None, description="Chiffre d'affaires (CT_SvCA)") - sv_resultat: Optional[float] = Field(None, description="Résultat financier (CT_SvResultat)") + forme_juridique: Optional[str] = Field( + None, description="Forme juridique SA, SARL (CT_SvFormeJuri)" + ) + effectif: Optional[str] = Field( + None, description="Nombre d'employés (CT_SvEffectif)" + ) + sv_regularite: Optional[str] = Field( + None, description="Régularité paiements (CT_SvRegul)" + ) + sv_cotation: Optional[str] = Field( + None, description="Cotation crédit (CT_SvCotation)" + ) + sv_objet_maj: Optional[str] = Field( + None, description="Objet dernière MAJ (CT_SvObjetMaj)" + ) + sv_chiffre_affaires: Optional[float] = Field( + None, description="Chiffre d'affaires (CT_SvCA)" + ) + sv_resultat: Optional[float] = Field( + None, description="Résultat financier (CT_SvResultat)" + ) # COMPTE GENERAL ET CATEGORIES - compte_general: Optional[str] = Field(None, description="Compte général principal (CG_NumPrinc)") - categorie_tarif: Optional[int] = Field(None, description="Catégorie tarifaire (N_CatTarif)") - categorie_compta: Optional[int] = Field(None, description="Catégorie comptable (N_CatCompta)") - + compte_general: Optional[str] = Field( + None, description="Compte général principal (CG_NumPrinc)" + ) + categorie_tarif: Optional[int] = Field( + None, description="Catégorie tarifaire (N_CatTarif)" + ) + categorie_compta: Optional[int] = Field( + None, description="Catégorie comptable (N_CatCompta)" + ) + # CONTACTS contacts: Optional[List[Contact]] = Field( - default_factory=list, - description="Liste des contacts du tiers" - ) \ No newline at end of file + default_factory=list, description="Liste des contacts du tiers" + ) diff --git a/schemas/tiers/type_tiers.py b/schemas/tiers/type_tiers.py index c5a8cf1..8c5d423 100644 --- a/schemas/tiers/type_tiers.py +++ b/schemas/tiers/type_tiers.py @@ -1,8 +1,10 @@ from enum import Enum + class TypeTiers(str, Enum): """Types de tiers possibles""" + ALL = "all" CLIENT = "client" FOURNISSEUR = "fournisseur" - PROSPECT = "prospect" \ No newline at end of file + PROSPECT = "prospect" diff --git a/schemas/user.py b/schemas/user.py new file mode 100644 index 0000000..1682184 --- /dev/null +++ b/schemas/user.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel +from typing import Optional + + +class UserResponse(BaseModel): + """Modèle de réponse pour un utilisateur""" + + id: str + email: str + nom: str + prenom: str + role: str + is_verified: bool + is_active: bool + created_at: str + last_login: Optional[str] = None + failed_login_attempts: int = 0 + + class Config: + from_attributes = True diff --git a/utils/generic_functions.py b/utils/generic_functions.py new file mode 100644 index 0000000..03490de --- /dev/null +++ b/utils/generic_functions.py @@ -0,0 +1,259 @@ + +from typing import Dict +from config import settings +import logging + +from data.data import templates_signature_email + +logger = logging.getLogger(__name__) + +async def universign_envoyer( + doc_id: str, + pdf_bytes: bytes, + email: str, + nom: str, + doc_data: Dict, + session: AsyncSession, +) -> Dict: + import requests + + try: + api_key = settings.universign_api_key + api_url = settings.universign_api_url + auth = (api_key, "") + + logger.info(f" Démarrage processus Universign pour {email}") + logger.info(f"Document: {doc_id} ({doc_data.get('type_label')})") + + if not pdf_bytes or len(pdf_bytes) == 0: + raise Exception("Le PDF généré est vide") + + logger.info(f"PDF valide : {len(pdf_bytes)} octets") + + logger.info("ÉTAPE 1/6 : Création transaction") + + response = requests.post( + f"{api_url}/transactions", + auth=auth, + json={ + "name": f"{doc_data.get('type_label', 'Document')} {doc_id}", + "language": "fr", + }, + timeout=30, + ) + + if response.status_code != 200: + logger.error(f"Erreur création transaction: {response.text}") + raise Exception(f"Erreur création transaction: {response.status_code}") + + transaction_id = response.json().get("id") + logger.info(f"Transaction créée: {transaction_id}") + + logger.info("ÉTAPE 2/6 : Upload PDF") + + files = { + "file": ( + f"{doc_data.get('type_label', 'Document')}_{doc_id}.pdf", + pdf_bytes, + "application/pdf", + ) + } + + response = requests.post( + f"{api_url}/files", + auth=auth, + files=files, + timeout=60, + ) + + if response.status_code not in [200, 201]: + logger.error(f"Erreur upload: {response.text}") + raise Exception(f"Erreur upload fichier: {response.status_code}") + + file_id = response.json().get("id") + logger.info(f"Fichier uploadé: {file_id}") + + logger.info("ÉTAPE 3/6 : Ajout document à transaction") + + response = requests.post( + f"{api_url}/transactions/{transaction_id}/documents", + auth=auth, + data={"document": file_id}, + timeout=30, + ) + + if response.status_code not in [200, 201]: + logger.error(f"Erreur ajout document: {response.text}") + raise Exception(f"Erreur ajout document: {response.status_code}") + + document_id = response.json().get("id") + logger.info(f"Document ajouté: {document_id}") + + logger.info("ÉTAPE 4/6 : Création champ signature") + + response = requests.post( + f"{api_url}/transactions/{transaction_id}/documents/{document_id}/fields", + auth=auth, + data={ + "type": "signature", + }, + timeout=30, + ) + + if response.status_code not in [200, 201]: + logger.error(f"Erreur création champ: {response.text}") + raise Exception(f"Erreur création champ: {response.status_code}") + + field_id = response.json().get("id") + logger.info(f"Champ créé: {field_id}") + + logger.info(" ÉTAPE 5/6 : Liaison signataire au champ") + + response = requests.post( + f"{api_url}/transactions/{transaction_id}/signatures", # /signatures pas /signers + auth=auth, + data={ + "signer": email, + "field": field_id, + }, + timeout=30, + ) + + if response.status_code not in [200, 201]: + logger.error(f"Erreur liaison signataire: {response.text}") + raise Exception(f"Erreur liaison signataire: {response.status_code}") + + logger.info(f"Signataire lié: {email}") + + logger.info("ÉTAPE 6/6 : Démarrage transaction") + + response = requests.post( + f"{api_url}/transactions/{transaction_id}/start", auth=auth, timeout=30 + ) + + if response.status_code not in [200, 201]: + logger.error(f"Erreur démarrage: {response.text}") + raise Exception(f"Erreur démarrage: {response.status_code}") + + final_data = response.json() + logger.info("Transaction démarrée") + + logger.info("Récupération URL de signature") + + signer_url = "" + + if final_data.get("actions"): + for action in final_data["actions"]: + if action.get("url"): + signer_url = action["url"] + break + + if not signer_url and final_data.get("signers"): + for signer in final_data["signers"]: + if signer.get("email") == email: + signer_url = signer.get("url", "") + break + + if not signer_url: + logger.error(f"URL introuvable dans: {final_data}") + raise ValueError("URL de signature non retournée par Universign") + + logger.info("URL récupérée") + + logger.info(" Préparation email") + + template = templates_signature_email["demande_signature"] + + type_labels = { + 0: "Devis", + 10: "Commande", + 30: "Bon de Livraison", + 60: "Facture", + 50: "Avoir", + } + + variables = { + "NOM_SIGNATAIRE": nom, + "TYPE_DOC": type_labels.get(doc_data.get("type_doc", 0), "Document"), + "NUMERO": doc_id, + "DATE": doc_data.get("date", datetime.now().strftime("%d/%m/%Y")), + "MONTANT_TTC": f"{doc_data.get('montant_ttc', 0):.2f}", + "SIGNER_URL": signer_url, + "CONTACT_EMAIL": settings.smtp_from, + } + + sujet = template["sujet"] + corps = template["corps_html"] + + for var, valeur in variables.items(): + sujet = sujet.replace(f"{{{{{var}}}}}", str(valeur)) + corps = corps.replace(f"{{{{{var}}}}}", str(valeur)) + + email_log = EmailLog( + id=str(uuid.uuid4()), + destinataire=email, + sujet=sujet, + corps_html=corps, + document_ids=doc_id, + type_document=doc_data.get("type_doc"), + statut=StatutEmailEnum.EN_ATTENTE, + date_creation=datetime.now(), + nb_tentatives=0, + ) + + session.add(email_log) + await session.flush() + + email_queue.enqueue(email_log.id) + + logger.info(f"Email mis en file pour {email}") + logger.info("🎉 Processus terminé avec succès") + + return { + "transaction_id": transaction_id, + "signer_url": signer_url, + "statut": "ENVOYE", + "email_log_id": email_log.id, + "email_sent": True, + } + + except Exception as e: + logger.error(f"Erreur Universign: {e}", exc_info=True) + return { + "error": str(e), + "statut": "ERREUR", + "email_sent": False, + } + + +async def universign_statut(transaction_id: str) -> Dict: + """Récupération statut signature""" + import requests + + try: + response = requests.get( + f"{settings.universign_api_url}/transactions/{transaction_id}", + auth=(settings.universign_api_key, ""), + timeout=10, + ) + + if response.status_code == 200: + data = response.json() + statut_map = { + "draft": "EN_ATTENTE", + "started": "EN_ATTENTE", + "completed": "SIGNE", + "refused": "REFUSE", + "expired": "EXPIRE", + "canceled": "REFUSE", + } + return { + "statut": statut_map.get(data.get("state"), "EN_ATTENTE"), + "date_signature": data.get("completed_at"), + } + else: + return {"statut": "ERREUR"} + + except Exception as e: + logger.error(f"Erreur statut Universign: {e}") + return {"statut": "ERREUR", "error": str(e)}