From 837368aef4264810159f3951edbed316977aaa44 Mon Sep 17 00:00:00 2001 From: jfgiraud Date: Sun, 26 Jul 2026 21:04:01 +0200 Subject: [PATCH] Manu 1 2 4 5 7 ok --- app/routes.py | 432 +++++++++++++++++- app/templates/base.html | 2 +- app/templates/gestion_ordres.html | 709 ++++++++++++++++++++++++++++++ app/templates/menu.html | 2 +- 4 files changed, 1125 insertions(+), 20 deletions(-) create mode 100644 app/templates/gestion_ordres.html diff --git a/app/routes.py b/app/routes.py index 3de8bf9..d9d851c 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,7 +1,6 @@ -from flask import Blueprint, render_template, request, redirect, url_for, session, flash, Response, session +from flask import Blueprint, render_template, request, redirect, url_for, session, flash, Response, jsonify import time import requests -import mysql.connector import csv import io from werkzeug.security import check_password_hash, generate_password_hash @@ -129,7 +128,6 @@ def gestion_utilisateurs(): return redirect(url_for('main.gestion_utilisateurs')) - # Lecture exacte de la table login de votre base de données users_query = text("SELECT id, login, nom, prenom, mail, actif FROM login ORDER BY id ASC") result = db.session.execute(users_query) @@ -146,7 +144,6 @@ 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(): @@ -199,7 +196,6 @@ def gestion_actions(): return redirect(url_for('main.gestion_actions')) - # 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) @@ -218,7 +214,6 @@ def gestion_actions(): return render_template('gestion_actions.html', actions=actions_list) - # Route pour la gestion des sociétés @main.route('/gestion-societes', methods=['GET', 'POST']) def gestion_societes(): @@ -274,7 +269,6 @@ def gestion_societes(): return redirect(url_for('main.gestion_societes')) - # Lecture de la table societe societes_query = text("SELECT id, Nom, Forme, Capital, siren, siret, RCS, TVA, NAF, Tel, Web, Mail FROM societe ORDER BY id ASC") result = db.session.execute(societes_query) @@ -297,8 +291,7 @@ def gestion_societes(): return render_template('gestion_societes.html', societes=societes) - -# Routes pour la gestion des imports exports des actions csv +# Routes pour la gestion des imports/exports des actions CSV @main.route('/gestion-import-export-actions-csv', methods=['GET']) def gestion_import_export_actions_csv(): if 'user_id' not in session: @@ -311,18 +304,14 @@ def export_actions_csv(): return redirect(url_for('main.index')) try: - # Récupération de toutes les données de la table actions query = text("SELECT id, isin, ticker, company_name, exchange, pays, currency, updated_at FROM actions") result = db.session.execute(query) - # Création du fichier CSV en mémoire output = io.StringIO() writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_MINIMAL) - # Écriture de l'en-tête du CSV writer.writerow(['id', 'isin', 'ticker', 'company_name', 'exchange', 'pays', 'currency', 'updated_at']) - # Écriture des lignes for row in result: writer.writerow([ row.id, @@ -337,7 +326,6 @@ def export_actions_csv(): output.seek(0) - # Envoi du fichier en téléchargement return Response( output.getvalue(), mimetype="text/csv", @@ -374,7 +362,6 @@ def import_actions_csv(): count = 0 for row in csv_reader: - # Vérifie qu'on a au moins les 6 colonnes de base if len(row) >= 6: isin = row[0].strip() if row[0] != '' else None ticker = row[1].strip() if row[1] != '' else None @@ -384,9 +371,8 @@ def import_actions_csv(): currency = row[5].strip() if row[5] != '' else '' if not isin or not ticker: - continue # Ignore les lignes mal formatées + continue - # Insertion ou mise à jour si le code ISIN existe déjà query = text(""" INSERT INTO actions (isin, ticker, company_name, exchange, pays, currency, updated_at) VALUES (:isin, :ticker, :company_name, :exchange, :pays, :currency, NOW()) @@ -409,4 +395,414 @@ def import_actions_csv(): return redirect(url_for('main.gestion_import_export_actions_csv')) else: flash("Veuillez fournir un fichier au format .csv valide.", "warning") - return redirect(url_for('main.gestion_import_export_actions_csv')) \ No newline at end of file + return redirect(url_for('main.gestion_import_export_actions_csv')) + + +# Route pour la gestion des ordres +@main.route("/gestion_ordres", methods=["GET", "POST"]) +def gestion_ordres(): + if "user_id" not in session: + return redirect(url_for("main.index")) + + selected_societe_id = ( + request.form.get("societe_id") + or request.args.get("societe_id") + or session.get("selected_societe_id") + ) + + societes = [] + selected_societe = "" + groupes_dict = {} + + with db.engine.connect() as connection: + result_soc = connection.execute( + text("SELECT id, Nom FROM societe ORDER BY Nom ASC") + ) + societes = [dict(row._mapping) for row in result_soc] + + if societes: + if not selected_societe_id: + selected_societe_id = societes[0]["id"] + else: + selected_societe_id = int(selected_societe_id) + + session["selected_societe_id"] = selected_societe_id + + for soc in societes: + if soc["id"] == selected_societe_id: + selected_societe = soc["Nom"] + break + + query = text(""" + SELECT o.ID as ordre_id, o.isin, o.PlusValue, o.date_plusvalue, + op.ID as pied_id, op.Date_op, op.Quantite, op.Prix_brut, op.Frais_brocker, + op.Frais_etat, op.Frais_autre, op.Ordre, op.Type, op.Etat, + a.ticker, a.company_name, a.currency + FROM ordre o + JOIN ordre_pied op ON o.ID = op.Id_entete + JOIN actions a ON o.isin = a.isin + WHERE o.id_societe = :soc_id + ORDER BY op.Date_op DESC + """) + + result_ordres = connection.execute( + query, {"soc_id": selected_societe_id} + ) + + for row in result_ordres: + r = dict(row._mapping) + isin = r["isin"] + + quantite = float(r["Quantite"]) if r["Quantite"] is not None else 0.0 + prix_brut = float(r["Prix_brut"]) if r["Prix_brut"] is not None else 0.0 + frais_brocker = ( + float(r["Frais_brocker"]) if r["Frais_brocker"] is not None else 0.0 + ) + frais_etat = ( + float(r["Frais_etat"]) if r["Frais_etat"] is not None else 0.0 + ) + frais_autre = ( + float(r["Frais_autre"]) if r["Frais_autre"] is not None else 0.0 + ) + + frais_total = frais_brocker + frais_etat + frais_autre + sens_ordre = str(r["Ordre"]).strip().lower() # 'achat' ou 'vente' + type_ordre = ( + str(r["Type"]).strip().lower() + if r["Type"] + else "long" # 'long' ou 'short' + ) + + # Calcul Prix Net unitaire + if sens_ordre == "vente": + prix_net = prix_brut - frais_total + else: + prix_net = prix_brut + frais_total + + # Calculs Ligne (Pied) + eng_brut = quantite * prix_brut + eng_net = quantite * prix_net + + pied_data = { + "id": r["pied_id"], + "date_op": r["Date_op"], + "quantite": quantite, + "prix_brut": prix_brut, + "frais": frais_total, + "frais_brocker": frais_brocker, + "frais_etat": frais_etat, + "frais_autre": frais_autre, + "prix_net": prix_net, + "eng_brut": eng_brut, + "eng_net": eng_net, + "ordre": r["Ordre"], + "type_ordre": r["Type"], + "etat": r["Etat"], + } + + if isin not in groupes_dict: + groupes_dict[isin] = { + "id": r["ordre_id"], + "isin": isin, + "company_name": r["company_name"], + "ticker": r["ticker"], + "pieds": [], + "total_quantite": 0.0, + "total_prix_brut": 0.0, + "total_frais": 0.0, + "total_prix_net": 0.0, + "total_eng_brut": 0.0, + "total_eng_net": 0.0, + } + + groupes_dict[isin]["pieds"].append(pied_data) + groupes_dict[isin]["total_prix_brut"] += prix_brut + groupes_dict[isin]["total_frais"] += frais_total + groupes_dict[isin]["total_prix_net"] += prix_net + + # --- APPLICATION DE VOS RÈGLES POUR LES TOTAUX --- + + # 1. QUANTITÉ + if sens_ordre == "achat" and type_ordre == "long": + groupes_dict[isin]["total_quantite"] += quantite + elif sens_ordre == "vente" and type_ordre == "long": + groupes_dict[isin]["total_quantite"] -= quantite + elif sens_ordre == "achat" and type_ordre == "short": + groupes_dict[isin]["total_quantite"] -= quantite + elif sens_ordre == "vente" and type_ordre == "short": + groupes_dict[isin]["total_quantite"] += quantite + + # 2. ENGAGEMENT BRUT + if sens_ordre == "achat" and type_ordre == "long": + groupes_dict[isin]["total_eng_brut"] += eng_brut + elif sens_ordre == "vente" and type_ordre == "long": + groupes_dict[isin]["total_eng_brut"] -= eng_brut + elif sens_ordre == "achat" and type_ordre == "short": + groupes_dict[isin]["total_eng_brut"] -= eng_brut + elif sens_ordre == "vente" and type_ordre == "short": + groupes_dict[isin]["total_eng_brut"] += eng_brut + + # 3. ENGAGEMENT NET + if sens_ordre == "achat" and type_ordre == "long": + groupes_dict[isin]["total_eng_net"] += eng_net + elif sens_ordre == "vente" and type_ordre == "long": + groupes_dict[isin]["total_eng_net"] -= eng_net + elif sens_ordre == "achat" and type_ordre == "short": + groupes_dict[isin]["total_eng_net"] -= eng_net + elif sens_ordre == "vente" and type_ordre == "short": + groupes_dict[isin]["total_eng_net"] += eng_net + + # --- VÉRIFICATION ET MISE À JOUR AUTOMATIQUE (EN BASE ET EN MÉMOIRE) --- + try: + with db.engine.begin() as conn: + for isin, groupe in groupes_dict.items(): + # Utilisation d'une tolérance de décimales (ex: 1e-9) pour éviter les erreurs d'arrondi flottant + if abs(groupe["total_quantite"]) < 1e-9: + ordre_id = groupe["id"] + + # 1. Sauvegarde en base de données + conn.execute( + text( + "UPDATE ordre_pied SET Etat = :nouvel_etat WHERE Id_entete =" + " :ordre_id" + ), + {"nouvel_etat": "soldé", "ordre_id": ordre_id}, + ) + + # 2. Mise à jour immédiate en mémoire pour l'affichage dans le template + for pied in groupe["pieds"]: + pied["etat"] = "soldé" + + except Exception as e: + # En cas d'erreur, SQLAlchemy gère le rollback automatique avec le bloc 'with connection.begin()' + pass + + groupes_ordres = list(groupes_dict.values()) + + return render_template( + "gestion_ordres.html", + societes=societes, + selected_societe_id=selected_societe_id, + selected_societe=selected_societe, + groupes_ordres=groupes_ordres, + ) + + +# Route pour la creation d'un ordre +@main.route("/creer-ordre", methods=["GET", "POST"]) +def creer_ordre(): + if "user_id" not in session: + return redirect(url_for("main.index")) + return render_template("creer_ordre.html") + + +# Route pour vérifier l'existence d'un ISIN +@main.route("/api/verifier_isin", methods=["POST"]) +def verifier_isin(): + data = request.get_json() + isin = data.get("isin", "").strip() + + query = text( + "SELECT company_name, ticker, currency FROM actions WHERE isin = :isin" + ) + action = db.session.execute(query, {"isin": isin}).fetchone() + + if action: + return jsonify({ + "exists": True, + "company_name": action.company_name, + "ticker": action.ticker, + "currency": action.currency, + }) + else: + return jsonify({"exists": False}) + + +# Route pour la création d'un ordre de traitement +@main.route("/creer-ordre-traitement", methods=["POST"]) +def creer_ordre_traitement(): + if "user_id" not in session: + return redirect(url_for("main.index")) + + isin = request.form.get("isin") + date_op = request.form.get("date_op") + quantite = float(request.form.get("quantite", 0)) + prix_brut = float(request.form.get("prix_brut", 0)) + + # Récupération des trois types de frais avec les bons noms d'inputs + frais_brocker = float(request.form.get("frais_brocker", 0)) + frais_etat = float(request.form.get("frais_etat", 0)) + frais_autre = float(request.form.get("frais_autre", 0)) + + ordre = request.form.get("ordre") + type_ordre = request.form.get("type_ordre") + etat = request.form.get("etat") + + # Récupération de l'id_societe courant stocké dans la session + id_societe = session.get("selected_societe_id") + + if not id_societe: + flash( + "Veuillez sélectionner une société active avant de créer un ordre.", + "danger", + ) + return redirect(url_for("main.gestion_ordres")) + + try: + # 1. Insertion dans la table 'ordre' avec l'isin et l'id_societe + query_ordre = text( + "INSERT INTO ordre (id_societe, isin) VALUES (:id_societe, :isin)" + ) + db.session.execute(query_ordre, {"id_societe": id_societe, "isin": isin}) + + # Récupérer l'ID généré pour l'en-tête (Id_entete) + result_id = db.session.execute( + text("SELECT LAST_INSERT_ID()") + ).scalar() + if not result_id: + result_id = db.session.execute( + text( + "SELECT ID FROM ordre WHERE isin = :isin ORDER BY ID DESC LIMIT" + " 1" + ), + {"isin": isin}, + ).fetchone()[0] + + # 2. Insertion dans la table 'ordre_pied' avec les 3 frais distincts + query_pied = text(""" + INSERT INTO ordre_pied (Id_entete, Date_op, Quantite, Prix_brut, Frais_brocker, Frais_etat, Frais_autre, Ordre, Type, Etat) + VALUES (:id_entete, :date_op, :quantite, :prix_brut, :frais_brocker, :frais_etat, :frais_autre, :ordre, :type_ordre, :etat) + """) + db.session.execute( + query_pied, + { + "id_entete": result_id, + "date_op": date_op, + "quantite": quantite, + "prix_brut": prix_brut, + "frais_brocker": frais_brocker, + "frais_etat": frais_etat, + "frais_autre": frais_autre, + "ordre": ordre, + "type_ordre": type_ordre, + "etat": etat, + }, + ) + + db.session.commit() + flash("Ordre créé avec succès !", "success") + except Exception as e: + db.session.rollback() + flash(f"Erreur lors de la création de l'ordre : {e}", "danger") + + return redirect(url_for("main.gestion_ordres")) + + +# Route pour créer une nouvelle ligne rattachée à un ordre existant (depuis le bouton "Créer") +@main.route("/ordre//ajouter-ligne", methods=["GET", "POST"]) +def ajouter_ligne_ordre(ordre_id): + if "user_id" not in session: + return redirect(url_for("main.index")) + + # Récupérer l'en-tête de l'ordre, l'ISIN et le nom de la société associée + query_info = text(""" + SELECT o.ID, o.isin, a.company_name + FROM ordre o + JOIN actions a ON o.isin = a.isin + WHERE o.ID = :ordre_id + """) + ordre_info = db.session.execute(query_info, {"ordre_id": ordre_id}).fetchone() + + if not ordre_info: + flash("Ordre introuvable.", "danger") + return redirect(url_for("main.gestion_ordres")) + + if request.method == "POST": + try: + query_pied = text(""" + INSERT INTO ordre_pied (Id_entete, Date_op, Quantite, Prix_brut, Frais_brocker, Frais_etat, Frais_autre, Ordre, Type, Etat) + VALUES (:id_entete, :date_op, :quantite, :prix_brut, :frais_brocker, :frais_etat, :frais_autre, :ordre, :type_ordre, :etat) + """) + db.session.execute(query_pied, { + "id_entete": ordre_id, + "date_op": request.form.get("date_op"), + "quantite": float(request.form.get("quantite", 0)), + "prix_brut": float(request.form.get("prix_brut", 0)), + "frais_brocker": float(request.form.get("frais_brocker", 0)), + "frais_etat": float(request.form.get("frais_etat", 0)), + "frais_autre": float(request.form.get("frais_autre", 0)), + "ordre": request.form.get("ordre"), + "type_ordre": request.form.get("type_ordre"), + "etat": request.form.get("etat"), + }) + db.session.commit() + flash("Ligne ajoutée avec succès à l'ordre !", "success") + return redirect(url_for("main.gestion_ordres")) + except Exception as e: + db.session.rollback() + flash(f"Erreur lors de l'ajout : {e}", "danger") + + contexte_form = { + "mode": "creer_ligne", + "isin": ordre_info.isin, + "company_name": ordre_info.company_name, + "action_url": url_for("main.ajouter_ligne_ordre", ordre_id=ordre_id) + } + return render_template("form_ordre_ligne.html", data=contexte_form, ligne=None) + + +# Route pour modifier une ligne spécifique (depuis le bouton "Modifier" de la ligne) +@main.route("/modifier-ligne-ordre/", methods=["GET", "POST"]) +def modifier_ligne_ordre(pied_id): + if "user_id" not in session: + return redirect(url_for("main.index")) + + # Récupérer la ligne du pied, l'ISIN et le nom de l'entreprise + query_get = text(""" + SELECT op.*, o.isin, a.company_name + FROM ordre_pied op + JOIN ordre o ON op.Id_entete = o.ID + JOIN actions a ON o.isin = a.isin + WHERE op.ID = :pied_id + """) + ligne = db.session.execute(query_get, {"pied_id": pied_id}).fetchone() + + if not ligne: + flash("Ligne d'ordre introuvable.", "danger") + return redirect(url_for("main.gestion_ordres")) + + if request.method == "POST": + try: + query_update = text(""" + UPDATE ordre_pied + SET Date_op = :date_op, Quantite = :quantite, Prix_brut = :prix_brut, + Frais_brocker = :frais_brocker, Frais_etat = :frais_etat, Frais_autre = :frais_autre, + Ordre = :ordre, Type = :type_ordre, Etat = :etat + WHERE ID = :pied_id + """) + db.session.execute(query_update, { + "date_op": request.form.get("date_op"), + "quantite": float(request.form.get("quantite", 0)), + "prix_brut": float(request.form.get("prix_brut", 0)), + "frais_brocker": float(request.form.get("frais_brocker", 0)), + "frais_etat": float(request.form.get("frais_etat", 0)), + "frais_autre": float(request.form.get("frais_autre", 0)), + "ordre": request.form.get("ordre"), + "type_ordre": request.form.get("type_ordre"), + "etat": request.form.get("etat"), + "pied_id": pied_id + }) + db.session.commit() + flash("Ligne modifiée avec succès !", "success") + return redirect(url_for("main.gestion_ordres")) + except Exception as e: + db.session.rollback() + flash(f"Erreur lors de la modification : {e}", "danger") + + contexte_form = { + "mode": "modifier_ligne", + "isin": ligne.isin, + "company_name": ligne.company_name, + "action_url": url_for("main.modifier_ligne_ordre", pied_id=pied_id) + } + return render_template("form_ordre_ligne.html", data=contexte_form, ligne=ligne) \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 7e694b1..d7d031b 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -72,7 +72,7 @@ -
+
{% block content %}{% endblock %}
diff --git a/app/templates/gestion_ordres.html b/app/templates/gestion_ordres.html new file mode 100644 index 0000000..8d0bddd --- /dev/null +++ b/app/templates/gestion_ordres.html @@ -0,0 +1,709 @@ +{% extends "base.html" %} {% block title %}Gestion des Ordres - Bolsa{% endblock +%} {% block header_title %}Gestion des ordres{% endblock %} {% block content %} +
+ +
+ {% set messages = get_flashed_messages(with_categories=true) %} {% if + messages %} {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} {% endif %} +
+ + +
+
+
+

