from flask import ( Blueprint, current_app, flash, jsonify, redirect, render_template, request, session, url_for, ) from sqlalchemy import text from app import csrf, db from app.decorators import login_required ordres_bp = Blueprint("ordres", __name__) # --------------------------------------------------------------------------- # Gestion des ordres # --------------------------------------------------------------------------- @ordres_bp.route("/gestion_ordres", methods=["GET", "POST"]) @login_required def gestion_ordres(): selected_societe_id = ( request.form.get("societe_id") or request.args.get("societe_id") or session.get("selected_societe_id") ) selected_annee = ( request.form.get("annee") or request.args.get("annee") or session.get("selected_annee", "Tout") ) session["selected_annee"] = selected_annee societes = [] selected_societe = "" groupes_dict = {} total_plus_value_globale = 0.0 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: try: selected_societe_id = int(selected_societe_id) except (TypeError, ValueError): selected_societe_id = societes[0]["id"] session["selected_societe_id"] = selected_societe_id for soc in societes: if soc["id"] == selected_societe_id: selected_societe = soc["nom"] break # --- CALCUL DE LA PLUS-VALUE TOTALE --- annee_str = str(selected_annee).strip().lower() if not annee_str or annee_str == "tout": query_pv = text( "SELECT SUM(plus_value) AS total_pv FROM ordre " "WHERE id_societe = :soc_id AND plus_value IS NOT NULL" ) res_pv = connection.execute( query_pv, {"soc_id": selected_societe_id} ).fetchone() else: query_pv = text( "SELECT SUM(plus_value) AS total_pv FROM ordre " "WHERE id_societe = :soc_id AND plus_value IS NOT NULL " "AND LEFT(CAST(date_plus_value AS CHAR), 4) = :annee" ) res_pv = connection.execute( query_pv, {"soc_id": selected_societe_id, "annee": annee_str} ).fetchone() if res_pv and res_pv.total_pv is not None: total_plus_value_globale = float(res_pv.total_pv) # --- CHARGEMENT DES ORDRES --- query = text( "SELECT o.id AS ordre_id, o.isin, o.plus_value, o.date_plus_value, " "op.id AS pied_id, op.date_op, op.quantite, op.prix_brut, " "op.frais_broker, op.frais_etat, op.frais_autre, " "op.sens, op.position, 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 o.id DESC, op.date_op DESC" ) result_ordres = connection.execute( query, {"soc_id": selected_societe_id} ) for row in result_ordres: r = dict(row._mapping) ordre_id = r["ordre_id"] 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_broker = ( float(r["frais_broker"]) if r["frais_broker"] 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_broker + frais_etat + frais_autre sens_ordre = str(r["sens"]).strip().lower() type_ordre = str(r["position"]).strip().lower() if r["position"] else "long" # Prix Net unitaire if sens_ordre == "vente": prix_net = prix_brut - (prix_brut * frais_total / 100) else: prix_net = prix_brut + (prix_brut * frais_total / 100) # Engagements ligne 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_broker": frais_broker, "frais_etat": frais_etat, "frais_autre": frais_autre, "prix_net": prix_net, "eng_brut": eng_brut, "eng_net": eng_net, "ordre": r["sens"], "type_ordre": r["position"], "etat": r["etat"], } if ordre_id not in groupes_dict: groupes_dict[ordre_id] = { "id": 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, "plus_value": r["plus_value"], "date_plusvalue": r["date_plus_value"], } groupes_dict[ordre_id]["pieds"].append(pied_data) groupes_dict[ordre_id]["total_prix_brut"] += prix_brut groupes_dict[ordre_id]["total_frais"] += frais_total groupes_dict[ordre_id]["total_prix_net"] += prix_net # Règles de totaux if sens_ordre == "achat": groupes_dict[ordre_id]["total_quantite"] += quantite groupes_dict[ordre_id]["total_eng_brut"] -= eng_brut groupes_dict[ordre_id]["total_eng_net"] -= eng_net elif sens_ordre == "vente": groupes_dict[ordre_id]["total_quantite"] -= quantite groupes_dict[ordre_id]["total_eng_brut"] += eng_brut groupes_dict[ordre_id]["total_eng_net"] += eng_net # --- MISE À JOUR AUTO DE LA PLUS-VALUE (ordres soldés) --- try: with db.engine.begin() as conn: for ordre_id, groupe in groupes_dict.items(): if abs(groupe["total_quantite"]) < 1e-9: total_eng_net = groupe["total_eng_net"] max_date = max( (p["date_op"] for p in groupe["pieds"] if p["date_op"] is not None), default=None, ) conn.execute( text( "UPDATE ordre_pied SET etat = :nouvel_etat " "WHERE id_entete = :ordre_id" ), {"nouvel_etat": "soldé", "ordre_id": ordre_id}, ) conn.execute( text( "UPDATE ordre SET plus_value = :plus_value, " "date_plus_value = :date_pv WHERE id = :ordre_id" ), { "plus_value": total_eng_net, "date_pv": max_date, "ordre_id": ordre_id, }, ) groupe["plus_value"] = total_eng_net groupe["date_plusvalue"] = max_date for pied in groupe["pieds"]: pied["etat"] = "soldé" except Exception as e: current_app.logger.error("Erreur MAJ plus-value : %s", e, exc_info=True) groupes_ordres = list(groupes_dict.values()) annees_combo = ["Tout"] + [str(an) for an in range(2020, 2041)] return render_template( "gestion_ordres.html", societes=societes, selected_societe_id=selected_societe_id, selected_societe=selected_societe, groupes_ordres=groupes_ordres, total_plus_value_globale=total_plus_value_globale, annees_combo=annees_combo, selected_annee=selected_annee, ) # --------------------------------------------------------------------------- # Création d'ordre (la saisie se fait via la modale de gestion_ordres ; # cette route est conservée pour compatibilité et redirige vers la liste) # --------------------------------------------------------------------------- @ordres_bp.route("/creer-ordre", methods=["GET", "POST"]) @login_required def creer_ordre(): return redirect(url_for("ordres.gestion_ordres")) # --------------------------------------------------------------------------- # API : vérifier l'existence d'un ISIN (JSON, exemptée de CSRF) # --------------------------------------------------------------------------- @ordres_bp.route("/api/verifier_isin", methods=["POST"]) @csrf.exempt def verifier_isin(): data = request.get_json(silent=True) or {} isin = (data.get("isin") or "").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, } ) return jsonify({"exists": False}) # --------------------------------------------------------------------------- # Traitement de la création d'un ordre (appelé par la modale) # --------------------------------------------------------------------------- @ordres_bp.route("/creer-ordre-traitement", methods=["POST"]) @login_required def creer_ordre_traitement(): isin = request.form.get("isin") date_op = request.form.get("date_op") selected_annee = request.form.get("annee", "tout") try: quantite = float(request.form.get("quantite") or 0) prix_brut = float(request.form.get("prix_brut") or 0) frais_broker = float(request.form.get("frais_broker") or 0) frais_etat = float(request.form.get("frais_etat") or 0) frais_autre = float(request.form.get("frais_autre") or 0) except ValueError: flash( "Veuillez saisir des valeurs numériques valides pour les quantités, " "prix et frais.", "danger", ) return redirect(url_for("ordres.gestion_ordres", annee=selected_annee)) sens = request.form.get("ordre") position = request.form.get("type_ordre") etat = request.form.get("etat") 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("ordres.gestion_ordres", annee=selected_annee)) try: query_ordre = text( "INSERT INTO ordre (id_societe, isin) VALUES (:id_societe, :isin)" ) db.session.execute(query_ordre, {"id_societe": id_societe, "isin": isin}) 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}, ).scalar() query_pied = text( "INSERT INTO ordre_pied (id_entete, date_op, quantite, prix_brut, " "frais_broker, frais_etat, frais_autre, sens, position, etat) " "VALUES (:id_entete, :date_op, :quantite, :prix_brut, :frais_broker, " ":frais_etat, :frais_autre, :sens, :position, :etat)" ) db.session.execute( query_pied, { "id_entete": result_id, "date_op": date_op, "quantite": quantite, "prix_brut": prix_brut, "frais_broker": frais_broker, "frais_etat": frais_etat, "frais_autre": frais_autre, "sens": sens, "position": position, "etat": etat, }, ) db.session.commit() flash("Ordre créé avec succès !", "success") except Exception as e: db.session.rollback() current_app.logger.error("Erreur création ordre : %s", e, exc_info=True) flash("Erreur lors de la création de l'ordre.", "danger") return redirect(url_for("ordres.gestion_ordres", annee=selected_annee)) # --------------------------------------------------------------------------- # Ajout d'une ligne à un ordre existant (POST depuis la modale) # --------------------------------------------------------------------------- @ordres_bp.route("/ordre//ajouter-ligne", methods=["GET", "POST"]) @login_required def ajouter_ligne_ordre(ordre_id): selected_annee = request.args.get("annee", "tout") if request.method == "POST": selected_annee = request.form.get("annee", "tout") try: quantite = float(request.form.get("quantite") or 0) prix_brut = float(request.form.get("prix_brut") or 0) frais_broker = float(request.form.get("frais_broker") or 0) frais_etat = float(request.form.get("frais_etat") or 0) frais_autre = float(request.form.get("frais_autre") or 0) except ValueError: flash("Veuillez saisir des valeurs numériques valides.", "danger") return redirect( url_for( "ordres.ajouter_ligne_ordre", ordre_id=ordre_id, annee=selected_annee, ) ) try: query_pied = text( "INSERT INTO ordre_pied (id_entete, date_op, quantite, prix_brut, " "frais_broker, frais_etat, frais_autre, sens, position, etat) " "VALUES (:id_entete, :date_op, :quantite, :prix_brut, :frais_broker, " ":frais_etat, :frais_autre, :sens, :position, :etat)" ) db.session.execute( query_pied, { "id_entete": ordre_id, "date_op": request.form.get("date_op"), "quantite": quantite, "prix_brut": prix_brut, "frais_broker": frais_broker, "frais_etat": frais_etat, "frais_autre": frais_autre, "sens": request.form.get("ordre"), "position": 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("ordres.gestion_ordres", annee=selected_annee)) except Exception as e: db.session.rollback() current_app.logger.error("Erreur ajout ligne : %s", e, exc_info=True) flash("Erreur lors de l'ajout de la ligne.", "danger") return redirect(url_for("ordres.gestion_ordres", annee=selected_annee)) # --------------------------------------------------------------------------- # Modification d'une ligne d'ordre (POST depuis la modale) # --------------------------------------------------------------------------- @ordres_bp.route("/modifier-ligne-ordre/", methods=["GET", "POST"]) @login_required def modifier_ligne_ordre(pied_id): selected_annee = request.args.get("annee", "tout") if request.method == "POST": selected_annee = request.form.get("annee", "tout") try: quantite = float(request.form.get("quantite") or 0) prix_brut = float(request.form.get("prix_brut") or 0) frais_broker = float(request.form.get("frais_broker") or 0) frais_etat = float(request.form.get("frais_etat") or 0) frais_autre = float(request.form.get("frais_autre") or 0) except ValueError: flash("Veuillez saisir des valeurs numériques valides.", "danger") return redirect( url_for( "ordres.modifier_ligne_ordre", pied_id=pied_id, annee=selected_annee, ) ) try: query_update = text( "UPDATE ordre_pied SET date_op = :date_op, quantite = :quantite, " "prix_brut = :prix_brut, frais_broker = :frais_broker, " "frais_etat = :frais_etat, frais_autre = :frais_autre, " "sens = :sens, position = :position, etat = :etat " "WHERE id = :pied_id" ) db.session.execute( query_update, { "date_op": request.form.get("date_op"), "quantite": quantite, "prix_brut": prix_brut, "frais_broker": frais_broker, "frais_etat": frais_etat, "frais_autre": frais_autre, "sens": request.form.get("ordre"), "position": 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("ordres.gestion_ordres", annee=selected_annee)) except Exception as e: db.session.rollback() current_app.logger.error("Erreur modification ligne : %s", e, exc_info=True) flash("Erreur lors de la modification de la ligne.", "danger") return redirect(url_for("ordres.gestion_ordres", annee=selected_annee))