from pydantic import BaseModel, Field, validator, field_validator from typing import List, Optional from datetime import date from utils import ( NomenclatureType, SuiviStockType, TypeArticle, normalize_enum_to_int, normalize_string_field, ) class Article(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") @field_validator("fournisseur_principal", mode="before") @classmethod def convert_fournisseur_principal(cls, v): if v in (None, "", " ", " "): return None if isinstance(v, str): v = v.strip() if not v: return None try: return int(v) except (ValueError, TypeError): return None return v @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 ArticleList(BaseModel): """Réponse pour une liste d'articles""" total: int = Field(..., description="Nombre total d'articles") articles: List[Article] = 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 ArticleCreate(BaseModel): 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") coef: Optional[float] = Field(None, ge=0, description="Coefficient") stock_reel: Optional[float] = Field(None, ge=0, description="Stock initial") stock_mini: Optional[float] = Field(None, ge=0, description="Stock minimum") stock_maxi: Optional[float] = Field(None, ge=0, description="Stock maximum") code_ean: Optional[str] = Field(None, max_length=13, description="Code-barres EAN") unite_vente: Optional[str] = Field("UN", max_length=10, description="Unité vente") tva_code: Optional[str] = Field(None, max_length=10, description="Code TVA") code_fiscal: Optional[str] = Field(None, max_length=10, description="Code fiscal") description: Optional[str] = Field( None, max_length=255, description="Description/Commentaire" ) pays: Optional[str] = Field(None, max_length=3, description="Pays d'origine") garantie: Optional[int] = Field(None, ge=0, description="Garantie en mois") delai: Optional[int] = Field(None, ge=0, description="Délai livraison jours") poids_net: Optional[float] = Field(None, ge=0, description="Poids net kg") poids_brut: Optional[float] = Field(None, ge=0, description="Poids brut kg") stat_01: Optional[str] = Field(None, max_length=20, description="Statistique 1") stat_02: Optional[str] = Field(None, max_length=20, description="Statistique 2") stat_03: Optional[str] = Field(None, max_length=20, description="Statistique 3") stat_04: Optional[str] = Field(None, max_length=20, description="Statistique 4") stat_05: Optional[str] = Field(None, max_length=20, description="Statistique 5") soumis_escompte: Optional[bool] = Field(None, description="Soumis à escompte") publie: Optional[bool] = Field(None, description="Publié web/catalogue") en_sommeil: Optional[bool] = Field(None, description="Article en sommeil") class ArticleUpdate(BaseModel): designation: Optional[str] = Field(None, 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") coef: Optional[float] = Field(None, ge=0, description="Coefficient") stock_reel: Optional[float] = Field(None, ge=0, description="Stock réel") stock_mini: Optional[float] = Field(None, ge=0, description="Stock minimum") stock_maxi: Optional[float] = Field(None, ge=0, description="Stock maximum") code_ean: Optional[str] = Field(None, max_length=13, description="Code-barres EAN") unite_vente: Optional[str] = Field(None, max_length=10, description="Unité vente") code_fiscal: Optional[str] = Field(None, max_length=10, description="Code fiscal") description: Optional[str] = Field(None, max_length=255, description="Description") pays: Optional[str] = Field(None, max_length=3, description="Pays d'origine") garantie: Optional[int] = Field(None, ge=0, description="Garantie en mois") delai: Optional[int] = Field(None, ge=0, description="Délai livraison jours") poids_net: Optional[float] = Field(None, ge=0, description="Poids net kg") poids_brut: Optional[float] = Field(None, ge=0, description="Poids brut kg") stat_01: Optional[str] = Field(None, max_length=20, description="Statistique 1") stat_02: Optional[str] = Field(None, max_length=20, description="Statistique 2") stat_03: Optional[str] = Field(None, max_length=20, description="Statistique 3") stat_04: Optional[str] = Field(None, max_length=20, description="Statistique 4") stat_05: Optional[str] = Field(None, max_length=20, description="Statistique 5") soumis_escompte: Optional[bool] = Field(None, description="Soumis à escompte") publie: Optional[bool] = Field(None, description="Publié web/catalogue") en_sommeil: Optional[bool] = Field(None, description="Article en sommeil") class MouvementStockLigne(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 EntreeStock(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[MouvementStockLigne] = 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 SortieStock(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[MouvementStockLigne] = 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 MouvementStock(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")