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 :
-
-
-
-
-
-
-
-
- |
-
- ⏰ 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 :
-
-
-
-
-
-
- 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 :
+
+
+
+
+
+
+
+
+ |
+
+ ⏰ 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 :
+
+
+
+
+
+
+ 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)}