diff --git a/schemas/articles/articles.py b/schemas/articles/articles.py index 6dd0fdb..ac8358f 100644 --- a/schemas/articles/articles.py +++ b/schemas/articles/articles.py @@ -2,6 +2,9 @@ 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""" @@ -419,18 +422,28 @@ class ArticleResponse(BaseModel): 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)", + emplacements: List[dict] = Field( + default_factory=list, description="Détail du stock par emplacement" ) nb_emplacements: int = Field(0, description="Nombre d'emplacements") - suivi_stock: Optional[bool] = Field( - None, description="Suivi de stock activé (AR_SuiviStock)" + # 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é", ) - nomenclature: Optional[bool] = Field( - None, description="Article avec nomenclature (AR_Nomencl)" + 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)" ) @@ -454,52 +467,44 @@ class ArticleResponse(BaseModel): 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)", - ) + gammes: List[dict] = Field(default_factory=list, description="Détail des gammes") 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)", + 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[ComposantModel] = Field( - default_factory=list, - description="Composants/Opérations de production (F_ARTCOMPO)", + 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[ComptaArticleModel] = Field( - default_factory=list, description="Comptabilité vente (F_ARTCOMPTA type 0)" + compta_vente: List[dict] = Field( + default_factory=list, description="Comptabilité vente" ) - compta_achat: List[ComptaArticleModel] = Field( - default_factory=list, description="Comptabilité achat (F_ARTCOMPTA type 1)" + compta_achat: List[dict] = Field( + default_factory=list, description="Comptabilité achat" ) - compta_stock: List[ComptaArticleModel] = Field( - default_factory=list, description="Comptabilité stock (F_ARTCOMPTA type 2)" + compta_stock: List[dict] = Field( + default_factory=list, description="Comptabilité stock" ) - fournisseurs: List[FournisseurArticleModel] = Field( - default_factory=list, - description="Tous les fournisseurs de l'article (F_ARTFOURNISS)", + 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[ReferenceEnumereeModel] = Field( - default_factory=list, description="Références énumérées (F_ARTENUMREF)" + 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[MediaArticleModel] = Field( - default_factory=list, description="Médias attachés (F_ARTICLEMEDIA)" - ) + 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[PrixGammeModel] = Field( - default_factory=list, description="Prix par combinaison de gammes (F_ARTPRIX)" + 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") @@ -516,51 +521,35 @@ class ArticleResponse(BaseModel): 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_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 (F_FAMILLE.FA_Type)" + None, description="Type de famille : 0=Détail, 1=Total" ) 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)" + 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 (F_FAMILLE.FA_SuiviStock)" - ) - famille_garantie: Optional[int] = Field( - None, description="Garantie de la famille (F_FAMILLE.FA_Garantie)" + 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 (F_FAMILLE.FA_UnitePoids)" - ) - famille_delai: Optional[int] = Field( - None, description="Délai de la famille (F_FAMILLE.FA_Delai)" + 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 (F_FAMILLE.FA_NbColis)" + None, description="Nombre de colis de la famille" ) 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)" + 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 (F_FAMILLE.FA_HorsStat)" - ) - famille_pays: Optional[str] = Field( - None, description="Pays de la famille (F_FAMILLE.FA_Pays)" + 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( @@ -572,21 +561,20 @@ class ArticleResponse(BaseModel): 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)", + None, description="N° compte du fournisseur principal" ) fournisseur_nom: Optional[str] = Field( - None, description="Nom du fournisseur principal (F_COMPTET.CT_Intitule)" + 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 (F_ENUMCOND.EC_Quantite)" + None, description="Quantité conditionnement" ) conditionnement_edi: Optional[str] = Field( - None, description="Code EDI conditionnement (F_ENUMCOND.EC_EdiCode)" + None, description="Code EDI conditionnement" ) nb_colis: Optional[int] = Field( @@ -669,13 +657,13 @@ class ArticleResponse(BaseModel): 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)" + None, description="Dénomination frais 1" ) frais_02_denomination: Optional[str] = Field( - None, description="Dénomination frais 2 (AR_Frais02FR_Denomination)" + None, description="Dénomination frais 2" ) frais_03_denomination: Optional[str] = Field( - None, description="Dénomination frais 3 (AR_Frais03FR_Denomination)" + None, description="Dénomination frais 3" ) tva_code: Optional[str] = Field(None, description="Code TVA (F_TAXE.TA_Code)") @@ -706,40 +694,54 @@ class ArticleResponse(BaseModel): None, description="Date de dernière modification (AR_DateModif)" ) - marque_commerciale: Optional[str] = Field( - None, description="Marque commerciale (champ personnalisé)" - ) + marque_commerciale: Optional[str] = Field(None, description="Marque commerciale") 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é)" + 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 (champ personnalisé)" + None, description="Date de 1ère commercialisation" ) interdire_commande: Optional[bool] = Field( - None, description="Interdire la commande (champ personnalisé)" + None, description="Interdire la commande" ) - exclure: Optional[bool] = Field( - None, description="Exclure de certains traitements (champ personnalisé)" + 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", ) - - @field_validator('unite_vente', 'unite_poids', 'gamme_1', 'gamme_2', 'conditionnement', mode='before') @classmethod - def convert_int_to_str(cls, v): - if v is None: - return None - if v == 0: - return "" - return str(v) - - @field_validator('suivi_stock', mode='before') + 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_int_to_bool(cls, v): - if v is None: - return None - return bool(v) + 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 = { @@ -748,23 +750,10 @@ class ArticleResponse(BaseModel): "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"} - ], + "suivi_stock": 1, + "suivi_stock_libelle": "CMUP", + "nomenclature": 0, + "nomenclature_libelle": "Non", } } @@ -785,18 +774,6 @@ class ArticleListResponse(BaseModel): 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""" diff --git a/utils/__init__.py b/utils/__init__.py index e69de29..385c325 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -0,0 +1,21 @@ +from enums import ( + TypeArticle, + TypeCompta, + TypeRessource, + TypeTiers, + TypeEmplacement, + TypeFamille, + NomenclatureType, + SuiviStockType, +) + +__all__ = [ + "TypeArticle", + "TypeCompta", + "TypeRessource", + "TypeTiers", + "TypeEmplacement", + "TypeFamille", + "NomenclatureType", + "SuiviStockType", +] diff --git a/utils/enums.py b/utils/enums.py new file mode 100644 index 0000000..646fd01 --- /dev/null +++ b/utils/enums.py @@ -0,0 +1,129 @@ +from enum import IntEnum +from typing import Optional + + +class SuiviStockType(IntEnum): + AUCUN = 0 + CMUP = 1 + FIFO_LIFO = 2 + SERIALISE = 3 + + @classmethod + def get_label(cls, value: Optional[int]) -> Optional[str]: + labels = {0: "Aucun", 1: "CMUP", 2: "FIFO/LIFO", 3: "Sérialisé"} + return labels.get(value) if value is not None else None + + +class NomenclatureType(IntEnum): + NON = 0 + FABRICATION = 1 + COMMERCIALE = 2 + + @classmethod + def get_label(cls, value: Optional[int]) -> Optional[str]: + labels = {0: "Non", 1: "Fabrication", 2: "Commerciale/Composé"} + return labels.get(value) if value is not None else None + + +class TypeArticle(IntEnum): + ARTICLE = 0 + PRESTATION = 1 + DIVERS = 2 + NOMENCLATURE = 3 + + @classmethod + def get_label(cls, value: Optional[int]) -> Optional[str]: + labels = { + 0: "Article", + 1: "Prestation de service", + 2: "Divers / Frais", + 3: "Nomenclature", + } + return labels.get(value) if value is not None else None + + +class TypeFamille(IntEnum): + DETAIL = 0 + TOTAL = 1 + + @classmethod + def get_label(cls, value: Optional[int]) -> Optional[str]: + labels = {0: "Détail", 1: "Total"} + return labels.get(value) if value is not None else None + + +class TypeCompta(IntEnum): + VENTE = 0 + ACHAT = 1 + STOCK = 2 + + @classmethod + def get_label(cls, value: Optional[int]) -> Optional[str]: + labels = {0: "Vente", 1: "Achat", 2: "Stock"} + return labels.get(value) if value is not None else None + + +class TypeRessource(IntEnum): + MAIN_OEUVRE = 0 + MACHINE = 1 + SOUS_TRAITANCE = 2 + + @classmethod + def get_label(cls, value: Optional[int]) -> Optional[str]: + labels = {0: "Main d'œuvre", 1: "Machine", 2: "Sous-traitance"} + return labels.get(value) if value is not None else None + + +class TypeTiers(IntEnum): + CLIENT = 0 + FOURNISSEUR = 1 + SALARIE = 2 + AUTRE = 3 + + @classmethod + def get_label(cls, value: Optional[int]) -> Optional[str]: + labels = {0: "Client", 1: "Fournisseur", 2: "Salarié", 3: "Autre"} + return labels.get(value) if value is not None else None + + +class TypeEmplacement(IntEnum): + NORMAL = 0 + QUARANTAINE = 1 + REBUT = 2 + + @classmethod + def get_label(cls, value: Optional[int]) -> Optional[str]: + labels = {0: "Normal", 1: "Quarantaine", 2: "Rebut"} + return labels.get(value) if value is not None else None + + +def normalize_enum_to_string(value, default="0") -> Optional[str]: + if value is None: + return None + if value == 0: + return None + return str(value) + + +def normalize_enum_to_int(value, default=0) -> Optional[int]: + if value is None: + return None + try: + return int(value) + except (ValueError, TypeError): + return default + + +def normalize_string_field(value) -> Optional[str]: + if value is None: + return None + if isinstance(value, int): + if value == 0: + return None + return str(value) + if isinstance(value, str): + stripped = value.strip() + if stripped in ("", "0"): + return None + return stripped + return str(value)