feat: système de projets (Option A) — grouper plusieurs runs sous un projet
Deploy via Portainer / deploy (push) Successful in 0s
Deploy via Portainer / deploy (push) Successful in 0s
This commit is contained in:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user