Restructured files and reorganization for better consistency

This commit is contained in:
Fanilo-Nantenaina 2025-12-29 18:26:00 +03:00
parent 0e2398278f
commit ffb7a50443
21 changed files with 3863 additions and 3334 deletions

3335
api.py

File diff suppressed because it is too large Load diff

377
data/data.py Normal file
View file

@ -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": """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: 'Segoe UI', Arial, sans-serif; background-color: #f4f7fa;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7fa; padding: 40px 0;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="color: #000; margin: 0; font-size: 24px; font-weight: 600;">
Signature Électronique Requise
</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px 30px;">
<p style="color: #2d3748; font-size: 16px; line-height: 1.6; margin: 0 0 20px;">
Bonjour <strong>{{NOM_SIGNATAIRE}}</strong>,
</p>
<p style="color: #4a5568; font-size: 15px; line-height: 1.6; margin: 0 0 25px;">
Nous vous invitons à signer électroniquement le document suivant :
</p>
<!-- Document Info Box -->
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f7fafc; border-left: 4px solid #667eea; border-radius: 4px; margin-bottom: 30px;">
<tr>
<td style="padding: 20px;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="color: #718096; font-size: 13px; padding: 5px 0;">Type de document</td>
<td style="color: #2d3748; font-size: 15px; font-weight: 600; text-align: right; padding: 5px 0;">{{TYPE_DOC}}</td>
</tr>
<tr>
<td style="color: #718096; font-size: 13px; padding: 5px 0;">Numéro</td>
<td style="color: #2d3748; font-size: 15px; font-weight: 600; text-align: right; padding: 5px 0;">{{NUMERO}}</td>
</tr>
<tr>
<td style="color: #718096; font-size: 13px; padding: 5px 0;">Date</td>
<td style="color: #2d3748; font-size: 15px; font-weight: 600; text-align: right; padding: 5px 0;">{{DATE}}</td>
</tr>
<tr>
<td style="color: #718096; font-size: 13px; padding: 5px 0;">Montant TTC</td>
<td style="color: #2d3748; font-size: 15px; font-weight: 600; text-align: right; padding: 5px 0;">{{MONTANT_TTC}} </td>
</tr>
</table>
</td>
</tr>
</table>
<p style="color: #4a5568; font-size: 15px; line-height: 1.6; margin: 0 0 30px;">
Cliquez sur le bouton ci-dessous pour accéder au document et apposer votre signature électronique sécurisée :
</p>
<!-- CTA Button -->
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" style="padding: 10px 0 30px;">
<a href="{{SIGNER_URL}}" style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #000; text-decoration: none; padding: 16px 40px; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);">
Signer le document
</a>
</td>
</tr>
</table>
<!-- Info Box -->
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #fffaf0; border: 1px solid #fbd38d; border-radius: 4px; margin-bottom: 20px;">
<tr>
<td style="padding: 15px;">
<p style="color: #744210; font-size: 13px; line-height: 1.5; margin: 0;">
<strong>Important :</strong> Ce lien de signature est valable pendant <strong>30 jours</strong>.
Nous vous recommandons de signer ce document dès que possible.
</p>
</td>
</tr>
</table>
<p style="color: #718096; font-size: 13px; line-height: 1.5; margin: 0;">
<strong>🔒 Signature électronique sécurisée</strong><br>
Votre signature est protégée par notre partenaire de confiance <strong>Universign</strong>,
certifié eIDAS et conforme au RGPD. Votre identité sera vérifiée et le document sera
horodaté de manière infalsifiable.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f7fafc; padding: 25px 30px; border-radius: 0 0 8px 8px; border-top: 1px solid #e2e8f0;">
<p style="color: #718096; font-size: 12px; line-height: 1.5; margin: 0 0 10px;">
Vous avez des questions ? Contactez-nous à <a href="mailto:{{CONTACT_EMAIL}}" style="color: #667eea; text-decoration: none;">{{CONTACT_EMAIL}}</a>
</p>
<p style="color: #a0aec0; font-size: 11px; line-height: 1.4; margin: 0;">
Cet email a été envoyé automatiquement par le système Sage 100c Dataven.<br>
Si vous avez reçu cet email par erreur, veuillez nous en informer.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
""",
"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": """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: 'Segoe UI', Arial, sans-serif; background-color: #f4f7fa;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7fa; padding: 40px 0;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); padding: 30px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 600;">
Document Signé avec Succès
</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px 30px;">
<p style="color: #2d3748; font-size: 16px; line-height: 1.6; margin: 0 0 20px;">
Bonjour <strong>{{NOM_SIGNATAIRE}}</strong>,
</p>
<p style="color: #4a5568; font-size: 15px; line-height: 1.6; margin: 0 0 25px;">
Nous confirmons la signature électronique du document suivant :
</p>
<!-- Success Box -->
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f0fff4; border-left: 4px solid #48bb78; border-radius: 4px; margin-bottom: 30px;">
<tr>
<td style="padding: 20px;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="color: #2f855a; font-size: 13px; padding: 5px 0;">Document</td>
<td style="color: #22543d; font-size: 15px; font-weight: 600; text-align: right; padding: 5px 0;">{{TYPE_DOC}} {{NUMERO}}</td>
</tr>
<tr>
<td style="color: #2f855a; font-size: 13px; padding: 5px 0;">Signé le</td>
<td style="color: #22543d; font-size: 15px; font-weight: 600; text-align: right; padding: 5px 0;">{{DATE_SIGNATURE}}</td>
</tr>
<tr>
<td style="color: #2f855a; font-size: 13px; padding: 5px 0;">ID Transaction</td>
<td style="color: #22543d; font-size: 13px; font-family: monospace; text-align: right; padding: 5px 0;">{{TRANSACTION_ID}}</td>
</tr>
</table>
</td>
</tr>
</table>
<p style="color: #4a5568; font-size: 15px; line-height: 1.6; margin: 0 0 25px;">
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é.
</p>
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #ebf8ff; border: 1px solid #90cdf4; border-radius: 4px; margin-bottom: 20px;">
<tr>
<td style="padding: 15px;">
<p style="color: #2c5282; font-size: 13px; line-height: 1.5; margin: 0;">
<strong>Signature certifiée :</strong> 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.
</p>
</td>
</tr>
</table>
<p style="color: #718096; font-size: 14px; line-height: 1.6; margin: 0;">
Merci pour votre confiance. Notre équipe reste à votre disposition pour toute question.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f7fafc; padding: 25px 30px; border-radius: 0 0 8px 8px; border-top: 1px solid #e2e8f0;">
<p style="color: #718096; font-size: 12px; line-height: 1.5; margin: 0 0 10px;">
Contact : <a href="mailto:{{CONTACT_EMAIL}}" style="color: #48bb78; text-decoration: none;">{{CONTACT_EMAIL}}</a>
</p>
<p style="color: #a0aec0; font-size: 11px; line-height: 1.4; margin: 0;">
Sage 100c Dataven - Système de signature électronique sécurisée
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
""",
"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": """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: 'Segoe UI', Arial, sans-serif; background-color: #f4f7fa;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7fa; padding: 40px 0;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%); padding: 30px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="color: #ffffff; margin: 0; font-size: 24px; font-weight: 600;">
Signature en Attente
</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: 40px 30px;">
<p style="color: #2d3748; font-size: 16px; line-height: 1.6; margin: 0 0 20px;">
Bonjour <strong>{{NOM_SIGNATAIRE}}</strong>,
</p>
<p style="color: #4a5568; font-size: 15px; line-height: 1.6; margin: 0 0 25px;">
Nous vous avons envoyé il y a <strong>{{NB_JOURS}}</strong> jours un document à signer électroniquement.
Nous constatons que celui-ci n'a pas encore été signé.
</p>
<!-- Warning Box -->
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #fffaf0; border-left: 4px solid #ed8936; border-radius: 4px; margin-bottom: 30px;">
<tr>
<td style="padding: 20px;">
<p style="color: #744210; font-size: 14px; line-height: 1.5; margin: 0 0 10px;">
<strong>Document en attente :</strong> {{TYPE_DOC}} {{NUMERO}}
</p>
<p style="color: #744210; font-size: 13px; line-height: 1.5; margin: 0;">
Le lien de signature expirera dans <strong>{{JOURS_RESTANTS}}</strong> jours
</p>
</td>
</tr>
</table>
<p style="color: #4a5568; font-size: 15px; line-height: 1.6; margin: 0 0 30px;">
Pour éviter tout retard dans le traitement de votre dossier, nous vous invitons à signer ce document dès maintenant :
</p>
<!-- CTA Button -->
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" style="padding: 10px 0 30px;">
<a href="{{SIGNER_URL}}" style="display: inline-block; background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%); color: #ffffff; text-decoration: none; padding: 16px 40px; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 4px 12px rgba(237, 137, 54, 0.4);">
Signer maintenant
</a>
</td>
</tr>
</table>
<p style="color: #718096; font-size: 13px; line-height: 1.5; margin: 0;">
Si vous rencontrez des difficultés ou avez des questions, n'hésitez pas à nous contacter.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f7fafc; padding: 25px 30px; border-radius: 0 0 8px 8px; border-top: 1px solid #e2e8f0;">
<p style="color: #718096; font-size: 12px; line-height: 1.5; margin: 0 0 10px;">
Contact : <a href="mailto:{{CONTACT_EMAIL}}" style="color: #ed8936; text-decoration: none;">{{CONTACT_EMAIL}}</a>
</p>
<p style="color: #a0aec0; font-size: 11px; line-height: 1.4; margin: 0;">
Sage 100c Dataven - Relance automatique
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
""",
"variables_disponibles": [
"NOM_SIGNATAIRE",
"TYPE_DOC",
"NUMERO",
"NB_JOURS",
"JOURS_RESTANTS",
"SIGNER_URL",
"CONTACT_EMAIL",
],
},
}

View file

@ -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"
]

View file

@ -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")

View file

@ -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
}
}

View file

@ -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,
}
}

View file

@ -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,
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,
}
}

View file

@ -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,
}
}

View file

@ -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

9
schemas/schema_mixte.py Normal file
View file

@ -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

829
schemas/tiers/clients.py Normal file
View file

@ -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",
}
}

View file

@ -1,13 +1,21 @@
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)")
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)")
@ -15,7 +23,9 @@ class Contact(BaseModel):
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)")
@ -39,3 +49,69 @@ class Contact(BaseModel):
if isinstance(v, int):
return cls.civilite_map.get(v, str(v))
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

View file

@ -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",
}
}

View file

@ -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"
default_factory=list, description="Liste des contacts du tiers"
)

View file

@ -1,7 +1,9 @@
from enum import Enum
class TypeTiers(str, Enum):
"""Types de tiers possibles"""
ALL = "all"
CLIENT = "client"
FOURNISSEUR = "fournisseur"

20
schemas/user.py Normal file
View file

@ -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

259
utils/generic_functions.py Normal file
View file

@ -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)}