diff --git a/.gitignore b/.gitignore
index c0590ff..6543188 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,41 @@
-.env
-*.txt
+# ----- Secrets / configs locales -----
+# Ne versionne JAMAIS de .env (mets un .env.example à la place)
+*.env
+
+# ----- Python -----
+.venv/
__pycache__/
*.pyc
+*.pyo
+*.pyd
*.log
-.DS_Store
-# Ignorer tous les fichiers de travail dans Excel/dev
-Excel/dev/*
+
+# ----- PyInstaller (builds) -----
+build/
+dist/
+*.spec
+*.toc
+*.pkg
+*.pyz
+*.zip
+
+# ----- Excel -----
+# Fichiers temporaires d'Excel (~$fichier.xlsm)
+~$*.xls*
+~$*.xlsm
+# On versionne 1 seul fichier prod "pivot" ; on ignore les exports datés
Excel/prod/Ratio_prod_20*.xlsm
!Excel/prod/Ratio_prod.xlsm
+
+# ----- Dossiers Auth livrables -----
+# On n’embarque pas les exe ni .env dans le repo (tu livreras ça en zip)
+Excel/**/Auth/*.exe
+Excel/**/Auth/*.dll
+Excel/**/Auth/*.pdb
+Excel/**/Auth/.env
+
+# ----- Systèmes / IDE -----
+.DS_Store
+Thumbs.db
+.idea/
+.vscode/
\ No newline at end of file
diff --git a/.idea/Ratio_Inventaires.iml b/.idea/Ratio_Inventaires.iml
index b6731d8..5c9e85e 100644
--- a/.idea/Ratio_Inventaires.iml
+++ b/.idea/Ratio_Inventaires.iml
@@ -4,7 +4,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..2e02e87
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ mariadb
+ true
+ org.mariadb.jdbc.Driver
+ jdbc:mariadb://162.19.78.131:3306/Acces
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 05172fb..5bcdc71 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,5 +3,5 @@
-
+
\ No newline at end of file
diff --git a/Excel/dev/Auth/auth_cli.py b/Excel/dev/Auth/auth_cli.py
new file mode 100644
index 0000000..b84bdd5
--- /dev/null
+++ b/Excel/dev/Auth/auth_cli.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+import os, sys, argparse, datetime
+import mysql.connector, bcrypt
+from dotenv import load_dotenv
+
+def read_stdin() -> str:
+ data = sys.stdin.read()
+ return data.rstrip("\r\n")
+
+def main():
+ load_dotenv()
+
+ ap = argparse.ArgumentParser(description="Auth bcrypt locale pour Excel")
+ ap.add_argument("--user", required=True, help="NomUtilisateur (clé primaire)")
+ ap.add_argument("--password-stdin", action="store_true",
+ help="Lire le mot de passe via STDIN (recommandé)")
+ args = ap.parse_args()
+
+ if not args.password_stdin:
+ print("ERR|Utiliser --password-stdin et fournir le mot de passe via STDIN", flush=True)
+ return 2
+
+ password = read_stdin()
+ if not password:
+ print("ERR|Mot de passe vide", flush=True)
+ return 2
+
+ # Récupération des infos de connexion depuis .env
+ host = os.getenv("DB_HOST")
+ db = os.getenv("DB_NAME")
+ user = os.getenv("DB_USER")
+ pwd = os.getenv("DB_PASSWORD")
+
+ try:
+ conn = mysql.connector.connect(
+ host=host, database=db, user=user, password=pwd, connection_timeout=5
+ )
+ cur = conn.cursor(dictionary=True)
+ cur.execute("""
+ SELECT NomUtilisateur, Nom_complet, Site, MotDePasseHash, DateExpiration
+ FROM Utilisateurs
+ WHERE NomUtilisateur = %s
+ LIMIT 1
+ """, (args.user,))
+ row = cur.fetchone()
+ except Exception as e:
+ print("ERR|Connexion base impossible", flush=True)
+ return 3
+ finally:
+ try:
+ cur.close()
+ conn.close()
+ except Exception:
+ pass
+
+ if not row or not row.get("MotDePasseHash"):
+ print("ERR|Utilisateur ou mot de passe invalide", flush=True)
+ return 1
+
+ try:
+ ok = bcrypt.checkpw(password.encode("utf-8"),
+ row["MotDePasseHash"].encode("ascii"))
+ except Exception:
+ ok = False
+
+ if not ok:
+ print("ERR|Utilisateur ou mot de passe invalide", flush=True)
+ return 1
+
+ # Contrôle expiration (si renseignée)
+ exp = row.get("DateExpiration")
+ if exp is not None:
+ today = datetime.date.today()
+ exp_date = exp if isinstance(exp, datetime.date) else getattr(exp, "date", lambda: today)()
+ if exp_date < today:
+ print(f"ERR|Compte expiré le {exp_date.isoformat()}", flush=True)
+ return 2
+
+ site = row.get("Site") or ""
+ nom = row.get("Nom_complet") or ""
+ print(f"OK|Site={site}|NomComplet={nom}", flush=True)
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/Excel/dev/Ratio_dev.xlsm b/Excel/dev/Ratio_dev.xlsm
new file mode 100644
index 0000000..5cd51d6
Binary files /dev/null and b/Excel/dev/Ratio_dev.xlsm differ
diff --git a/Excel/prod/Ratio_prod.xlsm b/Excel/prod/Ratio_prod.xlsm
index da601b7..5cd51d6 100644
Binary files a/Excel/prod/Ratio_prod.xlsm and b/Excel/prod/Ratio_prod.xlsm differ
diff --git a/Scripts/maj_prod_ratio.bat b/Scripts/maj_prod_ratio.bat
index a1e4cc0..ed89eda 100644
--- a/Scripts/maj_prod_ratio.bat
+++ b/Scripts/maj_prod_ratio.bat
@@ -1,39 +1,112 @@
@echo off
-setlocal
+setlocal EnableExtensions EnableDelayedExpansion
-REM === Définition des chemins ===
-set "DEV=C:\Users\miche\PycharmProjects\Ratio_Inventaires\Excel\dev\Ratio_dev.xlsm"
-set "PROD_DIR=C:\Users\miche\PycharmProjects\Ratio_Inventaires\Excel\prod"
-set "INSTALL_CLIENT=C:\Users\miche\PycharmProjects\Fichiers_Install_Clients\Ratio_Inventaires\Excel"
+REM === PARAMÈTRES A ADAPTER UNE FOIS ===
+set "ROOT=C:\Users\miche\PycharmProjects\Ratio_Inventaires"
+set "DEV_XLSM=%ROOT%\Excel\dev\Ratio_dev.xlsm"
+set "DEV_AUTH_PY=%ROOT%\Excel\dev\Auth\auth_cli.py"
+set "VENV_PY=%ROOT%\.venv\Scripts\python.exe"
-REM === Création du nom de fichier daté ===
-set "DATESTAMP=%date:~6,4%_%date:~3,2%_%date:~0,2%"
+set "PROD_DIR=%ROOT%\Excel\prod"
+set "PROD_XLSM=%PROD_DIR%\Ratio_prod.xlsm"
+set "PROD_AUTH_DIR=%PROD_DIR%\Auth"
+
+set "INSTALL_CLIENT=%USERPROFILE%\Desktop\Install_Ratio_Inv" REM <- dossier prêt à livrer au client
+set "CLIENT_AUTH_DIR=%INSTALL_CLIENT%\Auth"
+
+REM === DATE ISO robuste (indépendant des paramètres régionaux) ===
+for /f %%i in ('powershell -NoProfile -Command "(Get-Date).ToString(\"yyyy_MM_dd\")"') do set "DATESTAMP=%%i"
set "ARCHIVE_FILE=%PROD_DIR%\Ratio_prod_%DATESTAMP%.xlsm"
-echo Sauvegarde de la version actuelle de PROD...
-if exist "%PROD_DIR%\Ratio_prod.xlsm" copy /Y "%PROD_DIR%\Ratio_prod.xlsm" "%ARCHIVE_FILE%"
-
-echo Mise à jour du fichier Ratio_prod.xlsm depuis DEV...
-copy /Y "%DEV%" "%PROD_DIR%\Ratio_prod.xlsm"
-
-echo Copie vers le dossier INSTALL_CLIENT (version sans date)...
-copy /Y "%PROD_DIR%\Ratio_prod.xlsm" "%INSTALL_CLIENT%\Ratio_prod.xlsm"
+echo.
+echo === [1/6] Préparation des dossiers ========================================
+if not exist "%PROD_DIR%" mkdir "%PROD_DIR%"
+if not exist "%PROD_AUTH_DIR%" mkdir "%PROD_AUTH_DIR%"
+if not exist "%INSTALL_CLIENT%" mkdir "%INSTALL_CLIENT%"
+if not exist "%CLIENT_AUTH_DIR%" mkdir "%CLIENT_AUTH_DIR%"
echo.
-echo ✔️ Mise à jour complète effectuée (archive, prod, client)
-pause
-echo Nettoyage des anciennes archives (on garde les 5 plus récentes)...
-
-pushd "%PROD_DIR%"
-setlocal EnableDelayedExpansion
-set count=0
-
-for /f "delims=" %%f in ('dir /b /o-d "Ratio_prod_????_??_??.xlsm"') do (
- set /a count+=1
- if !count! gtr 5 (
- echo Suppression de l'ancienne archive : %%f
- del "%%f"
- )
+echo === [2/6] (Optionnel) Build de auth_cli.exe avec PyInstaller ==============
+if exist "%VENV_PY%" (
+ echo Compilation via venv: %VENV_PY%
+ "%VENV_PY%" -m PyInstaller --onefile --clean --distpath "%PROD_AUTH_DIR%" "%DEV_AUTH_PY%"
+ if errorlevel 1 (
+ echo [WARN] Echec de compilation. On continue avec l'exe existant s'il est présent.
+ )
+) else (
+ echo [WARN] VENV introuvable: %VENV_PY% -> build ignore
)
+
+echo.
+echo === [3/6] Sauvegarde de la prod courante ==================================
+if exist "%PROD_XLSM%" (
+ echo Sauvegarde -> "%ARCHIVE_FILE%"
+ copy /Y "%PROD_XLSM%" "%ARCHIVE_FILE%" >nul
+) else (
+ echo Pas de prod courante a sauvegarder.
+)
+
+echo.
+echo === [4/6] Mise a jour de la prod depuis le dev ============================
+if not exist "%DEV_XLSM%" (
+ echo [ERREUR] Fichier DEV introuvable: %DEV_XLSM%
+ goto :EOF
+)
+copy /Y "%DEV_XLSM%" "%PROD_XLSM%" >nul
+if errorlevel 1 (
+ echo [ERREUR] Copie du XLSM vers PROD echouee.
+ goto :EOF
+)
+
+REM On s'assure que l'exe est bien present et on deblocque Windows (SmartScreen)
+if exist "%PROD_AUTH_DIR%\auth_cli.exe" (
+ powershell -NoProfile -Command "Unblock-File -Path '%PROD_AUTH_DIR%\auth_cli.exe'" 2>nul
+) else (
+ echo [ERREUR] auth_cli.exe absent dans %PROD_AUTH_DIR%
+ echo Lance d'abord la compilation ou copie l'exe manuellement.
+ goto :EOF
+)
+
+REM Le .env doit exister dans PROD\Auth (modele ou reel)
+if not exist "%PROD_AUTH_DIR%\.env" (
+ echo [WARN] Aucun .env dans %PROD_AUTH_DIR% -> Pense a y mettre DB_HOST/DB_NAME/DB_USER/DB_PASSWORD
+)
+
+echo.
+echo === [5/6] Préparation du dossier client ===================================
+copy /Y "%PROD_XLSM%" "%INSTALL_CLIENT%\Ratio_prod.xlsm" >nul
+if errorlevel 1 (
+ echo [ERREUR] Copie du XLSM vers INSTALL_CLIENT echouee.
+ goto :EOF
+)
+
+copy /Y "%PROD_AUTH_DIR%\auth_cli.exe" "%CLIENT_AUTH_DIR%\auth_cli.exe" >nul
+powershell -NoProfile -Command "Unblock-File -Path '%CLIENT_AUTH_DIR%\auth_cli.exe'" 2>nul
+
+if exist "%PROD_AUTH_DIR%\.env" (
+ copy /Y "%PROD_AUTH_DIR%\.env" "%CLIENT_AUTH_DIR%\.env" >nul
+) else (
+ echo [INFO] Pas de .env a copier (client). Tu pourras le remplir a la main.
+)
+
+echo.
+echo === [6/6] Nettoyage: on garde 5 archives les plus recentes ===============
+pushd "%PROD_DIR%"
+set count=0
+for /f "delims=" %%f in ('dir /b /o-d "Ratio_prod_20??_??_??.xlsm"') do (
+ set /a count+=1
+ if !count! gtr 5 (
+ echo Suppression: %%f
+ del "%%f"
+ )
+)
+popd
+
+echo.
+echo [OK] Release terminee :
+echo - PROD : "%PROD_XLSM%"
+echo - AUTH : "%PROD_AUTH_DIR%\auth_cli.exe" + ".env"
+echo - CLIENT: "%INSTALL_CLIENT%\Ratio_prod.xlsm" + "Auth\*"
+echo.
+pause
endlocal
-popd
\ No newline at end of file
diff --git a/bcrypt_check.py b/bcrypt_check.py
new file mode 100644
index 0000000..896c317
--- /dev/null
+++ b/bcrypt_check.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+import sys, argparse, bcrypt
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument("--hash", required=True)
+ ap.add_argument("--password-stdin", action="store_true")
+ args = ap.parse_args()
+
+ if not args.password_stdin:
+ print("ERR")
+ return 2
+
+ password = sys.stdin.read().rstrip("\r\n")
+ try:
+ ok = bcrypt.checkpw(password.encode("utf-8"),
+ args.hash.encode("ascii"))
+ except Exception:
+ print("ERR")
+ return 2
+
+ print("OK" if ok else "ERR")
+ return 0 if ok else 1
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/last_ids.txt b/last_ids.txt
new file mode 100644
index 0000000..a3bb71b
--- /dev/null
+++ b/last_ids.txt
@@ -0,0 +1,3 @@
+Meudon:3
+Roissy:8
+Saclay:12
diff --git a/migre_bcrypt.py b/migre_bcrypt.py
new file mode 100644
index 0000000..70e77ea
--- /dev/null
+++ b/migre_bcrypt.py
@@ -0,0 +1,44 @@
+import os, sys, bcrypt, mysql.connector
+from dotenv import load_dotenv
+
+load_dotenv()
+
+conn = mysql.connector.connect(
+ host=os.getenv("DB_HOST"),
+ database=os.getenv("DB_NAME"),
+ user=os.getenv("DB_USER"),
+ password=os.getenv("DB_PASSWORD"),
+)
+try:
+ cur = conn.cursor(dictionary=True)
+ cur.execute("""
+ SELECT NomUtilisateur, MotDePasse
+ FROM Utilisateurs
+ WHERE MotDePasseHash IS NULL OR MotDePasseHash = ''
+ """)
+ rows = cur.fetchall()
+ print(f"{len(rows)} utilisateur(s) à migrer…")
+
+ for r in rows:
+ login = r["NomUtilisateur"]
+ plain = (r["MotDePasse"] or "").encode("utf-8")
+ if not plain:
+ print(f"- {login}: mot de passe vide — ignoré")
+ continue
+ hashed = bcrypt.hashpw(plain, bcrypt.gensalt(rounds=12)).decode("ascii")
+ cur.execute("""
+ UPDATE Utilisateurs
+ SET MotDePasseHash = %s
+ WHERE NomUtilisateur = %s
+ """, (hashed, login))
+ print(f"- {login}: OK")
+ conn.commit()
+ print("Migration terminée.")
+except Exception as e:
+ conn.rollback()
+ print("Erreur:", e)
+ sys.exit(1)
+finally:
+ cur.close()
+ conn.close()
+
diff --git a/mkhash.py b/mkhash.py
new file mode 100644
index 0000000..717b671
--- /dev/null
+++ b/mkhash.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+import sys, getpass, bcrypt
+
+def main():
+ if len(sys.argv) > 1:
+ password = sys.argv[1]
+ else:
+ password = getpass.getpass("Mot de passe: ")
+ h = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt(rounds=12)).decode("ascii")
+ print(h)
+
+if __name__ == "__main__":
+ main()