From b120c2bd728d41675a7be4d4497b1340c7472910 Mon Sep 17 00:00:00 2001 From: jfgiraud Date: Fri, 24 Jul 2026 14:38:35 +0200 Subject: [PATCH] Menu 1 4 5 ok + bugs --- Dockerfile | 2 +- app/routes.py | 79 +++++++------- app/templates/gestion_actions.html | 159 ++++++++++++++++------------- app/templates/menu.html | 2 +- create_db.py | 112 ++++++++++++++++++++ create_isin.py | 115 +++++++++++++++++++++ isin.csv | 141 +++++++++++++++++++++++++ requirements.txt | 17 +++ 8 files changed, 509 insertions(+), 118 deletions(-) create mode 100644 create_db.py create mode 100644 create_isin.py create mode 100644 isin.csv create mode 100644 requirements.txt diff --git a/Dockerfile b/Dockerfile index 34eba76..fae895d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.11-slim WORKDIR /app -RUN pip install --no-cache-dir flask flask-sqlalchemy pymysql cryptography +RUN pip install --no-cache-dir flask flask-sqlalchemy pymysql cryptography requests mysql-connector-python EXPOSE 5000 diff --git a/app/routes.py b/app/routes.py index 5b6ac4b..49e440e 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,4 +1,7 @@ from flask import Blueprint, render_template, request, redirect, url_for, session, flash +import time +import requests +import mysql.connector from werkzeug.security import check_password_hash, generate_password_hash from sqlalchemy import text from app import db @@ -141,6 +144,7 @@ def gestion_utilisateurs(): return render_template('gestion_utilisateurs.html', users=users) + # Route pour la gestion des actions @main.route('/gestion-actions', methods=['GET', 'POST']) def gestion_actions(): @@ -149,67 +153,52 @@ def gestion_actions(): if request.method == 'POST': action_type = request.form.get('action') - # On récupère aussi l'id caché s'il s'agit d'une modification action_id = request.form.get('id') - isin = request.form.get('isin') + isin = request.form.get('isin') or None ticker = request.form.get('ticker') - yahoo_code = request.form.get('yahoo_code') company_name = request.form.get('company_name') + exchange = request.form.get('exchange') + pays = request.form.get('pays') + currency = request.form.get('currency') if action_type == 'creer': - if isin: - try: - # Vérifier si l'ISIN existe déjà - check_query = text("SELECT COUNT(*) FROM actions WHERE isin = :isin") - exists = db.session.execute(check_query, {"isin": isin}).scalar() - - if exists > 0: - flash("L'action existe déjà, pas de création.", "warning") - else: - # L'id s'incrémente tout seul, on ne l'inclus pas dans l'INSERT - query = text(""" - INSERT INTO actions (isin, ticker, yahoo_code, company_name, updated_at) - VALUES (:isin, :ticker, :yahoo_code, :company_name, NOW()) - """) - db.session.execute(query, { - "isin": isin, - "ticker": ticker, - "yahoo_code": yahoo_code, - "company_name": company_name - }) - db.session.commit() - flash("Action créée", "success") - except Exception as e: - db.session.rollback() - flash("Erreur lors de la création de l'action.", "danger") - else: - flash("L'ISIN est obligatoire.", "danger") - + try: + query = text(""" + INSERT INTO actions (isin, ticker, company_name, exchange, pays, currency, updated_at) + VALUES (:isin, :ticker, :company_name, :exchange, :pays, :currency, NOW()) + """) + db.session.execute(query, { + "isin": isin, "ticker": ticker, "company_name": company_name, + "exchange": exchange, "pays": pays, "currency": currency + }) + db.session.commit() + flash("Action créée avec succès", "success") + except Exception as e: + db.session.rollback() + flash("Erreur lors de la création de l'action.", "danger") + elif action_type == 'modifier' and action_id: try: - # On met à jour en se basant sur l'id technique unique query = text(""" UPDATE actions - SET isin = :isin, ticker = :ticker, yahoo_code = :yahoo_code, company_name = :company_name, updated_at = NOW() + SET isin = :isin, ticker = :ticker, company_name = :company_name, exchange = :exchange, + pays = :pays, currency = :currency, updated_at = NOW() WHERE id = :id """) db.session.execute(query, { - "id": action_id, - "isin": isin, - "ticker": ticker, - "yahoo_code": yahoo_code, - "company_name": company_name + "id": action_id, "isin": isin, "ticker": ticker, "company_name": company_name, + "exchange": exchange, "pays": pays, "currency": currency }) db.session.commit() flash("Modification effectuée", "success") except Exception as e: db.session.rollback() - flash("Erreur lors de la modification (Vérifiez si l'ISIN n'est pas déjà utilisé par une autre ligne).", "danger") + flash("Erreur lors de la modification.", "danger") return redirect(url_for('main.gestion_actions')) - # On sélectionne l'id en plus dans la requête - actions_query = text("SELECT id, isin, ticker, yahoo_code, company_name, updated_at FROM actions ORDER BY company_name ASC") + # Récupération avec la nouvelle structure + actions_query = text("SELECT id, isin, ticker, company_name, exchange, pays, currency, updated_at FROM actions ORDER BY company_name ASC") result = db.session.execute(actions_query) actions_list = [] @@ -218,8 +207,10 @@ def gestion_actions(): 'id': row.id, 'isin': row.isin if row.isin else '', 'ticker': row.ticker if row.ticker else '', - 'yahoo_code': row.yahoo_code if row.yahoo_code else '', 'company_name': row.company_name if row.company_name else 'Sans nom', + 'exchange': row.exchange if row.exchange else '', + 'pays': row.pays if row.pays else '', + 'currency': row.currency if row.currency else '', 'updated_at': row.updated_at.strftime('%Y-%m-%d %H:%M:%S') if row.updated_at else '' }) @@ -302,4 +293,6 @@ def gestion_societes(): 'Mail': row.Mail if row.Mail else '' }) - return render_template('gestion_societes.html', societes=societes) \ No newline at end of file + return render_template('gestion_societes.html', societes=societes) + + diff --git a/app/templates/gestion_actions.html b/app/templates/gestion_actions.html index bed31a9..1790247 100644 --- a/app/templates/gestion_actions.html +++ b/app/templates/gestion_actions.html @@ -2,7 +2,7 @@ endblock %} {% block header_title %}Gestion actions{% endblock %} {% block content %}
- +
Rechercher (ISIN, Ticker, Nom, Pays) +
{% for a in actions %}
-
- {{ a.company_name }} - {{ a.ticker or 'N/A' }} -
- {{ a.isin }}{{ a.company_name }} + + {{ a.ticker or 'N/A' }} +
{% endfor %}
@@ -107,7 +107,6 @@ content %} action="" class="space-y-4 flex-1 flex flex-col justify-between" > -
@@ -118,7 +117,6 @@ content %}
-
- -
-
Nom de la société (company_name)
- -
Exchange
-
- +
Pays + +
+
+ + +
+
+
-
@@ -250,68 +262,69 @@ content %} }, 4000); }); + const container = document.getElementById("actions-list-container"); + if (container) { + container.addEventListener("click", (e) => { + const item = e.target.closest(".action-item"); + if (item) { + selectionnerActionItem(item); + } + }); + } + const premiereAction = document.querySelector(".action-item"); if (premiereAction) { - premiereAction.click(); + selectionnerActionItem(premiereAction); } }); - function selectionnerAction( - id, - isin, - ticker, - yahoo_code, - company_name, - updated_at, - ) { - document.getElementById("field-id").value = id; - document.getElementById("field-isin").value = isin; - document.getElementById("field-ticker").value = ticker; - document.getElementById("field-yahoo_code").value = yahoo_code; - document.getElementById("field-company_name").value = company_name; - document.getElementById("field-updated_at").value = updated_at; + function selectionnerActionItem(item) { + document.getElementById("field-id").value = item.dataset.id; + document.getElementById("field-isin").value = item.dataset.isin; + document.getElementById("field-ticker").value = item.dataset.ticker; + document.getElementById("field-company").value = item.dataset.company; + document.getElementById("field-exchange").value = item.dataset.exchange; + document.getElementById("field-pays").value = item.dataset.pays; + document.getElementById("field-currency").value = item.dataset.currency; + document.getElementById("field-updated_at").value = item.dataset.updated; document.querySelectorAll(".action-item").forEach((el) => { el.classList.remove("bg-blue-600", "text-white"); el.classList.add("hover:bg-gray-800", "text-gray-300"); - const badge = el.querySelector("span:nth-child(1) span:last-child"); + const badge = el.querySelector(".ticker-badge"); if (badge) { - badge.classList.remove("bg-blue-800"); + badge.classList.remove("bg-blue-800", "text-white"); badge.classList.add("bg-gray-800", "text-gray-400"); } }); - const selectedEl = document.querySelector(`[data-id="${id}"]`); - if (selectedEl) { - selectedEl.classList.remove("hover:bg-gray-800", "text-gray-300"); - selectedEl.classList.add("bg-blue-600", "text-white"); - const badge = selectedEl.querySelector( - "span:nth-child(1) span:last-child", - ); - if (badge) { - badge.classList.remove("bg-gray-800", "text-gray-400"); - badge.classList.add("bg-blue-800"); - } - selectedEl.scrollIntoView({ block: "nearest" }); + item.classList.remove("hover:bg-gray-800", "text-gray-300"); + item.classList.add("bg-blue-600", "text-white"); + const badge = item.querySelector(".ticker-badge"); + if (badge) { + badge.classList.remove("bg-gray-800", "text-gray-400"); + badge.classList.add("bg-blue-800", "text-white"); } + item.scrollIntoView({ block: "nearest" }); } function preparerCreation() { document.getElementById("field-id").value = ""; document.getElementById("field-isin").value = ""; document.getElementById("field-ticker").value = ""; - document.getElementById("field-yahoo_code").value = ""; - document.getElementById("field-company_name").value = ""; + document.getElementById("field-company").value = ""; + document.getElementById("field-exchange").value = ""; + document.getElementById("field-pays").value = ""; + document.getElementById("field-currency").value = ""; document.getElementById("field-updated_at").value = "Automatique à la création"; - // Retirer la sélection visuelle active de la liste document.querySelectorAll(".action-item").forEach((el) => { el.classList.remove("bg-blue-600", "text-white"); el.classList.add("hover:bg-gray-800", "text-gray-300"); - const badge = el.querySelector("span:nth-child(1) span:last-child"); + const badge = el.querySelector(".ticker-badge"); if (badge) { - badge.classList.remove("bg-blue-800"); + badge.classList.remove("bg-blue-800", "text-white"); badge.classList.add("bg-gray-800", "text-gray-400"); } }); @@ -338,7 +351,7 @@ content %} }); if (premierCorrespondant && terme !== "") { - premierCorrespondant.click(); + selectionnerActionItem(premierCorrespondant); } } @@ -347,7 +360,7 @@ content %} filtrerActions(); const premiereAction = document.querySelector(".action-item"); if (premiereAction) { - premiereAction.click(); + selectionnerActionItem(premiereAction); } } diff --git a/app/templates/menu.html b/app/templates/menu.html index a8c06d8..6b0cea8 100644 --- a/app/templates/menu.html +++ b/app/templates/menu.html @@ -12,7 +12,6 @@ header_title %}Tableau de bord{% endblock %} {% block content %}

-
+ {% endblock %} diff --git a/create_db.py b/create_db.py new file mode 100644 index 0000000..3b16a24 --- /dev/null +++ b/create_db.py @@ -0,0 +1,112 @@ +import time +import requests +import mysql.connector + +def importer_actions(): + API_KEY = "6a6235c8a742b2.89238466" + MYSQL = { + "host": "db", + "port": 3306, + "user": "root", + "password": "sysadm-1963", + "database": "bourse" + } + + EXCHANGES = [ + "PA", "US", "BR", "KLSE", "LS", "XETRA", "LU", "OL", "ST", "CO", "IR", "KO", "AU", "SHE", "SHG", "AT", + "F", "STU", "MU", "HA", "DU", "HM", "VI", "SW", "TO", "AU", "HK", "TWO", "MC" + ] + + print("Connexion à la base de données MySQL...") + try: + db_conn = mysql.connector.connect(**MYSQL) + cursor = db_conn.cursor() + print("Connecté avec succès à la base MySQL : [bourse]") + except Exception as e: + print(f"Erreur de connexion MySQL : {e}") + return + + print("Vidage de la table les_actions...") + try: + cursor.execute("TRUNCATE TABLE les_actions;") + db_conn.commit() + except Exception as e: + print(f"Erreur lors du TRUNCATE : {e}") + + sql = """ + INSERT INTO les_actions (isin, ticker, name, exchange, pays, devise) + VALUES (%s, %s, %s, %s, %s, %s) + """ + total_global = 0 + + try: + for exchange in EXCHANGES: + url = f"https://eodhd.com/api/exchange-symbol-list/{exchange}?api_token={API_KEY}&fmt=json" + print(f"\n--- Traitement de la bourse : {exchange} ---") + + req_timeout = 300 if exchange == "US" else 60 + + try: + r = requests.get(url, timeout=req_timeout) + if r.status_code != 200: + print(f"[{exchange}] Erreur HTTP {r.status_code}") + continue + data = r.json() + except Exception as err: + print(f"[{exchange}] Erreur requête ou timeout : {err}") + continue + + if not isinstance(data, list): + print(f"[{exchange}] Le format reçu n'est pas une liste.") + continue + + print(f"[{exchange}] {len(data)} éléments reçus. Insertion en cours...") + + lignes = [] + for item in data: + isin = item.get("ISIN") or item.get("isin") + ticker = item.get("Code") or item.get("code") + name = item.get("Name") or item.get("name") + exch = item.get("Exchange") or item.get("exchange") or exchange + pays = item.get("Country") or item.get("country") + devise = item.get("Currency") or item.get("currency") + + if not ticker: + continue + + lignes.append(( + str(isin)[:12] if isin else None, + str(ticker)[:20] if ticker else None, + str(name)[:150] if name else None, + str(exch)[:50] if exch else None, + str(pays)[:50] if pays else None, + str(devise)[:10] if devise else None + )) + + if lignes: + # Utilisation d'executemany par paquets pour un maximum de performance + batch_size = 5000 + for i in range(0, len(lignes), batch_size): + batch = lignes[i:i + batch_size] + cursor.executemany(sql, batch) + db_conn.commit() + + total_global += len(lignes) + print(f"-> Succès : {len(lignes)} actions insérées pour {exchange}.") + + time.sleep(1) + + # Vérification finale du nombre de lignes dans la table + cursor.execute("SELECT COUNT(*) FROM les_actions;") + count = cursor.fetchone()[0] + print(f"\nTerminé ! Total global traité : {total_global} lignes. Comptage réel en base : {count} lignes.") + + except Exception as e: + print(f"Erreur générale : {e}") + finally: + cursor.close() + db_conn.close() + print("Connexion fermée.") + +if __name__ == "__main__": + importer_actions() \ No newline at end of file diff --git a/create_isin.py b/create_isin.py new file mode 100644 index 0000000..8657696 --- /dev/null +++ b/create_isin.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +import csv +import mysql.connector + +# ========================================== +# CONFIGURATION +# ========================================== + +CSV_FILE = "/app/isin.csv" + +# Pays associé au fichier CSV +PAYS = "France" + +MYSQL = { + "host": "db", + "port": 3306, + "user": "root", + "password": "sysadm-1963", + "database": "bourse" +} + +# ========================================== +# CONNEXION MYSQL +# ========================================== + +try: + db = mysql.connector.connect(**MYSQL) + print("Connexion MySQL OK") +except Exception as e: + print("Erreur MySQL :", e) + exit(1) + +cursor = db.cursor(dictionary=True) + +# ========================================== +# CHARGEMENT DU CSV +# ========================================== + +print("Chargement du CSV...") + +csv_data = {} + +with open(CSV_FILE, newline="", encoding="utf-8-sig") as f: + + reader = csv.reader(f, delimiter=";") + + for row in reader: + + if len(row) < 3: + continue + + isin = row[0].strip().upper() + ticker = row[2].strip().upper() + + if ticker: + csv_data[(ticker, PAYS.upper())] = isin + +print(f"{len(csv_data)} ISIN chargés.") + +# ========================================== +# LECTURE DE LA TABLE +# ========================================== + +cursor.execute(""" +SELECT + id, + ticker, + pays, + isin +FROM les_actions +""") + +lignes = cursor.fetchall() + +print(f"{len(lignes)} actions lues.") + +# ========================================== +# MISE A JOUR +# ========================================== + +sql_update = """ +UPDATE les_actions +SET isin = %s +WHERE id = %s +""" + +update_data = [] + +for ligne in lignes: + + ticker = (ligne["ticker"] or "").strip().upper() + pays = (ligne["pays"] or "").strip().upper() + + cle = (ticker, pays) + + if cle in csv_data: + + update_data.append(( + csv_data[cle], + ligne["id"] + )) + +print(f"{len(update_data)} lignes à mettre à jour.") + +if update_data: + cursor.executemany(sql_update, update_data) + db.commit() + +print("===================================") +print(f"{len(update_data)} ISIN mis à jour.") +print("===================================") + +cursor.close() +db.close() \ No newline at end of file diff --git a/isin.csv b/isin.csv new file mode 100644 index 0000000..dc14d53 --- /dev/null +++ b/isin.csv @@ -0,0 +1,141 @@ +ISIN;nom;ticker +BE0003470755g;Solvay;SOLB +BE0003853703g;Montea C.V.A.;MONT +BE0974338700g;Titan Cement;TITC +FI0009000681p;Nokia Corporation;1NOKIA +FR0000031577p;Virbac;VIRP +FR0000031775p;Vicat;VCT +FR0000033219p;Altarea;ALTA +FR0000035081p;Icade;ICAD +FR0000039091p;Robertet;RBT +FR0000039299p;Bollore;BOL +FR0000044448p;Nexans;NEX +FR0000044471p;Ramsay Generale De Sante;GDS +FR0000045072p;Credit Agricole;ACA +FR0000045601p;Robertet Ci;CBE +FR0000045619p;Robertet CDV 87;CBR +FR0000050353p;Lisi;FII +FR0000050809p;Sopra Steria Group;SOP +FR0000051070p;Maurel Et Prom;MAU +FR0000051807p;Teleperformance;TEP +FR0000052292p;Hermes;RMS +FR0000053225p;Metropole Tv;MMT +FR0000054470p;Ubisoft Entertainment;UBI +FR0000054900p;TF1;TFI +FR0000060303p;Covivio Hotels;COVH +FR0000062234p;Compagnie Odet;ODET +FR0000064271p;STEF;STF +FR0000064578p;Covivio;COV +FR0000064784p;Peugeot Invest;PEUG +FR0000065484p;Lectra;LSS +FR0000071946p;Alten;ATE +FR0000073272p;Safran;SAF +FR0000073298p;Ipsos;IPS +FR0000076952p;Artois Nom.;ARTO +FR0000077919p;JC Decaux SE;DEC +FR0000120073p;Air Liquide;AI +FR0000120172p;Carrefour;CA +FR0000120271p;TotalEnergies;TTE +FR0000120321p;L'oreal;OR +FR0000120404p;Accor Hotels;AC +FR0000120503p;Bouygues;EN +FR0000120578p;Sanofi;SAN +FR0000120628p;Axa;CS +FR0000120644p;Danone;BN +FR0000120669p;North Atlantic Energies;NAE +FR0000120693p;Pernod Ricard;RI +FR0000120859p;Imerys;NK +FR0000120966p;Bic;BB +FR0000121014p;Lvmh;MC +FR0000121121p;Eurazeo;RF +FR0000121147p;Forvia;FRVIA +FR0000121204p;Wendel Invest.;MF +FR0000121220p;Sodexo;SW +FR0000121329p;Thales;HO +FR0000121485p;Kering;KER +FR0000121667p;EssilorLuxottica;EL +FR0000121709p;Seb;SK +FR0000121964p;Klepierre;LI +FR0000121972p;Schneider Electric;SU +FR0000124141p;Veolia;VIE +FR0000124570p;OPmobility;OPM +FR0000125007p;Saint Gobain;SGO +FR0000125338p;CapGemini;CAP +FR0000125486p;Vinci;DG +FR0000127771p;Vivendi;VIV +FR0000130213p;Lagardere;MMB +FR0000130395p;Remy Cointreau;RCO +FR0000130403p;Christian Dior;CDI +FR0000130452p;Eiffage;FGR +FR0000130577p;Publicis Groupe;PUB +FR0000130809p;Societe Generale;GLE +FR0000131104p;Bnp Paribas;BNP +FR0000131757p;Eramet;ERA +FR0000131906p;Renault;RNO +FR0000133308p;Orange;ORA +FR0004024222p;Interparfums;ITP +FR0004050250p;Neurones;NRO +FR0004125920p;Amundi;AMUN +FR0005691656p;Trigano;TRI +FR0006174348p;Bureau Veritas;BVI +FR0010040865p;Gecina Nom.;GFC +FR0010208488p;Engie;ENGI +FR0010220475p;Alstom;ALO +FR0010221234p;Eutelsat;ETL +FR0010241638p;Mercialys;MERY +FR0010259150p;Ipsen;IPN +FR0010282822p;Vusion;VU +FR0010307819p;Legrand SA;LR +FR0010313833p;Arkema;AKE +FR0010340141p;Adp;ADP +FR0010411983p;Scor;SCR +FR0010451203p;Rexel;RXL +FR0010481960p;Argan;ARG +FR0010533075p;Getlink;GET +FR0010667147p;Coface;COFA +FR0010828137p;Carmila;CARM +FR0010908533p;Edenred;EDEN +FR0010929125p;ID Logistics;IDL +FR0011726835p;GTT;GTT +FR0011995588p;Voltalia;VLTSA +FR0012435121p;Elis;ELIS +FR0012757854p;Spie;SPIE +FR0013154002p;Sartorius Stedim Biotech;DIM +FR0013176526p;Valeo;FR +FR0013227113p;Soitec Silicon;SOI +FR0013230612p;Tikehau Capital;TKO +FR0013258662p;Ayvens;AYV +FR0013269123p;Rubis;RUI +FR0013280286p;Biomerieux;BIM +FR0013326246p;Unibail Rodamco Westfield;URW +FR0013357621p;Wavestone;WAVE +FR0013447729p;Verallia;VRLA +FR0013451333p;FDJ United;FDJU +FR0013506730p;Vallourec;VK +FR0014000MR3p;Eurofins Scient.;ERF +FR0014003TT8p;Dassault Systemes;DSY +FR0014004L86p;Dassault Aviation;AM +FR0014005AL0p;Antin infra Partn;ANTIN +FR0014005HJ9p;OVHCLOUD;OVH +FR001400AJ45p;Michelin;ML +FR001400J770p;Air France - KLM;AF +FR001400PFU4p;Planisware;PLNW +FR001400Q9V2p;Exosens;EXENS +FR001400SF56p;Ldc;LOUP +FR001400SU99p;Moncey Fin. Nom.;FMONC +FR001400SUB7p;Cambodge Nom.;CBDG +FR001400X2S4p;Atos;ATO +FR00140182K6p;Worldline;WLN +GB00B5ZN1N88p;Segro PLC;SGRO +LU0088087324p;SES Sa;SESG +LU0569974404n;Aperam;APAM +LU1598757687n;Arcelor Mittal;MT +MA0000011488p;Maroc Telecom;IAM +MC0000031187p;Bains Mer Monaco;BAIN +NL0000226223p;Stmicroelectronics;STMPA +NL0000235190p;Airbus;AIR +NL0006294274p;Euronext;ENX +NL0014559478p;Technip Energies;TE +NL00150001Q9p;Stellantis;STLAP +NL0015001W49p;Pluxee;PLX +US2220702037p;Coty Inc.;COTY diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..75c801d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +blinker==1.9.0 +certifi==2026.7.22 +charset-normalizer==3.4.9 +click==8.4.2 +Flask==3.1.3 +Flask-SQLAlchemy==3.1.1 +greenlet==3.5.4 +idna==3.18 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.3 +mysql-connector-python==9.7.0 +requests==2.34.2 +SQLAlchemy==2.0.51 +typing_extensions==4.16.0 +urllib3==2.7.0 +Werkzeug==3.1.8