Templates, decomposition prix restructuree, TVA + VFL, fix DM buttons
This commit is contained in:
@@ -32,17 +32,61 @@ def init_db():
|
|||||||
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
|
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
|
||||||
price_per_kg REAL NOT NULL, recorded_at TEXT DEFAULT (datetime('now'))
|
price_per_kg REAL NOT NULL, recorded_at TEXT DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS machine_profiles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
printer_price REAL DEFAULT 400.0,
|
||||||
|
printer_lifespan_hours REAL DEFAULT 5000,
|
||||||
|
nozzle_price REAL DEFAULT 10.0,
|
||||||
|
nozzle_lifespan_hours REAL DEFAULT 250,
|
||||||
|
plate_price REAL DEFAULT 25.0,
|
||||||
|
plate_lifespan_hours REAL DEFAULT 1000,
|
||||||
|
printer_power_kw REAL DEFAULT 0.35,
|
||||||
|
electricity_price_kwh REAL DEFAULT 0.18,
|
||||||
|
notes TEXT DEFAULT '',
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS handling_profiles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
handling_rate_per_hour REAL DEFAULT 40.0,
|
||||||
|
handling_minutes_per_plate REAL DEFAULT 5,
|
||||||
|
notes TEXT DEFAULT '',
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS material_profiles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
material_margin_pct REAL DEFAULT 20.0,
|
||||||
|
notes TEXT DEFAULT '',
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS pricing_profiles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
design_multiplier REAL DEFAULT 1.80,
|
||||||
|
gross_margin_pct REAL DEFAULT 27.5,
|
||||||
|
notes TEXT DEFAULT '',
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
CREATE TABLE IF NOT EXISTS jobs (
|
CREATE TABLE IF NOT EXISTS jobs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
client_id INTEGER REFERENCES clients(id) ON DELETE SET NULL,
|
client_id INTEGER REFERENCES clients(id) ON DELETE SET NULL,
|
||||||
material_id INTEGER REFERENCES materials(id) ON DELETE SET NULL,
|
material_id INTEGER REFERENCES materials(id) ON DELETE SET NULL,
|
||||||
|
machine_profile_id INTEGER REFERENCES machine_profiles(id) ON DELETE SET NULL,
|
||||||
|
handling_profile_id INTEGER REFERENCES handling_profiles(id) ON DELETE SET NULL,
|
||||||
|
material_profile_id INTEGER REFERENCES material_profiles(id) ON DELETE SET NULL,
|
||||||
|
pricing_profile_id INTEGER REFERENCES pricing_profiles(id) ON DELETE SET NULL,
|
||||||
source_file TEXT DEFAULT '', name TEXT NOT NULL,
|
source_file TEXT DEFAULT '', name TEXT NOT NULL,
|
||||||
description TEXT DEFAULT '', weight_g REAL NOT NULL,
|
description TEXT DEFAULT '', weight_g REAL NOT NULL,
|
||||||
print_time_s INTEGER NOT NULL, design_multiplier REAL NOT NULL,
|
print_time_s INTEGER NOT NULL, design_multiplier REAL NOT NULL,
|
||||||
gross_margin_pct REAL NOT NULL DEFAULT 27.5, discount_pct REAL DEFAULT 0,
|
gross_margin_pct REAL NOT NULL DEFAULT 27.5, discount_pct REAL DEFAULT 0,
|
||||||
material_cost REAL, material_margin REAL, design_cost REAL,
|
material_cost REAL, material_margin REAL, design_cost REAL,
|
||||||
handling_cost REAL, wear_cost REAL, electricity_cost REAL,
|
handling_cost REAL, wear_cost REAL, electricity_cost REAL,
|
||||||
subtotal REAL, margin_amount REAL, tax_amount REAL, final_price REAL,
|
cout_fixe REAL, total_marge REAL,
|
||||||
|
subtotal REAL, margin_amount REAL,
|
||||||
|
cotisations_amount REAL, vfl_amount REAL, tva_amount REAL,
|
||||||
|
tax_amount REAL, final_price REAL,
|
||||||
notes TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now'))
|
notes TEXT DEFAULT '', created_at TEXT DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
''')
|
''')
|
||||||
@@ -52,7 +96,11 @@ def init_db():
|
|||||||
('nozzle_price','10.0'),('nozzle_lifespan_hours','250'),
|
('nozzle_price','10.0'),('nozzle_lifespan_hours','250'),
|
||||||
('plate_price','25.0'),('plate_lifespan_hours','1000'),
|
('plate_price','25.0'),('plate_lifespan_hours','1000'),
|
||||||
('handling_rate_per_hour','40.0'),('handling_minutes_per_plate','5'),
|
('handling_rate_per_hour','40.0'),('handling_minutes_per_plate','5'),
|
||||||
('material_margin_pct','20.0'),('tax_rate_pct','12.3'),('printer_power_kw','0.35'),
|
('material_margin_pct','20.0'),
|
||||||
|
('cotisations_rate_pct','12.3'),
|
||||||
|
('vfl_rate_pct','1.7'),
|
||||||
|
('tva_rate_pct','0.0'),
|
||||||
|
('printer_power_kw','0.35'),
|
||||||
]
|
]
|
||||||
conn.executemany('INSERT OR IGNORE INTO settings VALUES (?,?)', defaults)
|
conn.executemany('INSERT OR IGNORE INTO settings VALUES (?,?)', defaults)
|
||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
@@ -110,9 +158,27 @@ def parse_3mf(file_stream):
|
|||||||
|
|
||||||
def calc(weight_g, print_time_s, design_mult, gross_margin_pct, discount_pct,
|
def calc(weight_g, print_time_s, design_mult, gross_margin_pct, discount_pct,
|
||||||
price_per_kg_override=None, s=None):
|
price_per_kg_override=None, s=None):
|
||||||
|
"""
|
||||||
|
Decomposition:
|
||||||
|
Cout fixe = matiere + marge_matiere + usure + electricite
|
||||||
|
Manutention = handling (fixe par plateau)
|
||||||
|
Design = matiere × coeff_design
|
||||||
|
Sous-total = cout_fixe + handling + design
|
||||||
|
Marge brute = sous-total × gross_margin_pct
|
||||||
|
Total marge = handling + design + marge_brute
|
||||||
|
Prix HT base = cout_fixe + total_marge (= sous-total + marge)
|
||||||
|
Charges (sur CA HT) :
|
||||||
|
cotisations = prix_ht / (1 - cot - vfl) × cot
|
||||||
|
vfl = prix_ht / (1 - cot - vfl) × vfl
|
||||||
|
Prix HT net = prix_ht / (1 - cot - vfl)
|
||||||
|
TVA = prix_ht_net × tva_rate
|
||||||
|
Prix TTC = prix_ht_net × (1 + tva_rate)
|
||||||
|
Prix final = prix_ttc × (1 - discount)
|
||||||
|
"""
|
||||||
if s is None:
|
if s is None:
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
price_kg = price_per_kg_override if price_per_kg_override is not None else s['filament_price_per_kg']
|
|
||||||
|
price_kg = price_per_kg_override if price_per_kg_override is not None else s['filament_price_per_kg']
|
||||||
mat = (price_kg / 1000) * weight_g
|
mat = (price_kg / 1000) * weight_g
|
||||||
mat_margin = mat * (s['material_margin_pct'] / 100)
|
mat_margin = mat * (s['material_margin_pct'] / 100)
|
||||||
design = mat * design_mult
|
design = mat * design_mult
|
||||||
@@ -122,30 +188,65 @@ def calc(weight_g, print_time_s, design_mult, gross_margin_pct, discount_pct,
|
|||||||
plate_w = (s['plate_price'] / (s['plate_lifespan_hours'] * 3600)) * print_time_s
|
plate_w = (s['plate_price'] / (s['plate_lifespan_hours'] * 3600)) * print_time_s
|
||||||
wear = printer_w + nozzle_w + plate_w
|
wear = printer_w + nozzle_w + plate_w
|
||||||
elec = s['printer_power_kw'] * (print_time_s / 3600) * s['electricity_price_kwh']
|
elec = s['printer_power_kw'] * (print_time_s / 3600) * s['electricity_price_kwh']
|
||||||
subtotal = mat + mat_margin + design + handling + wear + elec
|
|
||||||
margin = subtotal * (gross_margin_pct / 100)
|
cout_fixe = mat + mat_margin + wear + elec
|
||||||
pre_tax = subtotal + margin
|
sous_total = cout_fixe + handling + design
|
||||||
tax_rate = s['tax_rate_pct'] / 100
|
margin = sous_total * (gross_margin_pct / 100)
|
||||||
with_tax = pre_tax / (1 - tax_rate)
|
total_marge= handling + design + margin
|
||||||
tax_amt = with_tax - pre_tax
|
prix_ht = cout_fixe + total_marge # = sous_total + margin
|
||||||
final = with_tax * (1 - discount_pct / 100)
|
|
||||||
|
cot_rate = s.get('cotisations_rate_pct', 12.3) / 100
|
||||||
|
vfl_rate = s.get('vfl_rate_pct', 0.0) / 100
|
||||||
|
tva_rate = s.get('tva_rate_pct', 0.0) / 100
|
||||||
|
total_charges = cot_rate + vfl_rate
|
||||||
|
|
||||||
|
# Charges calculées sur le CA (prix de vente HT net)
|
||||||
|
# prix_ht_net = prix_ht / (1 - total_charges) so that charges = prix_ht_net * total_charges
|
||||||
|
if total_charges < 1.0:
|
||||||
|
prix_ht_net = prix_ht / (1 - total_charges)
|
||||||
|
else:
|
||||||
|
prix_ht_net = prix_ht
|
||||||
|
|
||||||
|
cot_amount = prix_ht_net * cot_rate
|
||||||
|
vfl_amount = prix_ht_net * vfl_rate
|
||||||
|
tva_amount = prix_ht_net * tva_rate
|
||||||
|
prix_ttc = prix_ht_net + tva_amount
|
||||||
|
final = prix_ttc * (1 - discount_pct / 100)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'material_cost': round(mat, 4),
|
# Lignes détaillées
|
||||||
'material_margin': round(mat_margin, 4),
|
'material_cost': round(mat, 4),
|
||||||
'design_cost': round(design, 4),
|
'material_margin': round(mat_margin, 4),
|
||||||
'handling_cost': round(handling, 4),
|
'wear_cost': round(wear, 4),
|
||||||
'wear_cost': round(wear, 4),
|
'electricity_cost': round(elec, 4),
|
||||||
'electricity_cost': round(elec, 4),
|
'cout_fixe': round(cout_fixe, 4),
|
||||||
'subtotal': round(subtotal, 4),
|
'handling_cost': round(handling, 4),
|
||||||
'margin_amount': round(margin, 4),
|
'design_cost': round(design, 4),
|
||||||
'tax_amount': round(tax_amt, 4),
|
'subtotal': round(sous_total, 4),
|
||||||
'final_price': round(final, 2),
|
'margin_amount': round(margin, 4),
|
||||||
|
'total_marge': round(total_marge, 4),
|
||||||
|
'prix_ht': round(prix_ht, 4),
|
||||||
|
# Fiscal
|
||||||
|
'cotisations_amount': round(cot_amount, 4),
|
||||||
|
'vfl_amount': round(vfl_amount, 4),
|
||||||
|
'tva_amount': round(tva_amount, 4),
|
||||||
|
'prix_ht_net': round(prix_ht_net, 4),
|
||||||
|
'prix_ttc': round(prix_ttc, 4),
|
||||||
|
# Compat
|
||||||
|
'tax_amount': round(cot_amount + vfl_amount, 4),
|
||||||
|
'final_price': round(final, 2),
|
||||||
|
# Taux appliqués (pour affichage)
|
||||||
|
'_cot_pct': round(cot_rate * 100, 2),
|
||||||
|
'_vfl_pct': round(vfl_rate * 100, 2),
|
||||||
|
'_tva_pct': round(tva_rate * 100, 2),
|
||||||
}
|
}
|
||||||
|
|
||||||
def fmt_time(s):
|
def fmt_time(s):
|
||||||
return f"{s//3600}h{(s%3600)//60:02d}m"
|
return f"{s//3600}h{(s%3600)//60:02d}m"
|
||||||
app.jinja_env.filters['fmt_time'] = fmt_time
|
app.jinja_env.filters['fmt_time'] = fmt_time
|
||||||
|
|
||||||
|
# ─── Dashboard ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def dashboard():
|
def dashboard():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
@@ -164,6 +265,8 @@ def dashboard():
|
|||||||
conn.close()
|
conn.close()
|
||||||
return render_template('dashboard.html', jobs=recent, stats=stats, low_stock=low_stock)
|
return render_template('dashboard.html', jobs=recent, stats=stats, low_stock=low_stock)
|
||||||
|
|
||||||
|
# ─── Clients ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.route('/clients')
|
@app.route('/clients')
|
||||||
def clients():
|
def clients():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
@@ -206,6 +309,8 @@ def delete_client(id):
|
|||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
return redirect(url_for('clients'))
|
return redirect(url_for('clients'))
|
||||||
|
|
||||||
|
# ─── Matières ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.route('/materials')
|
@app.route('/materials')
|
||||||
def materials():
|
def materials():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
@@ -277,6 +382,188 @@ def delete_material(id):
|
|||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
return redirect(url_for('materials'))
|
return redirect(url_for('materials'))
|
||||||
|
|
||||||
|
# ─── Profils / Templates ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@app.route('/profiles')
|
||||||
|
def profiles():
|
||||||
|
conn = get_db()
|
||||||
|
machine = conn.execute('SELECT * FROM machine_profiles ORDER BY name').fetchall()
|
||||||
|
handling = conn.execute('SELECT * FROM handling_profiles ORDER BY name').fetchall()
|
||||||
|
material = conn.execute('SELECT * FROM material_profiles ORDER BY name').fetchall()
|
||||||
|
pricing = conn.execute('SELECT * FROM pricing_profiles ORDER BY name').fetchall()
|
||||||
|
conn.close()
|
||||||
|
return render_template('profiles.html',
|
||||||
|
machine_profiles=machine, handling_profiles=handling,
|
||||||
|
material_profiles=material, pricing_profiles=pricing)
|
||||||
|
|
||||||
|
@app.route('/profiles/machine/new', methods=['GET','POST'])
|
||||||
|
def new_machine_profile():
|
||||||
|
if request.method == 'POST':
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('''INSERT INTO machine_profiles
|
||||||
|
(name,printer_price,printer_lifespan_hours,nozzle_price,nozzle_lifespan_hours,
|
||||||
|
plate_price,plate_lifespan_hours,printer_power_kw,electricity_price_kwh,notes)
|
||||||
|
VALUES(?,?,?,?,?,?,?,?,?,?)''', (
|
||||||
|
request.form['name'],
|
||||||
|
float(request.form['printer_price']),
|
||||||
|
float(request.form['printer_lifespan_hours']),
|
||||||
|
float(request.form['nozzle_price']),
|
||||||
|
float(request.form['nozzle_lifespan_hours']),
|
||||||
|
float(request.form['plate_price']),
|
||||||
|
float(request.form['plate_lifespan_hours']),
|
||||||
|
float(request.form['printer_power_kw']),
|
||||||
|
float(request.form['electricity_price_kwh']),
|
||||||
|
request.form.get('notes','')
|
||||||
|
))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
return redirect(url_for('profiles') + '#machine')
|
||||||
|
return render_template('machine_profile_form.html', profile=None, settings=get_settings())
|
||||||
|
|
||||||
|
@app.route('/profiles/machine/<int:id>/edit', methods=['GET','POST'])
|
||||||
|
def edit_machine_profile(id):
|
||||||
|
conn = get_db()
|
||||||
|
profile = conn.execute('SELECT * FROM machine_profiles WHERE id=?',(id,)).fetchone()
|
||||||
|
if request.method == 'POST':
|
||||||
|
conn.execute('''UPDATE machine_profiles SET
|
||||||
|
name=?,printer_price=?,printer_lifespan_hours=?,nozzle_price=?,nozzle_lifespan_hours=?,
|
||||||
|
plate_price=?,plate_lifespan_hours=?,printer_power_kw=?,electricity_price_kwh=?,notes=?
|
||||||
|
WHERE id=?''', (
|
||||||
|
request.form['name'],
|
||||||
|
float(request.form['printer_price']),
|
||||||
|
float(request.form['printer_lifespan_hours']),
|
||||||
|
float(request.form['nozzle_price']),
|
||||||
|
float(request.form['nozzle_lifespan_hours']),
|
||||||
|
float(request.form['plate_price']),
|
||||||
|
float(request.form['plate_lifespan_hours']),
|
||||||
|
float(request.form['printer_power_kw']),
|
||||||
|
float(request.form['electricity_price_kwh']),
|
||||||
|
request.form.get('notes',''), id
|
||||||
|
))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
return redirect(url_for('profiles') + '#machine')
|
||||||
|
conn.close()
|
||||||
|
return render_template('machine_profile_form.html', profile=profile, settings=get_settings())
|
||||||
|
|
||||||
|
@app.route('/profiles/machine/<int:id>/delete', methods=['POST'])
|
||||||
|
def delete_machine_profile(id):
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('DELETE FROM machine_profiles WHERE id=?',(id,))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
return redirect(url_for('profiles') + '#machine')
|
||||||
|
|
||||||
|
@app.route('/profiles/handling/new', methods=['GET','POST'])
|
||||||
|
def new_handling_profile():
|
||||||
|
if request.method == 'POST':
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('''INSERT INTO handling_profiles(name,handling_rate_per_hour,handling_minutes_per_plate,notes)
|
||||||
|
VALUES(?,?,?,?)''', (
|
||||||
|
request.form['name'],
|
||||||
|
float(request.form['handling_rate_per_hour']),
|
||||||
|
float(request.form['handling_minutes_per_plate']),
|
||||||
|
request.form.get('notes','')
|
||||||
|
))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
return redirect(url_for('profiles') + '#handling')
|
||||||
|
return render_template('handling_profile_form.html', profile=None, settings=get_settings())
|
||||||
|
|
||||||
|
@app.route('/profiles/handling/<int:id>/edit', methods=['GET','POST'])
|
||||||
|
def edit_handling_profile(id):
|
||||||
|
conn = get_db()
|
||||||
|
profile = conn.execute('SELECT * FROM handling_profiles WHERE id=?',(id,)).fetchone()
|
||||||
|
if request.method == 'POST':
|
||||||
|
conn.execute('''UPDATE handling_profiles SET name=?,handling_rate_per_hour=?,
|
||||||
|
handling_minutes_per_plate=?,notes=? WHERE id=?''', (
|
||||||
|
request.form['name'],
|
||||||
|
float(request.form['handling_rate_per_hour']),
|
||||||
|
float(request.form['handling_minutes_per_plate']),
|
||||||
|
request.form.get('notes',''), id
|
||||||
|
))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
return redirect(url_for('profiles') + '#handling')
|
||||||
|
conn.close()
|
||||||
|
return render_template('handling_profile_form.html', profile=profile, settings=get_settings())
|
||||||
|
|
||||||
|
@app.route('/profiles/handling/<int:id>/delete', methods=['POST'])
|
||||||
|
def delete_handling_profile(id):
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('DELETE FROM handling_profiles WHERE id=?',(id,))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
return redirect(url_for('profiles') + '#handling')
|
||||||
|
|
||||||
|
@app.route('/profiles/material/new', methods=['GET','POST'])
|
||||||
|
def new_material_profile():
|
||||||
|
if request.method == 'POST':
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('INSERT INTO material_profiles(name,material_margin_pct,notes) VALUES(?,?,?)', (
|
||||||
|
request.form['name'],
|
||||||
|
float(request.form['material_margin_pct']),
|
||||||
|
request.form.get('notes','')
|
||||||
|
))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
return redirect(url_for('profiles') + '#material')
|
||||||
|
return render_template('material_profile_form.html', profile=None, settings=get_settings())
|
||||||
|
|
||||||
|
@app.route('/profiles/material/<int:id>/edit', methods=['GET','POST'])
|
||||||
|
def edit_material_profile(id):
|
||||||
|
conn = get_db()
|
||||||
|
profile = conn.execute('SELECT * FROM material_profiles WHERE id=?',(id,)).fetchone()
|
||||||
|
if request.method == 'POST':
|
||||||
|
conn.execute('UPDATE material_profiles SET name=?,material_margin_pct=?,notes=? WHERE id=?', (
|
||||||
|
request.form['name'],
|
||||||
|
float(request.form['material_margin_pct']),
|
||||||
|
request.form.get('notes',''), id
|
||||||
|
))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
return redirect(url_for('profiles') + '#material')
|
||||||
|
conn.close()
|
||||||
|
return render_template('material_profile_form.html', profile=profile, settings=get_settings())
|
||||||
|
|
||||||
|
@app.route('/profiles/material/<int:id>/delete', methods=['POST'])
|
||||||
|
def delete_material_profile(id):
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('DELETE FROM material_profiles WHERE id=?',(id,))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
return redirect(url_for('profiles') + '#material')
|
||||||
|
|
||||||
|
@app.route('/profiles/pricing/new', methods=['GET','POST'])
|
||||||
|
def new_pricing_profile():
|
||||||
|
if request.method == 'POST':
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('INSERT INTO pricing_profiles(name,design_multiplier,gross_margin_pct,notes) VALUES(?,?,?,?)', (
|
||||||
|
request.form['name'],
|
||||||
|
float(request.form['design_multiplier']),
|
||||||
|
float(request.form['gross_margin_pct']),
|
||||||
|
request.form.get('notes','')
|
||||||
|
))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
return redirect(url_for('profiles') + '#pricing')
|
||||||
|
return render_template('pricing_profile_form.html', profile=None, settings=get_settings())
|
||||||
|
|
||||||
|
@app.route('/profiles/pricing/<int:id>/edit', methods=['GET','POST'])
|
||||||
|
def edit_pricing_profile(id):
|
||||||
|
conn = get_db()
|
||||||
|
profile = conn.execute('SELECT * FROM pricing_profiles WHERE id=?',(id,)).fetchone()
|
||||||
|
if request.method == 'POST':
|
||||||
|
conn.execute('UPDATE pricing_profiles SET name=?,design_multiplier=?,gross_margin_pct=?,notes=? WHERE id=?', (
|
||||||
|
request.form['name'],
|
||||||
|
float(request.form['design_multiplier']),
|
||||||
|
float(request.form['gross_margin_pct']),
|
||||||
|
request.form.get('notes',''), id
|
||||||
|
))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
return redirect(url_for('profiles') + '#pricing')
|
||||||
|
conn.close()
|
||||||
|
return render_template('pricing_profile_form.html', profile=profile, settings=get_settings())
|
||||||
|
|
||||||
|
@app.route('/profiles/pricing/<int:id>/delete', methods=['POST'])
|
||||||
|
def delete_pricing_profile(id):
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('DELETE FROM pricing_profiles WHERE id=?',(id,))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
return redirect(url_for('profiles') + '#pricing')
|
||||||
|
|
||||||
|
# ─── Commandes ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.route('/jobs')
|
@app.route('/jobs')
|
||||||
def jobs():
|
def jobs():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
@@ -291,9 +578,14 @@ def jobs():
|
|||||||
@app.route('/jobs/new', methods=['GET','POST'])
|
@app.route('/jobs/new', methods=['GET','POST'])
|
||||||
def new_job():
|
def new_job():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
clients = conn.execute('SELECT * FROM clients ORDER BY name').fetchall()
|
clients = conn.execute('SELECT * FROM clients ORDER BY name').fetchall()
|
||||||
mats = conn.execute('SELECT * FROM materials ORDER BY type,name').fetchall()
|
mats = conn.execute('SELECT * FROM materials ORDER BY type,name').fetchall()
|
||||||
settings = get_settings()
|
machine_profiles = conn.execute('SELECT * FROM machine_profiles ORDER BY name').fetchall()
|
||||||
|
handling_profiles = conn.execute('SELECT * FROM handling_profiles ORDER BY name').fetchall()
|
||||||
|
material_profiles = conn.execute('SELECT * FROM material_profiles ORDER BY name').fetchall()
|
||||||
|
pricing_profiles = conn.execute('SELECT * FROM pricing_profiles ORDER BY name').fetchall()
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
weight_g = float(request.form['weight_g'])
|
weight_g = float(request.form['weight_g'])
|
||||||
hours = int(request.form.get('hours',0))
|
hours = int(request.form.get('hours',0))
|
||||||
@@ -305,37 +597,80 @@ def new_job():
|
|||||||
client_id = request.form.get('client_id') or None
|
client_id = request.form.get('client_id') or None
|
||||||
material_id = request.form.get('material_id') or None
|
material_id = request.form.get('material_id') or None
|
||||||
source_file = request.form.get('source_file','').strip()
|
source_file = request.form.get('source_file','').strip()
|
||||||
|
machine_pid = request.form.get('machine_profile_id') or None
|
||||||
|
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
|
||||||
|
|
||||||
|
s = dict(settings)
|
||||||
|
if machine_pid:
|
||||||
|
mp = conn.execute('SELECT * FROM machine_profiles WHERE id=?',(machine_pid,)).fetchone()
|
||||||
|
if mp:
|
||||||
|
s['printer_price'] = mp['printer_price']
|
||||||
|
s['printer_lifespan_hours'] = mp['printer_lifespan_hours']
|
||||||
|
s['nozzle_price'] = mp['nozzle_price']
|
||||||
|
s['nozzle_lifespan_hours'] = mp['nozzle_lifespan_hours']
|
||||||
|
s['plate_price'] = mp['plate_price']
|
||||||
|
s['plate_lifespan_hours'] = mp['plate_lifespan_hours']
|
||||||
|
s['printer_power_kw'] = mp['printer_power_kw']
|
||||||
|
s['electricity_price_kwh'] = mp['electricity_price_kwh']
|
||||||
|
if handling_pid:
|
||||||
|
hp = conn.execute('SELECT * FROM handling_profiles WHERE id=?',(handling_pid,)).fetchone()
|
||||||
|
if hp:
|
||||||
|
s['handling_rate_per_hour'] = hp['handling_rate_per_hour']
|
||||||
|
s['handling_minutes_per_plate'] = hp['handling_minutes_per_plate']
|
||||||
|
if material_pid:
|
||||||
|
marp = conn.execute('SELECT * FROM material_profiles WHERE id=?',(material_pid,)).fetchone()
|
||||||
|
if marp:
|
||||||
|
s['material_margin_pct'] = marp['material_margin_pct']
|
||||||
|
|
||||||
price_kg = None
|
price_kg = None
|
||||||
if material_id:
|
if material_id:
|
||||||
mat = conn.execute('SELECT price_per_kg FROM materials WHERE id=?',(material_id,)).fetchone()
|
mat = conn.execute('SELECT price_per_kg FROM materials WHERE id=?',(material_id,)).fetchone()
|
||||||
if mat:
|
if mat:
|
||||||
price_kg = mat['price_per_kg']
|
price_kg = mat['price_per_kg']
|
||||||
r = calc(weight_g, print_time_s, design_mult, gross_margin, discount, price_kg, settings)
|
|
||||||
conn.execute('''INSERT INTO jobs(client_id,material_id,source_file,name,description,
|
r = calc(weight_g, print_time_s, design_mult, gross_margin, discount, price_kg, s)
|
||||||
|
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,
|
||||||
weight_g,print_time_s,design_multiplier,gross_margin_pct,discount_pct,
|
weight_g,print_time_s,design_multiplier,gross_margin_pct,discount_pct,
|
||||||
material_cost,material_margin,design_cost,handling_cost,wear_cost,
|
material_cost,material_margin,design_cost,handling_cost,wear_cost,electricity_cost,
|
||||||
electricity_cost,subtotal,margin_amount,tax_amount,final_price,notes)
|
cout_fixe,total_marge,subtotal,margin_amount,
|
||||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
|
cotisations_amount,vfl_amount,tva_amount,tax_amount,final_price,notes)
|
||||||
(client_id,material_id,source_file,request.form['name'],request.form.get('description',''),
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
|
||||||
|
(client_id,material_id,machine_pid,handling_pid,material_pid,pricing_pid,
|
||||||
|
source_file,request.form['name'],request.form.get('description',''),
|
||||||
weight_g,print_time_s,design_mult,gross_margin,discount,
|
weight_g,print_time_s,design_mult,gross_margin,discount,
|
||||||
r['material_cost'],r['material_margin'],r['design_cost'],r['handling_cost'],
|
r['material_cost'],r['material_margin'],r['design_cost'],r['handling_cost'],
|
||||||
r['wear_cost'],r['electricity_cost'],r['subtotal'],r['margin_amount'],
|
r['wear_cost'],r['electricity_cost'],
|
||||||
r['tax_amount'],r['final_price'],request.form.get('notes','')))
|
r['cout_fixe'],r['total_marge'],r['subtotal'],r['margin_amount'],
|
||||||
|
r['cotisations_amount'],r['vfl_amount'],r['tva_amount'],r['tax_amount'],
|
||||||
|
r['final_price'],request.form.get('notes','')))
|
||||||
if material_id:
|
if material_id:
|
||||||
conn.execute('UPDATE materials SET stock_g=MAX(0,stock_g-?) WHERE id=?', (weight_g, material_id))
|
conn.execute('UPDATE materials SET stock_g=MAX(0,stock_g-?) WHERE id=?', (weight_g, material_id))
|
||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
return redirect(url_for('jobs'))
|
return redirect(url_for('jobs'))
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
return render_template('new_job.html', clients=clients, materials=mats, settings=settings)
|
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)
|
||||||
|
|
||||||
@app.route('/jobs/<int:id>')
|
@app.route('/jobs/<int:id>')
|
||||||
def job_detail(id):
|
def job_detail(id):
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
job = conn.execute('''
|
job = conn.execute('''
|
||||||
SELECT j.*, c.name as client_name,
|
SELECT j.*, c.name as client_name,
|
||||||
m.name as material_name, m.color as material_color, m.type as material_type
|
m.name as material_name, m.color as material_color, m.type as material_type,
|
||||||
FROM jobs j LEFT JOIN clients c ON j.client_id=c.id
|
mp.name as machine_profile_name, hp.name as handling_profile_name,
|
||||||
|
marp.name as material_profile_name, pp.name as pricing_profile_name
|
||||||
|
FROM jobs j
|
||||||
|
LEFT JOIN clients c ON j.client_id=c.id
|
||||||
LEFT JOIN materials m ON j.material_id=m.id
|
LEFT JOIN materials m ON j.material_id=m.id
|
||||||
|
LEFT JOIN machine_profiles mp ON j.machine_profile_id=mp.id
|
||||||
|
LEFT JOIN handling_profiles hp ON j.handling_profile_id=hp.id
|
||||||
|
LEFT JOIN material_profiles marp ON j.material_profile_id=marp.id
|
||||||
|
LEFT JOIN pricing_profiles pp ON j.pricing_profile_id=pp.id
|
||||||
WHERE j.id=?''', (id,)).fetchone()
|
WHERE j.id=?''', (id,)).fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
if not job:
|
if not job:
|
||||||
@@ -349,22 +684,49 @@ def delete_job(id):
|
|||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
return redirect(url_for('jobs'))
|
return redirect(url_for('jobs'))
|
||||||
|
|
||||||
|
# ─── API ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.route('/api/calculate', methods=['POST'])
|
@app.route('/api/calculate', methods=['POST'])
|
||||||
def api_calculate():
|
def api_calculate():
|
||||||
d = request.json
|
d = request.json
|
||||||
s = get_settings()
|
s = dict(get_settings())
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
if d.get('machine_profile_id'):
|
||||||
|
mp = conn.execute('SELECT * FROM machine_profiles WHERE id=?',(d['machine_profile_id'],)).fetchone()
|
||||||
|
if mp:
|
||||||
|
s['printer_price'] = mp['printer_price']
|
||||||
|
s['printer_lifespan_hours'] = mp['printer_lifespan_hours']
|
||||||
|
s['nozzle_price'] = mp['nozzle_price']
|
||||||
|
s['nozzle_lifespan_hours'] = mp['nozzle_lifespan_hours']
|
||||||
|
s['plate_price'] = mp['plate_price']
|
||||||
|
s['plate_lifespan_hours'] = mp['plate_lifespan_hours']
|
||||||
|
s['printer_power_kw'] = mp['printer_power_kw']
|
||||||
|
s['electricity_price_kwh'] = mp['electricity_price_kwh']
|
||||||
|
|
||||||
|
if d.get('handling_profile_id'):
|
||||||
|
hp = conn.execute('SELECT * FROM handling_profiles WHERE id=?',(d['handling_profile_id'],)).fetchone()
|
||||||
|
if hp:
|
||||||
|
s['handling_rate_per_hour'] = hp['handling_rate_per_hour']
|
||||||
|
s['handling_minutes_per_plate'] = hp['handling_minutes_per_plate']
|
||||||
|
|
||||||
|
if d.get('material_profile_id'):
|
||||||
|
marp = conn.execute('SELECT * FROM material_profiles WHERE id=?',(d['material_profile_id'],)).fetchone()
|
||||||
|
if marp:
|
||||||
|
s['material_margin_pct'] = marp['material_margin_pct']
|
||||||
|
|
||||||
price_kg = None
|
price_kg = None
|
||||||
if d.get('material_id'):
|
if d.get('material_id'):
|
||||||
conn = get_db()
|
|
||||||
mat = conn.execute('SELECT price_per_kg FROM materials WHERE id=?',(d['material_id'],)).fetchone()
|
mat = conn.execute('SELECT price_per_kg FROM materials WHERE id=?',(d['material_id'],)).fetchone()
|
||||||
conn.close()
|
|
||||||
if mat:
|
if mat:
|
||||||
price_kg = mat['price_per_kg']
|
price_kg = mat['price_per_kg']
|
||||||
|
conn.close()
|
||||||
|
|
||||||
result = calc(
|
result = calc(
|
||||||
weight_g=float(d['weight_g']),
|
weight_g=float(d['weight_g']),
|
||||||
print_time_s=int(d.get('hours',0))*3600 + int(d.get('minutes',0))*60,
|
print_time_s=int(d.get('hours',0))*3600 + int(d.get('minutes',0))*60,
|
||||||
design_mult=float(d['design_multiplier']),
|
design_mult=float(d.get('design_multiplier',1.80)),
|
||||||
gross_margin_pct=float(d['gross_margin_pct']),
|
gross_margin_pct=float(d.get('gross_margin_pct',27.5)),
|
||||||
discount_pct=float(d.get('discount_pct',0)),
|
discount_pct=float(d.get('discount_pct',0)),
|
||||||
price_per_kg_override=price_kg, s=s)
|
price_per_kg_override=price_kg, s=s)
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
@@ -389,16 +751,20 @@ def api_parse_3mf():
|
|||||||
result['filename'] = os.path.splitext(f.filename)[0]
|
result['filename'] = os.path.splitext(f.filename)[0]
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
|
# ─── Paramètres ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.route('/settings', methods=['GET','POST'])
|
@app.route('/settings', methods=['GET','POST'])
|
||||||
def settings():
|
def settings():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
for key in request.form:
|
for key in request.form:
|
||||||
conn.execute('UPDATE settings SET value=? WHERE key=?', (request.form[key], key))
|
conn.execute('INSERT OR REPLACE INTO settings VALUES (?,?)', (key, request.form[key]))
|
||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
return redirect(url_for('settings'))
|
return redirect(url_for('settings'))
|
||||||
return render_template('settings.html', settings=get_settings())
|
return render_template('settings.html', settings=get_settings())
|
||||||
|
|
||||||
|
# ─── Statistiques ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.route('/stats')
|
@app.route('/stats')
|
||||||
def stats():
|
def stats():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
@@ -419,6 +785,8 @@ def stats():
|
|||||||
conn.close()
|
conn.close()
|
||||||
return render_template('stats.html', by_client=by_client, by_month=by_month, by_material=by_material)
|
return render_template('stats.html', by_client=by_client, by_month=by_month, by_material=by_material)
|
||||||
|
|
||||||
|
# ─── Export CSV ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.route('/export/csv')
|
@app.route('/export/csv')
|
||||||
def export_csv():
|
def export_csv():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
@@ -427,9 +795,9 @@ def export_csv():
|
|||||||
COALESCE(m.name,'') as material, m.type as material_type,
|
COALESCE(m.name,'') as material, m.type as material_type,
|
||||||
j.source_file, j.name, j.weight_g, j.print_time_s,
|
j.source_file, j.name, j.weight_g, j.print_time_s,
|
||||||
j.design_multiplier, j.gross_margin_pct, j.discount_pct,
|
j.design_multiplier, j.gross_margin_pct, j.discount_pct,
|
||||||
j.material_cost, j.material_margin, j.design_cost, j.handling_cost,
|
j.cout_fixe, j.handling_cost, j.design_cost, j.total_marge,
|
||||||
j.wear_cost, j.electricity_cost, j.subtotal, j.margin_amount,
|
j.margin_amount, j.cotisations_amount, j.vfl_amount, j.tva_amount,
|
||||||
j.tax_amount, j.final_price
|
j.final_price
|
||||||
FROM jobs j LEFT JOIN clients c ON j.client_id=c.id
|
FROM jobs j LEFT JOIN clients c ON j.client_id=c.id
|
||||||
LEFT JOIN materials m ON j.material_id=m.id ORDER BY j.created_at DESC''').fetchall()
|
LEFT JOIN materials m ON j.material_id=m.id ORDER BY j.created_at DESC''').fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -437,8 +805,8 @@ def export_csv():
|
|||||||
w = csv.writer(out, delimiter=';')
|
w = csv.writer(out, delimiter=';')
|
||||||
w.writerow(['Date','Client','Matiere','Type','Fichier source','Nom',
|
w.writerow(['Date','Client','Matiere','Type','Fichier source','Nom',
|
||||||
'Poids(g)','Temps(s)','Coeff Design','Marge(%)','Remise(%)',
|
'Poids(g)','Temps(s)','Coeff Design','Marge(%)','Remise(%)',
|
||||||
'Matiere EUR','Marge Mat.','Design','Manutention','Usure',
|
'Cout fixe','Manutention','Design','Total marge',
|
||||||
'Electricite','Sous-total','Marge Brute','Cotisations','Prix Final'])
|
'Marge brute','Cotisations','VFL','TVA','Prix Final'])
|
||||||
for j in jobs:
|
for j in jobs:
|
||||||
w.writerow(list(j))
|
w.writerow(list(j))
|
||||||
return Response(out.getvalue(), mimetype='text/csv',
|
return Response(out.getvalue(), mimetype='text/csv',
|
||||||
|
|||||||
+17
-2
@@ -3,9 +3,24 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
container_name: 3d-pricing
|
container_name: 3d-pricing
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
|
||||||
- "5010:5000" # Accessible sur http://IP_DE_CETTE_VM:5010
|
|
||||||
volumes:
|
volumes:
|
||||||
|
# Données persistantes (base SQLite)
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_PATH=/data/pricing.db
|
- DATABASE_PATH=/data/pricing.db
|
||||||
|
labels:
|
||||||
|
# --- Traefik ---
|
||||||
|
- "traefik.enable=true"
|
||||||
|
# Remplacez pricing.votre-domaine.com par votre domaine réel
|
||||||
|
- "traefik.http.routers.3d-pricing.rule=Host(`pricing.votre-domaine.com`)"
|
||||||
|
- "traefik.http.routers.3d-pricing.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.3d-pricing.tls=true"
|
||||||
|
- "traefik.http.routers.3d-pricing.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.3d-pricing.loadbalancer.server.port=5000"
|
||||||
|
networks:
|
||||||
|
- traefik_proxy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik_proxy:
|
||||||
|
external: true
|
||||||
|
# Nom du réseau Traefik existant — ajustez si différent
|
||||||
|
|||||||
+9
-1
@@ -52,6 +52,8 @@
|
|||||||
.breakdown-row { display: flex; justify-content: space-between; padding: .2rem 0; font-size: .88rem; }
|
.breakdown-row { display: flex; justify-content: space-between; padding: .2rem 0; font-size: .88rem; }
|
||||||
.breakdown-row.total { font-weight: 700; font-size: 1rem; border-top: 2px solid #dee2e6; padding-top: .5rem; margin-top: .3rem; }
|
.breakdown-row.total { font-weight: 700; font-size: 1rem; border-top: 2px solid #dee2e6; padding-top: .5rem; margin-top: .3rem; }
|
||||||
.breakdown-row.subtotal { border-top: 1px solid #dee2e6; padding-top: .4rem; margin-top: .2rem; }
|
.breakdown-row.subtotal { border-top: 1px solid #dee2e6; padding-top: .4rem; margin-top: .2rem; }
|
||||||
|
.breakdown-row.section-header { font-weight: 600; font-size: .8rem; text-transform: uppercase;
|
||||||
|
letter-spacing: .04em; color: #6b7280; padding-top: .6rem; margin-top: .2rem; border-top: 1px dashed #e5e7eb; }
|
||||||
.breakdown-muted { color: #6b7280; }
|
.breakdown-muted { color: #6b7280; }
|
||||||
|
|
||||||
.final-price { font-size: 2rem; font-weight: 700; color: #ff6b35; }
|
.final-price { font-size: 2rem; font-weight: 700; color: #ff6b35; }
|
||||||
@@ -82,6 +84,9 @@
|
|||||||
<a href="{{ url_for('materials') }}" class="{% if 'material' in request.endpoint %}active{% endif %}">
|
<a href="{{ url_for('materials') }}" class="{% if 'material' in request.endpoint %}active{% endif %}">
|
||||||
<i class="bi bi-boxes"></i> Matières
|
<i class="bi bi-boxes"></i> Matières
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('profiles') }}" class="{% if 'profile' in request.endpoint %}active{% endif %}">
|
||||||
|
<i class="bi bi-bookmark-star"></i> Templates
|
||||||
|
</a>
|
||||||
<div class="nav-section">Analyse</div>
|
<div class="nav-section">Analyse</div>
|
||||||
<a href="{{ url_for('stats') }}" class="{% if request.endpoint=='stats' %}active{% endif %}">
|
<a href="{{ url_for('stats') }}" class="{% if request.endpoint=='stats' %}active{% endif %}">
|
||||||
<i class="bi bi-bar-chart-line"></i> Statistiques
|
<i class="bi bi-bar-chart-line"></i> Statistiques
|
||||||
@@ -107,4 +112,7 @@
|
|||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}{% if profile %}Modifier{% else %}Nouveau{% endif %} profil manutention{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<a href="{{ url_for('profiles') }}#handling" class="text-muted text-decoration-none small">
|
||||||
|
<i class="bi bi-arrow-left"></i> Templates
|
||||||
|
</a>
|
||||||
|
<h1 class="mt-1">
|
||||||
|
<i class="bi bi-hand-index me-2" style="color:#ff6b35"></i>
|
||||||
|
{% if profile %}Modifier{% else %}Nouveau{% endif %} profil manutention
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header py-3">Parametres</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nom du profil</label>
|
||||||
|
<input type="text" name="name" class="form-control" required
|
||||||
|
value="{{ profile.name if profile else '' }}"
|
||||||
|
placeholder="ex: Standard, Complexe, Rapide...">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Taux horaire (€/h)</label>
|
||||||
|
<input type="number" name="handling_rate_per_hour" class="form-control" step="0.5" min="0" required
|
||||||
|
value="{{ profile.handling_rate_per_hour if profile else settings.handling_rate_per_hour }}">
|
||||||
|
<div class="form-text">Votre cout de main d'oeuvre par heure.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Minutes de manutention par plateau</label>
|
||||||
|
<input type="number" name="handling_minutes_per_plate" class="form-control" step="0.5" min="0" required
|
||||||
|
value="{{ profile.handling_minutes_per_plate if profile else settings.handling_minutes_per_plate }}">
|
||||||
|
<div class="form-text">Preparation + retrait pieces + nettoyage plateau, par impression.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Notes</label>
|
||||||
|
<textarea name="notes" class="form-control" rows="2">{{ profile.notes if profile else '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn px-4 fw-bold" style="background:#ff6b35;color:#fff">
|
||||||
|
<i class="bi bi-save me-1"></i>Enregistrer
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('profiles') }}#handling" class="btn btn-outline-secondary">Annuler</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
+63
-25
@@ -22,7 +22,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-sm mb-0">
|
<table class="table table-sm mb-0">
|
||||||
<tr><th class="text-muted fw-normal" style="width:45%">Client</th><td>{{ job.client_name or '—' }}</td></tr>
|
<tr><th class="text-muted fw-normal" style="width:45%">Client</th><td>{{ job.client_name or '—' }}</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Matière</th>
|
<tr><th class="text-muted fw-normal">Matiere</th>
|
||||||
<td>
|
<td>
|
||||||
{% if job.material_name %}
|
{% if job.material_name %}
|
||||||
<span class="badge bg-secondary">{{ job.material_type }}</span> {{ job.material_name }}
|
<span class="badge bg-secondary">{{ job.material_type }}</span> {{ job.material_name }}
|
||||||
@@ -30,13 +30,22 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr><th class="text-muted fw-normal">Fichier source</th>
|
<tr><th class="text-muted fw-normal">Fichier source</th>
|
||||||
<td class="text-monospace small">{{ job.source_file or '—' }}</td></tr>
|
<td class="small font-monospace">{{ job.source_file or '—' }}</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Date</th><td>{{ job.created_at[:10] }}</td></tr>
|
<tr><th class="text-muted fw-normal">Date</th><td>{{ job.created_at[:10] }}</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Poids filament</th><td>{{ job.weight_g }} g</td></tr>
|
<tr><th class="text-muted fw-normal">Poids filament</th><td>{{ job.weight_g }} g</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Durée impression</th><td>{{ job.print_time_s | fmt_time }}</td></tr>
|
<tr><th class="text-muted fw-normal">Duree impression</th><td>{{ job.print_time_s | fmt_time }}</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Coeff design</th><td>×{{ job.design_multiplier }}</td></tr>
|
<tr><th class="text-muted fw-normal">Coeff design</th><td>×{{ job.design_multiplier }}</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Marge brute</th><td>{{ job.gross_margin_pct }} %</td></tr>
|
<tr><th class="text-muted fw-normal">Marge brute</th><td>{{ job.gross_margin_pct }} %</td></tr>
|
||||||
<tr><th class="text-muted fw-normal">Remise</th><td>{{ job.discount_pct }} %</td></tr>
|
<tr><th class="text-muted fw-normal">Remise</th><td>{{ job.discount_pct }} %</td></tr>
|
||||||
|
{% if job.machine_profile_name %}
|
||||||
|
<tr><th class="text-muted fw-normal">Profil machine</th><td>{{ job.machine_profile_name }}</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if job.handling_profile_name %}
|
||||||
|
<tr><th class="text-muted fw-normal">Profil manutention</th><td>{{ job.handling_profile_name }}</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if job.pricing_profile_name %}
|
||||||
|
<tr><th class="text-muted fw-normal">Profil tarif</th><td>{{ job.pricing_profile_name }}</td></tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
{% if job.notes %}
|
{% if job.notes %}
|
||||||
<hr>
|
<hr>
|
||||||
@@ -48,30 +57,56 @@
|
|||||||
|
|
||||||
<div class="col-md-7">
|
<div class="col-md-7">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header py-3"><i class="bi bi-receipt me-2"></i>Décomposition du prix</div>
|
<div class="card-header py-3"><i class="bi bi-receipt me-2"></i>Decomposition du prix</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Matière</span><span>{{ "%.4f"|format(job.material_cost) }} €</span></div>
|
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Marge matière</span><span>{{ "%.4f"|format(job.material_margin) }} €</span></div>
|
<!-- Cout fixe -->
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Design (×{{ job.design_multiplier }})</span><span>{{ "%.4f"|format(job.design_cost) }} €</span></div>
|
<div class="breakdown-row section-header"><span>Cout fixe</span></div>
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Manutention</span><span>{{ "%.4f"|format(job.handling_cost) }} €</span></div>
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Matiere</span><span>{{ "%.4f"|format(job.material_cost) }} €</span></div>
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Usure machine</span><span>{{ "%.4f"|format(job.wear_cost) }} €</span></div>
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Marge matiere</span><span>{{ "%.4f"|format(job.material_margin) }} €</span></div>
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Électricité</span><span>{{ "%.4f"|format(job.electricity_cost) }} €</span></div>
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Usure machine</span><span>{{ "%.4f"|format(job.wear_cost) }} €</span></div>
|
||||||
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Electricite</span><span>{{ "%.4f"|format(job.electricity_cost) }} €</span></div>
|
||||||
<div class="breakdown-row subtotal">
|
<div class="breakdown-row subtotal">
|
||||||
<span class="fw-semibold">Sous-total coûts</span>
|
<span class="fw-semibold">Total cout fixe</span>
|
||||||
<span class="fw-semibold">{{ "%.2f"|format(job.subtotal) }} €</span>
|
<span class="fw-semibold">
|
||||||
|
{% if job.cout_fixe %}{{ "%.2f"|format(job.cout_fixe) }}{% else %}{{ "%.2f"|format(job.material_cost + job.material_margin + job.wear_cost + job.electricity_cost) }}{% endif %} €
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Partie variable -->
|
||||||
|
<div class="breakdown-row section-header"><span>Partie variable</span></div>
|
||||||
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Manutention</span><span>{{ "%.4f"|format(job.handling_cost) }} €</span></div>
|
||||||
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Design (×{{ job.design_multiplier }})</span><span>{{ "%.4f"|format(job.design_cost) }} €</span></div>
|
||||||
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Marge brute ({{ job.gross_margin_pct }}%)</span><span>{{ "%.2f"|format(job.margin_amount) }} €</span></div>
|
||||||
|
<div class="breakdown-row subtotal">
|
||||||
|
<span class="fw-semibold">Total marge</span>
|
||||||
|
<span class="fw-semibold">
|
||||||
|
{% if job.total_marge %}{{ "%.2f"|format(job.total_marge) }}{% else %}{{ "%.2f"|format(job.handling_cost + job.design_cost + job.margin_amount) }}{% endif %} €
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fiscal -->
|
||||||
|
<div class="breakdown-row section-header"><span>Fiscal</span></div>
|
||||||
<div class="breakdown-row">
|
<div class="breakdown-row">
|
||||||
<span class="breakdown-muted">Marge brute ({{ job.gross_margin_pct }}%)</span>
|
<span class="breakdown-muted ps-2">Cotisations sociales</span>
|
||||||
<span>{{ "%.2f"|format(job.margin_amount) }} €</span>
|
<span>{% if job.cotisations_amount %}{{ "%.2f"|format(job.cotisations_amount) }}{% else %}{{ "%.2f"|format(job.tax_amount) }}{% endif %} €</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% if job.vfl_amount and job.vfl_amount > 0 %}
|
||||||
<div class="breakdown-row">
|
<div class="breakdown-row">
|
||||||
<span class="breakdown-muted">Cotisations micro-ent.</span>
|
<span class="breakdown-muted ps-2">VFL impot</span>
|
||||||
<span>{{ "%.2f"|format(job.tax_amount) }} €</span>
|
<span>{{ "%.2f"|format(job.vfl_amount) }} €</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if job.tva_amount and job.tva_amount > 0 %}
|
||||||
|
<div class="breakdown-row">
|
||||||
|
<span class="breakdown-muted ps-2">TVA</span>
|
||||||
|
<span>{{ "%.2f"|format(job.tva_amount) }} €</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if job.discount_pct > 0 %}
|
{% if job.discount_pct > 0 %}
|
||||||
<div class="breakdown-row text-danger">
|
<div class="breakdown-row text-danger">
|
||||||
<span>Remise ({{ job.discount_pct }}%)</span>
|
<span class="ps-2">Remise ({{ job.discount_pct }}%)</span>
|
||||||
<span>−{{ "%.2f"|format((job.subtotal + job.margin_amount + job.tax_amount) * job.discount_pct / 100) }} €</span>
|
<span>−{{ "%.2f"|format(job.final_price / (1 - job.discount_pct/100) * (job.discount_pct/100)) }} €</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="breakdown-row total">
|
<div class="breakdown-row total">
|
||||||
@@ -80,18 +115,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Barre de composition -->
|
<!-- Barre de composition -->
|
||||||
{% set total = job.final_price %}
|
{% set cout_f = job.cout_fixe or (job.material_cost + job.material_margin + job.wear_cost + job.electricity_cost) %}
|
||||||
|
{% set tot_m = job.total_marge or (job.handling_cost + job.design_cost + job.margin_amount) %}
|
||||||
|
{% set fiscal = (job.cotisations_amount or job.tax_amount or 0) + (job.vfl_amount or 0) + (job.tva_amount or 0) %}
|
||||||
|
{% set total = job.final_price %}
|
||||||
{% if total > 0 %}
|
{% if total > 0 %}
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="d-flex justify-content-between small text-muted mb-1">
|
<div class="d-flex justify-content-between small text-muted mb-1">
|
||||||
<span>Coûts ({{ (job.subtotal/total*100)|round(1) }}%)</span>
|
<span>Cout fixe ({{ (cout_f/total*100)|round(1) }}%)</span>
|
||||||
<span>Marge ({{ (job.margin_amount/total*100)|round(1) }}%)</span>
|
<span>Marge ({{ (tot_m/total*100)|round(1) }}%)</span>
|
||||||
<span>Taxes ({{ (job.tax_amount/total*100)|round(1) }}%)</span>
|
<span>Fiscal ({{ (fiscal/total*100)|round(1) }}%)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress" style="height:18px;border-radius:6px">
|
<div class="progress" style="height:18px;border-radius:6px">
|
||||||
<div class="progress-bar bg-secondary" style="width:{{ (job.subtotal/total*100)|round(1) }}%"></div>
|
<div class="progress-bar bg-secondary" style="width:{{ (cout_f/total*100)|round(1) }}%"></div>
|
||||||
<div class="progress-bar" style="width:{{ (job.margin_amount/total*100)|round(1) }}%;background:#ff6b35"></div>
|
<div class="progress-bar" style="width:{{ (tot_m/total*100)|round(1) }}%;background:#ff6b35"></div>
|
||||||
<div class="progress-bar bg-warning text-dark" style="width:{{ (job.tax_amount/total*100)|round(1) }}%"></div>
|
<div class="progress-bar bg-warning text-dark" style="width:{{ (fiscal/total*100)|round(1) }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}{% if profile %}Modifier{% else %}Nouveau{% endif %} profil machine{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<a href="{{ url_for('profiles') }}#machine" class="text-muted text-decoration-none small">
|
||||||
|
<i class="bi bi-arrow-left"></i> Templates
|
||||||
|
</a>
|
||||||
|
<h1 class="mt-1">
|
||||||
|
<i class="bi bi-cpu me-2" style="color:#ff6b35"></i>
|
||||||
|
{% if profile %}Modifier{% else %}Nouveau{% endif %} profil machine
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header py-3">Identite</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nom du profil</label>
|
||||||
|
<input type="text" name="name" class="form-control" required
|
||||||
|
value="{{ profile.name if profile else '' }}"
|
||||||
|
placeholder="ex: Bambu X1C standard, Imprimante A...">
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label fw-semibold">Notes</label>
|
||||||
|
<textarea name="notes" class="form-control" rows="2">{{ profile.notes if profile else '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header py-3"><i class="bi bi-printer me-2"></i>Imprimante</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">Prix d'achat (€)</label>
|
||||||
|
<input type="number" name="printer_price" class="form-control" step="0.01" min="0" required
|
||||||
|
value="{{ profile.printer_price if profile else settings.printer_price }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">Duree de vie (heures)</label>
|
||||||
|
<input type="number" name="printer_lifespan_hours" class="form-control" step="1" min="1" required
|
||||||
|
value="{{ profile.printer_lifespan_hours|int if profile else settings.printer_lifespan_hours|int }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header py-3"><i class="bi bi-circle me-2"></i>Buse (nozzle)</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">Prix de la buse (€)</label>
|
||||||
|
<input type="number" name="nozzle_price" class="form-control" step="0.01" min="0" required
|
||||||
|
value="{{ profile.nozzle_price if profile else settings.nozzle_price }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">Duree de vie (heures)</label>
|
||||||
|
<input type="number" name="nozzle_lifespan_hours" class="form-control" step="1" min="1" required
|
||||||
|
value="{{ profile.nozzle_lifespan_hours|int if profile else settings.nozzle_lifespan_hours|int }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header py-3"><i class="bi bi-square me-2"></i>Plateau (plate)</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">Prix du plateau (€)</label>
|
||||||
|
<input type="number" name="plate_price" class="form-control" step="0.01" min="0" required
|
||||||
|
value="{{ profile.plate_price if profile else settings.plate_price }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">Duree de vie (heures)</label>
|
||||||
|
<input type="number" name="plate_lifespan_hours" class="form-control" step="1" min="1" required
|
||||||
|
value="{{ profile.plate_lifespan_hours|int if profile else settings.plate_lifespan_hours|int }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header py-3"><i class="bi bi-lightning me-2"></i>Electricite</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">Puissance imprimante (kW)</label>
|
||||||
|
<input type="number" name="printer_power_kw" class="form-control" step="0.01" min="0" required
|
||||||
|
value="{{ profile.printer_power_kw if profile else settings.printer_power_kw }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold">Prix electricite (€/kWh)</label>
|
||||||
|
<input type="number" name="electricity_price_kwh" class="form-control" step="0.001" min="0" required
|
||||||
|
value="{{ profile.electricity_price_kwh if profile else settings.electricity_price_kwh }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn px-4 fw-bold" style="background:#ff6b35;color:#fff">
|
||||||
|
<i class="bi bi-save me-1"></i>Enregistrer
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('profiles') }}#machine" class="btn btn-outline-secondary">Annuler</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}{% if profile %}Modifier{% else %}Nouveau{% endif %} profil matiere{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<a href="{{ url_for('profiles') }}#material" class="text-muted text-decoration-none small">
|
||||||
|
<i class="bi bi-arrow-left"></i> Templates
|
||||||
|
</a>
|
||||||
|
<h1 class="mt-1">
|
||||||
|
<i class="bi bi-box me-2" style="color:#ff6b35"></i>
|
||||||
|
{% if profile %}Modifier{% else %}Nouveau{% endif %} profil matiere
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header py-3">Parametres</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nom du profil</label>
|
||||||
|
<input type="text" name="name" class="form-control" required
|
||||||
|
value="{{ profile.name if profile else '' }}"
|
||||||
|
placeholder="ex: PLA standard, Filament technique, Flexible...">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Marge sur matiere (%)</label>
|
||||||
|
<input type="number" name="material_margin_pct" class="form-control" step="0.5" min="0" max="200" required
|
||||||
|
value="{{ profile.material_margin_pct if profile else settings.material_margin_pct }}">
|
||||||
|
<div class="form-text">Applique sur le cout matiere brut (prix/kg x poids).</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Notes</label>
|
||||||
|
<textarea name="notes" class="form-control" rows="2">{{ profile.notes if profile else '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn px-4 fw-bold" style="background:#ff6b35;color:#fff">
|
||||||
|
<i class="bi bi-save me-1"></i>Enregistrer
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('profiles') }}#material" class="btn btn-outline-secondary">Annuler</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
+236
-66
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<div class="col-lg-7">
|
<div class="col-lg-7">
|
||||||
|
|
||||||
<!-- Import 3mf -->
|
<!-- Import 3mf -->
|
||||||
<div class="card mb-3 border border-warning border-opacity-50">
|
<div class="card mb-3 border border-warning border-opacity-50">
|
||||||
<div class="card-header py-3">
|
<div class="card-header py-3">
|
||||||
@@ -22,22 +23,26 @@
|
|||||||
</button>
|
</button>
|
||||||
<span id="parseStatus" class="text-muted small"></span>
|
<span id="parseStatus" class="text-muted small"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-text mt-1">Glissez votre fichier .3mf depuis Bambu Studio pour remplir poids et duree automatiquement.</div>
|
<div class="form-text mt-1">Votre fichier .3mf depuis Bambu Studio pour remplir poids et duree automatiquement.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST" id="jobForm">
|
<form method="POST" id="jobForm">
|
||||||
|
|
||||||
|
<!-- Donnees impression -->
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header py-3">Donnees impression</div>
|
<div class="card-header py-3">Donnees impression</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label class="form-label fw-semibold">Nom de la commande</label>
|
<label class="form-label fw-semibold">Nom de la commande</label>
|
||||||
<input type="text" name="name" id="jobName" class="form-control" required placeholder="ex: Marqueurs competition salle X">
|
<input type="text" name="name" id="jobName" class="form-control" required
|
||||||
|
placeholder="ex: Marqueurs competition salle X">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label fw-semibold">Fichier source</label>
|
<label class="form-label fw-semibold">Fichier source</label>
|
||||||
<input type="text" name="source_file" id="sourceFile" class="form-control" placeholder="nom_fichier.3mf">
|
<input type="text" name="source_file" id="sourceFile" class="form-control"
|
||||||
|
placeholder="nom_fichier.3mf">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
@@ -51,12 +56,13 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label fw-semibold">Matiere</label>
|
<label class="form-label fw-semibold">Matiere (filament)</label>
|
||||||
<select name="material_id" id="materialSelect" class="form-select">
|
<select name="material_id" id="materialSelect" class="form-select">
|
||||||
<option value="">Prix global (parametres)</option>
|
<option value="">Prix global (parametres)</option>
|
||||||
{% for m in materials %}
|
{% for m in materials %}
|
||||||
<option value="{{ m.id }}" data-price="{{ m.price_per_kg }}" data-stock="{{ m.stock_g }}" data-threshold="{{ m.low_stock_threshold_g }}">
|
<option value="{{ m.id }}" data-price="{{ m.price_per_kg }}"
|
||||||
{{ m.name }} ({{ m.type }}) - {{ m.price_per_kg }}EUR/kg
|
data-stock="{{ m.stock_g }}" data-threshold="{{ m.low_stock_threshold_g }}">
|
||||||
|
{{ m.name }} ({{ m.type }}) — {{ m.price_per_kg }} €/kg
|
||||||
{% if m.stock_g <= 0 %}[RUPTURE]{% elif m.stock_g <= m.low_stock_threshold_g %}[stock bas]{% endif %}
|
{% if m.stock_g <= 0 %}[RUPTURE]{% elif m.stock_g <= m.low_stock_threshold_g %}[stock bas]{% endif %}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -67,7 +73,8 @@
|
|||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label fw-semibold">Poids filament (g)</label>
|
<label class="form-label fw-semibold">Poids filament (g)</label>
|
||||||
<input type="number" name="weight_g" id="weight_g" class="form-control" step="0.01" min="0.1" required placeholder="423.97">
|
<input type="number" name="weight_g" id="weight_g" class="form-control"
|
||||||
|
step="0.01" min="0.1" required placeholder="423.97">
|
||||||
<div class="form-text">Champ Modele dans Bambu</div>
|
<div class="form-text">Champ Modele dans Bambu</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@@ -83,6 +90,71 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Profils -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<i class="bi bi-bookmark-star me-2"></i>Profils
|
||||||
|
<a href="{{ url_for('profiles') }}" class="btn btn-sm btn-outline-secondary ms-2 py-0" target="_blank">
|
||||||
|
<i class="bi bi-pencil-square"></i> Gerer
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold small">
|
||||||
|
<i class="bi bi-cpu me-1 text-muted"></i>Profil machine
|
||||||
|
</label>
|
||||||
|
<select name="machine_profile_id" id="machineProfileSelect" class="form-select form-select-sm">
|
||||||
|
<option value="">— Parametres globaux —</option>
|
||||||
|
{% for p in machine_profiles %}
|
||||||
|
<option value="{{ p.id }}">{{ p.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold small">
|
||||||
|
<i class="bi bi-hand-index me-1 text-muted"></i>Profil manutention
|
||||||
|
</label>
|
||||||
|
<select name="handling_profile_id" id="handlingProfileSelect" class="form-select form-select-sm">
|
||||||
|
<option value="">— Parametres globaux —</option>
|
||||||
|
{% for p in handling_profiles %}
|
||||||
|
<option value="{{ p.id }}">{{ p.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold small">
|
||||||
|
<i class="bi bi-box me-1 text-muted"></i>Profil matiere
|
||||||
|
</label>
|
||||||
|
<select name="material_profile_id" id="materialProfileSelect" class="form-select form-select-sm">
|
||||||
|
<option value="">— Parametres globaux —</option>
|
||||||
|
{% for p in material_profiles %}
|
||||||
|
<option value="{{ p.id }}" data-margin="{{ p.material_margin_pct }}">
|
||||||
|
{{ p.name }} ({{ p.material_margin_pct }}%)
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-semibold small">
|
||||||
|
<i class="bi bi-tags me-1 text-muted"></i>Profil tarif client
|
||||||
|
</label>
|
||||||
|
<select name="pricing_profile_id" id="pricingProfileSelect" class="form-select form-select-sm">
|
||||||
|
<option value="">— Manuel —</option>
|
||||||
|
{% for p in pricing_profiles %}
|
||||||
|
<option value="{{ p.id }}"
|
||||||
|
data-dm="{{ p.design_multiplier }}"
|
||||||
|
data-margin="{{ p.gross_margin_pct }}">
|
||||||
|
{{ p.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Parametres tarifaires -->
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header py-3">Parametres tarifaires</div>
|
<div class="card-header py-3">Parametres tarifaires</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -90,22 +162,31 @@
|
|||||||
<label class="form-label fw-semibold">
|
<label class="form-label fw-semibold">
|
||||||
Coefficient design <span class="badge bg-secondary ms-1" id="dmLabel">x1.80</span>
|
Coefficient design <span class="badge bg-secondary ms-1" id="dmLabel">x1.80</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="d-flex gap-2 mb-2">
|
<div class="d-flex gap-2 mb-2" id="dmButtons">
|
||||||
<button type="button" class="btn btn-sm btn-outline-success" onclick="setDM(0.80)">STL fourni (x0.80)</button>
|
<button type="button" id="btnDM080" class="btn btn-sm btn-outline-success" onclick="setDM(0.80)">
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="setDM(1.80)">Creation complete (x1.80)</button>
|
<i class="bi bi-check2 me-1 d-none" id="checkDM080"></i>STL fourni (x0.80)
|
||||||
|
</button>
|
||||||
|
<button type="button" id="btnDM180" class="btn btn-sm btn-outline-primary" onclick="setDM(1.80)">
|
||||||
|
<i class="bi bi-check2 me-1 d-none" id="checkDM180"></i>Creation complete (x1.80)
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<input type="number" name="design_multiplier" id="design_multiplier" class="form-control form-control-sm" step="0.05" min="0" value="1.80">
|
<input type="number" name="design_multiplier" id="design_multiplier"
|
||||||
|
class="form-control form-control-sm" step="0.05" min="0" value="1.80">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="form-label fw-semibold d-flex justify-content-between">
|
<label class="form-label fw-semibold d-flex justify-content-between">
|
||||||
Marge brute <span id="marginDisplay" class="text-muted">27.5 %</span>
|
Marge brute <span id="marginDisplay" class="fw-bold" style="color:#ff6b35">27.5 %</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="range" name="gross_margin_pct" id="gross_margin_pct" class="form-range" min="20" max="35" step="0.5" value="27.5">
|
<input type="range" name="gross_margin_pct" id="gross_margin_pct" class="form-range"
|
||||||
<div class="d-flex justify-content-between" style="font-size:.75rem;color:#6b7280"><span>20%</span><span>35%</span></div>
|
min="20" max="35" step="0.5" value="27.5">
|
||||||
|
<div class="d-flex justify-content-between" style="font-size:.75rem;color:#6b7280">
|
||||||
|
<span>20%</span><span>35%</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Remise client (%)</label>
|
<label class="form-label fw-semibold">Remise client (%)</label>
|
||||||
<input type="number" name="discount_pct" id="discount_pct" class="form-control" min="0" max="100" step="0.5" value="0">
|
<input type="number" name="discount_pct" id="discount_pct" class="form-control"
|
||||||
|
min="0" max="100" step="0.5" value="0">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Notes internes</label>
|
<label class="form-label fw-semibold">Notes internes</label>
|
||||||
@@ -120,9 +201,12 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Recap temps reel -->
|
||||||
<div class="col-lg-5">
|
<div class="col-lg-5">
|
||||||
<div class="card sticky-top" style="top:1.5rem">
|
<div class="card sticky-top" style="top:1.5rem">
|
||||||
<div class="card-header py-3"><i class="bi bi-receipt me-2"></i>Recapitulatif temps reel</div>
|
<div class="card-header py-3">
|
||||||
|
<i class="bi bi-receipt me-2"></i>Recapitulatif temps reel
|
||||||
|
</div>
|
||||||
<div class="card-body" id="breakdown">
|
<div class="card-body" id="breakdown">
|
||||||
<div class="text-center text-muted py-4">
|
<div class="text-center text-muted py-4">
|
||||||
<i class="bi bi-calculator fs-2"></i>
|
<i class="bi bi-calculator fs-2"></i>
|
||||||
@@ -136,6 +220,7 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
|
// ── 3MF parser ────────────────────────────────────────────────────────────────
|
||||||
function parse3mf() {
|
function parse3mf() {
|
||||||
const file = document.getElementById('file3mf').files[0];
|
const file = document.getElementById('file3mf').files[0];
|
||||||
if (!file) { alert('Selectionnez un fichier .3mf'); return; }
|
if (!file) { alert('Selectionnez un fichier .3mf'); return; }
|
||||||
@@ -147,87 +232,172 @@ function parse3mf() {
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(d => {
|
.then(d => {
|
||||||
let filled = [];
|
let filled = [];
|
||||||
if (d.weight_g) { document.getElementById('weight_g').value = d.weight_g; filled.push('poids'); }
|
if (d.weight_g) { document.getElementById('weight_g').value = d.weight_g; filled.push('poids'); }
|
||||||
if (d.hours != null){ document.getElementById('hours').value = d.hours; filled.push('heures'); }
|
if (d.hours != null) { document.getElementById('hours').value = d.hours; filled.push('heures'); }
|
||||||
if (d.minutes != null){ document.getElementById('minutes').value = d.minutes; }
|
if (d.minutes != null){ document.getElementById('minutes').value = d.minutes; }
|
||||||
if (d.filename) {
|
if (d.filename) {
|
||||||
document.getElementById('sourceFile').value = d.filename;
|
document.getElementById('sourceFile').value = d.filename;
|
||||||
if (!document.querySelector('[name=name]').value)
|
if (!document.getElementById('jobName').value)
|
||||||
document.querySelector('[name=name]').value = d.filename;
|
document.getElementById('jobName').value = d.filename;
|
||||||
}
|
}
|
||||||
status.textContent = filled.length ? 'Extrait: '+filled.join(', ') : 'Donnees non trouvees';
|
status.textContent = filled.length ? 'Extrait: ' + filled.join(', ') : 'Donnees non trouvees';
|
||||||
status.style.color = filled.length ? '#22c55e' : '#f59e0b';
|
status.style.color = filled.length ? '#22c55e' : '#f59e0b';
|
||||||
recalc();
|
recalc();
|
||||||
})
|
})
|
||||||
.catch(() => { status.textContent = 'Erreur de lecture'; status.style.color='#ef4444'; });
|
.catch(() => { status.textContent = 'Erreur de lecture'; status.style.color = '#ef4444'; });
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('materialSelect').addEventListener('change', function() {
|
// ── Coefficient design ────────────────────────────────────────────────────────
|
||||||
const opt = this.options[this.selectedIndex];
|
|
||||||
const stock = parseFloat(opt.dataset.stock)||0;
|
|
||||||
const threshold = parseFloat(opt.dataset.threshold)||200;
|
|
||||||
const info = document.getElementById('stockInfo');
|
|
||||||
if (!opt.value) { info.textContent=''; return; }
|
|
||||||
if (stock<=0) info.innerHTML='<span class="text-danger">RUPTURE DE STOCK</span>';
|
|
||||||
else if (stock<=threshold) info.innerHTML='<span class="text-warning">Stock bas: '+stock.toFixed(0)+' g</span>';
|
|
||||||
else info.innerHTML='<span class="text-success">Stock: '+stock.toFixed(0)+' g</span>';
|
|
||||||
recalc();
|
|
||||||
});
|
|
||||||
|
|
||||||
function setDM(val) {
|
function setDM(val) {
|
||||||
document.getElementById('design_multiplier').value = val;
|
document.getElementById('design_multiplier').value = val;
|
||||||
document.getElementById('dmLabel').textContent = 'x'+val.toFixed(2);
|
updateDMLabel(val);
|
||||||
recalc();
|
recalc();
|
||||||
}
|
}
|
||||||
document.getElementById('gross_margin_pct').addEventListener('input', function(){
|
|
||||||
document.getElementById('marginDisplay').textContent = this.value+' %'; recalc();
|
function updateDMLabel(val) {
|
||||||
|
val = parseFloat(val);
|
||||||
|
document.getElementById('dmLabel').textContent = 'x' + val.toFixed(2);
|
||||||
|
// Etat actif des boutons
|
||||||
|
const is080 = Math.abs(val - 0.80) < 0.001;
|
||||||
|
const is180 = Math.abs(val - 1.80) < 0.001;
|
||||||
|
document.getElementById('btnDM080').className = 'btn btn-sm ' + (is080 ? 'btn-success' : 'btn-outline-success');
|
||||||
|
document.getElementById('btnDM180').className = 'btn btn-sm ' + (is180 ? 'btn-primary' : 'btn-outline-primary');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('design_multiplier').addEventListener('input', function() {
|
||||||
|
updateDMLabel(this.value);
|
||||||
|
recalc();
|
||||||
});
|
});
|
||||||
document.getElementById('design_multiplier').addEventListener('input', function(){
|
|
||||||
document.getElementById('dmLabel').textContent = 'x'+parseFloat(this.value).toFixed(2); recalc();
|
// ── Marge ─────────────────────────────────────────────────────────────────────
|
||||||
|
function updateMarginDisplay(val) {
|
||||||
|
document.getElementById('marginDisplay').textContent = parseFloat(val).toFixed(1) + ' %';
|
||||||
|
}
|
||||||
|
|
||||||
|
const marginSlider = document.getElementById('gross_margin_pct');
|
||||||
|
marginSlider.addEventListener('input', function() { updateMarginDisplay(this.value); recalc(); });
|
||||||
|
marginSlider.addEventListener('change', function() { updateMarginDisplay(this.value); recalc(); });
|
||||||
|
|
||||||
|
// ── Profil tarif client ───────────────────────────────────────────────────────
|
||||||
|
document.getElementById('pricingProfileSelect').addEventListener('change', function() {
|
||||||
|
const opt = this.options[this.selectedIndex];
|
||||||
|
if (opt.value) {
|
||||||
|
const dm = parseFloat(opt.dataset.dm);
|
||||||
|
const margin = parseFloat(opt.dataset.margin);
|
||||||
|
if (!isNaN(dm)) {
|
||||||
|
document.getElementById('design_multiplier').value = dm;
|
||||||
|
updateDMLabel(dm);
|
||||||
|
}
|
||||||
|
if (!isNaN(margin)) {
|
||||||
|
marginSlider.value = margin;
|
||||||
|
updateMarginDisplay(margin);
|
||||||
|
}
|
||||||
|
recalc();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
document.getElementById('clientSelect').addEventListener('change', function(){
|
|
||||||
|
// ── Client : auto-remplir DM ──────────────────────────────────────────────────
|
||||||
|
document.getElementById('clientSelect').addEventListener('change', function() {
|
||||||
const dm = this.options[this.selectedIndex].dataset.dm;
|
const dm = this.options[this.selectedIndex].dataset.dm;
|
||||||
if (dm) setDM(parseFloat(dm));
|
if (dm) {
|
||||||
|
document.getElementById('design_multiplier').value = parseFloat(dm);
|
||||||
|
updateDMLabel(parseFloat(dm));
|
||||||
|
recalc();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Matiere : afficher stock ──────────────────────────────────────────────────
|
||||||
|
document.getElementById('materialSelect').addEventListener('change', function() {
|
||||||
|
const opt = this.options[this.selectedIndex];
|
||||||
|
const stock = parseFloat(opt.dataset.stock) || 0;
|
||||||
|
const threshold = parseFloat(opt.dataset.threshold) || 200;
|
||||||
|
const info = document.getElementById('stockInfo');
|
||||||
|
if (!opt.value) { info.textContent = ''; }
|
||||||
|
else if (stock <= 0) info.innerHTML = '<span class="text-danger">RUPTURE DE STOCK</span>';
|
||||||
|
else if (stock <= threshold) info.innerHTML = '<span class="text-warning">Stock bas : ' + stock.toFixed(0) + ' g</span>';
|
||||||
|
else info.innerHTML = '<span class="text-success">Stock : ' + stock.toFixed(0) + ' g</span>';
|
||||||
|
recalc();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Autres champs → recalc ────────────────────────────────────────────────────
|
||||||
['weight_g','hours','minutes','discount_pct'].forEach(id => {
|
['weight_g','hours','minutes','discount_pct'].forEach(id => {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) el.addEventListener('input', recalc);
|
if (el) el.addEventListener('input', recalc);
|
||||||
});
|
});
|
||||||
|
['machineProfileSelect','handlingProfileSelect','materialProfileSelect'].forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.addEventListener('change', recalc);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Calcul API ────────────────────────────────────────────────────────────────
|
||||||
function recalc() {
|
function recalc() {
|
||||||
const w = parseFloat(document.getElementById('weight_g').value)||0;
|
const w = parseFloat(document.getElementById('weight_g').value) || 0;
|
||||||
const h = parseInt(document.getElementById('hours').value)||0;
|
const h = parseInt(document.getElementById('hours').value) || 0;
|
||||||
const m = parseInt(document.getElementById('minutes').value)||0;
|
const m = parseInt(document.getElementById('minutes').value) || 0;
|
||||||
if (w<=0 || (h===0 && m===0)) return;
|
if (w <= 0 && h === 0 && m === 0) return;
|
||||||
|
if (w <= 0) return;
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
weight_g: w, hours: h, minutes: m,
|
weight_g: w,
|
||||||
design_multiplier: parseFloat(document.getElementById('design_multiplier').value)||1.80,
|
hours: h,
|
||||||
gross_margin_pct: parseFloat(document.getElementById('gross_margin_pct').value)||27.5,
|
minutes: m,
|
||||||
discount_pct: parseFloat(document.getElementById('discount_pct').value)||0,
|
design_multiplier: parseFloat(document.getElementById('design_multiplier').value) || 1.80,
|
||||||
material_id: document.getElementById('materialSelect').value||null,
|
gross_margin_pct: parseFloat(document.getElementById('gross_margin_pct').value) || 27.5,
|
||||||
|
discount_pct: parseFloat(document.getElementById('discount_pct').value) || 0,
|
||||||
|
material_id: document.getElementById('materialSelect').value || null,
|
||||||
|
machine_profile_id: document.getElementById('machineProfileSelect').value || null,
|
||||||
|
handling_profile_id: document.getElementById('handlingProfileSelect').value || null,
|
||||||
|
material_profile_id: document.getElementById('materialProfileSelect').value || null,
|
||||||
};
|
};
|
||||||
fetch('/api/calculate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
|
|
||||||
.then(r=>r.json()).then(d=>renderBreakdown(d,body));
|
fetch('/api/calculate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => renderBreakdown(d, body))
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmt(n) { return parseFloat(n).toFixed(4)+' EUR'; }
|
// ── Rendu du recap ────────────────────────────────────────────────────────────
|
||||||
function fmt2(n) { return parseFloat(n).toFixed(2)+' EUR'; }
|
function fmt(n) { return parseFloat(n).toFixed(4) + ' €'; }
|
||||||
|
function fmt2(n) { return parseFloat(n).toFixed(2) + ' €'; }
|
||||||
|
|
||||||
function renderBreakdown(d, body) {
|
function renderBreakdown(d, body) {
|
||||||
const dis = body.discount_pct>0
|
const dm = parseFloat(body.design_multiplier).toFixed(2);
|
||||||
? '<div class="breakdown-row text-danger"><span>Remise ('+body.discount_pct+'%)</span></div>' : '';
|
const mgn = parseFloat(body.gross_margin_pct).toFixed(1);
|
||||||
|
const dis = body.discount_pct > 0
|
||||||
|
? `<div class="breakdown-row text-danger"><span>Remise (${body.discount_pct}%)</span><span>-${fmt2(d.final_price / (1 - body.discount_pct/100) * (body.discount_pct/100))}</span></div>` : '';
|
||||||
|
const tvaRow = d._tva_pct > 0
|
||||||
|
? `<div class="breakdown-row"><span class="breakdown-muted">TVA (${d._tva_pct}%)</span><span>${fmt2(d.tva_amount)}</span></div>` : '';
|
||||||
|
const vflRow = d._vfl_pct > 0
|
||||||
|
? `<div class="breakdown-row"><span class="breakdown-muted">VFL impot (${d._vfl_pct}%)</span><span>${fmt2(d.vfl_amount)}</span></div>` : '';
|
||||||
|
|
||||||
document.getElementById('breakdown').innerHTML = `
|
document.getElementById('breakdown').innerHTML = `
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Matiere</span><span>${fmt(d.material_cost)}</span></div>
|
<div class="breakdown-row section-header"><span>Cout fixe</span></div>
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Marge matiere (20%)</span><span>${fmt(d.material_margin)}</span></div>
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Matiere</span><span>${fmt(d.material_cost)}</span></div>
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Design (x${body.design_multiplier.toFixed(2)})</span><span>${fmt(d.design_cost)}</span></div>
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Marge matiere</span><span>${fmt(d.material_margin)}</span></div>
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Manutention</span><span>${fmt(d.handling_cost)}</span></div>
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Usure machine</span><span>${fmt(d.wear_cost)}</span></div>
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Usure machine</span><span>${fmt(d.wear_cost)}</span></div>
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Electricite</span><span>${fmt(d.electricity_cost)}</span></div>
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Electricite</span><span>${fmt(d.electricity_cost)}</span></div>
|
<div class="breakdown-row subtotal"><span class="fw-semibold">Total cout fixe</span><span class="fw-semibold">${fmt2(d.cout_fixe)}</span></div>
|
||||||
<div class="breakdown-row subtotal"><span class="fw-semibold">Sous-total couts</span><span class="fw-semibold">${fmt2(d.subtotal)}</span></div>
|
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Marge brute (${body.gross_margin_pct}%)</span><span>${fmt2(d.margin_amount)}</span></div>
|
<div class="breakdown-row section-header"><span>Partie variable</span></div>
|
||||||
<div class="breakdown-row"><span class="breakdown-muted">Cotisations (12.3%)</span><span>${fmt2(d.tax_amount)}</span></div>
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Manutention</span><span>${fmt(d.handling_cost)}</span></div>
|
||||||
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Design (x${dm})</span><span>${fmt(d.design_cost)}</span></div>
|
||||||
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Marge brute (${mgn}%)</span><span>${fmt2(d.margin_amount)}</span></div>
|
||||||
|
<div class="breakdown-row subtotal"><span class="fw-semibold">Total marge</span><span class="fw-semibold">${fmt2(d.total_marge)}</span></div>
|
||||||
|
|
||||||
|
<div class="breakdown-row section-header"><span>Fiscal</span></div>
|
||||||
|
<div class="breakdown-row"><span class="breakdown-muted ps-2">Cotisations (${d._cot_pct}%)</span><span>${fmt2(d.cotisations_amount)}</span></div>
|
||||||
|
${vflRow}
|
||||||
|
${tvaRow}
|
||||||
${dis}
|
${dis}
|
||||||
<div class="breakdown-row total"><span>PRIX FINAL</span><span class="final-price">${fmt2(d.final_price)}</span></div>`;
|
<div class="breakdown-row total"><span>PRIX FINAL</span><span class="final-price">${fmt2(d.final_price)}</span></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Init
|
||||||
|
updateDMLabel(1.80);
|
||||||
|
updateMarginDisplay(27.5);
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}{% if profile %}Modifier{% else %}Nouveau{% endif %} profil tarif{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<a href="{{ url_for('profiles') }}#pricing" class="text-muted text-decoration-none small">
|
||||||
|
<i class="bi bi-arrow-left"></i> Templates
|
||||||
|
</a>
|
||||||
|
<h1 class="mt-1">
|
||||||
|
<i class="bi bi-tags me-2" style="color:#ff6b35"></i>
|
||||||
|
{% if profile %}Modifier{% else %}Nouveau{% endif %} profil tarif client
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<form method="POST">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header py-3">Parametres</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nom du profil</label>
|
||||||
|
<input type="text" name="name" class="form-control" required
|
||||||
|
value="{{ profile.name if profile else '' }}"
|
||||||
|
placeholder="ex: Client STL, Client standard, Client VIP...">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Coefficient design</label>
|
||||||
|
<div class="d-flex gap-2 mb-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-success" onclick="setDM(0.80)">STL fourni (x0.80)</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="setDM(1.80)">Creation complete (x1.80)</button>
|
||||||
|
</div>
|
||||||
|
<input type="number" name="design_multiplier" id="design_multiplier" class="form-control" step="0.05" min="0" required
|
||||||
|
value="{{ profile.design_multiplier if profile else 1.80 }}">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold d-flex justify-content-between">
|
||||||
|
Marge brute <span id="marginDisplay" class="text-muted">
|
||||||
|
{{ profile.gross_margin_pct if profile else 27.5 }} %
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" name="gross_margin_pct" id="gross_margin_pct" class="form-range"
|
||||||
|
min="20" max="35" step="0.5"
|
||||||
|
value="{{ profile.gross_margin_pct if profile else 27.5 }}">
|
||||||
|
<div class="d-flex justify-content-between" style="font-size:.75rem;color:#6b7280"><span>20%</span><span>35%</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Notes</label>
|
||||||
|
<textarea name="notes" class="form-control" rows="2">{{ profile.notes if profile else '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn px-4 fw-bold" style="background:#ff6b35;color:#fff">
|
||||||
|
<i class="bi bi-save me-1"></i>Enregistrer
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('profiles') }}#pricing" class="btn btn-outline-secondary">Annuler</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
function setDM(val) {
|
||||||
|
document.getElementById('design_multiplier').value = val;
|
||||||
|
}
|
||||||
|
document.getElementById('gross_margin_pct').addEventListener('input', function() {
|
||||||
|
document.getElementById('marginDisplay').textContent = this.value + ' %';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}Templates / Profils{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header d-flex justify-content-between align-items-center">
|
||||||
|
<h1><i class="bi bi-bookmark-star me-2" style="color:#ff6b35"></i>Templates & Profils</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs mb-4" id="profileTabs" role="tablist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if not request.args.get('tab') or request.args.get('tab')=='machine' %}active{% endif %}"
|
||||||
|
href="#machine" data-bs-toggle="tab" data-bs-target="#machine">
|
||||||
|
<i class="bi bi-cpu me-1"></i>Machine
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.args.get('tab')=='handling' %}active{% endif %}"
|
||||||
|
href="#handling" data-bs-toggle="tab" data-bs-target="#handling">
|
||||||
|
<i class="bi bi-hand-index me-1"></i>Manutention
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.args.get('tab')=='material' %}active{% endif %}"
|
||||||
|
href="#material" data-bs-toggle="tab" data-bs-target="#material">
|
||||||
|
<i class="bi bi-box me-1"></i>Matiere
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if request.args.get('tab')=='pricing' %}active{% endif %}"
|
||||||
|
href="#pricing" data-bs-toggle="tab" data-bs-target="#pricing">
|
||||||
|
<i class="bi bi-tags me-1"></i>Tarif client
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
|
||||||
|
<!-- Machine -->
|
||||||
|
<div class="tab-pane fade {% if not request.args.get('tab') or request.args.get('tab')=='machine' %}show active{% endif %}" id="machine">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<p class="text-muted mb-0 small">Imprimante, buse, plateau et electricite. Remplace les parametres globaux lors du calcul.</p>
|
||||||
|
<a href="{{ url_for('new_machine_profile') }}" class="btn btn-sm" style="background:#ff6b35;color:#fff">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i>Nouveau
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% if machine_profiles %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th>Imprimante</th>
|
||||||
|
<th>Buse</th>
|
||||||
|
<th>Plateau</th>
|
||||||
|
<th>Puissance</th>
|
||||||
|
<th>Electricite</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in machine_profiles %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-semibold">{{ p.name }}</td>
|
||||||
|
<td class="small text-muted">{{ p.printer_price }}€ / {{ p.printer_lifespan_hours|int }}h</td>
|
||||||
|
<td class="small text-muted">{{ p.nozzle_price }}€ / {{ p.nozzle_lifespan_hours|int }}h</td>
|
||||||
|
<td class="small text-muted">{{ p.plate_price }}€ / {{ p.plate_lifespan_hours|int }}h</td>
|
||||||
|
<td class="small text-muted">{{ p.printer_power_kw }} kW</td>
|
||||||
|
<td class="small text-muted">{{ p.electricity_price_kwh }} €/kWh</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="{{ url_for('edit_machine_profile', id=p.id) }}" class="btn btn-sm btn-outline-secondary py-0 me-1">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="{{ url_for('delete_machine_profile', id=p.id) }}" class="d-inline"
|
||||||
|
onsubmit="return confirm('Supprimer ce profil ?')">
|
||||||
|
<button class="btn btn-sm btn-outline-danger py-0"><i class="bi bi-trash"></i></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card"><div class="card-body text-center text-muted py-4">
|
||||||
|
<i class="bi bi-cpu fs-2"></i><p class="mt-2 small">Aucun profil machine.</p>
|
||||||
|
</div></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manutention -->
|
||||||
|
<div class="tab-pane fade {% if request.args.get('tab')=='handling' %}show active{% endif %}" id="handling">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<p class="text-muted mb-0 small">Taux horaire et temps de preparation par plateau.</p>
|
||||||
|
<a href="{{ url_for('new_handling_profile') }}" class="btn btn-sm" style="background:#ff6b35;color:#fff">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i>Nouveau
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% if handling_profiles %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr><th>Nom</th><th>Taux horaire</th><th>Minutes / plateau</th><th>Cout fixe par job</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in handling_profiles %}
|
||||||
|
{% set cost = (p.handling_rate_per_hour / 60) * p.handling_minutes_per_plate %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-semibold">{{ p.name }}</td>
|
||||||
|
<td>{{ p.handling_rate_per_hour }} €/h</td>
|
||||||
|
<td>{{ p.handling_minutes_per_plate }} min</td>
|
||||||
|
<td class="fw-semibold" style="color:#ff6b35">{{ "%.2f"|format(cost) }} €</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="{{ url_for('edit_handling_profile', id=p.id) }}" class="btn btn-sm btn-outline-secondary py-0 me-1">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="{{ url_for('delete_handling_profile', id=p.id) }}" class="d-inline"
|
||||||
|
onsubmit="return confirm('Supprimer ce profil ?')">
|
||||||
|
<button class="btn btn-sm btn-outline-danger py-0"><i class="bi bi-trash"></i></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card"><div class="card-body text-center text-muted py-4">
|
||||||
|
<i class="bi bi-hand-index fs-2"></i><p class="mt-2 small">Aucun profil manutention.</p>
|
||||||
|
</div></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Matiere -->
|
||||||
|
<div class="tab-pane fade {% if request.args.get('tab')=='material' %}show active{% endif %}" id="material">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<p class="text-muted mb-0 small">Marge appliquee sur le cout matiere. Utile pour differencier les types de filament (standard, technique, flexible...).</p>
|
||||||
|
<a href="{{ url_for('new_material_profile') }}" class="btn btn-sm" style="background:#ff6b35;color:#fff">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i>Nouveau
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% if material_profiles %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr><th>Nom</th><th>Marge matiere</th><th>Notes</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in material_profiles %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-semibold">{{ p.name }}</td>
|
||||||
|
<td><span class="badge bg-secondary">{{ p.material_margin_pct }} %</span></td>
|
||||||
|
<td class="small text-muted">{{ p.notes or '' }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="{{ url_for('edit_material_profile', id=p.id) }}" class="btn btn-sm btn-outline-secondary py-0 me-1">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="{{ url_for('delete_material_profile', id=p.id) }}" class="d-inline"
|
||||||
|
onsubmit="return confirm('Supprimer ce profil ?')">
|
||||||
|
<button class="btn btn-sm btn-outline-danger py-0"><i class="bi bi-trash"></i></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card"><div class="card-body text-center text-muted py-4">
|
||||||
|
<i class="bi bi-box fs-2"></i><p class="mt-2 small">Aucun profil matiere.</p>
|
||||||
|
</div></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tarif client -->
|
||||||
|
<div class="tab-pane fade {% if request.args.get('tab')=='pricing' %}show active{% endif %}" id="pricing">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<p class="text-muted mb-0 small">Coefficient design et marge brute. Creer un profil par type de relation client.</p>
|
||||||
|
<a href="{{ url_for('new_pricing_profile') }}" class="btn btn-sm" style="background:#ff6b35;color:#fff">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i>Nouveau
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% if pricing_profiles %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr><th>Nom</th><th>Coeff design</th><th>Marge brute</th><th>Notes</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in pricing_profiles %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-semibold">{{ p.name }}</td>
|
||||||
|
<td><span class="badge bg-primary">x{{ p.design_multiplier }}</span></td>
|
||||||
|
<td><span class="badge" style="background:#ff6b35">{{ p.gross_margin_pct }} %</span></td>
|
||||||
|
<td class="small text-muted">{{ p.notes or '' }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="{{ url_for('edit_pricing_profile', id=p.id) }}" class="btn btn-sm btn-outline-secondary py-0 me-1">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="{{ url_for('delete_pricing_profile', id=p.id) }}" class="d-inline"
|
||||||
|
onsubmit="return confirm('Supprimer ce profil ?')">
|
||||||
|
<button class="btn btn-sm btn-outline-danger py-0"><i class="bi bi-trash"></i></button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card"><div class="card-body text-center text-muted py-4">
|
||||||
|
<i class="bi bi-tags fs-2"></i><p class="mt-2 small">Aucun profil tarif client.</p>
|
||||||
|
</div></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Activer l'onglet correspondant au hash ou query param
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const hash = window.location.hash;
|
||||||
|
if (hash) {
|
||||||
|
const tab = document.querySelector('[data-bs-target="' + hash + '"]');
|
||||||
|
if (tab) new bootstrap.Tab(tab).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
+45
-13
@@ -1,9 +1,10 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}Paramètres{% endblock %}
|
{% block title %}Parametres{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1><i class="bi bi-sliders me-2 text-secondary"></i>Paramètres</h1>
|
<h1><i class="bi bi-sliders me-2 text-secondary"></i>Parametres globaux</h1>
|
||||||
|
<p class="text-muted small mt-1">Valeurs par defaut — remplacees si un profil est selectionne lors du calcul.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
@@ -19,10 +20,10 @@
|
|||||||
step="0.01" value="{{ settings.filament_price_per_kg }}">
|
step="0.01" value="{{ settings.filament_price_per_kg }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Marge matière (%)</label>
|
<label class="form-label">Marge matiere (%)</label>
|
||||||
<input type="number" name="material_margin_pct" class="form-control"
|
<input type="number" name="material_margin_pct" class="form-control"
|
||||||
step="0.5" value="{{ settings.material_margin_pct }}">
|
step="0.5" value="{{ settings.material_margin_pct }}">
|
||||||
<div class="form-text">Appliquée sur le coût matière brut.</div>
|
<div class="form-text">Appliquee sur le cout matiere brut.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +31,7 @@
|
|||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header py-3"><i class="bi bi-lightning-charge me-2"></i>Électricité</div>
|
<div class="card-header py-3"><i class="bi bi-lightning-charge me-2"></i>Electricite</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Prix kWh (€)</label>
|
<label class="form-label">Prix kWh (€)</label>
|
||||||
@@ -58,7 +59,7 @@
|
|||||||
step="1" value="{{ settings.printer_price }}">
|
step="1" value="{{ settings.printer_price }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="form-label">Durée de vie (h)</label>
|
<label class="form-label">Duree de vie (h)</label>
|
||||||
<input type="number" name="printer_lifespan_hours" class="form-control"
|
<input type="number" name="printer_lifespan_hours" class="form-control"
|
||||||
step="100" value="{{ settings.printer_lifespan_hours }}">
|
step="100" value="{{ settings.printer_lifespan_hours }}">
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +71,7 @@
|
|||||||
step="0.5" value="{{ settings.nozzle_price }}">
|
step="0.5" value="{{ settings.nozzle_price }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="form-label">Durée de vie buse (h)</label>
|
<label class="form-label">Duree de vie buse (h)</label>
|
||||||
<input type="number" name="nozzle_lifespan_hours" class="form-control"
|
<input type="number" name="nozzle_lifespan_hours" class="form-control"
|
||||||
step="10" value="{{ settings.nozzle_lifespan_hours }}">
|
step="10" value="{{ settings.nozzle_lifespan_hours }}">
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +83,7 @@
|
|||||||
step="0.5" value="{{ settings.plate_price }}">
|
step="0.5" value="{{ settings.plate_price }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label class="form-label">Durée de vie plateau (h)</label>
|
<label class="form-label">Duree de vie plateau (h)</label>
|
||||||
<input type="number" name="plate_lifespan_hours" class="form-control"
|
<input type="number" name="plate_lifespan_hours" class="form-control"
|
||||||
step="50" value="{{ settings.plate_lifespan_hours }}">
|
step="50" value="{{ settings.plate_lifespan_hours }}">
|
||||||
</div>
|
</div>
|
||||||
@@ -112,10 +113,41 @@
|
|||||||
<div class="card-header py-3"><i class="bi bi-percent me-2"></i>Fiscal</div>
|
<div class="card-header py-3"><i class="bi bi-percent me-2"></i>Fiscal</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Taux cotisations micro-entrepreneur (%)</label>
|
<label class="form-label fw-semibold">Cotisations sociales (%)</label>
|
||||||
<input type="number" name="tax_rate_pct" class="form-control"
|
<input type="number" name="cotisations_rate_pct" class="form-control"
|
||||||
step="0.1" value="{{ settings.tax_rate_pct }}">
|
step="0.1" value="{{ settings.cotisations_rate_pct }}">
|
||||||
<div class="form-text">12.3% pour vente de marchandises (2024).</div>
|
<div class="form-text">12.3% pour prestation de services (2024). Calculees sur le CA HT.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Versement Forfaitaire Liberatoire — impot (%)</label>
|
||||||
|
<input type="number" name="vfl_rate_pct" class="form-control"
|
||||||
|
step="0.1" value="{{ settings.vfl_rate_pct }}">
|
||||||
|
<div class="form-text">
|
||||||
|
1.7% pour prestation de services (option VFL). Mettre 0 si vous n'etes pas au VFL
|
||||||
|
(impot calcule separement sur declaration classique).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label fw-semibold">TVA (%)</label>
|
||||||
|
<input type="number" name="tva_rate_pct" class="form-control"
|
||||||
|
step="0.1" value="{{ settings.tva_rate_pct }}">
|
||||||
|
<div class="form-text">
|
||||||
|
<strong>0</strong> si vous etes en franchise en base de TVA (CA < seuil).
|
||||||
|
<strong>20</strong> si vous avez depasse le seuil et devez facturer la TVA.
|
||||||
|
La TVA est affichee separement et s'ajoute au prix HT.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% set total_charges = settings.cotisations_rate_pct + settings.vfl_rate_pct %}
|
||||||
|
<div class="alert alert-info py-2 mt-3 mb-0 small">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
Charges totales sur CA : <strong>{{ total_charges }} %</strong>
|
||||||
|
(cotisations {{ settings.cotisations_rate_pct }}% + VFL {{ settings.vfl_rate_pct }}%)
|
||||||
|
{% if settings.tva_rate_pct > 0 %}
|
||||||
|
+ TVA {{ settings.tva_rate_pct }}% ajoutee au prix HT
|
||||||
|
{% else %}
|
||||||
|
— franchise TVA active
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,7 +157,7 @@
|
|||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<button type="submit" class="btn px-4 py-2 fw-bold" style="background:#ff6b35;color:#fff">
|
<button type="submit" class="btn px-4 py-2 fw-bold" style="background:#ff6b35;color:#fff">
|
||||||
<i class="bi bi-save me-2"></i>Enregistrer les paramètres
|
<i class="bi bi-save me-2"></i>Enregistrer les parametres
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user