From 5e4231e115e5202c49aac670a4dfb67d341f7733 Mon Sep 17 00:00:00 2001 From: fanilo Date: Fri, 2 Jan 2026 12:00:41 +0100 Subject: [PATCH] Refactored code readability, modularity and usability --- main.py | 37 +++---- utils/__init__.py | 27 +++++ utils/articles/__init__.py | 0 utils/articles/articles_data_sql.py | 74 ++++++++++--- utils/documents/__init__.py | 0 utils/documents/devis/devis_extraction.py | 2 +- utils/documents/documents_data_sql.py | 1 - utils/enums.py | 129 ++++++++++++++++++++++ utils/functions/__init__.py | 0 utils/functions/functions.py | 2 +- utils/functions/items_to_dict.py | 2 +- utils/tiers/clients/__init__.py | 0 utils/tiers/contacts/__init__.py | 0 utils/tiers/fournisseurs/__init__.py | 0 14 files changed, 232 insertions(+), 42 deletions(-) create mode 100644 utils/__init__.py create mode 100644 utils/articles/__init__.py create mode 100644 utils/documents/__init__.py create mode 100644 utils/enums.py create mode 100644 utils/functions/__init__.py create mode 100644 utils/tiers/clients/__init__.py create mode 100644 utils/tiers/contacts/__init__.py create mode 100644 utils/tiers/fournisseurs/__init__.py diff --git a/main.py b/main.py index 469ec99..e817cfb 100644 --- a/main.py +++ b/main.py @@ -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", ) diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..562d73f --- /dev/null +++ b/utils/__init__.py @@ -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", +] diff --git a/utils/articles/__init__.py b/utils/articles/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/articles/articles_data_sql.py b/utils/articles/articles_data_sql.py index 12de60a..425f35c 100644 --- a/utils/articles/articles_data_sql.py +++ b/utils/articles/articles_data_sql.py @@ -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})") diff --git a/utils/documents/__init__.py b/utils/documents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/documents/devis/devis_extraction.py b/utils/documents/devis/devis_extraction.py index 48de6d4..acab3d1 100644 --- a/utils/documents/devis/devis_extraction.py +++ b/utils/documents/devis/devis_extraction.py @@ -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: diff --git a/utils/documents/documents_data_sql.py b/utils/documents/documents_data_sql.py index 1d5bc16..514bf60 100644 --- a/utils/documents/documents_data_sql.py +++ b/utils/documents/documents_data_sql.py @@ -1,5 +1,4 @@ import win32com.client -from typing import Optional import logging from utils.functions.functions import ( 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) diff --git a/utils/functions/__init__.py b/utils/functions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/functions/functions.py b/utils/functions/functions.py index 5858f13..553074c 100644 --- a/utils/functions/functions.py +++ b/utils/functions/functions.py @@ -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__) diff --git a/utils/functions/items_to_dict.py b/utils/functions/items_to_dict.py index f275a07..da0da4e 100644 --- a/utils/functions/items_to_dict.py +++ b/utils/functions/items_to_dict.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Any +from typing import Dict import logging from utils.functions.functions import _safe_strip diff --git a/utils/tiers/clients/__init__.py b/utils/tiers/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/tiers/contacts/__init__.py b/utils/tiers/contacts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/tiers/fournisseurs/__init__.py b/utils/tiers/fournisseurs/__init__.py new file mode 100644 index 0000000..e69de29