+ Ordres - + {{ selected_societe }} +

+

+ Suivi et historique des ordres de bourse par société. +

+
+ +
+
+ +
+ + + + + Retour au menu + +
+
+ + +
+ + + {% if groupes_ordres %} {% for groupe in groupes_ordres %} + + + + + + + + + + + + + + + + + + + {% for p in groupe.pieds %} + + + + + + + + + + + + + + {% endfor %} + + + + + + + {% if groupe.total_quantite != 0 %} {% set total_prix_net_moyen = + groupe.total_eng_net / groupe.total_quantite %} {% set + total_prix_brut_moyen = groupe.total_eng_brut / + groupe.total_quantite %} {% set total_frais_moyen = + total_prix_net_moyen - total_prix_brut_moyen %} + + + + + + + {% else %} + + + + + + {% endif %} + + + + + + + + {% endfor %} {% else %} + + + + {% endif %} + +
+ {{ groupe.isin }} --- {{ groupe.company_name }} ( {{ groupe.ticker + }} ) +
DateQtéPrix BrutFraisPrix NetEng. BrutEng. NetOrdreTypeÉtat + +
{{ p.date_op }} + {{ "{:,.0f}".format(p.quantite).replace(",", " ") }} + {{ "{:,.3f}".format(p.prix_brut) }}{{ "{:,.3f}".format(p.frais) }}{{ "{:,.3f}".format(p.prix_net) }}{{ "{:,.3f}".format(p.eng_brut) }}{{ "{:,.3f}".format(p.eng_net) }} + + {{ p.ordre | upper }} + + {{ p.type_ordre }} + + {{ p.etat }} + + + +
TOTAL : + {{ "{:,.0f}".format(groupe.total_quantite).replace(",", " ") }} + + {{ "{:,.3f}".format(total_prix_brut_moyen) }} + + {{ "{:,.3f}".format(total_frais_moyen) }} + + {{ "{:,.3f}".format(total_prix_net_moyen) }} + + {{ "{:,.3f}".format(groupe.total_eng_brut) }} + + {{ "{:,.3f}".format(groupe.total_eng_net) }} + --- + {{ "{:,.3f}".format(groupe.total_eng_brut) }} + + + {{ "{:,.3f}".format(groupe.total_eng_net) }} + +
+ Aucun ordre trouvé pour cette société. +
+
+
+
+ + + + + +{% endblock %} diff --git a/app/templates/menu.html b/app/templates/menu.html index 4d3937c..c32a4ff 100644 --- a/app/templates/menu.html +++ b/app/templates/menu.html @@ -23,7 +23,7 @@ header_title %}Tableau de bord{% endblock %} {% block content %} 2. Ordres