feat(articles): add enum support and field normalization for article schema
This commit is contained in:
parent
bd432a15b6
commit
1d4ea92e86
3 changed files with 253 additions and 126 deletions
|
|
@ -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)
|
||||
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', mode='before')
|
||||
@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"""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
from enums import (
|
||||
TypeArticle,
|
||||
TypeCompta,
|
||||
TypeRessource,
|
||||
TypeTiers,
|
||||
TypeEmplacement,
|
||||
TypeFamille,
|
||||
NomenclatureType,
|
||||
SuiviStockType,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"TypeArticle",
|
||||
"TypeCompta",
|
||||
"TypeRessource",
|
||||
"TypeTiers",
|
||||
"TypeEmplacement",
|
||||
"TypeFamille",
|
||||
"NomenclatureType",
|
||||
"SuiviStockType",
|
||||
]
|
||||
129
utils/enums.py
Normal file
129
utils/enums.py
Normal file
|
|
@ -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)
|
||||
Loading…
Reference in a new issue