diff --git a/app.py b/app.py index 94db72b..082adc9 100644 --- a/app.py +++ b/app.py @@ -15,6 +15,14 @@ def init_db(): conn = get_db() conn.executescript(''' CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT); + CREATE TABLE IF NOT EXISTS projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + client_id INTEGER REFERENCES clients(id) ON DELETE SET NULL, + deadline TEXT DEFAULT NULL, + notes TEXT DEFAULT '', + created_at TEXT DEFAULT (datetime('now')) + ); CREATE TABLE IF NOT EXISTS clients ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT DEFAULT '', @@ -177,6 +185,7 @@ def init_db(): 'ALTER TABLE clients ADD COLUMN default_handling_profile_id INTEGER', 'ALTER TABLE clients ADD COLUMN default_material_profile_id INTEGER', 'ALTER TABLE clients ADD COLUMN default_pricing_profile_id INTEGER', + 'ALTER TABLE jobs ADD COLUMN project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL', ] for sql in migrations: try: @@ -592,6 +601,85 @@ def delete_client(id): conn.commit(); conn.close() return redirect(url_for('clients')) +# ─── Projets ────────────────────────────────────────────────────────────────── + +@app.route('/projects') +def projects(): + conn = get_db() + rows = conn.execute(''' + SELECT p.*, c.name as client_name, + COUNT(j.id) as job_count, + COALESCE(SUM(j.order_qty), 0) as total_pieces, + COALESCE(SUM(j.final_price * j.order_qty), 0) as total_revenue + FROM projects p + LEFT JOIN clients c ON c.id = p.client_id + LEFT JOIN jobs j ON j.project_id = p.id + GROUP BY p.id ORDER BY p.created_at DESC + ''').fetchall() + conn.close() + return render_template('projects.html', projects=rows) + +@app.route('/projects/new', methods=['GET','POST']) +def new_project(): + conn = get_db() + if request.method == 'POST': + conn.execute('INSERT INTO projects(name,client_id,deadline,notes) VALUES(?,?,?,?)', + (request.form['name'], + int(request.form['client_id']) if request.form.get('client_id') else None, + request.form.get('deadline') or None, + request.form.get('notes',''))) + conn.commit(); conn.close() + return redirect(url_for('projects')) + clients = conn.execute('SELECT id,name FROM clients ORDER BY name').fetchall() + conn.close() + return render_template('project_form.html', project=None, clients=clients) + +@app.route('/projects/', methods=['GET']) +def project_detail(id): + conn = get_db() + project = conn.execute(''' + SELECT p.*, c.name as client_name + FROM projects p LEFT JOIN clients c ON c.id=p.client_id + WHERE p.id=?''', (id,)).fetchone() + if not project: + conn.close(); return redirect(url_for('projects')) + jobs = conn.execute(''' + SELECT j.*, c.name as client_name, m.name as material_name, m.type as material_type + FROM jobs j + LEFT JOIN clients c ON c.id=j.client_id + LEFT JOIN materials m ON m.id=j.material_id + WHERE j.project_id=? + ORDER BY j.created_at''', (id,)).fetchall() + conn.close() + total_pieces = sum((j['order_qty'] or 1) for j in jobs) + total_revenue = sum((j['final_price'] or 0) * (j['order_qty'] or 1) for j in jobs) + return render_template('project_detail.html', project=project, jobs=jobs, + total_pieces=total_pieces, total_revenue=total_revenue) + +@app.route('/projects//edit', methods=['GET','POST']) +def edit_project(id): + conn = get_db() + project = conn.execute('SELECT * FROM projects WHERE id=?',(id,)).fetchone() + if request.method == 'POST': + conn.execute('UPDATE projects SET name=?,client_id=?,deadline=?,notes=? WHERE id=?', + (request.form['name'], + int(request.form['client_id']) if request.form.get('client_id') else None, + request.form.get('deadline') or None, + request.form.get('notes',''), id)) + conn.commit(); conn.close() + return redirect(url_for('project_detail', id=id)) + clients = conn.execute('SELECT id,name FROM clients ORDER BY name').fetchall() + conn.close() + return render_template('project_form.html', project=project, clients=clients) + +@app.route('/projects//delete', methods=['POST']) +def delete_project(id): + conn = get_db() + conn.execute('UPDATE jobs SET project_id=NULL WHERE project_id=?',(id,)) + conn.execute('DELETE FROM projects WHERE id=?',(id,)) + conn.commit(); conn.close() + return redirect(url_for('projects')) + # ─── Matières ───────────────────────────────────────────────────────────────── @app.route('/materials') @@ -885,6 +973,7 @@ def new_job(): handling_pid = request.form.get('handling_profile_id') or None material_pid = request.form.get('material_profile_id') or None pricing_pid = request.form.get('pricing_profile_id') or None + project_id = request.form.get('project_id') or None pieces_per_plate = max(1, int(request.form.get('pieces_per_plate', 1))) order_qty = max(1, int(request.form.get('order_qty', 1))) @@ -919,15 +1008,15 @@ def new_job(): r = calc(weight_g, print_time_s, design_mult, gross_margin, discount, price_kg, s, pieces_per_plate) conn.execute('''INSERT INTO jobs(client_id,material_id,machine_profile_id,handling_profile_id, - material_profile_id,pricing_profile_id,source_file,name,description, + material_profile_id,pricing_profile_id,project_id,source_file,name,description, weight_g,print_time_s,design_multiplier,gross_margin_pct,discount_pct, pieces_per_plate,order_qty, material_cost,material_margin,design_cost,handling_cost,wear_cost,electricity_cost, cout_fixe,total_marge,subtotal,margin_amount, cotisations_amount,vfl_amount,tva_amount,other_taxes_amount,tax_amount, price_per_piece,final_price,notes) - VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', - (client_id,material_id,machine_pid,handling_pid,material_pid,pricing_pid, + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', + (client_id,material_id,machine_pid,handling_pid,material_pid,pricing_pid,project_id, source_file,request.form['name'],request.form.get('description',''), weight_g,print_time_s,design_mult,gross_margin,discount, pieces_per_plate,order_qty, @@ -942,10 +1031,12 @@ def new_job(): conn.commit(); conn.close() return redirect(url_for('jobs')) + projects = conn.execute('SELECT id,name FROM projects ORDER BY name').fetchall() conn.close() return render_template('new_job.html', clients=clients, materials=mats, settings=settings, machine_profiles=machine_profiles, handling_profiles=handling_profiles, - material_profiles=material_profiles, pricing_profiles=pricing_profiles) + material_profiles=material_profiles, pricing_profiles=pricing_profiles, + projects=projects) @app.route('/jobs/') def job_detail(id): diff --git a/templates/base.html b/templates/base.html index 49dedee..b21be72 100644 --- a/templates/base.html +++ b/templates/base.html @@ -87,6 +87,9 @@ Templates + + Projets + Planning diff --git a/templates/new_job.html b/templates/new_job.html index 282c578..6bcf857 100644 --- a/templates/new_job.html +++ b/templates/new_job.html @@ -46,7 +46,7 @@
-
+
-
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + Annuler +
+ +
+
+
+ +{% endblock %} diff --git a/templates/projects.html b/templates/projects.html new file mode 100644 index 0000000..177cdf1 --- /dev/null +++ b/templates/projects.html @@ -0,0 +1,71 @@ +{% extends 'base.html' %} +{% block title %}Projets{% endblock %} + +{% block content %} + + +{% if projects %} +
+ {% for p in projects %} +
+
+
+
+
+
+ + {{ p.name }} + +
+ {{ p.client_name or 'Sans client' }} +
+ {% if p.deadline %} + {{ p.deadline }} + {% endif %} +
+
+
+
Runs
+
{{ p.job_count }}
+
+
+
Pièces totales
+
{{ p.total_pieces }}
+
+
+
CA total
+
{{ "%.2f"|format(p.total_revenue) }} €
+
+
+
+ +
+
+ {% endfor %} +
+{% else %} +
+ + Aucun projet. Créer le premier. +
+{% endif %} +{% endblock %}