Merge remote-tracking branch 'origin/main'
This commit is contained in:
+2
-2
@@ -23,6 +23,6 @@ ENV/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
Dockerfile
|
#Dockerfile
|
||||||
docker-compose.yml
|
#docker-compose.yml
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
|||||||
+22
-3
@@ -54,9 +54,28 @@ def create_app():
|
|||||||
else:
|
else:
|
||||||
raise Exception("Impossible d'initialiser la base de données après plusieurs tentatives.")
|
raise Exception("Impossible d'initialiser la base de données après plusieurs tentatives.")
|
||||||
|
|
||||||
# Enregistrement des routes (Blueprint)
|
# Enregistrement des routes (Blueprints)
|
||||||
from app.routes import main
|
from app.routes import (
|
||||||
app.register_blueprint(main)
|
actions_bp,
|
||||||
|
analyse_bp,
|
||||||
|
auth_bp,
|
||||||
|
comptes_bp,
|
||||||
|
historique_bp,
|
||||||
|
menu_bp,
|
||||||
|
ordres_bp,
|
||||||
|
societes_bp,
|
||||||
|
utilisateurs_bp,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.register_blueprint(auth_bp)
|
||||||
|
app.register_blueprint(menu_bp)
|
||||||
|
app.register_blueprint(utilisateurs_bp)
|
||||||
|
app.register_blueprint(actions_bp)
|
||||||
|
app.register_blueprint(societes_bp)
|
||||||
|
app.register_blueprint(historique_bp)
|
||||||
|
app.register_blueprint(analyse_bp)
|
||||||
|
app.register_blueprint(ordres_bp)
|
||||||
|
app.register_blueprint(comptes_bp)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import redirect, session, url_for
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
if "user_id" not in session:
|
||||||
|
return redirect(url_for("auth.index"))
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated
|
||||||
-1651
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
from .actions import actions_bp
|
||||||
|
from .analyse import analyse_bp
|
||||||
|
from .auth import auth_bp
|
||||||
|
from .comptes import comptes_bp
|
||||||
|
from .historique import historique_bp
|
||||||
|
from .menu import menu_bp
|
||||||
|
from .ordres import ordres_bp
|
||||||
|
from .societes import societes_bp
|
||||||
|
from .utilisateurs import utilisateurs_bp
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"auth_bp",
|
||||||
|
"menu_bp",
|
||||||
|
"utilisateurs_bp",
|
||||||
|
"actions_bp",
|
||||||
|
"societes_bp",
|
||||||
|
"historique_bp",
|
||||||
|
"analyse_bp",
|
||||||
|
"ordres_bp",
|
||||||
|
"comptes_bp",
|
||||||
|
]
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.decorators import login_required
|
||||||
|
|
||||||
|
actions_bp = Blueprint("actions", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@actions_bp.route("/gestion-actions", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def gestion_actions():
|
||||||
|
if request.method == "POST":
|
||||||
|
action_type = request.form.get("action")
|
||||||
|
action_id = request.form.get("id")
|
||||||
|
isin = request.form.get("isin") or None
|
||||||
|
ticker = request.form.get("ticker")
|
||||||
|
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":
|
||||||
|
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:
|
||||||
|
db.session.rollback()
|
||||||
|
flash("Erreur lors de la création de l'action.", "danger")
|
||||||
|
|
||||||
|
elif action_type == "modifier" and action_id:
|
||||||
|
try:
|
||||||
|
query = text(
|
||||||
|
"UPDATE actions 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,
|
||||||
|
"company_name": company_name,
|
||||||
|
"exchange": exchange,
|
||||||
|
"pays": pays,
|
||||||
|
"currency": currency,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Modification effectuée", "success")
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
flash("Erreur lors de la modification.", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for("actions.gestion_actions"))
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
{
|
||||||
|
"id": row.id,
|
||||||
|
"isin": row.isin if row.isin else "",
|
||||||
|
"ticker": row.ticker if row.ticker 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 "",
|
||||||
|
}
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
return render_template("gestion_actions.html", actions=actions_list)
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
from flask import Blueprint, jsonify, render_template, request
|
||||||
|
|
||||||
|
from app.decorators import login_required
|
||||||
|
|
||||||
|
analyse_bp = Blueprint("analyse", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@analyse_bp.route("/analyse", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def analyse():
|
||||||
|
"""Page d'analyse technique. Si un ISIN est passé en paramètre (?isin=...),
|
||||||
|
on l'analyse et on affiche la fiche complète ; sinon on affiche la page
|
||||||
|
de recherche."""
|
||||||
|
isin = request.args.get("isin", "").strip().upper()
|
||||||
|
|
||||||
|
if not isin:
|
||||||
|
return render_template("analyse.html", resultat=None)
|
||||||
|
|
||||||
|
from app.services.analysis import analyse_action
|
||||||
|
|
||||||
|
print(f"[analyse] ISIN demandé : {isin}", flush=True)
|
||||||
|
resultat = analyse_action(isin)
|
||||||
|
|
||||||
|
if resultat is None:
|
||||||
|
# ISIN absent du référentiel actions
|
||||||
|
return render_template(
|
||||||
|
"analyse.html",
|
||||||
|
resultat={
|
||||||
|
"isin": isin,
|
||||||
|
"action": None,
|
||||||
|
"erreur": f"Aucune donnée trouvée pour l'ISIN {isin}. "
|
||||||
|
f"Vérifiez le code ou importez d'abord l'historique.",
|
||||||
|
},
|
||||||
|
isin_recherche=isin,
|
||||||
|
)
|
||||||
|
|
||||||
|
if resultat.get("erreur"):
|
||||||
|
# ISIN présent mais historique insuffisant
|
||||||
|
print(f"[analyse] {isin} : {resultat['erreur']}", flush=True)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"[analyse] {isin} : cours={resultat.get('cours')} "
|
||||||
|
f"score={resultat.get('score')} tendance={resultat.get('tendance')}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template("analyse.html", resultat=resultat, isin_recherche=isin)
|
||||||
|
|
||||||
|
|
||||||
|
@analyse_bp.route("/analyse/<isin>", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def analyse_isin(isin):
|
||||||
|
"""Page d'analyse technique complète pour un ISIN donné."""
|
||||||
|
from app.services.analysis import analyse_action
|
||||||
|
|
||||||
|
isin = isin.strip().upper()
|
||||||
|
resultat = analyse_action(isin)
|
||||||
|
return render_template("analyse.html", resultat=resultat, isin_recherche=isin)
|
||||||
|
|
||||||
|
|
||||||
|
@analyse_bp.route("/api/analyse/<isin>", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def api_analyse(isin):
|
||||||
|
"""API JSON retournant l'analyse technique complète d'un ISIN."""
|
||||||
|
from app.services.analysis import analyse_action
|
||||||
|
|
||||||
|
isin = isin.strip().upper()
|
||||||
|
resultat = analyse_action(isin)
|
||||||
|
if resultat is None:
|
||||||
|
return jsonify({"erreur": f"ISIN {isin} introuvable dans le référentiel actions."}), 404
|
||||||
|
if resultat.get("erreur"):
|
||||||
|
return jsonify(resultat), 200
|
||||||
|
return jsonify(resultat)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
from flask import Blueprint, flash, redirect, render_template, request, session, url_for
|
||||||
|
from sqlalchemy import text
|
||||||
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
|
||||||
|
auth_bp = Blueprint("auth", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route("/", methods=["GET", "POST"])
|
||||||
|
def index():
|
||||||
|
if "user_id" in session:
|
||||||
|
return redirect(url_for("menu.menu"))
|
||||||
|
|
||||||
|
if "login_attempts" not in session:
|
||||||
|
session["login_attempts"] = 0
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
user_login = request.form.get("login")
|
||||||
|
user_pass = request.form.get("pass")
|
||||||
|
|
||||||
|
query = text(
|
||||||
|
"SELECT id, pass AS user_pass, nom, prenom FROM login "
|
||||||
|
"WHERE login = :login AND actif = 1"
|
||||||
|
)
|
||||||
|
result = db.session.execute(query, {"login": user_login}).fetchone()
|
||||||
|
|
||||||
|
if result and check_password_hash(result.user_pass, user_pass):
|
||||||
|
# Régénération de session : protection contre la fixation de session
|
||||||
|
session.clear()
|
||||||
|
session["user_id"] = result.id
|
||||||
|
session["user_name"] = f"{result.prenom} {result.nom}"
|
||||||
|
session["login_attempts"] = 0
|
||||||
|
return redirect(url_for("menu.menu"))
|
||||||
|
else:
|
||||||
|
session["login_attempts"] += 1
|
||||||
|
essais = session["login_attempts"]
|
||||||
|
if essais >= 3:
|
||||||
|
session["login_attempts"] = 0
|
||||||
|
flash(
|
||||||
|
"Trop de tentatives. Réessayez dans quelques instants.",
|
||||||
|
"danger",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
flash(
|
||||||
|
f"Identifiant ou mot de passe incorrect. "
|
||||||
|
f"Il vous reste {3 - essais} essai(s).",
|
||||||
|
"danger",
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route("/logout")
|
||||||
|
def logout():
|
||||||
|
session.clear()
|
||||||
|
return redirect(url_for("auth.index"))
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
current_app,
|
||||||
|
flash,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
session,
|
||||||
|
url_for,
|
||||||
|
)
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.decorators import login_required
|
||||||
|
|
||||||
|
comptes_bp = Blueprint("comptes", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Gestion des comptes
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@comptes_bp.route("/gestion_comptes", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def gestion_comptes():
|
||||||
|
from app.models import Societe, Compte
|
||||||
|
|
||||||
|
societes = Societe.query.order_by(Societe.nom.asc()).all()
|
||||||
|
|
||||||
|
# La société sélectionnée vient du formulaire (changement manuel),
|
||||||
|
# ou d'un paramètre d'URL (redirection après création/modification),
|
||||||
|
# ou de la session (mémorisation entre navigations).
|
||||||
|
selected_societe_id = (
|
||||||
|
request.form.get("societe_id")
|
||||||
|
or request.args.get("societe_id")
|
||||||
|
or session.get("selected_compte_societe_id")
|
||||||
|
)
|
||||||
|
if not selected_societe_id and societes:
|
||||||
|
selected_societe_id = societes[0].id
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
selected_societe_id = int(selected_societe_id) if selected_societe_id else None
|
||||||
|
except ValueError:
|
||||||
|
selected_societe_id = societes[0].id if societes else None
|
||||||
|
|
||||||
|
# Mémorisation en session : la société active ne change que manuellement
|
||||||
|
if selected_societe_id is not None:
|
||||||
|
session["selected_compte_societe_id"] = selected_societe_id
|
||||||
|
|
||||||
|
selected_societe_nom = ""
|
||||||
|
ecritures = []
|
||||||
|
total_credit = 0.0
|
||||||
|
total_debit = 0.0
|
||||||
|
# Agrégats financiers (voir règles de calcul dans la documentation)
|
||||||
|
tcredit = 0.0 # Depot : somme des crédits des écritures manuelles
|
||||||
|
tdebit = 0.0 # Retrait : somme des débits des écritures manuelles
|
||||||
|
engagement = 0.0 # ENGA. : positions actives (achat +, vente -)
|
||||||
|
pv = 0.0 # PV : positions soldées (achat -, vente +)
|
||||||
|
dispo = 0.0 # DISPO = TOTAL CREDIT - TOTAL DEBIT (toutes écritures)
|
||||||
|
solde = 0.0 # SOLDE = DISPO + ENGA.
|
||||||
|
|
||||||
|
if selected_societe_id:
|
||||||
|
societe_obj = Societe.query.get(selected_societe_id)
|
||||||
|
if societe_obj:
|
||||||
|
selected_societe_nom = societe_obj.nom
|
||||||
|
|
||||||
|
# Tri par date décroissante (du plus récent au plus ancien)
|
||||||
|
ecritures = (
|
||||||
|
Compte.query.filter_by(id_societe=selected_societe_id)
|
||||||
|
.order_by(Compte.date_operation.desc(), Compte.id.desc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
# Totaux bruts du tableau (toutes écritures confondues)
|
||||||
|
total_credit = sum(float(e.credit or 0) for e in ecritures)
|
||||||
|
total_debit = sum(float(e.debit or 0) for e in ecritures)
|
||||||
|
|
||||||
|
# --- Dépôts / Retraits : uniquement les écritures manuelles ---
|
||||||
|
for e in ecritures:
|
||||||
|
if e.source == "manuel":
|
||||||
|
tcredit += float(e.credit or 0)
|
||||||
|
tdebit += float(e.debit or 0)
|
||||||
|
|
||||||
|
# --- DISPO = TOTAL CREDIT - TOTAL DEBIT (toutes écritures) ---
|
||||||
|
dispo = total_credit - total_debit
|
||||||
|
|
||||||
|
# --- Engagement & PV : depuis les lignes d'ordres (ordre_pied) ---
|
||||||
|
# Règles :
|
||||||
|
# etat='actif' : achat -> engagement += qté*(prix_brut+frais)
|
||||||
|
# vente -> engagement -= qté*(prix_brut-frais)
|
||||||
|
# etat='soldé' : achat -> PV -= qté*(prix_brut+frais)
|
||||||
|
# vente -> PV += qté*(prix_brut-frais)
|
||||||
|
# etat='annulé' : ignoré (filtré en SQL)
|
||||||
|
# Filtres : société en cours + lignes non annulées
|
||||||
|
query_ordres = text(
|
||||||
|
"SELECT op.sens, op.etat, op.quantite, op.prix_brut, "
|
||||||
|
"op.frais_broker, op.frais_etat, op.frais_autre "
|
||||||
|
"FROM ordre o "
|
||||||
|
"JOIN ordre_pied op ON op.id_entete = o.id "
|
||||||
|
"WHERE o.id_societe = :soc_id "
|
||||||
|
"AND op.etat <> 'annulé'"
|
||||||
|
)
|
||||||
|
for row in db.session.execute(query_ordres, {"soc_id": selected_societe_id}):
|
||||||
|
quantite = float(row.quantite or 0)
|
||||||
|
prix_brut = float(row.prix_brut or 0)
|
||||||
|
frais_broker = float(row.frais_broker or 0)
|
||||||
|
frais_etat = float(row.frais_etat or 0)
|
||||||
|
frais_autre = float(row.frais_autre or 0)
|
||||||
|
|
||||||
|
# Achat : on paie le prix brut + les frais
|
||||||
|
montant_achat = quantite * (prix_brut + frais_broker + frais_etat + frais_autre)
|
||||||
|
# Vente : on encaisse le prix brut - les frais
|
||||||
|
montant_vente = quantite * (prix_brut - frais_broker - frais_etat - frais_autre)
|
||||||
|
|
||||||
|
sens = (row.sens or "").strip().lower()
|
||||||
|
etat = (row.etat or "").strip().lower()
|
||||||
|
|
||||||
|
if etat == "actif":
|
||||||
|
if sens == "achat":
|
||||||
|
engagement += montant_achat
|
||||||
|
elif sens == "vente":
|
||||||
|
engagement -= montant_vente
|
||||||
|
elif etat == "soldé" or etat == "solde":
|
||||||
|
if sens == "achat":
|
||||||
|
pv -= montant_achat
|
||||||
|
elif sens == "vente":
|
||||||
|
pv += montant_vente
|
||||||
|
# etat == 'annulé' : ne rien faire (déjà filtré en SQL)
|
||||||
|
|
||||||
|
solde = dispo + engagement
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"gestion_comptes.html",
|
||||||
|
societes=societes,
|
||||||
|
selected_societe_id=selected_societe_id,
|
||||||
|
selected_societe=selected_societe_nom,
|
||||||
|
ecritures=ecritures,
|
||||||
|
total_credit=total_credit,
|
||||||
|
total_debit=total_debit,
|
||||||
|
tcredit=tcredit,
|
||||||
|
tdebit=tdebit,
|
||||||
|
engagement=engagement,
|
||||||
|
pv=pv,
|
||||||
|
dispo=dispo,
|
||||||
|
solde=solde,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Génération automatique des écritures de compte depuis les ordres
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@comptes_bp.route("/generer_ecritures_ordres", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def generer_ecritures_ordres():
|
||||||
|
"""Génère les écritures comptables (table `compte`) à partir des ordres
|
||||||
|
et de leurs lignes (`ordre_pied`) pour la société actuellement sélectionnée.
|
||||||
|
|
||||||
|
Règles :
|
||||||
|
- achat : debit = quantite * (prix_brut + frais_broker + frais_etat + frais_autre), credit = 0
|
||||||
|
- vente : credit = quantite * (prix_brut + frais_broker + frais_etat + frais_autre), debit = 0
|
||||||
|
- libelle= "Achat/Vente actions {isin} {company_name}"
|
||||||
|
- source = 'ordre' (non modifiable depuis l'IHM)
|
||||||
|
Idempotent : supprime d'abord les écritures source='ordre' de la société.
|
||||||
|
"""
|
||||||
|
id_societe = request.form.get("id_societe") or session.get(
|
||||||
|
"selected_compte_societe_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not id_societe:
|
||||||
|
flash("Aucune société sélectionnée pour la génération.", "danger")
|
||||||
|
return redirect(url_for("comptes.gestion_comptes"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
id_societe_int = int(id_societe)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
flash("Identifiant de société invalide.", "danger")
|
||||||
|
return redirect(url_for("comptes.gestion_comptes"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Suppression des anciennes écritures générées pour cette société
|
||||||
|
db.session.execute(
|
||||||
|
text(
|
||||||
|
"DELETE FROM compte WHERE id_societe = :soc_id AND source = 'ordre'"
|
||||||
|
),
|
||||||
|
{"soc_id": id_societe_int},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Parcours des ordres de la société, puis de leurs lignes
|
||||||
|
# Filtres : société en cours + lignes non annulées
|
||||||
|
query = text(
|
||||||
|
"SELECT o.id, o.isin, a.company_name, "
|
||||||
|
"op.date_op, op.quantite, op.prix_brut, "
|
||||||
|
"op.frais_broker, op.frais_etat, op.frais_autre, op.sens "
|
||||||
|
"FROM ordre o "
|
||||||
|
"JOIN ordre_pied op ON op.id_entete = o.id "
|
||||||
|
"JOIN actions a ON o.isin = a.isin "
|
||||||
|
"WHERE o.id_societe = :soc_id "
|
||||||
|
"AND op.etat <> 'annulé' "
|
||||||
|
"ORDER BY o.id, op.date_op"
|
||||||
|
)
|
||||||
|
lignes = db.session.execute(query, {"soc_id": id_societe_int}).fetchall()
|
||||||
|
|
||||||
|
insert = text(
|
||||||
|
"INSERT INTO compte (id_societe, date_operation, debit, credit, "
|
||||||
|
"libelle, source) "
|
||||||
|
"VALUES (:id_societe, :date_op, :debit, :credit, :libelle, 'ordre')"
|
||||||
|
)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
from markupsafe import escape
|
||||||
|
|
||||||
|
for row in lignes:
|
||||||
|
quantite = float(row.quantite or 0)
|
||||||
|
prix_brut = float(row.prix_brut or 0)
|
||||||
|
frais_broker = float(row.frais_broker or 0)
|
||||||
|
frais_etat = float(row.frais_etat or 0)
|
||||||
|
frais_autre = float(row.frais_autre or 0)
|
||||||
|
|
||||||
|
sens = (row.sens or "").strip().lower()
|
||||||
|
isin = row.isin or ""
|
||||||
|
# Nom de l'action échappé puis mis en gras (affichage |safe côté template)
|
||||||
|
company = f"<strong>{escape(row.company_name or '')}</strong>"
|
||||||
|
|
||||||
|
if sens == "achat":
|
||||||
|
# Achat : on paie le prix brut + les frais
|
||||||
|
montant = quantite * (prix_brut + frais_broker + frais_etat + frais_autre)
|
||||||
|
debit = montant
|
||||||
|
credit = 0.0
|
||||||
|
libelle = f"Achat actions {isin} {company}".strip()
|
||||||
|
elif sens == "vente":
|
||||||
|
# Vente : on encaisse le prix brut - les frais
|
||||||
|
montant = quantite * (prix_brut - frais_broker - frais_etat - frais_autre)
|
||||||
|
debit = 0.0
|
||||||
|
credit = montant
|
||||||
|
libelle = f"Vente actions {isin} {company}".strip()
|
||||||
|
else:
|
||||||
|
# Sens inattendu : on ignore la ligne
|
||||||
|
continue
|
||||||
|
|
||||||
|
db.session.execute(
|
||||||
|
insert,
|
||||||
|
{
|
||||||
|
"id_societe": id_societe_int,
|
||||||
|
"date_op": row.date_op,
|
||||||
|
"debit": debit,
|
||||||
|
"credit": credit,
|
||||||
|
"libelle": libelle,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash(
|
||||||
|
f"Génération terminée : {count} écriture(s) créée(s) pour "
|
||||||
|
f"la société.",
|
||||||
|
"success",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error("Erreur génération écritures : %s", e, exc_info=True)
|
||||||
|
flash("Erreur lors de la génération des écritures.", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for("comptes.gestion_comptes", societe_id=id_societe_int))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Création d'une écriture comptable
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@comptes_bp.route("/creer_compte_traitement", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def creer_compte_traitement():
|
||||||
|
id_societe = request.form.get("id_societe")
|
||||||
|
date_operation = request.form.get("date_operation")
|
||||||
|
libelle = request.form.get("libelle", "").strip()
|
||||||
|
debit = request.form.get("debit", "0")
|
||||||
|
credit = request.form.get("credit", "0")
|
||||||
|
|
||||||
|
if not id_societe or not date_operation:
|
||||||
|
flash("Veuillez remplir tous les champs obligatoires.", "danger")
|
||||||
|
return redirect(url_for("comptes.gestion_comptes", societe_id=id_societe or ""))
|
||||||
|
|
||||||
|
try:
|
||||||
|
debit_val = float(debit) if debit else 0.0
|
||||||
|
credit_val = float(credit) if credit else 0.0
|
||||||
|
id_societe_int = int(id_societe)
|
||||||
|
except ValueError:
|
||||||
|
flash("Veuillez saisir des valeurs numériques valides.", "danger")
|
||||||
|
return redirect(url_for("comptes.gestion_comptes", societe_id=id_societe or ""))
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.execute(
|
||||||
|
text(
|
||||||
|
"INSERT INTO compte (id_societe, date_operation, libelle, "
|
||||||
|
"debit, credit) VALUES (:id_societe, :date_op, :libelle, :debit, :credit)"
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"id_societe": id_societe_int,
|
||||||
|
"date_op": date_operation,
|
||||||
|
"libelle": libelle or None,
|
||||||
|
"debit": debit_val,
|
||||||
|
"credit": credit_val,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Écriture créée avec succès !", "success")
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error("Erreur création écriture : %s", e, exc_info=True)
|
||||||
|
flash("Erreur lors de la création de l'écriture.", "danger")
|
||||||
|
|
||||||
|
# On reste sur la même société après validation
|
||||||
|
return redirect(url_for("comptes.gestion_comptes", societe_id=id_societe_int))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Modification d'une écriture comptable
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@comptes_bp.route("/modifier_compte/<int:id>", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def modifier_compte(id):
|
||||||
|
date_operation = request.form.get("date_operation")
|
||||||
|
libelle = request.form.get("libelle", "").strip()
|
||||||
|
debit = request.form.get("debit", "0")
|
||||||
|
credit = request.form.get("credit", "0")
|
||||||
|
|
||||||
|
# Récupération de la société de l'écriture avant modification (pour la redirection)
|
||||||
|
id_societe = request.form.get("id_societe")
|
||||||
|
if not id_societe:
|
||||||
|
row = db.session.execute(
|
||||||
|
text("SELECT id_societe FROM compte WHERE id = :id"), {"id": id}
|
||||||
|
).fetchone()
|
||||||
|
id_societe = str(row.id_societe) if row else ""
|
||||||
|
|
||||||
|
if not date_operation:
|
||||||
|
flash("Veuillez remplir tous les champs obligatoires.", "danger")
|
||||||
|
return redirect(url_for("comptes.gestion_comptes", societe_id=id_societe))
|
||||||
|
|
||||||
|
try:
|
||||||
|
debit_val = float(debit) if debit else 0.0
|
||||||
|
credit_val = float(credit) if credit else 0.0
|
||||||
|
except ValueError:
|
||||||
|
flash("Veuillez saisir des valeurs numériques valides.", "danger")
|
||||||
|
return redirect(url_for("comptes.gestion_comptes", societe_id=id_societe))
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.execute(
|
||||||
|
text(
|
||||||
|
"UPDATE compte SET date_operation = :date_op, "
|
||||||
|
"libelle = :libelle, debit = :debit, credit = :credit WHERE id = :id"
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"date_op": date_operation,
|
||||||
|
"libelle": libelle or None,
|
||||||
|
"debit": debit_val,
|
||||||
|
"credit": credit_val,
|
||||||
|
"id": id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Écriture modifiée avec succès !", "success")
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error("Erreur modification écriture : %s", e, exc_info=True)
|
||||||
|
flash("Erreur lors de la modification de l'écriture.", "danger")
|
||||||
|
|
||||||
|
# On reste sur la même société après validation
|
||||||
|
return redirect(url_for("comptes.gestion_comptes", societe_id=id_societe))
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
import csv
|
||||||
|
import io
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
Response,
|
||||||
|
current_app,
|
||||||
|
flash,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
url_for,
|
||||||
|
)
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.decorators import login_required
|
||||||
|
|
||||||
|
historique_bp = Blueprint("historique", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Import / Export des actions CSV
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@historique_bp.route("/gestion-import-export-actions-csv", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def gestion_import_export_actions_csv():
|
||||||
|
return render_template("gestion_import_export_actions_csv.html")
|
||||||
|
|
||||||
|
|
||||||
|
@historique_bp.route("/export-actions-csv", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def export_actions_csv():
|
||||||
|
try:
|
||||||
|
query = text(
|
||||||
|
"SELECT id, isin, ticker, company_name, exchange, pays, currency, "
|
||||||
|
"updated_at FROM actions"
|
||||||
|
)
|
||||||
|
result = db.session.execute(query)
|
||||||
|
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output, delimiter=";", quoting=csv.QUOTE_MINIMAL)
|
||||||
|
writer.writerow(
|
||||||
|
["id", "isin", "ticker", "company_name", "exchange", "pays", "currency", "updated_at"]
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in result:
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
row.id,
|
||||||
|
row.isin or "",
|
||||||
|
row.ticker or "",
|
||||||
|
row.company_name or "",
|
||||||
|
row.exchange or "",
|
||||||
|
row.pays or "",
|
||||||
|
row.currency or "",
|
||||||
|
row.updated_at.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
if row.updated_at
|
||||||
|
else "",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
return Response(
|
||||||
|
output.getvalue(),
|
||||||
|
mimetype="text/csv",
|
||||||
|
headers={"Content-Disposition": "attachment;filename=actions_export.csv"},
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error("Erreur export CSV : %s", e, exc_info=True)
|
||||||
|
flash("Erreur lors de l'exportation du fichier CSV.", "danger")
|
||||||
|
return redirect(url_for("historique.gestion_import_export_actions_csv"))
|
||||||
|
|
||||||
|
|
||||||
|
@historique_bp.route("/import-actions-csv", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def import_actions_csv():
|
||||||
|
if "file" not in request.files:
|
||||||
|
flash("Aucun fichier sélectionné.", "danger")
|
||||||
|
return redirect(url_for("historique.gestion_import_export_actions_csv"))
|
||||||
|
|
||||||
|
file = request.files["file"]
|
||||||
|
|
||||||
|
if file.filename == "":
|
||||||
|
flash("Aucun fichier sélectionné.", "danger")
|
||||||
|
return redirect(url_for("historique.gestion_import_export_actions_csv"))
|
||||||
|
|
||||||
|
if not file.filename.endswith(".csv"):
|
||||||
|
flash("Veuillez fournir un fichier au format .csv valide.", "warning")
|
||||||
|
return redirect(url_for("historique.gestion_import_export_actions_csv"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
stream = io.TextIOWrapper(file.stream, encoding="utf-8")
|
||||||
|
csv_reader = csv.reader(stream, delimiter=";")
|
||||||
|
|
||||||
|
header = next(csv_reader, None)
|
||||||
|
if not header:
|
||||||
|
flash("Le fichier CSV est vide.", "danger")
|
||||||
|
return redirect(url_for("historique.gestion_import_export_actions_csv"))
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for row in csv_reader:
|
||||||
|
if len(row) >= 6:
|
||||||
|
isin = row[0].strip() if row[0] != "" else None
|
||||||
|
ticker = row[1].strip() if row[1] != "" else None
|
||||||
|
company_name = row[2].strip() if row[2] != "" else None
|
||||||
|
exchange = row[3].strip() if row[3] != "" else ""
|
||||||
|
pays = row[4].strip() if row[4] != "" else ""
|
||||||
|
currency = row[5].strip() if row[5] != "" else ""
|
||||||
|
|
||||||
|
if not isin or not ticker:
|
||||||
|
continue
|
||||||
|
|
||||||
|
query = text(
|
||||||
|
"INSERT INTO actions (isin, ticker, company_name, exchange, "
|
||||||
|
"pays, currency, updated_at) "
|
||||||
|
"VALUES (:isin, :ticker, :company_name, :exchange, :pays, "
|
||||||
|
":currency, NOW()) "
|
||||||
|
"ON DUPLICATE KEY UPDATE ticker = :ticker, "
|
||||||
|
"company_name = :company_name, exchange = :exchange, "
|
||||||
|
"pays = :pays, currency = :currency, updated_at = NOW()"
|
||||||
|
)
|
||||||
|
db.session.execute(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"isin": isin,
|
||||||
|
"ticker": ticker,
|
||||||
|
"company_name": company_name,
|
||||||
|
"exchange": exchange,
|
||||||
|
"pays": pays,
|
||||||
|
"currency": currency,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Importation réussie : {count} actions traitées.", "success")
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error("Erreur import CSV : %s", e, exc_info=True)
|
||||||
|
flash(f"Erreur lors de l'importation du fichier : {str(e)}", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for("historique.gestion_import_export_actions_csv"))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Import historique CSV (cotation d'une action)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
@historique_bp.route("/gestion-import-historique-csv", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def gestion_import_historique_csv():
|
||||||
|
return render_template("gestion_import_historique_csv.html")
|
||||||
|
|
||||||
|
|
||||||
|
def _convertir_vol(valeur):
|
||||||
|
"""Convertit une valeur de volume du CSV en entier.
|
||||||
|
Suffixes gérés : 'K' (x1000), 'M' (x1 000 000). Sinon entier brut.
|
||||||
|
"""
|
||||||
|
if valeur is None:
|
||||||
|
return None
|
||||||
|
s = str(valeur).strip()
|
||||||
|
if s == "" or s.upper() == "N/A" or s.upper() == "NA":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
if s.upper().endswith("K"):
|
||||||
|
return int(float(s[:-1]) * 1000)
|
||||||
|
if s.upper().endswith("M"):
|
||||||
|
return int(float(s[:-1]) * 1000000)
|
||||||
|
return int(float(s))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _to_decimal(valeur):
|
||||||
|
"""Convertit une valeur CSV en float (Decimal), None si vide/invalide.
|
||||||
|
Retire le suffixe '%' (ex: '0.7%' -> 0.7) et les séparateurs de milliers."""
|
||||||
|
if valeur is None:
|
||||||
|
return None
|
||||||
|
s = str(valeur).strip()
|
||||||
|
if s == "" or s.upper() in ("N/A", "NA"):
|
||||||
|
return None
|
||||||
|
# Suppression d'un éventuel suffixe '%' (et espaces autour)
|
||||||
|
if s.endswith("%"):
|
||||||
|
s = s[:-1].strip()
|
||||||
|
# Suppression d'un éventuel séparateur de milliers (virgule ou espace)
|
||||||
|
s = s.replace(" ", "").replace(",", "")
|
||||||
|
try:
|
||||||
|
return float(s)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@historique_bp.route("/import-historique-csv", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def import_historique_csv():
|
||||||
|
isin = (request.form.get("isin") or "").strip().upper()
|
||||||
|
|
||||||
|
# Vérification que l'ISIN existe bien dans le référentiel actions
|
||||||
|
if not isin:
|
||||||
|
flash("Veuillez saisir un code ISIN.", "danger")
|
||||||
|
return redirect(url_for("historique.gestion_import_historique_csv"))
|
||||||
|
|
||||||
|
action = db.session.execute(
|
||||||
|
text("SELECT company_name FROM actions WHERE isin = :isin"), {"isin": isin}
|
||||||
|
).fetchone()
|
||||||
|
if not action:
|
||||||
|
flash(f"L'ISIN {isin} n'existe pas dans le référentiel actions.", "danger")
|
||||||
|
return redirect(url_for("historique.gestion_import_historique_csv"))
|
||||||
|
|
||||||
|
if "file" not in request.files:
|
||||||
|
flash("Aucun fichier sélectionné.", "danger")
|
||||||
|
return redirect(url_for("historique.gestion_import_historique_csv"))
|
||||||
|
|
||||||
|
file = request.files["file"]
|
||||||
|
if file.filename == "":
|
||||||
|
flash("Aucun fichier sélectionné.", "danger")
|
||||||
|
return redirect(url_for("historique.gestion_import_historique_csv"))
|
||||||
|
|
||||||
|
if not file.filename.lower().endswith(".csv"):
|
||||||
|
flash("Veuillez fournir un fichier au format .csv valide.", "warning")
|
||||||
|
return redirect(url_for("historique.gestion_import_historique_csv"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Lecture du fichier brut avec fallback d'encodage (utf-8 puis latin-1)
|
||||||
|
raw_bytes = file.stream.read()
|
||||||
|
# Suppression d'un éventuel BOM UTF-8 (\xef\xbb\xbf) en début de fichier
|
||||||
|
if raw_bytes.startswith(b"\xef\xbb\xbf"):
|
||||||
|
raw_bytes = raw_bytes[3:]
|
||||||
|
try:
|
||||||
|
content = raw_bytes.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
content = raw_bytes.decode("latin-1")
|
||||||
|
stream = io.StringIO(content)
|
||||||
|
|
||||||
|
# Auto-détection du séparateur (, ; ou tabulation) sur la 1re ligne
|
||||||
|
premiere_ligne = stream.readline()
|
||||||
|
stream.seek(0)
|
||||||
|
for delim in (",", ";", "\t"):
|
||||||
|
nb = len(premiere_ligne.split(delim))
|
||||||
|
if nb > 1:
|
||||||
|
separateur = delim
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
separateur = ","
|
||||||
|
|
||||||
|
csv_reader = csv.reader(stream, delimiter=separateur)
|
||||||
|
|
||||||
|
header = next(csv_reader, None)
|
||||||
|
if not header:
|
||||||
|
flash("Le fichier CSV est vide.", "danger")
|
||||||
|
return redirect(url_for("historique.gestion_import_historique_csv"))
|
||||||
|
|
||||||
|
# Normalisation des en-têtes : strip + suppression des quotes + lower
|
||||||
|
def _norm_header(h):
|
||||||
|
h = h.strip()
|
||||||
|
if len(h) >= 2 and h[0] in ('"', "'") and h[-1] == h[0]:
|
||||||
|
h = h[1:-1]
|
||||||
|
return h.strip().lower()
|
||||||
|
|
||||||
|
header_norm = [_norm_header(h) for h in header]
|
||||||
|
|
||||||
|
# Mapping alias -> nom canonique (couvre les variantes EODHD, Yahoo, etc.)
|
||||||
|
alias = {
|
||||||
|
"date": "date",
|
||||||
|
"price": "price", "close": "price", "last": "price",
|
||||||
|
"adj_close": "price", "adjusted_close": "price", "adj. close": "price",
|
||||||
|
"open": "open",
|
||||||
|
"hight": "hight", "high": "hight",
|
||||||
|
"low": "low",
|
||||||
|
"vol": "vol", "vol.": "vol", "volume": "vol",
|
||||||
|
"change": "change", "change %": "change", "change_pct": "change",
|
||||||
|
"pct_change": "change", "change_percent": "change", "chg": "change",
|
||||||
|
}
|
||||||
|
# Construction du dictionnaire nom_canonique -> index
|
||||||
|
idx = {}
|
||||||
|
for i, h in enumerate(header_norm):
|
||||||
|
canon = alias.get(h)
|
||||||
|
if canon and canon not in idx:
|
||||||
|
idx[canon] = i
|
||||||
|
|
||||||
|
def col(row, name):
|
||||||
|
"""Récupère la valeur d'une colonne par son nom canonique."""
|
||||||
|
if name not in idx:
|
||||||
|
return None
|
||||||
|
i = idx[name]
|
||||||
|
return row[i] if i < len(row) else None
|
||||||
|
|
||||||
|
# Vérification : la colonne 'date' est obligatoire
|
||||||
|
if "date" not in idx:
|
||||||
|
flash(
|
||||||
|
f"Pas OK - Colonne 'date' introuvable dans l'en-tête CSV. "
|
||||||
|
f"En-tête détecté : {','.join(header)}",
|
||||||
|
"danger",
|
||||||
|
)
|
||||||
|
return redirect(url_for("historique.gestion_import_historique_csv"))
|
||||||
|
|
||||||
|
# Requête d'insertion (INSERT IGNORE grâce à l'unique key isin+date)
|
||||||
|
insert = text(
|
||||||
|
"INSERT IGNORE INTO historique "
|
||||||
|
"(isin, date, price, `open`, hight, low, vol, `change`) "
|
||||||
|
"VALUES (:isin, :date, :price, :open, :hight, :low, :vol, :change)"
|
||||||
|
)
|
||||||
|
|
||||||
|
count_insere = 0
|
||||||
|
count_ignore = 0
|
||||||
|
count_erreurs = 0
|
||||||
|
exemples_erreurs = []
|
||||||
|
|
||||||
|
for row in csv_reader:
|
||||||
|
date_raw = col(row, "date")
|
||||||
|
if not date_raw:
|
||||||
|
count_erreurs += 1
|
||||||
|
if len(exemples_erreurs) < 5:
|
||||||
|
exemples_erreurs.append(f"Date vide | ligne: {row}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Normalisation de la date -> objet date (pour insertion YYYY-MM-DD)
|
||||||
|
# L'ordre compte : %Y/%d/%m (AAAA/JJ/MM) AVANT %Y/%m/%d pour éviter
|
||||||
|
# l'ambiguïté (un jour >12 invaliderait %Y/%m/%d et tomberait juste sur %Y/%d/%m).
|
||||||
|
date_str = date_raw.strip()[:10]
|
||||||
|
date_val = None
|
||||||
|
for fmt in ("%Y-%m-%d", "%Y/%d/%m", "%Y/%m/%d", "%m/%d/%Y", "%d/%m/%Y", "%d-%m-%Y"):
|
||||||
|
try:
|
||||||
|
date_val = datetime.strptime(date_str, fmt).date()
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if date_val is None:
|
||||||
|
count_erreurs += 1
|
||||||
|
if len(exemples_erreurs) < 10:
|
||||||
|
exemples_erreurs.append(
|
||||||
|
f"Date invalide '{date_raw}' (nb colonnes ligne={len(row)}) | ligne: {row}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
price = _to_decimal(col(row, "price"))
|
||||||
|
open_ = _to_decimal(col(row, "open"))
|
||||||
|
hight = _to_decimal(col(row, "hight"))
|
||||||
|
low = _to_decimal(col(row, "low"))
|
||||||
|
vol = _convertir_vol(col(row, "vol"))
|
||||||
|
change = _to_decimal(col(row, "change"))
|
||||||
|
|
||||||
|
result = db.session.execute(
|
||||||
|
insert,
|
||||||
|
{
|
||||||
|
"isin": isin,
|
||||||
|
"date": date_val,
|
||||||
|
"price": price,
|
||||||
|
"open": open_,
|
||||||
|
"hight": hight,
|
||||||
|
"low": low,
|
||||||
|
"vol": vol,
|
||||||
|
"change": change,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# rowcount == 1 si réellement inséré, 0 si ignoré (doublon unique key)
|
||||||
|
if result.rowcount == 1:
|
||||||
|
count_insere += 1
|
||||||
|
else:
|
||||||
|
count_ignore += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
detail = ""
|
||||||
|
if exemples_erreurs:
|
||||||
|
detail = " | Exemples d'erreurs: " + " ; ".join(exemples_erreurs)
|
||||||
|
flash(
|
||||||
|
f"OK - Importation réussie pour {action.company_name} ({isin}) : "
|
||||||
|
f"{count_insere} ligne(s) insérée(s), "
|
||||||
|
f"{count_ignore} ligne(s) déjà existante(s) ignorée(s), "
|
||||||
|
f"{count_erreurs} ligne(s) en erreur. "
|
||||||
|
f"[Séparateur: '{separateur}' | En-tête: {','.join(header)}]"
|
||||||
|
f"{detail}",
|
||||||
|
"success",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error("Erreur import historique CSV : %s", e, exc_info=True)
|
||||||
|
flash(f"Pas OK - Erreur lors de l'importation du fichier : {str(e)}", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for("historique.gestion_import_historique_csv"))
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
|
from app.decorators import login_required
|
||||||
|
|
||||||
|
menu_bp = Blueprint("menu", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@menu_bp.route("/menu")
|
||||||
|
@login_required
|
||||||
|
def menu():
|
||||||
|
return render_template("menu.html")
|
||||||
@@ -0,0 +1,476 @@
|
|||||||
|
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 - frais_total
|
||||||
|
else:
|
||||||
|
prix_net = prix_brut + frais_total
|
||||||
|
|
||||||
|
# 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/<int:ordre_id>/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/<int:pied_id>", 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))
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
from flask import Blueprint, current_app, flash, redirect, render_template, request, url_for
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.decorators import login_required
|
||||||
|
|
||||||
|
societes_bp = Blueprint("societes", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@societes_bp.route("/gestion-societes", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def gestion_societes():
|
||||||
|
if request.method == "POST":
|
||||||
|
action = request.form.get("action")
|
||||||
|
soc_id = request.form.get("id")
|
||||||
|
|
||||||
|
nom = request.form.get("nom")
|
||||||
|
forme = request.form.get("forme")
|
||||||
|
capital = request.form.get("capital")
|
||||||
|
siren = request.form.get("siren")
|
||||||
|
siret = request.form.get("siret")
|
||||||
|
rcs = request.form.get("rcs")
|
||||||
|
tva = request.form.get("tva")
|
||||||
|
naf = request.form.get("naf")
|
||||||
|
tel = request.form.get("tel")
|
||||||
|
web = request.form.get("web")
|
||||||
|
mail = request.form.get("mail")
|
||||||
|
|
||||||
|
if action == "creer":
|
||||||
|
try:
|
||||||
|
query = text(
|
||||||
|
"INSERT INTO societe (nom, forme, capital, siren, siret, rcs, "
|
||||||
|
"tva, naf, tel, web, mail) "
|
||||||
|
"VALUES (:nom, :forme, :capital, :siren, :siret, :rcs, :tva, "
|
||||||
|
":naf, :tel, :web, :mail)"
|
||||||
|
)
|
||||||
|
db.session.execute(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"nom": nom,
|
||||||
|
"forme": forme,
|
||||||
|
"capital": capital,
|
||||||
|
"siren": siren,
|
||||||
|
"siret": siret,
|
||||||
|
"rcs": rcs,
|
||||||
|
"tva": tva,
|
||||||
|
"naf": naf,
|
||||||
|
"tel": tel,
|
||||||
|
"web": web,
|
||||||
|
"mail": mail,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Société créée avec succès.", "success")
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error("Erreur création société : %s", e, exc_info=True)
|
||||||
|
flash("Erreur lors de la création de la société.", "danger")
|
||||||
|
|
||||||
|
elif action == "modifier" and soc_id:
|
||||||
|
try:
|
||||||
|
query = text(
|
||||||
|
"UPDATE societe SET nom = :nom, forme = :forme, "
|
||||||
|
"capital = :capital, siren = :siren, siret = :siret, "
|
||||||
|
"rcs = :rcs, tva = :tva, naf = :naf, tel = :tel, web = :web, "
|
||||||
|
"mail = :mail WHERE id = :id"
|
||||||
|
)
|
||||||
|
db.session.execute(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"id": soc_id,
|
||||||
|
"nom": nom,
|
||||||
|
"forme": forme,
|
||||||
|
"capital": capital,
|
||||||
|
"siren": siren,
|
||||||
|
"siret": siret,
|
||||||
|
"rcs": rcs,
|
||||||
|
"tva": tva,
|
||||||
|
"naf": naf,
|
||||||
|
"tel": tel,
|
||||||
|
"web": web,
|
||||||
|
"mail": mail,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Société modifiée avec succès.", "success")
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error("Erreur modification société : %s", e, exc_info=True)
|
||||||
|
flash("Erreur lors de la modification de la société.", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for("societes.gestion_societes"))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
societes = [
|
||||||
|
{
|
||||||
|
"id": row.id,
|
||||||
|
"nom": row.nom if row.nom else "",
|
||||||
|
"forme": row.forme if row.forme else "",
|
||||||
|
"capital": row.capital if row.capital is not None else 0,
|
||||||
|
"siren": row.siren if row.siren is not None else "",
|
||||||
|
"siret": row.siret if row.siret is not None else "",
|
||||||
|
"rcs": row.rcs if row.rcs else "",
|
||||||
|
"tva": row.tva if row.tva else "",
|
||||||
|
"naf": row.naf if row.naf else "",
|
||||||
|
"tel": row.tel if row.tel else "",
|
||||||
|
"web": row.web if row.web else "",
|
||||||
|
"mail": row.mail if row.mail else "",
|
||||||
|
}
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
return render_template("gestion_societes.html", societes=societes)
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||||
|
from sqlalchemy import text
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.decorators import login_required
|
||||||
|
|
||||||
|
utilisateurs_bp = Blueprint("utilisateurs", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@utilisateurs_bp.route("/gestion-utilisateurs", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def gestion_utilisateurs():
|
||||||
|
if request.method == "POST":
|
||||||
|
action = request.form.get("action")
|
||||||
|
user_id = request.form.get("id")
|
||||||
|
user_actif = request.form.get("actif", "1")
|
||||||
|
user_prenom = request.form.get("prenom")
|
||||||
|
user_nom = request.form.get("nom")
|
||||||
|
user_login = request.form.get("login")
|
||||||
|
user_mail = request.form.get("mail")
|
||||||
|
user_pass = request.form.get("pass")
|
||||||
|
|
||||||
|
if action == "creer":
|
||||||
|
if user_pass:
|
||||||
|
hashed_pass = generate_password_hash(user_pass)
|
||||||
|
query = text(
|
||||||
|
"INSERT INTO login (login, nom, prenom, mail, pass, actif) "
|
||||||
|
"VALUES (:login, :nom, :prenom, :mail, :pass, :actif)"
|
||||||
|
)
|
||||||
|
db.session.execute(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"login": user_login,
|
||||||
|
"nom": user_nom,
|
||||||
|
"prenom": user_prenom,
|
||||||
|
"mail": user_mail,
|
||||||
|
"pass": hashed_pass,
|
||||||
|
"actif": user_actif,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Utilisateur créé avec succès.", "success")
|
||||||
|
else:
|
||||||
|
flash(
|
||||||
|
"Le mot de passe est obligatoire pour créer un utilisateur.",
|
||||||
|
"danger",
|
||||||
|
)
|
||||||
|
|
||||||
|
elif action == "modifier" and user_id:
|
||||||
|
if user_pass:
|
||||||
|
hashed_pass = generate_password_hash(user_pass)
|
||||||
|
query = text(
|
||||||
|
"UPDATE login SET login = :login, nom = :nom, prenom = :prenom, "
|
||||||
|
"mail = :mail, pass = :pass, actif = :actif WHERE id = :id"
|
||||||
|
)
|
||||||
|
db.session.execute(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"id": user_id,
|
||||||
|
"login": user_login,
|
||||||
|
"nom": user_nom,
|
||||||
|
"prenom": user_prenom,
|
||||||
|
"mail": user_mail,
|
||||||
|
"pass": hashed_pass,
|
||||||
|
"actif": user_actif,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
query = text(
|
||||||
|
"UPDATE login SET login = :login, nom = :nom, prenom = :prenom, "
|
||||||
|
"mail = :mail, actif = :actif WHERE id = :id"
|
||||||
|
)
|
||||||
|
db.session.execute(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"id": user_id,
|
||||||
|
"login": user_login,
|
||||||
|
"nom": user_nom,
|
||||||
|
"prenom": user_prenom,
|
||||||
|
"mail": user_mail,
|
||||||
|
"actif": user_actif,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Utilisateur modifié avec succès.", "success")
|
||||||
|
|
||||||
|
return redirect(url_for("utilisateurs.gestion_utilisateurs"))
|
||||||
|
|
||||||
|
users_query = text(
|
||||||
|
"SELECT id, login, nom, prenom, mail, actif FROM login ORDER BY id ASC"
|
||||||
|
)
|
||||||
|
result = db.session.execute(users_query)
|
||||||
|
|
||||||
|
users = [
|
||||||
|
{
|
||||||
|
"id": row.id,
|
||||||
|
"login": row.login if row.login else "",
|
||||||
|
"nom": row.nom if row.nom else "",
|
||||||
|
"prenom": row.prenom if row.prenom else "",
|
||||||
|
"mail": row.mail if row.mail else "",
|
||||||
|
"actif": row.actif if row.actif is not None else 1,
|
||||||
|
}
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
return render_template("gestion_utilisateurs.html", users=users)
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<!-- Barre de recherche ISIN -->
|
<!-- Barre de recherche ISIN -->
|
||||||
<div class="bg-gray-900 border border-gray-800 rounded-2xl shadow-2xl w-full max-w-[98%] p-4 mb-4">
|
<div class="bg-gray-900 border border-gray-800 rounded-2xl shadow-2xl w-full max-w-[98%] p-4 mb-4">
|
||||||
<form method="GET" action="{{ url_for('main.analyse') }}" class="flex items-center gap-3 flex-wrap">
|
<form method="GET" action="{{ url_for('analyse.analyse') }}" class="flex items-center gap-3 flex-wrap">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<div class="flex-1 min-w-[200px]">
|
<div class="flex-1 min-w-[200px]">
|
||||||
<label class="block text-xs font-medium text-gray-400 mb-1">Entrer un ISIN</label>
|
<label class="block text-xs font-medium text-gray-400 mb-1">Entrer un ISIN</label>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
<i class="fa-solid fa-magnifying-glass-chart"></i> Analyser
|
<i class="fa-solid fa-magnifying-glass-chart"></i> Analyser
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.menu') }}"
|
href="{{ url_for('menu.menu') }}"
|
||||||
class="px-5 py-2.5 bg-gray-800 hover:bg-gray-700 text-gray-200 text-sm font-semibold rounded-lg transition duration-200 cursor-pointer self-end"
|
class="px-5 py-2.5 bg-gray-800 hover:bg-gray-700 text-gray-200 text-sm font-semibold rounded-lg transition duration-200 cursor-pointer self-end"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-house"></i> Retour au menu
|
<i class="fa-solid fa-house"></i> Retour au menu
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<!-- Gauche : Logo -->
|
<!-- Gauche : Logo -->
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.menu') if session.get('user_id') else url_for('main.index') }}"
|
href="{{ url_for('menu.menu') if session.get('user_id') else url_for('auth.index') }}"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="{{ url_for('static', filename='images/GFBlancLogo.png') }}"
|
src="{{ url_for('static', filename='images/GFBlancLogo.png') }}"
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
{% if session.get('user_id') %}
|
{% if session.get('user_id') %}
|
||||||
<div class="pt-1.5 flex justify-end">
|
<div class="pt-1.5 flex justify-end">
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.logout') }}"
|
href="{{ url_for('auth.logout') }}"
|
||||||
class="no-underline px-2.5 py-1 bg-red-950/60 hover:bg-red-900/80 border border-red-900/60 text-red-300 rounded text-[11px] font-sans transition duration-200"
|
class="no-underline px-2.5 py-1 bg-red-950/60 hover:bg-red-900/80 border border-red-900/60 text-red-300 rounded text-[11px] font-sans transition duration-200"
|
||||||
>
|
>
|
||||||
Se déconnecter
|
Se déconnecter
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ content %}
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.menu') }}"
|
href="{{ url_for('menu.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"
|
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
|
Retour au menu
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ content %}
|
|||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="{{ url_for('main.gestion_comptes') }}"
|
action="{{ url_for('comptes.gestion_comptes') }}"
|
||||||
class="m-0"
|
class="m-0"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
@@ -65,7 +65,7 @@ content %}
|
|||||||
<!-- Génération automatique des écritures depuis les ordres -->
|
<!-- Génération automatique des écritures depuis les ordres -->
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="{{ url_for('main.generer_ecritures_ordres') }}"
|
action="{{ url_for('comptes.generer_ecritures_ordres') }}"
|
||||||
class="m-0"
|
class="m-0"
|
||||||
onsubmit="return confirm('Générer / régénérer les écritures comptables à partir des ordres de cette société ? Les écritures générées existantes seront remplacées.');"
|
onsubmit="return confirm('Générer / régénérer les écritures comptables à partir des ordres de cette société ? Les écritures générées existantes seront remplacées.');"
|
||||||
>
|
>
|
||||||
@@ -81,7 +81,7 @@ content %}
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.menu') }}"
|
href="{{ url_for('menu.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"
|
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
|
Retour au menu
|
||||||
@@ -256,7 +256,7 @@ content %}
|
|||||||
<form
|
<form
|
||||||
id="form-modal-ecriture"
|
id="form-modal-ecriture"
|
||||||
method="POST"
|
method="POST"
|
||||||
action="{{ url_for('main.creer_compte_traitement') }}"
|
action="{{ url_for('comptes.creer_compte_traitement') }}"
|
||||||
class="space-y-4"
|
class="space-y-4"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
@@ -371,7 +371,7 @@ content %}
|
|||||||
const title = document.getElementById("modal-title");
|
const title = document.getElementById("modal-title");
|
||||||
|
|
||||||
modal.classList.remove("hidden");
|
modal.classList.remove("hidden");
|
||||||
form.action = "{{ url_for('main.creer_compte_traitement') }}";
|
form.action = "{{ url_for('comptes.creer_compte_traitement') }}";
|
||||||
title.innerHTML =
|
title.innerHTML =
|
||||||
'<i class="fa-solid fa-plus-circle text-emerald-500"></i> Nouvelle écriture';
|
'<i class="fa-solid fa-plus-circle text-emerald-500"></i> Nouvelle écriture';
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ endblock %} {% block header_title %}Gestion CSV{% endblock %} {% block content
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.menu') }}"
|
href="{{ url_for('menu.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"
|
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
|
Retour au menu
|
||||||
@@ -124,7 +124,7 @@ endblock %} {% block header_title %}Gestion CSV{% endblock %} {% block content
|
|||||||
>
|
>
|
||||||
<!-- Formulaire ou lien pour l'Export (réduit) -->
|
<!-- Formulaire ou lien pour l'Export (réduit) -->
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.export_actions_csv') }}"
|
href="{{ url_for('historique.export_actions_csv') }}"
|
||||||
class="w-full sm:w-1/4 flex items-center justify-center px-4 py-3 bg-blue-600 hover:bg-blue-500 text-white text-sm font-semibold rounded-xl shadow-lg transition duration-200"
|
class="w-full sm:w-1/4 flex items-center justify-center px-4 py-3 bg-blue-600 hover:bg-blue-500 text-white text-sm font-semibold rounded-xl shadow-lg transition duration-200"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -145,7 +145,7 @@ endblock %} {% block header_title %}Gestion CSV{% endblock %} {% block content
|
|||||||
|
|
||||||
<!-- Formulaire pour l'Import (prend toute la place restante) -->
|
<!-- Formulaire pour l'Import (prend toute la place restante) -->
|
||||||
<form
|
<form
|
||||||
action="{{ url_for('main.import_actions_csv') }}"
|
action="{{ url_for('historique.import_actions_csv') }}"
|
||||||
method="POST"
|
method="POST"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
class="w-full sm:w-3/4 flex items-center gap-2"
|
class="w-full sm:w-3/4 flex items-center gap-2"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.menu') }}"
|
href="{{ url_for('menu.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"
|
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
|
Retour au menu
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<!-- 2. Corps : Box d'importation -->
|
<!-- 2. Corps : Box d'importation -->
|
||||||
<form
|
<form
|
||||||
id="form-import-historique"
|
id="form-import-historique"
|
||||||
action="{{ url_for('main.import_historique_csv') }}"
|
action="{{ url_for('historique.import_historique_csv') }}"
|
||||||
method="POST"
|
method="POST"
|
||||||
enctype="multipart/form-data"
|
enctype="multipart/form-data"
|
||||||
class="space-y-6"
|
class="space-y-6"
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
<!-- Formulaire combiné / Filtre Année + Société -->
|
<!-- Formulaire combiné / Filtre Année + Société -->
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="{{ url_for('main.gestion_ordres') }}"
|
action="{{ url_for('ordres.gestion_ordres') }}"
|
||||||
class="flex items-center gap-2 m-0"
|
class="flex items-center gap-2 m-0"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
|
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="{{ url_for('main.gestion_ordres') }}"
|
action="{{ url_for('ordres.gestion_ordres') }}"
|
||||||
class="m-0"
|
class="m-0"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.menu') }}"
|
href="{{ url_for('menu.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"
|
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
|
Retour au menu
|
||||||
@@ -304,7 +304,7 @@
|
|||||||
<form
|
<form
|
||||||
id="form-modal-ordre"
|
id="form-modal-ordre"
|
||||||
method="POST"
|
method="POST"
|
||||||
action="{{ url_for('main.creer_ordre_traitement') }}"
|
action="{{ url_for('ordres.creer_ordre_traitement') }}"
|
||||||
class="space-y-4"
|
class="space-y-4"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
@@ -599,7 +599,7 @@
|
|||||||
const btnValider = document.getElementById("btn-valider");
|
const btnValider = document.getElementById("btn-valider");
|
||||||
|
|
||||||
modal.classList.remove("hidden");
|
modal.classList.remove("hidden");
|
||||||
form.action = "{{ url_for('main.creer_ordre_traitement') }}";
|
form.action = "{{ url_for('ordres.creer_ordre_traitement') }}";
|
||||||
title.innerHTML =
|
title.innerHTML =
|
||||||
'<i class="fa-solid fa-plus-circle text-emerald-500"></i> Créer un Nouvel Ordre';
|
'<i class="fa-solid fa-plus-circle text-emerald-500"></i> Créer un Nouvel Ordre';
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ content %}
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.menu') }}"
|
href="{{ url_for('menu.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"
|
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
|
Retour au menu
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ content %}
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.menu') }}"
|
href="{{ url_for('menu.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"
|
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
|
Retour au menu
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ block content %}
|
|||||||
</div>
|
</div>
|
||||||
{% endif %} {% endwith %}
|
{% endif %} {% endwith %}
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('main.index') }}" class="space-y-4">
|
<form method="POST" action="{{ url_for('auth.index') }}" class="space-y-4">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<div>
|
<div>
|
||||||
<label for="login" class="block text-sm font-medium text-gray-300 mb-1"
|
<label for="login" class="block text-sm font-medium text-gray-300 mb-1"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ header_title %}Tableau de bord{% endblock %} {% block content %}
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-8">
|
||||||
<!-- btn 1 -->
|
<!-- btn 1 -->
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.gestion_actions') }}"
|
href="{{ url_for('actions.gestion_actions') }}"
|
||||||
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]"
|
||||||
>
|
>
|
||||||
1. Gérer les actions
|
1. Gérer les actions
|
||||||
@@ -23,7 +23,7 @@ header_title %}Tableau de bord{% endblock %} {% block content %}
|
|||||||
|
|
||||||
<!-- btn 2 -->
|
<!-- btn 2 -->
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.gestion_ordres') }}"
|
href="{{ url_for('ordres.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
|
||||||
@@ -31,7 +31,7 @@ header_title %}Tableau de bord{% endblock %} {% block content %}
|
|||||||
|
|
||||||
<!-- btn 3 -->
|
<!-- btn 3 -->
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.gestion_comptes') }}"
|
href="{{ url_for('comptes.gestion_comptes') }}"
|
||||||
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]"
|
||||||
>
|
>
|
||||||
3. Comptes
|
3. Comptes
|
||||||
@@ -39,7 +39,7 @@ header_title %}Tableau de bord{% endblock %} {% block content %}
|
|||||||
|
|
||||||
<!-- btn 4 -->
|
<!-- btn 4 -->
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.gestion_societes') }}"
|
href="{{ url_for('societes.gestion_societes') }}"
|
||||||
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]"
|
||||||
>
|
>
|
||||||
4. Gestion sociétés
|
4. Gestion sociétés
|
||||||
@@ -47,7 +47,7 @@ header_title %}Tableau de bord{% endblock %} {% block content %}
|
|||||||
|
|
||||||
<!-- btn 5 -->
|
<!-- btn 5 -->
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.gestion_utilisateurs') }}"
|
href="{{ url_for('utilisateurs.gestion_utilisateurs') }}"
|
||||||
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]"
|
||||||
>
|
>
|
||||||
5. Gestion utilisateurs
|
5. Gestion utilisateurs
|
||||||
@@ -55,7 +55,7 @@ header_title %}Tableau de bord{% endblock %} {% block content %}
|
|||||||
|
|
||||||
<!-- btn 6 -->
|
<!-- btn 6 -->
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.analyse') }}"
|
href="{{ url_for('analyse.analyse') }}"
|
||||||
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]"
|
||||||
>
|
>
|
||||||
6. Stat historique
|
6. Stat historique
|
||||||
@@ -63,7 +63,7 @@ header_title %}Tableau de bord{% endblock %} {% block content %}
|
|||||||
|
|
||||||
<!-- btn 7 -->
|
<!-- btn 7 -->
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.gestion_import_export_actions_csv') }}"
|
href="{{ url_for('historique.gestion_import_export_actions_csv') }}"
|
||||||
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]"
|
||||||
>
|
>
|
||||||
7. Import/Export Actions CSV
|
7. Import/Export Actions CSV
|
||||||
@@ -71,7 +71,7 @@ header_title %}Tableau de bord{% endblock %} {% block content %}
|
|||||||
|
|
||||||
<!-- btn 8 -->
|
<!-- btn 8 -->
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('main.gestion_import_historique_csv') }}"
|
href="{{ url_for('historique.gestion_import_historique_csv') }}"
|
||||||
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]"
|
||||||
>
|
>
|
||||||
8. Import historique CSV
|
8. Import historique CSV
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
.
|
||||||
|
├── AGENTS.md
|
||||||
|
├── app
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── models.py
|
||||||
|
│ ├── __pycache__
|
||||||
|
│ │ ├── __init__.cpython-311.pyc
|
||||||
|
│ │ └── routes.cpython-311.pyc
|
||||||
|
│ ├── routes.py
|
||||||
|
│ ├── services
|
||||||
|
│ │ ├── analysis.py
|
||||||
|
│ │ ├── database.py
|
||||||
|
│ │ ├── indicators.py
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ └── __pycache__
|
||||||
|
│ │ ├── analysis.cpython-311.pyc
|
||||||
|
│ │ ├── database.cpython-311.pyc
|
||||||
|
│ │ ├── indicators.cpython-311.pyc
|
||||||
|
│ │ └── __init__.cpython-311.pyc
|
||||||
|
│ ├── static
|
||||||
|
│ │ ├── css
|
||||||
|
│ │ ├── images
|
||||||
|
│ │ │ ├── favicon.png
|
||||||
|
│ │ │ └── GFBlancLogo.png
|
||||||
|
│ │ └── js
|
||||||
|
│ │ └── charts.js
|
||||||
|
│ └── templates
|
||||||
|
│ ├── analyse.html
|
||||||
|
│ ├── base.html
|
||||||
|
│ ├── gestion_actions.html
|
||||||
|
│ ├── gestion_comptes.html
|
||||||
|
│ ├── gestion_import_export_actions_csv.html
|
||||||
|
│ ├── gestion_import_historique_csv.html
|
||||||
|
│ ├── gestion_ordres.html
|
||||||
|
│ ├── gestion_societes.html
|
||||||
|
│ ├── gestion_utilisateurs.html
|
||||||
|
│ ├── index.html
|
||||||
|
│ └── menu.html
|
||||||
|
├── arborescence.txt
|
||||||
|
├── .devcontainer
|
||||||
|
│ └── devcontainer.json
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── Dockerfile
|
||||||
|
├── .dockerignore
|
||||||
|
├── .env
|
||||||
|
├── .git
|
||||||
|
│ ├── COMMIT_EDITMSG
|
||||||
|
│ ├── config
|
||||||
|
│ ├── description
|
||||||
|
│ ├── HEAD
|
||||||
|
│ ├── hooks
|
||||||
|
│ │ ├── applypatch-msg.sample
|
||||||
|
│ │ ├── commit-msg.sample
|
||||||
|
│ │ ├── fsmonitor-watchman.sample
|
||||||
|
│ │ ├── post-update.sample
|
||||||
|
│ │ ├── pre-applypatch.sample
|
||||||
|
│ │ ├── pre-commit.sample
|
||||||
|
│ │ ├── pre-merge-commit.sample
|
||||||
|
│ │ ├── prepare-commit-msg.sample
|
||||||
|
│ │ ├── pre-push.sample
|
||||||
|
│ │ ├── pre-rebase.sample
|
||||||
|
│ │ ├── pre-receive.sample
|
||||||
|
│ │ ├── push-to-checkout.sample
|
||||||
|
│ │ ├── sendemail-validate.sample
|
||||||
|
│ │ └── update.sample
|
||||||
|
│ ├── index
|
||||||
|
│ ├── info
|
||||||
|
│ │ └── exclude
|
||||||
|
│ ├── logs
|
||||||
|
│ │ ├── HEAD
|
||||||
|
│ │ └── refs
|
||||||
|
│ │ ├── heads
|
||||||
|
│ │ │ └── main
|
||||||
|
│ │ └── remotes
|
||||||
|
│ │ └── origin
|
||||||
|
│ │ ├── HEAD
|
||||||
|
│ │ └── main
|
||||||
|
│ ├── objects
|
||||||
|
│ │ ├── 00
|
||||||
|
│ │ │ └── bd6ff6fdd29e050127343d217b47a267df712c
|
||||||
|
│ │ ├── 01
|
||||||
|
│ │ │ └── e0502fd1dd04a4689ab4093c65b049888d2848
|
||||||
|
│ │ ├── 02
|
||||||
|
│ │ │ └── 07cb7e5bc7d04931b6485cf180d1884e9689f9
|
||||||
|
│ │ ├── 04
|
||||||
|
│ │ │ ├── 08ddf18487e77856a3410c54a512343f0add37
|
||||||
|
│ │ │ └── e6dccc12264d2ac6554b7120b5f3b73833f2a9
|
||||||
|
│ │ ├── 07
|
||||||
|
│ │ │ └── 9031ef84904e911299a303bfb5096834df5d70
|
||||||
|
│ │ ├── 0a
|
||||||
|
│ │ │ └── dc0e5948aba4d913acd5c47ea3084ef42072c8
|
||||||
|
│ │ ├── 0c
|
||||||
|
│ │ │ └── 134ee9dce2f8ecafb266fa9fcca840416135dd
|
||||||
|
│ │ ├── 0d
|
||||||
|
│ │ │ └── a3b25bfcd0156e1868a116b69e2ba57b14d001
|
||||||
|
│ │ ├── 10
|
||||||
|
│ │ │ └── 9ca2310b66a98ec003c85f0f1113cbde00b24d
|
||||||
|
│ │ ├── 11
|
||||||
|
│ │ │ └── 2ab5b90d0d231b89e053775c3a4abe3f7fb0ad
|
||||||
|
│ │ ├── 12
|
||||||
|
│ │ │ └── f8d7e25f2d157a304a8167bd16c888b37fa2fa
|
||||||
|
│ │ ├── 15
|
||||||
|
│ │ │ └── f531461d36bd5067dcf9b1d7df8d284682c700
|
||||||
|
│ │ ├── 17
|
||||||
|
│ │ │ └── 902472fb99110700c56b93b6edd5eed36e865f
|
||||||
|
│ │ ├── 1b
|
||||||
|
│ │ │ └── 21a3e17e1ecc4f961b8575ce0cd8ae0ac8e79f
|
||||||
|
│ │ ├── 1e
|
||||||
|
│ │ │ └── b57085067042a37dbb9960d814ca9bf4b89747
|
||||||
|
│ │ ├── 1f
|
||||||
|
│ │ │ ├── 07e48b3a620bf69ec029e531e3ebbcf9718a3e
|
||||||
|
│ │ │ ├── 2089047f9abbd69b117158a36a2d8a529ca90b
|
||||||
|
│ │ │ └── 47a7c5e161f973eefdc3f8074fb31e4c7ec92b
|
||||||
|
│ │ ├── 20
|
||||||
|
│ │ │ └── 57d693cd0b411a9c468ddec6f3732ec7117e03
|
||||||
|
│ │ ├── 24
|
||||||
|
│ │ │ └── 3f4879653fdb1f4b1b5ce5d06a1a7ebe4aea57
|
||||||
|
│ │ ├── 26
|
||||||
|
│ │ │ ├── b6cd9e153bd5a18968289008c4868e643700d7
|
||||||
|
│ │ │ └── f28323844f6bfd9eec20261f44e3e57b6974b5
|
||||||
|
│ │ ├── 28
|
||||||
|
│ │ │ └── 7192f782efafb53ae63f2c879fb8c5e0cdf69e
|
||||||
|
│ │ ├── 2a
|
||||||
|
│ │ │ └── 1425272ff8d7a216715148ff59c7bb53ab3686
|
||||||
|
│ │ ├── 34
|
||||||
|
│ │ │ └── eba76fb863d7f19e15e72456dc97e8392d7fdd
|
||||||
|
│ │ ├── 36
|
||||||
|
│ │ │ └── 2ccfc03743c02d80b01ed9dfcd92caa35b14ca
|
||||||
|
│ │ ├── 37
|
||||||
|
│ │ │ └── e5ecfa8dbe2a7f89f1b2738ed01227cd8880e3
|
||||||
|
│ │ ├── 3b
|
||||||
|
│ │ │ ├── 0b0bad6147de77557e8edfbaca6194b2530c1f
|
||||||
|
│ │ │ ├── 16a2452244bf4c7f34e7931a5d92523c85ffd6
|
||||||
|
│ │ │ └── 517e20c82a2cbf1915dc8e31cf1b6fb86c7ebb
|
||||||
|
│ │ ├── 3d
|
||||||
|
│ │ │ └── e8bf9d59f8409f1dc3f72e95ee0156e5bedab8
|
||||||
|
│ │ ├── 3f
|
||||||
|
│ │ │ └── 1b0becfcf2ce89ce53dee705f727da43b57065
|
||||||
|
│ │ ├── 40
|
||||||
|
│ │ │ ├── cd5eed2a199166541eedd182945a7568546f03
|
||||||
|
│ │ │ └── ed00b9c9a18bf78ea2806b05e4eb9f638018fe
|
||||||
|
│ │ ├── 41
|
||||||
|
│ │ │ └── 39ac631013376273c279668af6fce888eb61fb
|
||||||
|
│ │ ├── 44
|
||||||
|
│ │ │ └── 5b141a30570cf3c4c430f38aca3998e40c5365
|
||||||
|
│ │ ├── 46
|
||||||
|
│ │ │ └── 885453b18d8ff255ed537b1f06abc5de83f803
|
||||||
|
│ │ ├── 49
|
||||||
|
│ │ │ ├── 3740d03f7d957864dd6cd0c187a4bea57c0bab
|
||||||
|
│ │ │ └── e440e17c5be6abc96049f11a244e2ea580ee52
|
||||||
|
│ │ ├── 4c
|
||||||
|
│ │ │ └── 78c73f7e5be001c434da09bd9ddfe5153fc25c
|
||||||
|
│ │ ├── 4d
|
||||||
|
│ │ │ ├── 3937c88f7b778219e034addb342d3b90b3ada2
|
||||||
|
│ │ │ ├── 71371f2136c3ef10bf2e1376ebaa3f2958bca6
|
||||||
|
│ │ │ └── e00820c85d27e1917915962b70d753364ea7fe
|
||||||
|
│ │ ├── 50
|
||||||
|
│ │ │ └── 5b9801c3db189ea858001626ae4741b3918bf6
|
||||||
|
│ │ ├── 52
|
||||||
|
│ │ │ ├── 1ae0f442e731579df467694823a050b3665f12
|
||||||
|
│ │ │ └── 3957cd018743a763ec75e678d75b1c70b8ba82
|
||||||
|
│ │ ├── 54
|
||||||
|
│ │ │ └── 814d5f0580b785d5b8fe123ca89af21f3e11ac
|
||||||
|
│ │ ├── 55
|
||||||
|
│ │ │ ├── 636030fef0de78019f78e8d5ef947f67e12eb2
|
||||||
|
│ │ │ └── f58e0a29126613c2d9517e1bebda9cc47b7802
|
||||||
|
│ │ ├── 56
|
||||||
|
│ │ │ └── 4f4846027c485e92cb280f84a470e40bd62447
|
||||||
|
│ │ ├── 57
|
||||||
|
│ │ │ └── 2e8acf0e6194567bd8d59b3ac342195f6d3966
|
||||||
|
│ │ ├── 59
|
||||||
|
│ │ │ └── af163824c44be62dfbb06df9e71769563a33e1
|
||||||
|
│ │ ├── 5a
|
||||||
|
│ │ │ └── 26a100fa4c7811822dd8dd3c895f60c364de15
|
||||||
|
│ │ ├── 5b
|
||||||
|
│ │ │ └── 6ac4bd24d2dfcc5dd2e94cd16b123e85bec09d
|
||||||
|
│ │ ├── 60
|
||||||
|
│ │ │ ├── 615fd6903be90071138712ae7175ae321d0645
|
||||||
|
│ │ │ └── 7fe553d40f715855d6fc92058e190eb08f2669
|
||||||
|
│ │ ├── 63
|
||||||
|
│ │ │ └── 3368c1d7cd2077e27d9d3ceb03a4032a2cc9c9
|
||||||
|
│ │ ├── 65
|
||||||
|
│ │ │ └── 92ffbcc5cb696c191ef2b69194de0b810b19f8
|
||||||
|
│ │ ├── 66
|
||||||
|
│ │ │ └── e2180d741309c85885cff73321ae9b198fff92
|
||||||
|
│ │ ├── 67
|
||||||
|
│ │ │ └── a0147412bd348f67a3b168a1c7496b0b7082f3
|
||||||
|
│ │ ├── 6b
|
||||||
|
│ │ │ └── 0cea8b3fe318c88578bff8627532672199f8ba
|
||||||
|
│ │ ├── 6d
|
||||||
|
│ │ │ └── 018a514d3d9a98f2d3f4d5822be8a83b6deb8c
|
||||||
|
│ │ ├── 6e
|
||||||
|
│ │ │ └── 5d15c70803ba6c93947cd46f891524f9534ae9
|
||||||
|
│ │ ├── 70
|
||||||
|
│ │ │ └── 6bdce8292187fd421e751d94ee444632e7b945
|
||||||
|
│ │ ├── 71
|
||||||
|
│ │ │ └── 1ff3311b54b32fff3606cf4c635359d60f4c60
|
||||||
|
│ │ ├── 72
|
||||||
|
│ │ │ ├── 352c8c35a4cde1575d9b205785ba6474b8b637
|
||||||
|
│ │ │ └── 47f112f4f746c8ae33031011a3b0af449fc5d4
|
||||||
|
│ │ ├── 74
|
||||||
|
│ │ │ └── 536771507e4561928a31c31e3cd1c9e5f89428
|
||||||
|
│ │ ├── 75
|
||||||
|
│ │ │ ├── 92ef5ca86e1351534de19233c580f0c259d4f0
|
||||||
|
│ │ │ └── c801d89931e2c327fed73a95da59e6b830e089
|
||||||
|
│ │ ├── 7e
|
||||||
|
│ │ │ ├── 39538816f4b9dacf025dff4100efa246a44c0b
|
||||||
|
│ │ │ └── 694b15fcebcd3ac03ddbc248154aac4d698206
|
||||||
|
│ │ ├── 82
|
||||||
|
│ │ │ └── bfeb65501ede38ef71f68d75161df39992917e
|
||||||
|
│ │ ├── 83
|
||||||
|
│ │ │ ├── 218e12f47b9ea37d97a9e71a138e0343a4ef41
|
||||||
|
│ │ │ └── 7368aef4264810159f3951edbed316977aaa44
|
||||||
|
│ │ ├── 85
|
||||||
|
│ │ │ ├── 38f78cce5d41ea9e620668a243e160dda226e0
|
||||||
|
│ │ │ ├── eb889e34fb76fde82c995c0e0d3496aeb21a09
|
||||||
|
│ │ │ └── f8daca661ff70b9692ac0d02cee98d3215c7ec
|
||||||
|
│ │ ├── 86
|
||||||
|
│ │ │ └── 57696025f6bc6d93d2141302f7f746a0ded69b
|
||||||
|
│ │ ├── 87
|
||||||
|
│ │ │ └── b390931d5dd16697c7e346a91309843f1da5a0
|
||||||
|
│ │ ├── 8b
|
||||||
|
│ │ │ └── 084e5b50e0ce6a5a1a6349540fe8ef9daffcae
|
||||||
|
│ │ ├── 8c
|
||||||
|
│ │ │ └── 69706e4f8b5599372f65127837a1645debdabb
|
||||||
|
│ │ ├── 8d
|
||||||
|
│ │ │ └── 0bddd1b2f664f31617688340dbe10e67007e68
|
||||||
|
│ │ ├── 90
|
||||||
|
│ │ │ └── ebd9162d702e2ab49c4d5db4ab282997e65640
|
||||||
|
│ │ ├── 94
|
||||||
|
│ │ │ └── 8c55763940965d16649effe28b26f4ba1cbbbb
|
||||||
|
│ │ ├── 97
|
||||||
|
│ │ │ ├── 6a00e0fa6d49a08d7711fed68e6e01847c2865
|
||||||
|
│ │ │ └── 7599c17a31a9a5e12a5a34bcbcc2ced5c2d641
|
||||||
|
│ │ ├── 99
|
||||||
|
│ │ │ └── 426023929b3e0ec04abf4a8b3f29ced2799a87
|
||||||
|
│ │ ├── 9c
|
||||||
|
│ │ │ └── 32e129e3dd47b2e5e09c61ea5f1b4bef7c2c33
|
||||||
|
│ │ ├── a1
|
||||||
|
│ │ │ └── 0d068b5d17148c5a6f9452e9ad0b39794fc9b2
|
||||||
|
│ │ ├── a8
|
||||||
|
│ │ │ ├── 37fe05c7e3115b53f8955f83d0c4a39d11ef10
|
||||||
|
│ │ │ └── c06d8055c22bc855d46eee3f611dc95902e454
|
||||||
|
│ │ ├── a9
|
||||||
|
│ │ │ └── 0bec8d42e49980ab4f00d64decd060ebe70057
|
||||||
|
│ │ ├── b0
|
||||||
|
│ │ │ └── 6517db23c58555c268884c502e5020a94b4fa4
|
||||||
|
│ │ ├── b1
|
||||||
|
│ │ │ ├── 20c2bd728d41675a7be4d4497b1340c7472910
|
||||||
|
│ │ │ └── 6b6eef9fc8833a6dc404fa3f6ec2228b396e37
|
||||||
|
│ │ ├── b2
|
||||||
|
│ │ │ └── c241797b5f9473c35ffeff236d8c2097c41002
|
||||||
|
│ │ ├── b6
|
||||||
|
│ │ │ └── 7ecfc5d115379a92ffa17747bb7c294b42a6cb
|
||||||
|
│ │ ├── b8
|
||||||
|
│ │ │ └── e0eba8c9f85c6ecf2fe3d353824f542fb57c15
|
||||||
|
│ │ ├── b9
|
||||||
|
│ │ │ └── 74611196cf8def7d9dcaa4ac270f2a0c863cc1
|
||||||
|
│ │ ├── ba
|
||||||
|
│ │ │ └── 4279c3c3b5125d11c5c025798fcd2a33d2fab4
|
||||||
|
│ │ ├── bb
|
||||||
|
│ │ │ └── e0360e7535ce1604e3f66451e0ab6f32dbac12
|
||||||
|
│ │ ├── be
|
||||||
|
│ │ │ ├── d31a9eeaf84dee7039b8809110286c469eee69
|
||||||
|
│ │ │ └── ebbdc835054832a5b92348b8752f2622f88636
|
||||||
|
│ │ ├── c1
|
||||||
|
│ │ │ └── 54b83412cb4198a6f2e29deb09606ccc2d2a23
|
||||||
|
│ │ ├── c2
|
||||||
|
│ │ │ └── 2368611269a9fb12cfc034289cf671469704a1
|
||||||
|
│ │ ├── c3
|
||||||
|
│ │ │ └── 2a4fffdd5d82253a687a8c7c39d71f3cd889f7
|
||||||
|
│ │ ├── c4
|
||||||
|
│ │ │ └── 1f306b9abb67f5ccb24b3b35ab18b1524399ad
|
||||||
|
│ │ ├── c8
|
||||||
|
│ │ │ └── d18a6cef831ca56e18f7d4ff5d7c7ec58b3010
|
||||||
|
│ │ ├── cb
|
||||||
|
│ │ │ └── 33281ba30cd46880876234e8d2ea7065ecf3de
|
||||||
|
│ │ ├── cd
|
||||||
|
│ │ │ └── 902d78081683c69945b34ce67a14add6426b28
|
||||||
|
│ │ ├── ce
|
||||||
|
│ │ │ └── dab25514e00d824c2f9d345f64e756e473173a
|
||||||
|
│ │ ├── cf
|
||||||
|
│ │ │ └── ad237cf6df989cb1d54aad4bc5eaa55b8b5c98
|
||||||
|
│ │ ├── d0
|
||||||
|
│ │ │ └── c784fa00768246e4c95c168a38206f055c166f
|
||||||
|
│ │ ├── d6
|
||||||
|
│ │ │ └── b8bc493ff42da63649b8be6ec989a12319a567
|
||||||
|
│ │ ├── d7
|
||||||
|
│ │ │ └── d031b8e073f1e07f73ad468b8031e1950c6eff
|
||||||
|
│ │ ├── d8
|
||||||
|
│ │ │ └── 65d3501ada082e077cdaa32439dc22a4966561
|
||||||
|
│ │ ├── d9
|
||||||
|
│ │ │ └── d851c33add105e23ba383feb3cd131bfc6a8fc
|
||||||
|
│ │ ├── dc
|
||||||
|
│ │ │ ├── 14d536b88178d3d74870613cfc938fd90c793a
|
||||||
|
│ │ │ └── 6abeb9e2c702591fe216dd10b8b04941e117f7
|
||||||
|
│ │ ├── dd
|
||||||
|
│ │ │ ├── 736b72374acab09a8e84bd1bda04327c63c804
|
||||||
|
│ │ │ └── f0420841e632270a504ac90e7e8ae4837d56e8
|
||||||
|
│ │ ├── df
|
||||||
|
│ │ │ ├── 76b34a23247f8020fc8b7aefbde2074c812837
|
||||||
|
│ │ │ └── 8b33f6a40ca1c710ebc114d16b9a548db16a37
|
||||||
|
│ │ ├── e0
|
||||||
|
│ │ │ └── e7d51df0cdcc54399d200e1c1604d0fa813378
|
||||||
|
│ │ ├── e3
|
||||||
|
│ │ │ ├── 1e4572cabd86823c7be31f034c223b1e57b3d7
|
||||||
|
│ │ │ └── 8c247ebbc0189b3af17e3294e1429f22e5f61b
|
||||||
|
│ │ ├── e6
|
||||||
|
│ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
||||||
|
│ │ ├── ef
|
||||||
|
│ │ │ └── 58a8a234dd56e699430061b86b005f73939464
|
||||||
|
│ │ ├── f2
|
||||||
|
│ │ │ └── fd513263d428a680a98a494a7492f8da0de43a
|
||||||
|
│ │ ├── f3
|
||||||
|
│ │ │ └── 7f5b1e4509262eb65122f91220f04f080182ed
|
||||||
|
│ │ ├── f7
|
||||||
|
│ │ │ └── 2cd45703375c00e975a09cee50da3619d3277e
|
||||||
|
│ │ ├── f8
|
||||||
|
│ │ │ └── 91e8a154f2d4357cbb7e4af8c1805dfa310baf
|
||||||
|
│ │ ├── fa
|
||||||
|
│ │ │ ├── e895d8ca270a7538d299e63ab2dd4663523f95
|
||||||
|
│ │ │ └── f1bce15949cbe30f2cf840aa5e9e9e975829ab
|
||||||
|
│ │ ├── fb
|
||||||
|
│ │ │ └── 4fd6e9a038e3fd4660396a0afd78f0f272aa48
|
||||||
|
│ │ ├── fc
|
||||||
|
│ │ │ └── 1ca97cb1f9f2d092206a57833214b2fa28972a
|
||||||
|
│ │ ├── ff
|
||||||
|
│ │ │ └── 14f22d999bb7f217cc67c7a2cb02e5a3f1cf4b
|
||||||
|
│ │ ├── info
|
||||||
|
│ │ └── pack
|
||||||
|
│ ├── opencode
|
||||||
|
│ └── refs
|
||||||
|
│ ├── heads
|
||||||
|
│ │ └── main
|
||||||
|
│ ├── remotes
|
||||||
|
│ │ └── origin
|
||||||
|
│ │ ├── HEAD
|
||||||
|
│ │ └── main
|
||||||
|
│ └── tags
|
||||||
|
├── .gitignore
|
||||||
|
├── migrations
|
||||||
|
│ ├── apply_migrations.py
|
||||||
|
│ └── schema.sql
|
||||||
|
├── mysql_data
|
||||||
|
│ ├── auto.cnf
|
||||||
|
│ ├── binlog.000001
|
||||||
|
│ ├── binlog.000002
|
||||||
|
│ ├── binlog.000003
|
||||||
|
│ ├── binlog.000004
|
||||||
|
│ ├── binlog.000005
|
||||||
|
│ ├── binlog.index
|
||||||
|
│ ├── bourse_db [error opening dir]
|
||||||
|
│ ├── ca-key.pem
|
||||||
|
│ ├── ca.pem
|
||||||
|
│ ├── client-cert.pem
|
||||||
|
│ ├── client-key.pem
|
||||||
|
│ ├── #ib_16384_0.dblwr
|
||||||
|
│ ├── #ib_16384_1.dblwr
|
||||||
|
│ ├── ib_buffer_pool
|
||||||
|
│ ├── ibdata1
|
||||||
|
│ ├── #innodb_redo [error opening dir]
|
||||||
|
│ ├── #innodb_temp [error opening dir]
|
||||||
|
│ ├── mysql [error opening dir]
|
||||||
|
│ ├── mysql.ibd
|
||||||
|
│ ├── mysql.sock -> /var/run/mysqld/mysqld.sock
|
||||||
|
│ ├── performance_schema [error opening dir]
|
||||||
|
│ ├── private_key.pem
|
||||||
|
│ ├── public_key.pem
|
||||||
|
│ ├── server-cert.pem
|
||||||
|
│ ├── server-key.pem
|
||||||
|
│ ├── sys [error opening dir]
|
||||||
|
│ ├── undo_001
|
||||||
|
│ └── undo_002
|
||||||
|
├── requirements.txt
|
||||||
|
├── run.py
|
||||||
|
└── .vscode
|
||||||
|
└── settings.json
|
||||||
|
|
||||||
|
147 directories, 230 files
|
||||||
|
|
||||||
|
tree -a > arborescence.txt
|
||||||
Reference in New Issue
Block a user