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

3369
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.tiers import (
from schemas.tiers.type_tiers import (TypeTiers,) 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__ = [ __all__ = [
"TiersDetails", "TiersDetails",
"TypeTiers", "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,30 +1,40 @@
from typing import Optional, ClassVar
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
from typing import Optional, ClassVar
class Contact(BaseModel): class Contact(BaseModel):
"""Contact associé à un tiers""" """Contact associé à un tiers"""
numero: Optional[str] = Field(None, description="Code du tiers parent (CT_Num)") 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)") contact_numero: Optional[int] = Field(
n_contact: Optional[int] = Field(None, description="Numéro de référence contact (N_Contact)") None, description="Numéro unique du contact (CT_No)"
)
civilite: Optional[str] = Field(None, description="Civilité : M., Mme, Mlle (CT_Civilite)") 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)") nom: Optional[str] = Field(None, description="Nom de famille (CT_Nom)")
prenom: Optional[str] = Field(None, description="Prénom (CT_Prenom)") prenom: Optional[str] = Field(None, description="Prénom (CT_Prenom)")
fonction: Optional[str] = Field(None, description="Fonction/Titre (CT_Fonction)") fonction: Optional[str] = Field(None, description="Fonction/Titre (CT_Fonction)")
service_code: Optional[int] = Field(None, description="Code du service (N_Service)") service_code: Optional[int] = Field(None, description="Code du service (N_Service)")
telephone: Optional[str] = Field(None, description="Téléphone fixe (CT_Telephone)") 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)") telecopie: Optional[str] = Field(None, description="Fax (CT_Telecopie)")
email: Optional[str] = Field(None, description="Adresse email (CT_EMail)") email: Optional[str] = Field(None, description="Adresse email (CT_EMail)")
facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)") facebook: Optional[str] = Field(None, description="Profil Facebook (CT_Facebook)")
linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)") linkedin: Optional[str] = Field(None, description="Profil LinkedIn (CT_LinkedIn)")
skype: Optional[str] = Field(None, description="Identifiant Skype (CT_Skype)") skype: Optional[str] = Field(None, description="Identifiant Skype (CT_Skype)")
est_defaut: Optional[bool] = Field(False, description="Contact par défaut") est_defaut: Optional[bool] = Field(False, description="Contact par défaut")
civilite_map: ClassVar[dict] = { civilite_map: ClassVar[dict] = {
0: "M.", 0: "M.",
1: "Mme", 1: "Mme",
@ -38,4 +48,70 @@ class Contact(BaseModel):
return v return v
if isinstance(v, int): if isinstance(v, int):
return cls.civilite_map.get(v, str(v)) return cls.civilite_map.get(v, str(v))
return 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 typing import List, Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from schemas.tiers.contact import Contact 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): class TiersDetails(BaseModel):
# IDENTIFICATION # IDENTIFICATION
numero: Optional[str] = Field(None, description="Code tiers (CT_Num)") numero: Optional[str] = Field(None, description="Code tiers (CT_Num)")
intitule: Optional[str] = Field(None, description="Raison sociale ou Nom complet (CT_Intitule)") intitule: Optional[str] = Field(
type_tiers: Optional[int] = Field(None, description="Type : 0=Client, 1=Fournisseur (CT_Type)") None, description="Raison sociale ou Nom complet (CT_Intitule)"
qualite: Optional[str] = Field(None, description="Qualité Sage : CLI, FOU, PRO (CT_Qualite)") )
classement: Optional[str] = Field(None, description="Code de classement (CT_Classement)") type_tiers: Optional[int] = Field(
raccourci: Optional[str] = Field(None, description="Code raccourci 7 car. (CT_Raccourci)") 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)") 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)") code_naf: Optional[str] = Field(None, description="Code NAF/APE (CT_Ape)")
# ADRESSE # 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)") 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)") code_postal: Optional[str] = Field(None, description="Code postal (CT_CodePostal)")
ville: Optional[str] = Field(None, description="Ville (CT_Ville)") ville: Optional[str] = Field(None, description="Ville (CT_Ville)")
region: Optional[str] = Field(None, description="Région/État (CT_CodeRegion)") 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)") taux04: Optional[float] = Field(None, description="Taux personnalisé 4 (CT_Taux04)")
# STATISTIQUES # STATISTIQUES
statistique01: Optional[str] = Field(None, description="Statistique 1 (CT_Statistique01)") statistique01: Optional[str] = Field(
statistique02: Optional[str] = Field(None, description="Statistique 2 (CT_Statistique02)") None, description="Statistique 1 (CT_Statistique01)"
statistique03: Optional[str] = Field(None, description="Statistique 3 (CT_Statistique03)") )
statistique04: Optional[str] = Field(None, description="Statistique 4 (CT_Statistique04)") statistique02: Optional[str] = Field(
statistique05: Optional[str] = Field(None, description="Statistique 5 (CT_Statistique05)") None, description="Statistique 2 (CT_Statistique02)"
statistique06: Optional[str] = Field(None, description="Statistique 6 (CT_Statistique06)") )
statistique07: Optional[str] = Field(None, description="Statistique 7 (CT_Statistique07)") statistique03: Optional[str] = Field(
statistique08: Optional[str] = Field(None, description="Statistique 8 (CT_Statistique08)") None, description="Statistique 3 (CT_Statistique03)"
statistique09: Optional[str] = Field(None, description="Statistique 9 (CT_Statistique09)") )
statistique10: Optional[str] = Field(None, description="Statistique 10 (CT_Statistique10)") 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 # COMMERCIAL
encours_autorise: Optional[float] = Field(None, description="Encours maximum autorisé (CT_Encours)") encours_autorise: Optional[float] = Field(
assurance_credit: Optional[float] = Field(None, description="Montant assurance crédit (CT_Assurance)") None, description="Encours maximum autorisé (CT_Encours)"
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)") 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 # 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)") 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)") type_facture: Optional[int] = Field(
est_prospect: Optional[bool] = Field(None, description="True si prospect (CT_Prospect=1)") None, description="Type facture 0=Facture, 1=BL (CT_Facture)"
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)") est_prospect: Optional[bool] = Field(
validation_echeance: Optional[int] = Field(None, description="Valider les échéances (CT_ValidEch)") None, description="True si prospect (CT_Prospect=1)"
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)") bl_en_facture: Optional[int] = Field(
exclure_penalites: Optional[bool] = Field(None, description="Exclure des pénalités (CT_NotPenal)") None, description="Imprimer BL en facture (CT_BLFact)"
bon_a_payer: Optional[int] = Field(None, description="Bon à payer obligatoire (CT_BonAPayer)") )
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 # LOGISTIQUE
priorite_livraison: Optional[int] = Field(None, description="Priorité livraison (CT_PrioriteLivr)") priorite_livraison: Optional[int] = Field(
livraison_partielle: Optional[int] = Field(None, description="Livraison partielle (CT_LivrPartielle)") None, description="Priorité livraison (CT_PrioriteLivr)"
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)") 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
commentaire: Optional[str] = Field(None, description="Commentaire libre (CT_Commentaire)") commentaire: Optional[str] = Field(
None, description="Commentaire libre (CT_Commentaire)"
)
# ANALYTIQUE # 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 # ORGANISATION / SURVEILLANCE
mode_reglement_code: Optional[int] = Field(None, description="Code mode règlement (MR_No)") mode_reglement_code: Optional[int] = Field(
surveillance_active: Optional[bool] = Field(None, description="Surveillance financière (CT_Surveillance)") 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)") 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)") forme_juridique: Optional[str] = Field(
effectif: Optional[str] = Field(None, description="Nombre d'employés (CT_SvEffectif)") None, description="Forme juridique SA, SARL (CT_SvFormeJuri)"
sv_regularite: Optional[str] = Field(None, description="Régularité paiements (CT_SvRegul)") )
sv_cotation: Optional[str] = Field(None, description="Cotation crédit (CT_SvCotation)") effectif: Optional[str] = Field(
sv_objet_maj: Optional[str] = Field(None, description="Objet dernière MAJ (CT_SvObjetMaj)") None, description="Nombre d'employés (CT_SvEffectif)"
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)") 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 ET CATEGORIES
compte_general: Optional[str] = Field(None, description="Compte général principal (CG_NumPrinc)") compte_general: Optional[str] = Field(
categorie_tarif: Optional[int] = Field(None, description="Catégorie tarifaire (N_CatTarif)") None, description="Compte général principal (CG_NumPrinc)"
categorie_compta: Optional[int] = Field(None, description="Catégorie comptable (N_CatCompta)") )
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
contacts: Optional[List[Contact]] = Field( contacts: Optional[List[Contact]] = Field(
default_factory=list, default_factory=list, description="Liste des contacts du tiers"
description="Liste des contacts du tiers" )
)

View file

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

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