Refactored code readability, modularity and usability

This commit is contained in:
fanilo 2026-01-02 12:00:41 +01:00
parent 0788f46bde
commit 5e4231e115
14 changed files with 232 additions and 42 deletions

37
main.py
View file

@ -1,18 +1,12 @@
from fastapi import FastAPI, HTTPException, Header, Depends, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field, validator, EmailStr, field_validator
from typing import Optional, List, Dict
from typing import Optional
from datetime import datetime, date
from decimal import Decimal
from enum import Enum, IntEnum
import uvicorn
import logging
import win32com.client
import time
from config import settings, validate_settings
from sage_connector import SageConnector
import pyodbc
import os
from schemas import (
TiersListRequest,
ContactCreateRequest,
@ -25,12 +19,8 @@ from schemas import (
FiltreRequest,
ChampLibreRequest,
CodeRequest,
TransformationRequest,
TypeDocument,
DevisRequest,
DocumentGetRequest,
StatutRequest,
TypeTiers,
FournisseurCreateRequest,
FournisseurUpdateGatewayRequest,
AvoirCreateGatewayRequest,
@ -43,7 +33,6 @@ from schemas import (
LivraisonUpdateGatewayRequest,
ArticleCreateRequest,
ArticleUpdateGatewayRequest,
MouvementStockLigneRequest,
EntreeStockRequest,
SortieStockRequest,
FamilleCreate,
@ -326,11 +315,11 @@ def transformer_document(
)
transformations_valides = {
(0, 10),
(10, 30),
(10, 60),
(30, 60),
(0, 60),
(0, 10),
(10, 30),
(10, 60),
(30, 60),
(0, 60),
}
if (type_source, type_cible) not in transformations_valides:
@ -446,7 +435,7 @@ def lire_remise_max_client(code: str):
if not client_obj:
raise HTTPException(404, f"Client {code} introuvable")
remise_max = 10.0
remise_max = 10.0
try:
remise_max = float(getattr(client_obj, "CT_RemiseMax", 10.0))
@ -636,7 +625,9 @@ def livraisons_list(
livraisons = sage.lister_toutes_livraisons_cache(filtre)
if statut is not None:
livraisons = [ligne for ligne in livraisons if ligne.get("statut") == statut]
livraisons = [
ligne for ligne in livraisons if ligne.get("statut") == statut
]
livraisons = livraisons[:limit]
@ -1029,7 +1020,7 @@ def lister_depots():
factory_depot = sage.cial.FactoryDepot
index = 1
while index <= 100:
while index <= 100:
try:
persist = factory_depot.List(index)
@ -1098,8 +1089,8 @@ def lister_depots():
principal = True
depot_info = {
"code": code,
"numero": numero,
"code": code,
"numero": numero,
"intitule": intitule,
"adresse": adresse_complete,
"contact": contact,
@ -1344,6 +1335,6 @@ if __name__ == "__main__":
"main:app",
host=settings.api_host,
port=settings.api_port,
reload=False,
reload=False,
log_level="info",
)

27
utils/__init__.py Normal file
View file

@ -0,0 +1,27 @@
from .enums import (
TypeArticle,
TypeCompta,
TypeRessource,
TypeTiers,
TypeEmplacement,
TypeFamille,
NomenclatureType,
SuiviStockType,
normalize_enum_to_string,
normalize_enum_to_int,
normalize_string_field,
)
__all__ = [
"TypeArticle",
"TypeCompta",
"TypeRessource",
"TypeTiers",
"TypeEmplacement",
"TypeFamille",
"NomenclatureType",
"SuiviStockType",
"normalize_enum_to_string",
"normalize_enum_to_int",
"normalize_string_field",
]

View file

View file

@ -2,6 +2,13 @@ from typing import Dict, List
import win32com.client
import logging
from utils.functions.functions import _safe_strip
from utils import (
NomenclatureType,
SuiviStockType,
TypeArticle,
normalize_enum_to_int,
normalize_string_field,
)
logger = logging.getLogger(__name__)
@ -922,9 +929,14 @@ def _enrichir_conditionnements(articles: List[Dict], cursor) -> List[Dict]:
def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict:
"""
Mappe les données brutes de la DB vers le format ArticleResponse
avec normalisation des types et génération des libellés
"""
article = {}
def get_val(sql_col, default=None, convert_type=None):
"""Récupère et convertit une valeur depuis row_data"""
val = row_data.get(sql_col, default)
if val is None:
return default
@ -940,6 +952,7 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict:
return val
# === CHAMPS DE BASE ===
article["reference"] = get_val("AR_Ref", convert_type=str)
article["designation"] = get_val("AR_Design", convert_type=str)
article["code_ean"] = get_val("AR_CodeBarre", convert_type=str)
@ -947,6 +960,7 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict:
article["edi_code"] = get_val("AR_EdiCode", convert_type=str)
article["raccourci"] = get_val("AR_Raccourci", convert_type=str)
# === PRIX ===
article["prix_vente"] = get_val("AR_PrixVen", 0.0, float)
article["prix_achat"] = get_val("AR_PrixAch", 0.0, float)
article["coef"] = get_val("AR_Coef", 0.0, float)
@ -960,40 +974,60 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict:
article["cout_standard"] = get_val("AR_CoutStd", 0.0, float)
article["unite_vente"] = get_val("AR_UniteVen", convert_type=str)
article["unite_poids"] = get_val("AR_UnitePoids", convert_type=str)
# === UNITÉS ET POIDS (avec normalisation string) ===
article["unite_vente"] = normalize_string_field(get_val("AR_UniteVen"))
article["unite_poids"] = normalize_string_field(get_val("AR_UnitePoids"))
article["poids_net"] = get_val("AR_PoidsNet", 0.0, float)
article["poids_brut"] = get_val("AR_PoidsBrut", 0.0, float)
article["gamme_1"] = get_val("AR_Gamme1", convert_type=str)
article["gamme_2"] = get_val("AR_Gamme2", convert_type=str)
# === GAMMES (avec normalisation string) ===
article["gamme_1"] = normalize_string_field(get_val("AR_Gamme1"))
article["gamme_2"] = normalize_string_field(get_val("AR_Gamme2"))
# === TYPE ARTICLE (avec libellé) ===
type_val = get_val("AR_Type", 0, int)
article["type_article"] = type_val
article["type_article_libelle"] = _get_type_article_libelle(type_val)
article["type_article_libelle"] = TypeArticle.get_label(type_val)
# === FAMILLE ===
article["famille_code"] = get_val("FA_CodeFamille", convert_type=str)
# === NATURE ET GARANTIE ===
article["nature"] = get_val("AR_Nature", 0, int)
article["garantie"] = get_val("AR_Garantie", 0, int)
article["code_fiscal"] = get_val("AR_CodeFiscal", convert_type=str)
article["pays"] = get_val("AR_Pays", convert_type=str)
article["code_fiscal"] = normalize_string_field(get_val("AR_CodeFiscal"))
article["pays"] = normalize_string_field(get_val("AR_Pays"))
# === FOURNISSEUR ===
article["fournisseur_principal"] = get_val("CO_No", 0, int)
article["conditionnement"] = get_val("AR_Condition", convert_type=str)
# === CONDITIONNEMENT (avec normalisation string) ===
article["conditionnement"] = normalize_string_field(get_val("AR_Condition"))
article["nb_colis"] = get_val("AR_NbColis", 0, int)
article["prevision"] = get_val("AR_Prevision", False, bool)
article["suivi_stock"] = get_val("AR_SuiviStock", False, bool)
article["nomenclature"] = get_val("AR_Nomencl", False, bool)
# === SUIVI STOCK (avec libellé) ===
suivi_stock_val = normalize_enum_to_int(get_val("AR_SuiviStock"))
article["suivi_stock"] = suivi_stock_val
article["suivi_stock_libelle"] = SuiviStockType.get_label(suivi_stock_val)
# === NOMENCLATURE (avec libellé) ===
nomenclature_val = normalize_enum_to_int(get_val("AR_Nomencl"))
article["nomenclature"] = nomenclature_val
article["nomenclature_libelle"] = NomenclatureType.get_label(nomenclature_val)
article["qte_composant"] = get_val("AR_QteComp", 0.0, float)
article["qte_operatoire"] = get_val("AR_QteOperatoire", 0.0, float)
# === STATUT ARTICLE ===
sommeil = get_val("AR_Sommeil", 0, int)
article["est_actif"] = sommeil == 0
article["en_sommeil"] = sommeil == 1
article["article_substitut"] = get_val("AR_Substitut", convert_type=str)
article["article_substitut"] = normalize_string_field(get_val("AR_Substitut"))
article["soumis_escompte"] = get_val("AR_Escompte", False, bool)
article["delai"] = get_val("AR_Delai", 0, int)
# === STATISTIQUES ===
article["stat_01"] = get_val("AR_Stat01", convert_type=str)
article["stat_02"] = get_val("AR_Stat02", convert_type=str)
article["stat_03"] = get_val("AR_Stat03", convert_type=str)
@ -1001,14 +1035,17 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict:
article["stat_05"] = get_val("AR_Stat05", convert_type=str)
article["hors_statistique"] = get_val("AR_HorsStat", False, bool)
# === CATÉGORIES COMPTABLES ===
article["categorie_1"] = get_val("CL_No1", 0, int)
article["categorie_2"] = get_val("CL_No2", 0, int)
article["categorie_3"] = get_val("CL_No3", 0, int)
article["categorie_4"] = get_val("CL_No4", 0, int)
# === DATE MODIFICATION ===
date_modif = get_val("AR_DateModif")
article["date_modification"] = str(date_modif) if date_modif else None
# === PARAMÈTRES DE VENTE ===
article["vente_debit"] = get_val("AR_VteDebit", False, bool)
article["non_imprimable"] = get_val("AR_NotImp", False, bool)
article["transfere"] = get_val("AR_Transfere", False, bool)
@ -1021,17 +1058,20 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict:
article["sous_traitance"] = get_val("AR_SousTraitance", False, bool)
article["criticite"] = get_val("AR_Criticite", 0, int)
article["reprise_code_defaut"] = get_val("RP_CodeDefaut", convert_type=str)
# === PARAMÈTRES DE PRODUCTION ===
article["reprise_code_defaut"] = normalize_string_field(get_val("RP_CodeDefaut"))
article["delai_fabrication"] = get_val("AR_DelaiFabrication", 0, int)
article["delai_peremption"] = get_val("AR_DelaiPeremption", 0, int)
article["delai_securite"] = get_val("AR_DelaiSecurite", 0, int)
article["type_lancement"] = get_val("AR_TypeLancement", 0, int)
article["cycle"] = get_val("AR_Cycle", 1, int)
# === MÉDIA ET LANGUES ===
article["photo"] = get_val("AR_Photo", convert_type=str)
article["langue_1"] = get_val("AR_Langue1", convert_type=str)
article["langue_2"] = get_val("AR_Langue2", convert_type=str)
# === FRAIS ===
article["frais_01_denomination"] = get_val(
"AR_Frais01FR_Denomination", convert_type=str
)
@ -1042,6 +1082,7 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict:
"AR_Frais03FR_Denomination", convert_type=str
)
# === CHAMPS PERSONNALISÉS ===
article["marque_commerciale"] = get_val("Marque commerciale", convert_type=str)
objectif_val = get_val("Objectif / Qtés vendues")
@ -1066,6 +1107,7 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict:
article["interdire_commande"] = get_val("AR_InterdireCommande", False, bool)
article["exclure"] = get_val("AR_Exclure", False, bool)
# === INITIALISATION DES CHAMPS DE STOCK (remplis par enrichissement) ===
article["stock_reel"] = 0.0
article["stock_mini"] = 0.0
article["stock_maxi"] = 0.0
@ -1073,6 +1115,7 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict:
article["stock_commande"] = 0.0
article["stock_disponible"] = 0.0
# === INITIALISATION DES CHAMPS DE FAMILLE (remplis par enrichissement) ===
article["famille_libelle"] = None
article["famille_type"] = None
article["famille_unite_vente"] = None
@ -1089,6 +1132,7 @@ def _mapper_article_depuis_row(row_data: Dict, colonnes_config: Dict) -> Dict:
article["famille_hors_stat"] = None
article["famille_pays"] = None
# === INITIALISATION DES CHAMPS FOURNISSEUR/TVA (remplis par enrichissement) ===
article["fournisseur_nom"] = None
article["tva_code"] = None
article["tva_taux"] = None
@ -1208,9 +1252,9 @@ def _enrichir_fournisseurs_articles(articles: List[Dict], cursor) -> List[Dict]:
fournisseur_map = {}
for fourn_row in fournisseur_rows:
num = int(fourn_row[0])
nom = _safe_strip(fourn_row[1])
type_ct = int(fourn_row[2])
num = int(fourn_row[0])
nom = _safe_strip(fourn_row[1])
type_ct = int(fourn_row[2])
fournisseur_map[num] = nom
logger.debug(f" → Fournisseur mappé : {num} = {nom} (Type={type_ct})")

View file

View file

@ -1,4 +1,4 @@
from typing import Dict, List, Optional, Any
from typing import Dict
def _extraire_infos_devis(doc, numero: str, champs_modifies: list) -> Dict:

View file

@ -1,5 +1,4 @@
import win32com.client
from typing import Optional
import logging
from utils.functions.functions import (

129
utils/enums.py Normal file
View 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)

View file

View file

@ -1,5 +1,5 @@
from typing import Optional
from datetime import datetime, timedelta, date
from datetime import datetime, date
import logging
logger = logging.getLogger(__name__)

View file

@ -1,4 +1,4 @@
from typing import Dict, List, Optional, Any
from typing import Dict
import logging
from utils.functions.functions import _safe_strip

View file

View file

View file