feat: système de projets (Option A) — grouper plusieurs runs sous un projet
Deploy via Portainer / deploy (push) Successful in 0s

This commit is contained in:
Jo
2026-07-06 23:31:17 +02:00
parent 9cb7c71d6f
commit ddd0f6bee5
6 changed files with 373 additions and 6 deletions
+95 -4
View File
@@ -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/<int:id>', 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/<int:id>/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/<int:id>/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/<int:id>')
def job_detail(id):