Manu 1 2 4 5 7 ok
This commit is contained in:
+414
-18
@@ -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 time
|
||||||
import requests
|
import requests
|
||||||
import mysql.connector
|
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
@@ -129,7 +128,6 @@ def gestion_utilisateurs():
|
|||||||
|
|
||||||
return redirect(url_for('main.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")
|
users_query = text("SELECT id, login, nom, prenom, mail, actif FROM login ORDER BY id ASC")
|
||||||
result = db.session.execute(users_query)
|
result = db.session.execute(users_query)
|
||||||
|
|
||||||
@@ -146,7 +144,6 @@ def gestion_utilisateurs():
|
|||||||
|
|
||||||
return render_template('gestion_utilisateurs.html', users=users)
|
return render_template('gestion_utilisateurs.html', users=users)
|
||||||
|
|
||||||
|
|
||||||
# Route pour la gestion des actions
|
# Route pour la gestion des actions
|
||||||
@main.route('/gestion-actions', methods=['GET', 'POST'])
|
@main.route('/gestion-actions', methods=['GET', 'POST'])
|
||||||
def gestion_actions():
|
def gestion_actions():
|
||||||
@@ -199,7 +196,6 @@ def gestion_actions():
|
|||||||
|
|
||||||
return redirect(url_for('main.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")
|
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)
|
result = db.session.execute(actions_query)
|
||||||
|
|
||||||
@@ -218,7 +214,6 @@ def gestion_actions():
|
|||||||
|
|
||||||
return render_template('gestion_actions.html', actions=actions_list)
|
return render_template('gestion_actions.html', actions=actions_list)
|
||||||
|
|
||||||
|
|
||||||
# Route pour la gestion des sociétés
|
# Route pour la gestion des sociétés
|
||||||
@main.route('/gestion-societes', methods=['GET', 'POST'])
|
@main.route('/gestion-societes', methods=['GET', 'POST'])
|
||||||
def gestion_societes():
|
def gestion_societes():
|
||||||
@@ -274,7 +269,6 @@ def gestion_societes():
|
|||||||
|
|
||||||
return redirect(url_for('main.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")
|
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)
|
result = db.session.execute(societes_query)
|
||||||
|
|
||||||
@@ -297,8 +291,7 @@ def gestion_societes():
|
|||||||
|
|
||||||
return render_template('gestion_societes.html', societes=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'])
|
@main.route('/gestion-import-export-actions-csv', methods=['GET'])
|
||||||
def gestion_import_export_actions_csv():
|
def gestion_import_export_actions_csv():
|
||||||
if 'user_id' not in session:
|
if 'user_id' not in session:
|
||||||
@@ -311,18 +304,14 @@ def export_actions_csv():
|
|||||||
return redirect(url_for('main.index'))
|
return redirect(url_for('main.index'))
|
||||||
|
|
||||||
try:
|
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")
|
query = text("SELECT id, isin, ticker, company_name, exchange, pays, currency, updated_at FROM actions")
|
||||||
result = db.session.execute(query)
|
result = db.session.execute(query)
|
||||||
|
|
||||||
# Création du fichier CSV en mémoire
|
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_MINIMAL)
|
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'])
|
writer.writerow(['id', 'isin', 'ticker', 'company_name', 'exchange', 'pays', 'currency', 'updated_at'])
|
||||||
|
|
||||||
# Écriture des lignes
|
|
||||||
for row in result:
|
for row in result:
|
||||||
writer.writerow([
|
writer.writerow([
|
||||||
row.id,
|
row.id,
|
||||||
@@ -337,7 +326,6 @@ def export_actions_csv():
|
|||||||
|
|
||||||
output.seek(0)
|
output.seek(0)
|
||||||
|
|
||||||
# Envoi du fichier en téléchargement
|
|
||||||
return Response(
|
return Response(
|
||||||
output.getvalue(),
|
output.getvalue(),
|
||||||
mimetype="text/csv",
|
mimetype="text/csv",
|
||||||
@@ -374,7 +362,6 @@ def import_actions_csv():
|
|||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
for row in csv_reader:
|
for row in csv_reader:
|
||||||
# Vérifie qu'on a au moins les 6 colonnes de base
|
|
||||||
if len(row) >= 6:
|
if len(row) >= 6:
|
||||||
isin = row[0].strip() if row[0] != '' else None
|
isin = row[0].strip() if row[0] != '' else None
|
||||||
ticker = row[1].strip() if row[1] != '' 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 ''
|
currency = row[5].strip() if row[5] != '' else ''
|
||||||
|
|
||||||
if not isin or not ticker:
|
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("""
|
query = text("""
|
||||||
INSERT INTO actions (isin, ticker, company_name, exchange, pays, currency, updated_at)
|
INSERT INTO actions (isin, ticker, company_name, exchange, pays, currency, updated_at)
|
||||||
VALUES (:isin, :ticker, :company_name, :exchange, :pays, :currency, NOW())
|
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'))
|
return redirect(url_for('main.gestion_import_export_actions_csv'))
|
||||||
else:
|
else:
|
||||||
flash("Veuillez fournir un fichier au format .csv valide.", "warning")
|
flash("Veuillez fournir un fichier au format .csv valide.", "warning")
|
||||||
return redirect(url_for('main.gestion_import_export_actions_csv'))
|
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/<int:ordre_id>/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/<int:pied_id>", 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)
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Container dynamique -->
|
<!-- Container dynamique -->
|
||||||
<main class="flex-grow container mx-auto px-4 py-8">
|
<main class="flex-grow w-full px-2 py-4">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,709 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}Gestion des Ordres - Bolsa{% endblock
|
||||||
|
%} {% block header_title %}Gestion des ordres{% endblock %} {% block content %}
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center py-2 px-2 relative w-full"
|
||||||
|
>
|
||||||
|
<!-- Système de Toasts -->
|
||||||
|
<div
|
||||||
|
id="toast-container"
|
||||||
|
class="fixed top-6 left-1/2 transform -translate-x-1/2 z-50 space-y-3 w-full max-w-md px-4 pointer-events-none"
|
||||||
|
>
|
||||||
|
{% set messages = get_flashed_messages(with_categories=true) %} {% if
|
||||||
|
messages %} {% for category, message in messages %}
|
||||||
|
<div
|
||||||
|
class="toast-message pointer-events-auto flex items-center justify-center px-4 py-3 rounded-xl shadow-2xl text-white text-sm font-medium transition-all duration-300 transform translate-y-0 opacity-100 text-center {% if category == 'success' %} bg-emerald-600 border border-emerald-500 {% elif category == 'warning' %} bg-amber-600 border border-amber-500 {% else %} bg-rose-600 border border-rose-500 {% endif %}"
|
||||||
|
>
|
||||||
|
<span>{{ message }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %} {% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Box principale -->
|
||||||
|
<div
|
||||||
|
class="bg-gray-900 border border-gray-800 p-4 sm:p-6 rounded-2xl shadow-2xl w-full max-w-[98%]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center mb-4 border-b border-gray-800 pb-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-extrabold text-white flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-list-check text-blue-500"></i> Ordres -
|
||||||
|
<span class="text-amber-400">{{ selected_societe }}</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
Suivi et historique des ordres de bourse par société.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="{{ url_for('main.gestion_ordres') }}"
|
||||||
|
class="m-0"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name="societe_id"
|
||||||
|
class="bg-gray-800 border border-gray-700 text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-blue-500"
|
||||||
|
onchange="this.form.submit()"
|
||||||
|
>
|
||||||
|
{% for soc in societes %} {% if soc.id == selected_societe_id %}
|
||||||
|
<option value="{{ soc.id }}" selected>{{ soc.Nom }}</option>
|
||||||
|
{% else %}
|
||||||
|
<option value="{{ soc.id }}">{{ soc.Nom }}</option>
|
||||||
|
{% endif %} {% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="ouvrirModalOrdre()"
|
||||||
|
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white text-sm font-medium rounded-lg transition duration-200 flex items-center gap-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-plus"></i> Créer ordre
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="{{ url_for('main.menu') }}"
|
||||||
|
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-gray-200 text-sm font-medium rounded-lg transition duration-200"
|
||||||
|
>
|
||||||
|
Retour au menu
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tableau -->
|
||||||
|
<div
|
||||||
|
class="bg-gray-950 border border-gray-800 rounded-xl p-2 overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table class="w-full text-left border-collapse text-sm">
|
||||||
|
<tbody class="divide-y divide-gray-800 text-gray-300">
|
||||||
|
{% if groupes_ordres %} {% for groupe in groupes_ordres %}
|
||||||
|
<tr class="bg-blue-950 text-white font-semibold">
|
||||||
|
<td colspan="12" class="py-2.5 px-3 text-base">
|
||||||
|
{{ groupe.isin }} --- {{ groupe.company_name }} ( {{ groupe.ticker
|
||||||
|
}} )
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr
|
||||||
|
class="bg-blue-950/60 text-gray-400 uppercase tracking-wider text-xs border-t border-b border-gray-800"
|
||||||
|
>
|
||||||
|
<th class="py-2 px-3">Date</th>
|
||||||
|
<th class="py-2 px-3">Qté</th>
|
||||||
|
<th class="py-2 px-3">Prix Brut</th>
|
||||||
|
<th class="py-2 px-3">Frais</th>
|
||||||
|
<th class="py-2 px-3">Prix Net</th>
|
||||||
|
<th class="py-2 px-3">Eng. Brut</th>
|
||||||
|
<th class="py-2 px-3">Eng. Net</th>
|
||||||
|
<th class="py-2 px-3">Ordre</th>
|
||||||
|
<th class="py-2 px-3">Type</th>
|
||||||
|
<th class="py-2 px-3">État</th>
|
||||||
|
<th class="py-2 px-3 text-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="ouvrirModalLigneOrdre('{{ groupe.id }}', '{{ groupe.isin }}', '{{ groupe.company_name }}', '{{ groupe.ticker }}')"
|
||||||
|
class="px-3.5 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-semibold rounded shadow transition cursor-pointer"
|
||||||
|
>
|
||||||
|
Créer
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% for p in groupe.pieds %}
|
||||||
|
<tr class="hover:bg-gray-900/50 transition">
|
||||||
|
<td class="py-2 px-3 whitespace-nowrap">{{ p.date_op }}</td>
|
||||||
|
<td class="py-2 px-3">
|
||||||
|
{{ "{:,.0f}".format(p.quantite).replace(",", " ") }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3">{{ "{:,.3f}".format(p.prix_brut) }}</td>
|
||||||
|
<td class="py-2 px-3">{{ "{:,.3f}".format(p.frais) }}</td>
|
||||||
|
<td class="py-2 px-3">{{ "{:,.3f}".format(p.prix_net) }}</td>
|
||||||
|
<td class="py-2 px-3">{{ "{:,.3f}".format(p.eng_brut) }}</td>
|
||||||
|
<td class="py-2 px-3">{{ "{:,.3f}".format(p.eng_net) }}</td>
|
||||||
|
<td class="py-2 px-3">
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 rounded text-xs font-semibold {% if p.ordre == 'achat' %}bg-blue-900 text-blue-200{% else %}bg-rose-900 text-rose-200{% endif %}"
|
||||||
|
>
|
||||||
|
{{ p.ordre | upper }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3">{{ p.type_ordre }}</td>
|
||||||
|
<td class="py-2 px-3">
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 rounded text-xs font-medium {% if p.etat == 'actif' %}bg-emerald-900 text-emerald-200 {% elif p.etat == 'soldé' %}bg-gray-800 text-gray-300 {% else %}bg-amber-900 text-amber-200{% endif %}"
|
||||||
|
>
|
||||||
|
{{ p.etat }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3 text-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="ouvrirModalModifierLigne('{{ p.id }}', '{{ groupe.isin }}', '{{ groupe.company_name }}', '{{ groupe.ticker }}', '{{ p.date_op }}', '{{ p.quantite }}', '{{ p.prix_brut }}', '{{ p.frais_brocker }}', '{{ p.frais_etat }}', '{{ p.frais_autre }}', '{{ p.ordre }}', '{{ p.type_ordre }}', '{{ p.etat }}')"
|
||||||
|
class="px-3.5 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs font-semibold rounded shadow transition inline-flex items-center justify-center cursor-pointer"
|
||||||
|
title="Modifier"
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- LIGNE TOTAL -->
|
||||||
|
<tr
|
||||||
|
class="bg-blue-950/60 text-white uppercase tracking-wider text-xs border-t border-b border-gray-800 font-bold"
|
||||||
|
>
|
||||||
|
<td class="py-2.5 px-3">TOTAL :</td>
|
||||||
|
<td class="py-2.5 px-3">
|
||||||
|
{{ "{:,.0f}".format(groupe.total_quantite).replace(",", " ") }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
<td class="py-2.5 px-3">
|
||||||
|
{{ "{:,.3f}".format(total_prix_brut_moyen) }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 px-3">
|
||||||
|
{{ "{:,.3f}".format(total_frais_moyen) }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 px-3">
|
||||||
|
{{ "{:,.3f}".format(total_prix_net_moyen) }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 px-3">
|
||||||
|
{{ "{:,.3f}".format(groupe.total_eng_brut) }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 px-3">
|
||||||
|
{{ "{:,.3f}".format(groupe.total_eng_net) }}
|
||||||
|
</td>
|
||||||
|
{% else %}
|
||||||
|
<td class="py-2.5 px-3">-</td>
|
||||||
|
<td class="py-2.5 px-3">-</td>
|
||||||
|
<td class="py-2.5 px-3">-</td>
|
||||||
|
<td class="py-2.5 px-3">
|
||||||
|
{{ "{:,.3f}".format(groupe.total_eng_brut) }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 px-3">
|
||||||
|
<span
|
||||||
|
class="{% if groupe.total_eng_net >= 0 %}text-emerald-400{% else %}text-rose-400{% endif %}"
|
||||||
|
>
|
||||||
|
{{ "{:,.3f}".format(groupe.total_eng_net) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<td class="py-2.5 px-3" colspan="4"></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="11" class="h-3 bg-transparent border-0"></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %} {% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="11" class="text-center text-gray-500 py-8">
|
||||||
|
Aucun ordre trouvé pour cette société.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Popup Unique (Création d'ordre, Ajout de ligne, et Modification de ligne) -->
|
||||||
|
<div
|
||||||
|
id="modal-creer-ordre"
|
||||||
|
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-gray-900 border border-gray-800 rounded-2xl p-6 w-full max-w-xl shadow-2xl text-white relative max-h-[90vh] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onclick="fermerModalOrdre()"
|
||||||
|
class="absolute top-4 right-4 text-gray-400 hover:text-white cursor-pointer"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-xmark text-xl"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h3 id="modal-title" class="text-xl font-bold mb-4 flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-plus-circle text-emerald-500"></i> Créer un Nouvel
|
||||||
|
Ordre
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<form
|
||||||
|
id="form-modal-ordre"
|
||||||
|
method="POST"
|
||||||
|
action="{{ url_for('main.creer_ordre_traitement') }}"
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
|
<!-- Bloc ISIN -->
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-400">Code ISIN</label>
|
||||||
|
<div class="flex gap-2 mt-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="input-isin"
|
||||||
|
name="isin"
|
||||||
|
required
|
||||||
|
placeholder="Ex: NL00150001Q9"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500 uppercase"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="btn-verifier-isin"
|
||||||
|
onclick="verifierIsin()"
|
||||||
|
class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium rounded-lg transition cursor-pointer"
|
||||||
|
>
|
||||||
|
Vérifier
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p id="isin-status" class="text-xs mt-1"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nom de l'action en lecture seule -->
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-400">Nom de l'action</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="input-company-name"
|
||||||
|
readonly
|
||||||
|
placeholder="Sera affiché après vérification..."
|
||||||
|
class="w-full bg-gray-950 border border-gray-800 rounded-lg px-3 py-2 text-sm text-amber-400 font-semibold focus:outline-none cursor-default mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Informations complémentaires -->
|
||||||
|
<div
|
||||||
|
id="action-info"
|
||||||
|
class="hidden bg-gray-950 p-3 rounded-xl border border-gray-800 text-xs space-y-1 text-gray-300"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<strong>Ticker :</strong> <span id="lbl-ticker"></span> |
|
||||||
|
<strong>Devise :</strong> <span id="lbl-currency"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ligne 1 : Date, Quantité, Prix Brut -->
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-400">Date Opération</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="input-date-op"
|
||||||
|
name="date_op"
|
||||||
|
required
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-400">Quantité</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
id="input-quantite"
|
||||||
|
name="quantite"
|
||||||
|
required
|
||||||
|
oninput="calculerTotauxModal()"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-400">Prix Brut</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.0001"
|
||||||
|
id="input-prix-brut"
|
||||||
|
name="prix_brut"
|
||||||
|
required
|
||||||
|
oninput="calculerTotauxModal()"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ligne 2 : Les 3 champs de frais -->
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-400">Frais Broker</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.0001"
|
||||||
|
id="input-frais-brocker"
|
||||||
|
name="frais_brocker"
|
||||||
|
value="0.0000"
|
||||||
|
oninput="calculerTotauxModal()"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-400">Frais État</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.0001"
|
||||||
|
id="input-frais-etat"
|
||||||
|
name="frais_etat"
|
||||||
|
value="0.0000"
|
||||||
|
oninput="calculerTotauxModal()"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-400">Autres Frais</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.0001"
|
||||||
|
id="input-frais-autre"
|
||||||
|
name="frais_autre"
|
||||||
|
value="0.0000"
|
||||||
|
oninput="calculerTotauxModal()"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aperçu dynamique des calculs en temps réel -->
|
||||||
|
<div
|
||||||
|
class="bg-gray-950 p-3 rounded-xl border border-gray-800 text-xs grid grid-cols-2 gap-2 text-gray-300"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Total Frais :
|
||||||
|
<span id="apercu-frais" class="text-amber-400 font-semibold"
|
||||||
|
>0.000</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Prix Net :
|
||||||
|
<span id="apercu-prix-net" class="text-emerald-400 font-semibold"
|
||||||
|
>0.000</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-400">Sens (Ordre)</label>
|
||||||
|
<select
|
||||||
|
id="input-ordre"
|
||||||
|
name="ordre"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||||
|
>
|
||||||
|
<option value="achat">Achat</option>
|
||||||
|
<option value="vente">Vente</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-400">Type</label>
|
||||||
|
<select
|
||||||
|
id="input-type-ordre"
|
||||||
|
name="type_ordre"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||||
|
>
|
||||||
|
<option value="long">Long</option>
|
||||||
|
<option value="short">Short</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-400">État</label>
|
||||||
|
<select
|
||||||
|
id="input-etat"
|
||||||
|
name="etat"
|
||||||
|
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-blue-500"
|
||||||
|
>
|
||||||
|
<option value="actif">Actif</option>
|
||||||
|
<option value="soldé">Soldé</option>
|
||||||
|
<option value="annulé">Annulé</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="fermerModalOrdre()"
|
||||||
|
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-lg text-sm transition cursor-pointer"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
id="btn-valider"
|
||||||
|
disabled
|
||||||
|
class="px-4 py-2 bg-emerald-600/50 cursor-not-allowed text-white rounded-lg text-sm transition"
|
||||||
|
>
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const toasts = document.querySelectorAll(".toast-message");
|
||||||
|
toasts.forEach((toast) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = "0";
|
||||||
|
toast.style.transform = "translateY(-10px)";
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 4000);
|
||||||
|
});
|
||||||
|
calculerTotauxModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calcul dynamique : Prix Net unitaire = Prix Brut + Frais Totaux
|
||||||
|
function calculerTotauxModal() {
|
||||||
|
const quantite =
|
||||||
|
parseFloat(document.getElementById("input-quantite").value) || 0;
|
||||||
|
const prixBrut =
|
||||||
|
parseFloat(document.getElementById("input-prix-brut").value) || 0;
|
||||||
|
const fraisBrocker =
|
||||||
|
parseFloat(document.getElementById("input-frais-brocker").value) || 0;
|
||||||
|
const fraisEtat =
|
||||||
|
parseFloat(document.getElementById("input-frais-etat").value) || 0;
|
||||||
|
const fraisAutre =
|
||||||
|
parseFloat(document.getElementById("input-frais-autre").value) || 0;
|
||||||
|
|
||||||
|
const selectOrdre = document.getElementById("input-ordre");
|
||||||
|
const sensOrdre = selectOrdre
|
||||||
|
? selectOrdre.value.trim().toLowerCase()
|
||||||
|
: "achat";
|
||||||
|
|
||||||
|
const totalFrais = fraisBrocker + fraisEtat + fraisAutre;
|
||||||
|
|
||||||
|
let prixNet = 0;
|
||||||
|
if (sensOrdre === "vente") {
|
||||||
|
prixNet = prixBrut - totalFrais;
|
||||||
|
} else {
|
||||||
|
prixNet = prixBrut + totalFrais;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Engagement Net = Prix Net * Quantité
|
||||||
|
const engNet = quantite * prixNet;
|
||||||
|
|
||||||
|
// Mise à jour des éléments dans le DOM (adaptez les IDs selon vos balises HTML)
|
||||||
|
document.getElementById("apercu-frais").textContent = totalFrais.toFixed(4);
|
||||||
|
document.getElementById("apercu-prix-net").textContent = prixNet.toFixed(4);
|
||||||
|
const apercuEngNet = document.getElementById("apercu-eng-net");
|
||||||
|
if (apercuEngNet) {
|
||||||
|
apercuEngNet.textContent = engNet.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Écouteurs pour déclencher le calcul lors des saisies
|
||||||
|
[
|
||||||
|
"input-quantite",
|
||||||
|
"input-prix-brut",
|
||||||
|
"input-frais-brocker",
|
||||||
|
"input-frais-etat",
|
||||||
|
"input-frais-autre",
|
||||||
|
"input-ordre",
|
||||||
|
].forEach((id) => {
|
||||||
|
document.getElementById(id)?.addEventListener("input", calculerTotauxModal);
|
||||||
|
document
|
||||||
|
.getElementById(id)
|
||||||
|
?.addEventListener("change", calculerTotauxModal);
|
||||||
|
});
|
||||||
|
|
||||||
|
function ouvrirModalOrdre() {
|
||||||
|
const modal = document.getElementById("modal-creer-ordre");
|
||||||
|
const form = document.getElementById("form-modal-ordre");
|
||||||
|
const title = document.getElementById("modal-title");
|
||||||
|
const isinInput = document.getElementById("input-isin");
|
||||||
|
const btnVerif = document.getElementById("btn-verifier-isin");
|
||||||
|
const companyInput = document.getElementById("input-company-name");
|
||||||
|
const btnValider = document.getElementById("btn-valider");
|
||||||
|
|
||||||
|
modal.classList.remove("hidden");
|
||||||
|
form.action = "{{ url_for('main.creer_ordre_traitement') }}";
|
||||||
|
title.innerHTML =
|
||||||
|
'<i class="fa-solid fa-plus-circle text-emerald-500"></i> Créer un Nouvel Ordre';
|
||||||
|
|
||||||
|
isinInput.value = "";
|
||||||
|
isinInput.readOnly = false;
|
||||||
|
isinInput.classList.remove("bg-gray-950", "cursor-default");
|
||||||
|
isinInput.classList.add("bg-gray-800");
|
||||||
|
|
||||||
|
btnVerif.style.display = "inline-block";
|
||||||
|
companyInput.value = "";
|
||||||
|
|
||||||
|
form.reset();
|
||||||
|
document.getElementById("action-info").classList.add("hidden");
|
||||||
|
calculerTotauxModal();
|
||||||
|
|
||||||
|
btnValider.disabled = true;
|
||||||
|
btnValider.classList.add("bg-emerald-600/50", "cursor-not-allowed");
|
||||||
|
btnValider.classList.remove(
|
||||||
|
"bg-emerald-600",
|
||||||
|
"hover:bg-emerald-500",
|
||||||
|
"cursor-pointer",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ouvrirModalLigneOrdre(ordreId, isin, companyName, ticker) {
|
||||||
|
const modal = document.getElementById("modal-creer-ordre");
|
||||||
|
const form = document.getElementById("form-modal-ordre");
|
||||||
|
const title = document.getElementById("modal-title");
|
||||||
|
const isinInput = document.getElementById("input-isin");
|
||||||
|
const btnVerif = document.getElementById("btn-verifier-isin");
|
||||||
|
const companyInput = document.getElementById("input-company-name");
|
||||||
|
const btnValider = document.getElementById("btn-valider");
|
||||||
|
|
||||||
|
modal.classList.remove("hidden");
|
||||||
|
form.action = `/ordre/${ordreId}/ajouter-ligne`;
|
||||||
|
title.innerHTML =
|
||||||
|
'<i class="fa-solid fa-plus text-emerald-500"></i> Ajouter une ligne à l\'ordre';
|
||||||
|
|
||||||
|
isinInput.value = isin;
|
||||||
|
isinInput.readOnly = true;
|
||||||
|
isinInput.classList.add("bg-gray-950", "cursor-default");
|
||||||
|
isinInput.classList.remove("bg-gray-800");
|
||||||
|
|
||||||
|
btnVerif.style.display = "none";
|
||||||
|
companyInput.value = companyName + (ticker ? ` (${ticker})` : "");
|
||||||
|
|
||||||
|
form.reset();
|
||||||
|
isinInput.value = isin;
|
||||||
|
companyInput.value = companyName + (ticker ? ` (${ticker})` : "");
|
||||||
|
document.getElementById("action-info").classList.add("hidden");
|
||||||
|
calculerTotauxModal();
|
||||||
|
|
||||||
|
btnValider.disabled = false;
|
||||||
|
btnValider.classList.remove("bg-emerald-600/50", "cursor-not-allowed");
|
||||||
|
btnValider.classList.add(
|
||||||
|
"bg-emerald-600",
|
||||||
|
"hover:bg-emerald-500",
|
||||||
|
"cursor-pointer",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ouvrirModalModifierLigne(
|
||||||
|
piedId,
|
||||||
|
isin,
|
||||||
|
companyName,
|
||||||
|
ticker,
|
||||||
|
dateOp,
|
||||||
|
quantite,
|
||||||
|
prixBrut,
|
||||||
|
fraisBrocker,
|
||||||
|
fraisEtat,
|
||||||
|
fraisAutre,
|
||||||
|
ordre,
|
||||||
|
typeOrdre,
|
||||||
|
etat,
|
||||||
|
) {
|
||||||
|
const modal = document.getElementById("modal-creer-ordre");
|
||||||
|
const form = document.getElementById("form-modal-ordre");
|
||||||
|
const title = document.getElementById("modal-title");
|
||||||
|
const isinInput = document.getElementById("input-isin");
|
||||||
|
const btnVerif = document.getElementById("btn-verifier-isin");
|
||||||
|
const companyInput = document.getElementById("input-company-name");
|
||||||
|
const btnValider = document.getElementById("btn-valider");
|
||||||
|
|
||||||
|
modal.classList.remove("hidden");
|
||||||
|
form.action = `/modifier-ligne-ordre/${piedId}`;
|
||||||
|
title.innerHTML =
|
||||||
|
'<i class="fa-solid fa-pen text-blue-500"></i> Modifier la ligne d\'ordre';
|
||||||
|
|
||||||
|
isinInput.value = isin;
|
||||||
|
isinInput.readOnly = true;
|
||||||
|
isinInput.classList.add("bg-gray-950", "cursor-default");
|
||||||
|
isinInput.classList.remove("bg-gray-800");
|
||||||
|
|
||||||
|
btnVerif.style.display = "none";
|
||||||
|
companyInput.value = companyName + (ticker ? ` (${ticker})` : "");
|
||||||
|
|
||||||
|
document.getElementById("input-date-op").value = dateOp;
|
||||||
|
document.getElementById("input-quantite").value = quantite;
|
||||||
|
document.getElementById("input-prix-brut").value = prixBrut;
|
||||||
|
document.getElementById("input-frais-brocker").value = fraisBrocker;
|
||||||
|
document.getElementById("input-frais-etat").value = fraisEtat;
|
||||||
|
document.getElementById("input-frais-autre").value = fraisAutre;
|
||||||
|
document.getElementById("input-ordre").value = ordre.toLowerCase();
|
||||||
|
document.getElementById("input-type-ordre").value = typeOrdre.toLowerCase();
|
||||||
|
document.getElementById("input-etat").value = etat.toLowerCase();
|
||||||
|
document.getElementById("action-info").classList.add("hidden");
|
||||||
|
|
||||||
|
calculerTotauxModal();
|
||||||
|
|
||||||
|
btnValider.disabled = false;
|
||||||
|
btnValider.classList.remove("bg-emerald-600/50", "cursor-not-allowed");
|
||||||
|
btnValider.classList.add(
|
||||||
|
"bg-emerald-600",
|
||||||
|
"hover:bg-emerald-500",
|
||||||
|
"cursor-pointer",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fermerModalOrdre() {
|
||||||
|
document.getElementById("modal-creer-ordre").classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifierIsin() {
|
||||||
|
const isin = document.getElementById("input-isin").value.trim();
|
||||||
|
const statusEl = document.getElementById("isin-status");
|
||||||
|
const infoBox = document.getElementById("action-info");
|
||||||
|
const companyInput = document.getElementById("input-company-name");
|
||||||
|
const btnValider = document.getElementById("btn-valider");
|
||||||
|
|
||||||
|
if (!isin) {
|
||||||
|
statusEl.textContent = "Veuillez saisir un code ISIN.";
|
||||||
|
statusEl.className = "text-xs mt-1 text-rose-400";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch("/api/verifier_isin", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ isin: isin }),
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.exists) {
|
||||||
|
statusEl.textContent = "✔ Action reconnue dans la base.";
|
||||||
|
statusEl.className = "text-xs mt-1 text-emerald-400";
|
||||||
|
|
||||||
|
companyInput.value = data.company_name || data.nom || "";
|
||||||
|
|
||||||
|
document.getElementById("lbl-ticker").textContent =
|
||||||
|
data.ticker || "N/A";
|
||||||
|
document.getElementById("lbl-currency").textContent = data.currency;
|
||||||
|
infoBox.classList.remove("hidden");
|
||||||
|
|
||||||
|
btnValider.disabled = false;
|
||||||
|
btnValider.classList.remove(
|
||||||
|
"bg-emerald-600/50",
|
||||||
|
"cursor-not-allowed",
|
||||||
|
);
|
||||||
|
btnValider.classList.add(
|
||||||
|
"bg-emerald-600",
|
||||||
|
"hover:bg-emerald-500",
|
||||||
|
"cursor-pointer",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
statusEl.textContent =
|
||||||
|
"✖ Cet ISIN n'existe pas dans la table actions.";
|
||||||
|
statusEl.className = "text-xs mt-1 text-rose-400";
|
||||||
|
|
||||||
|
companyInput.value = "";
|
||||||
|
infoBox.classList.add("hidden");
|
||||||
|
|
||||||
|
btnValider.disabled = true;
|
||||||
|
btnValider.classList.add("bg-emerald-600/50", "cursor-not-allowed");
|
||||||
|
btnValider.classList.remove(
|
||||||
|
"bg-emerald-600",
|
||||||
|
"hover:bg-emerald-500",
|
||||||
|
"cursor-pointer",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Erreur lors de la vérification de l'ISIN:", error);
|
||||||
|
statusEl.textContent = "Erreur de communication avec le serveur.";
|
||||||
|
statusEl.className = "text-xs mt-1 text-rose-400";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -23,7 +23,7 @@ header_title %}Tableau de bord{% endblock %} {% block content %}
|
|||||||
|
|
||||||
<!-- btn 2 -->
|
<!-- btn 2 -->
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="{{ url_for('main.gestion_ordres') }}"
|
||||||
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
||||||
>
|
>
|
||||||
2. Ordres
|
2. Ordres
|
||||||
|
|||||||
Reference in New Issue
Block a user