from pydantic import BaseModel, Field, validator, field_validator from typing import List, Optional from datetime import date, datetime from utils import NomenclatureType, SuiviStockType, TypeArticle from utils.enums import normalize_enum_to_int, normalize_string_field 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[dict] = Field( default_factory=list, description="Détail du stock par emplacement" ) nb_emplacements: int = Field(0, description="Nombre d'emplacements") # Champs énumérés normalisés suivi_stock: Optional[int] = Field( None, description="Type de suivi de stock (AR_SuiviStock): 0=Aucun, 1=CMUP, 2=FIFO/LIFO, 3=Sérialisé", ) suivi_stock_libelle: Optional[str] = Field( None, description="Libellé du type de suivi de stock" ) nomenclature: Optional[int] = Field( None, description="Type de nomenclature (AR_Nomencl): 0=Non, 1=Fabrication, 2=Commerciale", ) nomenclature_libelle: Optional[str] = Field( None, description="Libellé du type de nomenclature" ) 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[dict] = Field(default_factory=list, description="Détail des gammes") nb_gammes: int = Field(0, description="Nombre de gammes") tarifs_clients: List[dict] = Field( default_factory=list, description="Tarifs spécifiques par client/catégorie" ) nb_tarifs_clients: int = Field(0, description="Nombre de tarifs clients") composants: List[dict] = Field( default_factory=list, description="Composants/Opérations de production" ) nb_composants: int = Field(0, description="Nombre de composants") compta_vente: List[dict] = Field( default_factory=list, description="Comptabilité vente" ) compta_achat: List[dict] = Field( default_factory=list, description="Comptabilité achat" ) compta_stock: List[dict] = Field( default_factory=list, description="Comptabilité stock" ) fournisseurs: List[dict] = Field( default_factory=list, description="Tous les fournisseurs de l'article" ) nb_fournisseurs: int = Field(0, description="Nombre de fournisseurs") refs_enumerees: List[dict] = Field( default_factory=list, description="Références énumérées" ) nb_refs_enumerees: int = Field(0, description="Nombre de références énumérées") medias: List[dict] = Field(default_factory=list, description="Médias attachés") nb_medias: int = Field(0, description="Nombre de médias") prix_gammes: List[dict] = Field( default_factory=list, description="Prix par combinaison de gammes" ) 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") famille_type: Optional[int] = Field( None, description="Type de famille : 0=Détail, 1=Total" ) famille_unite_vente: Optional[str] = Field( None, description="Unité de vente de la famille" ) famille_coef: Optional[float] = Field(None, description="Coefficient de la famille") famille_suivi_stock: Optional[bool] = Field( None, description="Suivi stock de la famille" ) famille_garantie: Optional[int] = Field(None, description="Garantie de la famille") famille_unite_poids: Optional[str] = Field( None, description="Unité de poids de la famille" ) famille_delai: Optional[int] = Field(None, description="Délai de la famille") famille_nb_colis: Optional[int] = Field( None, description="Nombre de colis de la famille" ) famille_code_fiscal: Optional[str] = Field( None, description="Code fiscal de la famille" ) famille_escompte: Optional[bool] = Field(None, description="Escompte de la famille") famille_centrale: Optional[bool] = Field(None, description="Famille centrale") famille_nature: Optional[int] = Field(None, description="Nature de la famille") famille_hors_stat: Optional[bool] = Field( None, description="Hors statistique famille" ) famille_pays: Optional[str] = Field(None, description="Pays de la famille") 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" ) fournisseur_nom: Optional[str] = Field( None, description="Nom du fournisseur principal" ) conditionnement: Optional[str] = Field( None, description="Conditionnement d'achat (AR_Condition)" ) conditionnement_qte: Optional[float] = Field( None, description="Quantité conditionnement" ) conditionnement_edi: Optional[str] = Field( None, description="Code EDI conditionnement" ) 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" ) frais_02_denomination: Optional[str] = Field( None, description="Dénomination frais 2" ) frais_03_denomination: Optional[str] = Field( None, description="Dénomination frais 3" ) 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") objectif_qtes_vendues: Optional[str] = Field( None, description="Objectif / Quantités vendues" ) pourcentage_or: Optional[str] = Field(None, description="Pourcentage teneur en or") premiere_commercialisation: Optional[str] = Field( None, description="Date de 1ère commercialisation" ) interdire_commande: Optional[bool] = Field( None, description="Interdire la commande" ) exclure: Optional[bool] = Field(None, description="Exclure de certains traitements") # ===== VALIDATEURS ===== @field_validator( "unite_vente", "unite_poids", "gamme_1", "gamme_2", "conditionnement", "code_fiscal", "pays", "article_substitut", "reprise_code_defaut", mode="before", ) @classmethod def convert_string_fields(cls, v): """Convertit les champs string qui peuvent venir comme int depuis la DB""" return normalize_string_field(v) @field_validator("suivi_stock", "nomenclature", mode="before") @classmethod def convert_enum_fields(cls, v): """Convertit les champs énumérés en int""" return normalize_enum_to_int(v) def model_post_init(self, __context): """Génère automatiquement les libellés après l'initialisation""" if self.suivi_stock is not None: self.suivi_stock_libelle = SuiviStockType.get_label(self.suivi_stock) if self.nomenclature is not None: self.nomenclature_libelle = NomenclatureType.get_label(self.nomenclature) if self.type_article is not None: self.type_article_libelle = TypeArticle.get_label(self.type_article) class Config: json_schema_extra = { "example": { "reference": "BAGUE-001", "designation": "Bague Or 18K Diamant", "prix_vente": 1299.00, "stock_reel": 15.0, "suivi_stock": 1, "suivi_stock_libelle": "CMUP", "nomenclature": 0, "nomenclature_libelle": "Non", } } 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 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: json_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")