From a0f9eeedece5f66ce4e47787d4766affd3e9a143 Mon Sep 17 00:00:00 2001 From: fanilo Date: Sat, 3 Jan 2026 15:02:57 +0100 Subject: [PATCH] Added better article's data handling and more enriched --- sage_connector.py | 686 +++++++++++------------------------ schemas/articles/articles.py | 52 ++- utils/__init__.py | 9 + utils/article_fields.py | 170 +++++++++ 4 files changed, 431 insertions(+), 486 deletions(-) create mode 100644 utils/article_fields.py diff --git a/sage_connector.py b/sage_connector.py index 3c9f043..7b28ad3 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -1,5 +1,5 @@ import win32com.client -import pythoncom +import pythoncom from datetime import datetime, date from typing import Dict, List, Optional import threading @@ -81,6 +81,12 @@ from utils.tiers.contacts.contacts import ( _lire_contact_depuis_base, ) +from utils import ( + valider_donnees_creation, + mapper_champ_api_vers_sage, + CHAMPS_STOCK_INITIAL, +) + logger = logging.getLogger(__name__) @@ -165,7 +171,7 @@ class SageConnector: try: with self._get_sql_connection() as conn: - cursor = conn.cursor() + conn.cursor() except Exception as e: logger.warning(f"SQL non disponible: {e}") logger.warning(" Les lectures utiliseront COM (plus lent)") @@ -544,7 +550,7 @@ class SageConnector: logger.debug(f" CT_Intitule: '{intitule}'") try: - fournisseur.CT_Type = 1 + fournisseur.CT_Type = 1 logger.debug(" CT_Type: 1 (Fournisseur)") except Exception: logger.debug(" CT_Type non défini (géré par FactoryFournisseur)") @@ -720,7 +726,7 @@ class SageConnector: "numero": num_final, "intitule": intitule, "compte_collectif": compte, - "type": 1, + "type": 1, "est_fournisseur": True, "adresse": adresse or None, "code_postal": code_postal or None, @@ -766,7 +772,7 @@ class SageConnector: if not persist: raise ValueError(f"Fournisseur {code} introuvable") - fournisseur = _cast_client(persist) + fournisseur = _cast_client(persist) if not fournisseur: raise ValueError(f"Impossible de charger le fournisseur {code}") @@ -1576,7 +1582,6 @@ class SageConnector: doc.SetDefaultClient(client_obj) logger.info(f" Client {devis_data['client']['code']} associé") doc.DO_Statut = 0 - doc.Write() @@ -2923,7 +2928,6 @@ class SageConnector: logger.info(f" CLIENT: {numero_client}") logger.info(f" CONTACT: {prenom} {nom}") - logger.info(f"[1] Chargement du client: {numero_client}") factory_client = self.cial.CptaApplication.FactoryClient try: @@ -2937,7 +2941,6 @@ class SageConnector: except Exception as e: raise ValueError(f"Client {numero_client} introuvable: {e}") - logger.info("[2] Creation via FactoryTiersContact") if not hasattr(client_obj, "FactoryTiersContact"): @@ -2951,7 +2954,6 @@ class SageConnector: persist = factory_contact.Create() logger.info(f" Objet cree: {type(persist).__name__}") - contact = None interfaces_a_tester = [ "IBOTiersContact3", @@ -2981,7 +2983,6 @@ class SageConnector: "Impossible de caster vers une interface contact valide" ) - logger.info("[3] Configuration du contact") if hasattr(contact, "_prop_map_put_"): @@ -3029,7 +3030,6 @@ class SageConnector: except Exception as e: logger.warning(f" WARN ServiceContact: {e}") - logger.info("[4] Coordonnees (Telecom)") if hasattr(contact, "Telecom"): @@ -3060,7 +3060,6 @@ class SageConnector: except Exception as e: logger.warning(f" WARN Erreur Telecom: {e}") - logger.info("[5] Reseaux sociaux") if contact_data.get("facebook"): @@ -3093,9 +3092,6 @@ class SageConnector: except Exception as e: logger.warning(f" WARN SetDefault(): {e}") - - - logger.info("[6] Enregistrement du contact") contact_cree_malgre_erreur = False @@ -3107,14 +3103,12 @@ class SageConnector: contact.Write() logger.info(" Write() reussi") - try: contact.Read() logger.info(" Read() reussi") except Exception as read_err: logger.warning(f" WARN Read() échoué: {read_err}") - try: contact_no = getattr(contact, "CT_No", None) n_contact = getattr(contact, "N_Contact", None) @@ -3124,7 +3118,6 @@ class SageConnector: except Exception: pass - if not contact_no: logger.info( " 🔍 CT_No non disponible via COM - Recherche en base..." @@ -3153,7 +3146,6 @@ class SageConnector: erreur_com = str(e) logger.warning(f" Write() a levé une exception: {erreur_com}") - if ( "existe déjà" in erreur_com.lower() or "already exists" in erreur_com.lower() @@ -3162,12 +3154,10 @@ class SageConnector: " 🔍 Erreur 'existe déjà' détectée - Vérification en base..." ) - import time time.sleep(0.5) - contact_sql = _chercher_contact_en_base( conn, numero_client=numero_client, @@ -3187,11 +3177,9 @@ class SageConnector: logger.error(" Contact NON trouvé en base - Erreur réelle") raise RuntimeError(f"Echec enregistrement: {erreur_com}") else: - logger.error(f" Erreur Write non gérée: {erreur_com}") raise RuntimeError(f"Echec enregistrement: {erreur_com}") - est_defaut = contact_data.get("est_defaut", False) if est_defaut and (contact_no or n_contact): logger.info("[7] Definition comme contact par defaut") @@ -3221,7 +3209,6 @@ class SageConnector: logger.warning(f" WARN Echec: {e}") est_defaut = False - logger.info("=" * 80) if contact_cree_malgre_erreur: logger.info( @@ -3240,7 +3227,6 @@ class SageConnector: logger.info("[7] Construction du retour") contact_dict = None - if contact_no: logger.info(f" Stratégie 1: Lecture base (CT_No={contact_no})") try: @@ -3265,7 +3251,6 @@ class SageConnector: logger.error(f" Erreur lecture base: {e}", exc_info=True) contact_dict = None - if not contact_dict: logger.info(" Stratégie 2: Lecture objet COM (_contact_to_dict)") try: @@ -3285,7 +3270,6 @@ class SageConnector: logger.error(f" Erreur _contact_to_dict: {e}", exc_info=True) contact_dict = None - if not contact_dict: logger.info(" Stratégie 3: Construction manuelle (fallback)") contact_dict = self._construire_contact_minimal( @@ -3300,7 +3284,6 @@ class SageConnector: f" Contact minimal construit: {len(contact_dict)} champs" ) - if not contact_dict or not isinstance(contact_dict, dict): logger.error( f" ERREUR: contact_dict invalide: type={type(contact_dict)}, value={contact_dict}" @@ -3309,10 +3292,8 @@ class SageConnector: "Impossible de construire le dictionnaire de retour" ) - contact_dict["est_defaut"] = est_defaut - logger.info(" DICT FINAL AVANT RETURN:") logger.info(f" Type: {type(contact_dict)}") logger.info(f" Len: {len(contact_dict)}") @@ -3387,9 +3368,6 @@ class SageConnector: logger.info(f"[MODIFICATION CONTACT] CT_No={contact_numero}") logger.info("=" * 80) - - - logger.info("[1] Chargement du client") factory_client = self.cial.CptaApplication.FactoryClient @@ -3404,16 +3382,12 @@ class SageConnector: except Exception as e: raise ValueError(f"Client {numero} introuvable: {e}") - - - logger.info("[2] Chargement du contact") contact = None nom_recherche = None prenom_recherche = None - try: with self._get_sql_connection() as conn: cursor = conn.cursor() @@ -3449,8 +3423,6 @@ class SageConnector: except Exception as e: raise ValueError(f"Erreur lecture contact en base: {e}") - - logger.info(" Strategie 1: Parcours FactoryTiersContact du client...") try: factory_contact = client_obj.FactoryTiersContact @@ -3490,7 +3462,6 @@ class SageConnector: getattr(temp_contact, "Prenom", "") or "" ) - if ( nom_com.strip().lower() == nom_recherche.lower() and prenom_com.strip().lower() @@ -3509,7 +3480,6 @@ class SageConnector: except Exception as e: logger.warning(f" Strategie 1 echouee: {e}") - if not contact: logger.info(" Strategie 2: FactoryDossierContact.ReadNomPrenom...") try: @@ -3530,7 +3500,6 @@ class SageConnector: except Exception as e: logger.warning(f" Strategie 2 echouee: {e}") - if not contact: logger.info(" Strategie 3: Variations nom/prenom...") try: @@ -3565,7 +3534,6 @@ class SageConnector: except Exception as e: logger.warning(f" Strategie 3 echouee: {e}") - if not contact: logger.info( " Strategie 4: Parcours global FactoryDossierContact..." @@ -3609,7 +3577,6 @@ class SageConnector: except Exception as e: logger.warning(f" Strategie 4 echouee: {e}") - if not contact: logger.error( f" ECHEC: Impossible de charger le contact CT_No={contact_numero}" @@ -3621,9 +3588,6 @@ class SageConnector: logger.info(f" OK Contact charge: {contact.Nom}") - - - logger.info("[3] Application des modifications") modifications_appliquees = [] @@ -3738,9 +3702,6 @@ class SageConnector: f" Modifications preparees: {', '.join(modifications_appliquees) if modifications_appliquees else 'aucune'}" ) - - - logger.info("[4] Enregistrement") try: @@ -3765,9 +3726,6 @@ class SageConnector: except Exception as e: logger.warning(f" WARN Read() apres Write: {e}") - - - est_defaut_demande = updates.get("est_defaut") est_actuellement_defaut = False @@ -3804,14 +3762,10 @@ class SageConnector: except Exception as e: logger.warning(f" WARN Echec definition contact defaut: {e}") - - - logger.info("[6] Construction du retour") contact_dict = None - try: with self._get_sql_connection() as conn: contact_dict = _lire_contact_depuis_base( @@ -3822,7 +3776,6 @@ class SageConnector: except Exception as e: logger.warning(f" Lecture base echouee: {e}") - if not contact_dict: try: contact_dict = _contact_to_dict( @@ -3836,7 +3789,6 @@ class SageConnector: except Exception as e: logger.warning(f" _contact_to_dict echoue: {e}") - if not contact_dict: logger.info(" Construction manuelle du retour") contact_dict = { @@ -4235,10 +4187,10 @@ class SageConnector: COMPTES_DEFAUT.get(type_tiers, "4110000"), "4110000", "411000", - "411", + "411", "4010000", "401000", - "401", + "401", ] for test_compte in comptes_a_tester: @@ -4411,17 +4363,13 @@ class SageConnector: logger.info(f" Portable = {portable}") if client_data.get("facebook"): - facebook = clean_str( - client_data["facebook"], 69 - ) + facebook = clean_str(client_data["facebook"], 69) if not try_set_attribute(telecom_obj, "Facebook", facebook): try_set_attribute(client, "CT_Facebook", facebook) logger.info(f" Facebook = {facebook}") if client_data.get("linkedin"): - linkedin = clean_str( - client_data["linkedin"], 69 - ) + linkedin = clean_str(client_data["linkedin"], 69) if not try_set_attribute(telecom_obj, "LinkedIn", linkedin): try_set_attribute(client, "CT_LinkedIn", linkedin) logger.info(f" LinkedIn = {linkedin}") @@ -8240,6 +8188,10 @@ class SageConnector: try: logger.info("[ARTICLE] === CREATION ARTICLE ===") + valide, erreur = valider_donnees_creation(article_data) + if not valide: + raise ValueError(erreur) + transaction_active = False try: self.cial.CptaApplication.BeginTrans() @@ -8249,81 +8201,8 @@ class SageConnector: logger.debug(f"BeginTrans non disponible : {e}") try: - depots_disponibles = [] - depot_a_utiliser = None - depot_code_demande = article_data.get("depot_code") - - try: - factory_depot = self.cial.FactoryDepot - index = 1 - - while index <= 100: - try: - persist = factory_depot.List(index) - if persist is None: - break - - depot_obj = win32com.client.CastTo(persist, "IBODepot3") - depot_obj.Read() - - code = getattr(depot_obj, "DE_Code", "").strip() - if not code: - index += 1 - continue - - numero = int(getattr(depot_obj, "Compteur", 0)) - intitule = getattr( - depot_obj, "DE_Intitule", f"Depot {code}" - ) - - depot_info = { - "code": code, - "numero": numero, - "intitule": intitule, - "objet": depot_obj, - } - - depots_disponibles.append(depot_info) - - if depot_code_demande and code == depot_code_demande: - depot_a_utiliser = depot_info - elif not depot_code_demande and not depot_a_utiliser: - depot_a_utiliser = depot_info - - index += 1 - - except Exception as e: - if "Acces refuse" in str(e): - break - index += 1 - - except Exception as e: - logger.warning(f"[DEPOT] Erreur découverte dépôts : {e}") - - if not depots_disponibles: - raise ValueError( - "Aucun dépôt trouvé dans Sage. Créez d'abord un dépôt." - ) - - if not depot_a_utiliser: - depot_a_utiliser = depots_disponibles[0] - - logger.info( - f"[DEPOT] Utilisation : '{depot_a_utiliser['code']}' ({depot_a_utiliser['intitule']})" - ) - reference = article_data.get("reference", "").upper().strip() - if not reference: - raise ValueError("La référence est obligatoire") - - if len(reference) > 18: - raise ValueError( - "La référence ne peut pas dépasser 18 caractères" - ) - designation = article_data.get("designation", "").strip() - if not designation: - raise ValueError("La désignation est obligatoire") if len(designation) > 69: designation = designation[:69] @@ -8334,9 +8213,10 @@ class SageConnector: logger.info(f"[ARTICLE] Référence : {reference}") logger.info(f"[ARTICLE] Désignation : {designation}") - logger.info(f"[ARTICLE] Stock réel demandé : {stock_reel}") - logger.info(f"[ARTICLE] Stock mini demandé : {stock_mini}") - logger.info(f"[ARTICLE] Stock maxi demandé : {stock_maxi}") + if stock_reel or stock_mini or stock_maxi: + logger.info( + f"[ARTICLE] Stocks demandés : réel={stock_reel}, mini={stock_mini}, maxi={stock_maxi}" + ) factory = self.cial.FactoryArticle try: @@ -8350,11 +8230,8 @@ class SageConnector: or "non trouve" in error_msg or "-2607" in error_msg ): - logger.debug( - f"[ARTICLE] {reference} n'existe pas encore, création possible" - ) + logger.debug(f"[ARTICLE] {reference} n'existe pas encore") else: - logger.error(f"[ARTICLE] Erreur vérification : {e}") raise persist = factory.Create() @@ -8365,14 +8242,12 @@ class SageConnector: article.AR_Design = designation logger.info("[MODELE] Recherche article modèle via SQL...") - article_modele_ref = None article_modele = None try: with self._get_sql_connection() as conn: cursor = conn.cursor() - cursor.execute( """ SELECT TOP 1 AR_Ref @@ -8381,22 +8256,18 @@ class SageConnector: ORDER BY AR_Ref """ ) - row = cursor.fetchone() - if row: article_modele_ref = _safe_strip(row.AR_Ref) logger.info( f" [SQL] Article modèle trouvé : {article_modele_ref}" ) - except Exception as e: logger.warning(f" [SQL] Erreur recherche article : {e}") if article_modele_ref: try: persist_modele = factory.ReadReference(article_modele_ref) - if persist_modele: article_modele = win32com.client.CastTo( persist_modele, "IBOArticle3" @@ -8405,7 +8276,6 @@ class SageConnector: logger.info( f" [OK] Article modèle chargé : {article_modele_ref}" ) - except Exception as e: logger.warning(f" [WARN] Erreur chargement modèle : {e}") article_modele = None @@ -8443,7 +8313,6 @@ class SageConnector: logger.info( f" [FAMILLE] Code personnalisé demandé : {famille_code_personnalise}" ) - try: famille_existe_sql = False famille_code_exact = None @@ -8451,7 +8320,6 @@ class SageConnector: try: with self._get_sql_connection() as conn: cursor = conn.cursor() - cursor.execute( """ SELECT FA_CodeFamille, FA_Type @@ -8460,9 +8328,7 @@ class SageConnector: """, (famille_code_personnalise.upper(),), ) - row = cursor.fetchone() - if row: famille_code_exact = _safe_strip( row.FA_CodeFamille @@ -8475,7 +8341,6 @@ class SageConnector: raise ValueError( f"Famille '{famille_code_personnalise}' introuvable" ) - except ValueError: raise except Exception as e_sql: @@ -8513,7 +8378,6 @@ class SageConnector: break index += 1 - except Exception as e: if "Accès refusé" in str(e): break @@ -8529,11 +8393,9 @@ class SageConnector: raise ValueError( f"Famille '{famille_code_personnalise}' inaccessible via COM" ) - except Exception as e: logger.warning(f" [COM] Erreur scanner : {e}") raise - except ValueError: raise except Exception as e: @@ -8558,46 +8420,43 @@ class SageConnector: article.AR_Type = int(getattr(article_modele, "AR_Type", 0)) article.AR_Nature = int(getattr(article_modele, "AR_Nature", 0)) article.AR_Nomencl = int(getattr(article_modele, "AR_Nomencl", 0)) - article.AR_SuiviStock = 2 logger.info(" [OK] AR_SuiviStock=2 (FIFO/LIFO)") - prix_vente = article_data.get("prix_vente") - if prix_vente is not None: + logger.info("[CHAMPS] Application des champs fournis...") + + for champ_api, valeur in article_data.items(): + if ( + champ_api in ["reference", "designation", "famille"] + or champ_api in CHAMPS_STOCK_INITIAL + ): + continue + + champ_sage = mapper_champ_api_vers_sage(champ_api) + try: - article.AR_PrixVen = float(prix_vente) - logger.info(f" Prix vente : {prix_vente} EUR") + if champ_sage == "AR_PrixVen": + article.AR_PrixVen = float(valeur) + logger.info(f" {champ_sage} = {valeur}") + elif champ_sage == "AR_PrixAch": + try: + article.AR_PrixAch = float(valeur) + except Exception: + article.AR_PrixAchat = float(valeur) + logger.info(f" {champ_sage} = {valeur}") + elif champ_sage == "AR_CodeBarre": + article.AR_CodeBarre = str(valeur) + logger.info(f" {champ_sage} = {valeur}") + elif champ_sage == "AR_Commentaire": + article.AR_Commentaire = str(valeur) + logger.info(f" {champ_sage} défini") + elif hasattr(article, champ_sage): + setattr(article, champ_sage, valeur) + logger.info(f" {champ_sage} = {valeur}") except Exception as e: - logger.warning(f" Prix vente erreur : {str(e)[:100]}") - - prix_achat = article_data.get("prix_achat") - if prix_achat is not None: - try: - try: - article.AR_PrixAch = float(prix_achat) - logger.info( - f" Prix achat (AR_PrixAch) : {prix_achat} EUR" - ) - except Exception: - article.AR_PrixAchat = float(prix_achat) - logger.info( - f" Prix achat (AR_PrixAchat) : {prix_achat} EUR" - ) - except Exception as e: - logger.warning(f" Prix achat erreur : {str(e)[:100]}") - - code_ean = article_data.get("code_ean") - if code_ean: - article.AR_CodeBarre = str(code_ean) - logger.info(f" Code EAN/Barre : {code_ean}") - - description = article_data.get("description") - if description: - try: - article.AR_Commentaire = description - logger.info(" Description définie") - except Exception: - pass + logger.warning( + f" {champ_sage} non assignable : {str(e)[:100]}" + ) logger.info("[ARTICLE] Écriture dans Sage...") @@ -8606,101 +8465,52 @@ class SageConnector: logger.info(" [OK] Write() réussi") except Exception as e: error_detail = str(e) - try: sage_error = self.cial.CptaApplication.LastError if sage_error: error_detail = f"{sage_error.Description} (Code: {sage_error.Number})" except Exception: pass - logger.error(f" [ERREUR] Write() échoué : {error_detail}") raise RuntimeError(f"Échec création article : {error_detail}") - stock_defini = False - stock_erreur = None - - has_stock_values = stock_reel or stock_mini or stock_maxi - - if has_stock_values: - logger.info( - f"[STOCK] Définition stock dans F_ARTSTOCK (dépôt '{depot_a_utiliser['code']}')..." - ) - - try: - depot_obj = depot_a_utiliser["objet"] - - factory_stock = None - for factory_name in [ - "FactoryArticleStock", - "FactoryDepotStock", - ]: - try: - factory_stock = getattr( - depot_obj, factory_name, None - ) - if factory_stock: - logger.info( - f" Factory trouvée : {factory_name}" - ) - break - except Exception: - continue - - if not factory_stock: - raise RuntimeError( - "Factory de stock introuvable sur le dépôt" - ) - - stock_persist = factory_stock.Create() - stock_obj = win32com.client.CastTo( - stock_persist, "IBODepotStock3" - ) - stock_obj.SetDefault() - - stock_obj.AR_Ref = reference - - if stock_reel: - stock_obj.AS_QteSto = float(stock_reel) - logger.info(f" AS_QteSto = {stock_reel}") - - if stock_mini: - try: - stock_obj.AS_QteMini = float(stock_mini) - logger.info(f" AS_QteMini = {stock_mini}") - except Exception as e: - logger.warning(f" AS_QteMini non défini : {e}") - - if stock_maxi: - try: - stock_obj.AS_QteMaxi = float(stock_maxi) - logger.info(f" AS_QteMaxi = {stock_maxi}") - except Exception as e: - logger.warning(f" AS_QteMaxi non défini : {e}") - - stock_obj.Write() - - stock_defini = True - logger.info( - f" [OK] Stock défini dans F_ARTSTOCK : réel={stock_reel}, min={stock_mini}, max={stock_maxi}" - ) - - except Exception as e: - stock_erreur = str(e) - logger.error( - f" [ERREUR] Stock non défini dans F_ARTSTOCK : {e}", - exc_info=True, - ) - if transaction_active: try: self.cial.CptaApplication.CommitTrans() - logger.info( - "[COMMIT] Transaction committée - Article persiste dans Sage" - ) + logger.info("[COMMIT] Transaction committée") except Exception as e: logger.warning(f"[COMMIT] Erreur commit : {e}") + has_stock_values = stock_reel or stock_mini or stock_maxi + + if has_stock_values: + logger.info("[STOCK] Initialisation via creer_entree_stock...") + try: + lignes = [ + { + "article_ref": reference, + "quantite": stock_reel if stock_reel else 0.0, + "stock_mini": stock_mini, + "stock_maxi": stock_maxi, + } + ] + + entree_stock_data = { + "date_mouvement": datetime.now().date(), + "reference": f"INIT-{reference}", + "lignes": lignes, + } + + resultat_stock = self.creer_entree_stock(entree_stock_data) + logger.info( + f"[STOCK] Entrée créée : {resultat_stock.get('numero')}" + ) + except Exception as e: + logger.error( + f"[STOCK] Erreur initialisation stock : {e}", + exc_info=True, + ) + logger.info("[VERIF] Relecture article créé...") article_cree_persist = factory.ReadReference(reference) @@ -8720,7 +8530,6 @@ class SageConnector: try: with self._get_sql_connection() as conn: cursor = conn.cursor() - cursor.execute( """ SELECT @@ -8734,7 +8543,6 @@ class SageConnector: """, (reference.upper(),), ) - depot_rows = cursor.fetchall() for depot_row in depot_rows: @@ -8746,47 +8554,34 @@ class SageConnector: { "depot_code": _safe_strip(depot_row[0]), "quantite": qte, - "qte_mini": ( - float(depot_row[2]) - if depot_row[2] - else 0.0 - ), - "qte_maxi": ( - float(depot_row[3]) - if depot_row[3] - else 0.0 - ), + "qte_mini": float(depot_row[2]) + if depot_row[2] + else 0.0, + "qte_maxi": float(depot_row[3]) + if depot_row[3] + else 0.0, } ) logger.info( - f"[VERIF] Stock dans F_ARTSTOCK : {stock_total} unités dans {len(stocks_par_depot)} dépôt(s)" + f"[VERIF] Stock total : {stock_total} unités dans {len(stocks_par_depot)} dépôt(s)" ) - except Exception as e: - logger.warning( - f"[VERIF] Impossible de vérifier le stock dans F_ARTSTOCK : {e}" - ) + logger.warning(f"[VERIF] Impossible de vérifier le stock : {e}") logger.info( - f"[OK] ARTICLE CREE : {reference} - Stock total : {stock_total}" + f"[OK] ARTICLE CREE : {reference} - Stock : {stock_total}" ) - logger.info("[EXTRACTION] Extraction complète de l'article créé...") - resultat = _extraire_article(article_cree) if not resultat: - resultat = { - "reference": reference, - "designation": designation, - } + resultat = {"reference": reference, "designation": designation} resultat["stock_reel"] = stock_total if stock_mini: resultat["stock_mini"] = float(stock_mini) - if stock_maxi: resultat["stock_maxi"] = float(stock_maxi) @@ -8794,47 +8589,8 @@ class SageConnector: resultat["stock_reserve"] = 0.0 resultat["stock_commande"] = 0.0 - if prix_vente is not None: - resultat["prix_vente"] = float(prix_vente) - - if prix_achat is not None: - resultat["prix_achat"] = float(prix_achat) - - if description: - resultat["description"] = description - - if code_ean: - resultat["code_ean"] = str(code_ean) - resultat["code_barre"] = str(code_ean) - - if famille_code_personnalise and famille_trouvee: - resultat["famille_code"] = famille_code_personnalise - try: - if famille_obj: - famille_obj.Read() - resultat["famille_libelle"] = getattr( - famille_obj, "FA_Intitule", "" - ) - except Exception: - pass - if stocks_par_depot: resultat["stocks_par_depot"] = stocks_par_depot - resultat["depot_principal"] = { - "code": depot_a_utiliser["code"], - "intitule": depot_a_utiliser["intitule"], - } - - resultat["suivi_stock_active"] = stock_defini - - if has_stock_values and not stock_defini and stock_erreur: - resultat["avertissement"] = ( - f"Stock demandé mais non défini dans F_ARTSTOCK : {stock_erreur}" - ) - - logger.info( - f"[EXTRACTION] Article extrait et enrichi avec {len(resultat)} champs" - ) return resultat @@ -8845,14 +8601,12 @@ class SageConnector: except Exception: pass raise - except Exception as e: if transaction_active: try: self.cial.CptaApplication.RollbackTrans() except Exception: pass - logger.error(f"Erreur creation article : {e}", exc_info=True) raise RuntimeError(f"Erreur creation article : {str(e)}") @@ -8865,6 +8619,16 @@ class SageConnector: raise RuntimeError("Connexion Sage non établie") try: + from utils.article_fields import ( + valider_donnees_modification, + mapper_champ_api_vers_sage, + CHAMPS_ASSIGNABLES_MODIFICATION, + ) + + valide, erreur = valider_donnees_modification(article_data) + if not valide: + raise ValueError(erreur) + with self._com_context(), self._lock_com: logger.info(f"[ARTICLE] === MODIFICATION {reference} ===") @@ -8898,7 +8662,6 @@ class SageConnector: try: with self._get_sql_connection() as conn: cursor = conn.cursor() - cursor.execute( """ SELECT FA_CodeFamille, FA_Type @@ -8907,9 +8670,7 @@ class SageConnector: """, (famille_code_demande,), ) - row = cursor.fetchone() - if row: famille_code_exact = _safe_strip(row.FA_CodeFamille) famille_type = row.FA_Type if len(row) > 1 else 0 @@ -8929,7 +8690,6 @@ class SageConnector: raise ValueError( f"Famille '{famille_code_demande}' introuvable dans Sage" ) - except ValueError: raise except Exception as e: @@ -8938,7 +8698,6 @@ class SageConnector: if famille_existe_sql and famille_code_exact: logger.info(" [COM] Recherche via scanner...") - factory_famille = self.cial.FactoryFamille famille_obj = None @@ -8971,14 +8730,12 @@ class SageConnector: break index += 1 - except Exception as e: if "Accès refusé" in str(e) or "Access" in str( e ): break index += 1 - except Exception as e: logger.warning( f" [COM] Scanner échoué : {str(e)[:200]}" @@ -8993,105 +8750,100 @@ class SageConnector: ) else: raise ValueError( - f"Famille '{famille_code_demande}' trouvée en SQL mais inaccessible via COM. " - f"Essayez avec une autre famille." + f"Famille '{famille_code_demande}' trouvée en SQL mais inaccessible via COM" ) - except ValueError: raise except Exception as e: logger.error(f" [ERREUR] Changement famille : {e}") raise ValueError(f"Impossible de changer la famille : {str(e)}") - if "designation" in article_data: - designation = str(article_data["designation"])[:69].strip() - article.AR_Design = designation - champs_modifies.append("designation") - logger.info(f" [OK] Désignation : {designation}") + for champ_api, valeur in article_data.items(): + if champ_api == "famille": + continue + + champ_sage = mapper_champ_api_vers_sage(champ_api) + + if ( + champ_sage not in CHAMPS_ASSIGNABLES_MODIFICATION + and champ_sage + not in [ + "AR_Stock", + "AR_StockMini", + "AR_StockMaxi", + ] + ): + logger.debug( + f" [SKIP] Champ {champ_api} non dans la whitelist" + ) + continue - if "prix_vente" in article_data: try: - prix_vente = float(article_data["prix_vente"]) - article.AR_PrixVen = prix_vente - champs_modifies.append("prix_vente") - logger.info(f" [OK] Prix vente : {prix_vente} EUR") - except Exception as e: - logger.warning(f" [WARN] Prix vente : {e}") + if champ_sage == "AR_Design": + designation = str(valeur)[:69].strip() + article.AR_Design = designation + champs_modifies.append("designation") + logger.info(f" [OK] {champ_sage} = {designation}") - if "prix_achat" in article_data: - try: - prix_achat = float(article_data["prix_achat"]) + elif champ_sage == "AR_PrixVen": + prix_vente = float(valeur) + article.AR_PrixVen = prix_vente + champs_modifies.append("prix_vente") + logger.info(f" [OK] {champ_sage} = {prix_vente}") - try: - article.AR_PrixAch = prix_achat + elif champ_sage == "AR_PrixAch": + prix_achat = float(valeur) + try: + article.AR_PrixAch = prix_achat + except Exception: + article.AR_PrixAchat = prix_achat champs_modifies.append("prix_achat") + logger.info(f" [OK] {champ_sage} = {prix_achat}") + + elif champ_sage == "AR_Stock": + stock_reel = float(valeur) + ancien_stock = float(getattr(article, "AR_Stock", 0.0)) + article.AR_Stock = stock_reel + champs_modifies.append("stock_reel") logger.info( - f" [OK] Prix achat (AR_PrixAch) : {prix_achat} EUR" - ) - except Exception: - article.AR_PrixAchat = prix_achat - champs_modifies.append("prix_achat") - logger.info( - f" [OK] Prix achat (AR_PrixAchat) : {prix_achat} EUR" + f" [OK] Stock : {ancien_stock} -> {stock_reel}" ) + if stock_reel > ancien_stock: + logger.info( + f" [+] Stock augmenté de {stock_reel - ancien_stock}" + ) + + elif champ_sage == "AR_StockMini": + stock_mini = float(valeur) + article.AR_StockMini = stock_mini + champs_modifies.append("stock_mini") + logger.info(f" [OK] {champ_sage} = {stock_mini}") + + elif champ_sage == "AR_StockMaxi": + stock_maxi = float(valeur) + article.AR_StockMaxi = stock_maxi + champs_modifies.append("stock_maxi") + logger.info(f" [OK] {champ_sage} = {stock_maxi}") + + elif champ_sage == "AR_CodeBarre": + code_ean = str(valeur)[:13].strip() + article.AR_CodeBarre = code_ean + champs_modifies.append("code_ean") + logger.info(f" [OK] {champ_sage} = {code_ean}") + + elif champ_sage == "AR_Commentaire": + description = str(valeur)[:255].strip() + article.AR_Commentaire = description + champs_modifies.append("description") + logger.info(" [OK] Description définie") + + elif hasattr(article, champ_sage): + setattr(article, champ_sage, valeur) + champs_modifies.append(champ_api) + logger.info(f" [OK] {champ_sage} = {valeur}") except Exception as e: - logger.warning(f" [WARN] Prix achat : {e}") - - if "stock_reel" in article_data: - try: - stock_reel = float(article_data["stock_reel"]) - ancien_stock = float(getattr(article, "AR_Stock", 0.0)) - - article.AR_Stock = stock_reel - champs_modifies.append("stock_reel") - - logger.info(f" [OK] Stock : {ancien_stock} -> {stock_reel}") - - if stock_reel > ancien_stock: - logger.info( - f" [+] Stock augmenté de {stock_reel - ancien_stock}" - ) - - except Exception as e: - logger.error(f" [ERREUR] Stock : {e}") - raise ValueError(f"Impossible de modifier le stock: {e}") - - if "stock_mini" in article_data: - try: - stock_mini = float(article_data["stock_mini"]) - article.AR_StockMini = stock_mini - champs_modifies.append("stock_mini") - logger.info(f" [OK] Stock mini : {stock_mini}") - except Exception as e: - logger.warning(f" [WARN] Stock mini : {e}") - - if "stock_maxi" in article_data: - try: - stock_maxi = float(article_data["stock_maxi"]) - article.AR_StockMaxi = stock_maxi - champs_modifies.append("stock_maxi") - logger.info(f" [OK] Stock maxi : {stock_maxi}") - except Exception as e: - logger.warning(f" [WARN] Stock maxi : {e}") - - if "code_ean" in article_data: - try: - code_ean = str(article_data["code_ean"])[:13].strip() - article.AR_CodeBarre = code_ean - champs_modifies.append("code_ean") - logger.info(f" [OK] Code EAN : {code_ean}") - except Exception as e: - logger.warning(f" [WARN] Code EAN : {e}") - - if "description" in article_data: - try: - description = str(article_data["description"])[:255].strip() - article.AR_Commentaire = description - champs_modifies.append("description") - logger.info(" [OK] Description définie") - except Exception as e: - logger.warning(f" [WARN] Description : {e}") + logger.warning(f" [WARN] {champ_sage} : {e}") if not champs_modifies: logger.warning("[ARTICLE] Aucun champ à modifier") @@ -9100,16 +8852,13 @@ class SageConnector: logger.info( f"[ARTICLE] Champs à modifier : {', '.join(champs_modifies)}" ) - logger.info("[ARTICLE] Écriture des modifications...") try: article.Write() logger.info("[ARTICLE] Write() réussi") - except Exception as e: error_detail = str(e) - try: sage_error = self.cial.CptaApplication.LastError if sage_error: @@ -9118,7 +8867,6 @@ class SageConnector: ) except Exception: pass - logger.error(f"[ARTICLE] Erreur Write() : {error_detail}") raise RuntimeError(f"Échec modification : {error_detail}") @@ -9136,42 +8884,25 @@ class SageConnector: "designation": getattr(article, "AR_Design", ""), } - if "prix_vente" in article_data: - resultat["prix_vente"] = float(article_data["prix_vente"]) - - if "prix_achat" in article_data: - resultat["prix_achat"] = float(article_data["prix_achat"]) - - if "stock_reel" in article_data: - resultat["stock_reel"] = float(article_data["stock_reel"]) - - if "stock_mini" in article_data: - resultat["stock_mini"] = float(article_data["stock_mini"]) - - if "stock_maxi" in article_data: - resultat["stock_maxi"] = float(article_data["stock_maxi"]) - - if "code_ean" in article_data: - resultat["code_ean"] = str(article_data["code_ean"]) - resultat["code_barre"] = str(article_data["code_ean"]) - - if "description" in article_data: - resultat["description"] = str(article_data["description"]) - - if "famille" in article_data: - resultat["famille_code"] = ( - famille_code_exact if "famille_code_exact" in locals() else "" - ) + for champ_api in article_data.keys(): + if champ_api in [ + "prix_vente", + "prix_achat", + "stock_reel", + "stock_mini", + "stock_maxi", + ]: + resultat[champ_api] = article_data[champ_api] + elif champ_api in ["code_ean", "description"]: + resultat[champ_api] = article_data[champ_api] return resultat except ValueError as e: logger.error(f"[ARTICLE] Erreur métier : {e}") raise - except Exception as e: logger.error(f"[ARTICLE] Erreur technique : {e}", exc_info=True) - error_message = str(e) if self.cial: try: @@ -9180,7 +8911,6 @@ class SageConnector: error_message = f"Erreur Sage: {err.Description}" except Exception: pass - raise RuntimeError(f"Erreur technique Sage : {error_message}") def creer_famille(self, famille_data: dict) -> dict: @@ -9237,7 +8967,7 @@ class SageConnector: index += 1 except ValueError: - raise + raise except Exception: index += 1 except ValueError: @@ -9251,7 +8981,7 @@ class SageConnector: famille.FA_Intitule = intitule try: - famille.FA_Type = 0 + famille.FA_Type = 0 logger.info("[FAMILLE] Type : 0 (Détail)") except Exception as e: logger.warning(f"[FAMILLE] FA_Type non défini : {e}") @@ -9315,7 +9045,7 @@ class SageConnector: resultat = { "code": getattr(famille, "FA_CodeFamille", "").strip(), "intitule": getattr(famille, "FA_Intitule", "").strip(), - "type": 0, + "type": 0, "type_libelle": "Détail", } @@ -9696,10 +9426,8 @@ class SageConnector: { "cb_marq": to_int(row[idx]), "cb_createur": to_str(row[idx + 1]), - "cb_modification": row[ - idx + 2 - ], - "cb_creation": row[idx + 3], + "cb_modification": row[idx + 2], + "cb_creation": row[idx + 3], "cb_creation_user": to_str(row[idx + 4]), } ) @@ -9712,7 +9440,7 @@ class SageConnector: "tva_vente_1": to_str(row[idx + 2]), "tva_vente_2": to_str(row[idx + 3]), "tva_vente_3": to_str(row[idx + 4]), - "tva_vente_date_1": row[idx + 5], + "tva_vente_date_1": row[idx + 5], "tva_vente_date_2": row[idx + 6], "tva_vente_date_3": row[idx + 7], "type_facture_vente": to_int(row[idx + 8]), @@ -9972,7 +9700,7 @@ class SageConnector: try: factory_doc = self.cial.FactoryDocumentStock - persist_doc = factory_doc.CreateType(180) + persist_doc = factory_doc.CreateType(180) doc = win32com.client.CastTo(persist_doc, "IBODocumentStock3") doc.SetDefault() @@ -10502,7 +10230,7 @@ class SageConnector: try: factory = self.cial.FactoryDocumentStock - persist = factory.CreateType(181) + persist = factory.CreateType(181) doc = win32com.client.CastTo(persist, "IBODocumentStock3") doc.SetDefault() @@ -10569,12 +10297,12 @@ class SageConnector: numero_lot = ligne_data.get("numero_lot") - if ar_suivi == 1: + if ar_suivi == 1: if numero_lot: logger.warning("[STOCK] CMUP : Suppression du lot") numero_lot = None - elif ar_suivi == 2: + elif ar_suivi == 2: if not numero_lot: import uuid @@ -10641,7 +10369,7 @@ class SageConnector: logger.info("[STOCK] Write() réussi") ligne_obj.Read() - ref_verifiee = article_ref + ref_verifiee = article_ref try: article_lie_obj = getattr(ligne_obj, "Article", None) diff --git a/schemas/articles/articles.py b/schemas/articles/articles.py index fb8b7ae..05e666e 100644 --- a/schemas/articles/articles.py +++ b/schemas/articles/articles.py @@ -1,28 +1,66 @@ - -from pydantic import BaseModel, Field, validator, EmailStr, field_validator +from pydantic import BaseModel, Field, validator from typing import Optional, List, Dict -from enum import Enum, IntEnum -from datetime import datetime, date +from datetime import date + class ArticleCreate(BaseModel): reference: str = Field(..., description="Référence article (max 18 car)") designation: str = Field(..., description="Désignation (max 69 car)") + famille: Optional[str] = Field(None, 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, description="Code-barres EAN") unite_vente: Optional[str] = Field("UN", description="Unité de vente") tva_code: Optional[str] = Field(None, description="Code TVA") + code_fiscal: Optional[str] = Field(None, description="Code fiscal") + description: Optional[str] = Field(None, description="Description/Commentaire") + pays: Optional[str] = Field(None, 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, description="Statistique 1") + stat_02: Optional[str] = Field(None, description="Statistique 2") + stat_03: Optional[str] = Field(None, description="Statistique 3") + stat_04: Optional[str] = Field(None, description="Statistique 4") + stat_05: Optional[str] = Field(None, 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): - """Modèle pour modification article côté gateway""" + reference: str = Field(..., description="Référence de l'article à modifier") + article_data: Dict = Field(..., description="Données à modifier") - reference: str - article_data: Dict + class Config: + json_schema_extra = { + "example": { + "reference": "ART001", + "article_data": { + "designation": "Nouvelle désignation", + "prix_vente": 150.0, + "famille": "FAM01", + "stock_reel": 100.0, + "stock_mini": 10.0, + "code_fiscal": "V19", + "garantie": 24, + }, + } + } class MouvementStockLigneRequest(BaseModel): diff --git a/utils/__init__.py b/utils/__init__.py index 562d73f..3907f87 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -12,6 +12,12 @@ from .enums import ( normalize_string_field, ) +from .article_fields import ( + valider_donnees_creation, + mapper_champ_api_vers_sage, + CHAMPS_STOCK_INITIAL, +) + __all__ = [ "TypeArticle", "TypeCompta", @@ -24,4 +30,7 @@ __all__ = [ "normalize_enum_to_string", "normalize_enum_to_int", "normalize_string_field", + "valider_donnees_creation", + "mapper_champ_api_vers_sage", + "CHAMPS_STOCK_INITIAL", ] diff --git a/utils/article_fields.py b/utils/article_fields.py new file mode 100644 index 0000000..8d1772e --- /dev/null +++ b/utils/article_fields.py @@ -0,0 +1,170 @@ +from typing import Dict, Any, Optional +import logging + +logger = logging.getLogger(__name__) + + +CHAMPS_ASSIGNABLES_CREATION = { + "AR_Design": {"max_length": 69, "required": True, "description": "Désignation"}, + "AR_PrixVen": {"type": float, "min": 0, "description": "Prix de vente HT"}, + "AR_PrixAch": {"type": float, "min": 0, "description": "Prix achat HT"}, + "AR_PrixAchat": {"type": float, "min": 0, "description": "Prix achat HT (alias)"}, + "AR_CodeBarre": {"max_length": 13, "description": "Code-barres EAN"}, + "AR_Commentaire": {"max_length": 255, "description": "Description/Commentaire"}, + "AR_UniteVen": {"max_length": 10, "description": "Unité de vente"}, + "AR_CodeFiscal": {"max_length": 10, "description": "Code fiscal/TVA"}, + "AR_Pays": {"max_length": 3, "description": "Pays d'origine"}, + "AR_Garantie": {"type": int, "min": 0, "description": "Garantie en mois"}, + "AR_Delai": {"type": int, "min": 0, "description": "Délai livraison jours"}, + "AR_Coef": {"type": float, "min": 0, "description": "Coefficient"}, + "AR_PoidsNet": {"type": float, "min": 0, "description": "Poids net kg"}, + "AR_PoidsBrut": {"type": float, "min": 0, "description": "Poids brut kg"}, + "AR_Stat01": {"max_length": 20, "description": "Statistique 1"}, + "AR_Stat02": {"max_length": 20, "description": "Statistique 2"}, + "AR_Stat03": {"max_length": 20, "description": "Statistique 3"}, + "AR_Stat04": {"max_length": 20, "description": "Statistique 4"}, + "AR_Stat05": {"max_length": 20, "description": "Statistique 5"}, + "AR_Escompte": {"type": bool, "description": "Soumis à escompte"}, + "AR_Publie": {"type": bool, "description": "Publié web/catalogue"}, + "AR_Sommeil": {"type": int, "values": [0, 1], "description": "Actif/Sommeil"}, +} + +CHAMPS_ASSIGNABLES_MODIFICATION = { + **CHAMPS_ASSIGNABLES_CREATION, + "AR_Stock": {"type": float, "min": 0, "description": "Stock réel"}, + "AR_StockMini": {"type": float, "min": 0, "description": "Stock minimum"}, + "AR_StockMaxi": {"type": float, "min": 0, "description": "Stock maximum"}, +} + +CHAMPS_OBJETS_SPECIAUX = { + "Unite": {"description": "Unité de vente (objet)", "copie_modele": True}, + "Famille": {"description": "Famille article (objet)", "validation_sql": True}, +} + +CHAMPS_STOCK_INITIAL = { + "stock_reel": {"type": float, "min": 0, "description": "Stock initial"}, + "stock_mini": {"type": float, "min": 0, "description": "Stock minimum"}, + "stock_maxi": {"type": float, "min": 0, "description": "Stock maximum"}, +} + + +def valider_champ( + nom_champ: str, valeur: Any, config: Dict +) -> tuple[bool, Optional[str]]: + if valeur is None: + if config.get("required"): + return False, f"Le champ {nom_champ} est obligatoire" + return True, None + + if "type" in config: + expected_type = config["type"] + try: + if expected_type is float: + valeur = float(valeur) + elif expected_type is int: + valeur = int(valeur) + elif expected_type is bool: + valeur = bool(valeur) + except (ValueError, TypeError): + return ( + False, + f"Le champ {nom_champ} doit être de type {expected_type.__name__}", + ) + + if "min" in config: + if isinstance(valeur, (int, float)) and valeur < config["min"]: + return False, f"Le champ {nom_champ} doit être >= {config['min']}" + + if "max_length" in config: + if isinstance(valeur, str) and len(valeur) > config["max_length"]: + return ( + False, + f"Le champ {nom_champ} ne peut dépasser {config['max_length']} caractères", + ) + + if "values" in config: + if valeur not in config["values"]: + return False, f"Le champ {nom_champ} doit être parmi {config['values']}" + + return True, None + + +def valider_donnees_creation(data: Dict) -> tuple[bool, Optional[str]]: + if "reference" not in data or not data["reference"]: + return False, "Le champ 'reference' est obligatoire" + + if len(str(data["reference"])) > 18: + return False, "La référence ne peut dépasser 18 caractères" + + if "designation" not in data or not data["designation"]: + return False, "Le champ 'designation' est obligatoire" + + for champ, valeur in data.items(): + if champ in CHAMPS_ASSIGNABLES_CREATION: + valide, erreur = valider_champ( + champ, valeur, CHAMPS_ASSIGNABLES_CREATION[champ] + ) + if not valide: + return False, erreur + + return True, None + + +def valider_donnees_modification(data: Dict) -> tuple[bool, Optional[str]]: + if not data: + return False, "Aucun champ à modifier" + + for champ, valeur in data.items(): + if champ in ["famille", "stock_reel", "stock_mini", "stock_maxi"]: + continue + + if champ in CHAMPS_ASSIGNABLES_MODIFICATION: + valide, erreur = valider_champ( + champ, valeur, CHAMPS_ASSIGNABLES_MODIFICATION[champ] + ) + if not valide: + return False, erreur + + return True, None + + +def mapper_champ_api_vers_sage(champ_api: str) -> Optional[str]: + mapping = { + "designation": "AR_Design", + "prix_vente": "AR_PrixVen", + "prix_achat": "AR_PrixAch", + "code_ean": "AR_CodeBarre", + "code_barre": "AR_CodeBarre", + "description": "AR_Commentaire", + "unite_vente": "AR_UniteVen", + "code_fiscal": "AR_CodeFiscal", + "tva_code": "AR_CodeFiscal", + "pays": "AR_Pays", + "garantie": "AR_Garantie", + "delai": "AR_Delai", + "coef": "AR_Coef", + "coefficient": "AR_Coef", + "poids_net": "AR_PoidsNet", + "poids_brut": "AR_PoidsBrut", + "stat_01": "AR_Stat01", + "stat_02": "AR_Stat02", + "stat_03": "AR_Stat03", + "stat_04": "AR_Stat04", + "stat_05": "AR_Stat05", + "soumis_escompte": "AR_Escompte", + "publie": "AR_Publie", + "en_sommeil": "AR_Sommeil", + "stock_reel": "AR_Stock", + "stock_mini": "AR_StockMini", + "stock_maxi": "AR_StockMaxi", + } + return mapping.get(champ_api, champ_api) + + +def obtenir_champs_assignables() -> Dict[str, Any]: + return { + "creation": list(CHAMPS_ASSIGNABLES_CREATION.keys()), + "modification": list(CHAMPS_ASSIGNABLES_MODIFICATION.keys()), + "objets_speciaux": list(CHAMPS_OBJETS_SPECIAUX.keys()), + "stock_initial": list(CHAMPS_STOCK_INITIAL.keys()), + }