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,
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id INTEGER REFERENCES clients(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,
|
||||
description TEXT DEFAULT '', weight_g 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,
|
||||
material_cost REAL, material_margin REAL, design_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'))
|
||||
);
|
||||
''')
|
||||
@@ -52,7 +96,11 @@ def init_db():
|
||||
('nozzle_price','10.0'),('nozzle_lifespan_hours','250'),
|
||||
('plate_price','25.0'),('plate_lifespan_hours','1000'),
|
||||
('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.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,
|
||||
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:
|
||||
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_margin = mat * (s['material_margin_pct'] / 100)
|
||||
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
|
||||
wear = printer_w + nozzle_w + plate_w
|
||||
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)
|
||||
pre_tax = subtotal + margin
|
||||
tax_rate = s['tax_rate_pct'] / 100
|
||||
with_tax = pre_tax / (1 - tax_rate)
|
||||
tax_amt = with_tax - pre_tax
|
||||
final = with_tax * (1 - discount_pct / 100)
|
||||
|
||||
cout_fixe = mat + mat_margin + wear + elec
|
||||
sous_total = cout_fixe + handling + design
|
||||
margin = sous_total * (gross_margin_pct / 100)
|
||||
total_marge= handling + design + margin
|
||||
prix_ht = cout_fixe + total_marge # = sous_total + margin
|
||||
|
||||
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 {
|
||||
'material_cost': round(mat, 4),
|
||||
'material_margin': round(mat_margin, 4),
|
||||
'design_cost': round(design, 4),
|
||||
'handling_cost': round(handling, 4),
|
||||
'wear_cost': round(wear, 4),
|
||||
'electricity_cost': round(elec, 4),
|
||||
'subtotal': round(subtotal, 4),
|
||||
'margin_amount': round(margin, 4),
|
||||
'tax_amount': round(tax_amt, 4),
|
||||
'final_price': round(final, 2),
|
||||
# Lignes détaillées
|
||||
'material_cost': round(mat, 4),
|
||||
'material_margin': round(mat_margin, 4),
|
||||
'wear_cost': round(wear, 4),
|
||||
'electricity_cost': round(elec, 4),
|
||||
'cout_fixe': round(cout_fixe, 4),
|
||||
'handling_cost': round(handling, 4),
|
||||
'design_cost': round(design, 4),
|
||||
'subtotal': round(sous_total, 4),
|
||||
'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):
|
||||
return f"{s//3600}h{(s%3600)//60:02d}m"
|
||||
app.jinja_env.filters['fmt_time'] = fmt_time
|
||||
|
||||
# ─── Dashboard ────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route('/')
|
||||
def dashboard():
|
||||
conn = get_db()
|
||||
@@ -164,6 +265,8 @@ def dashboard():
|
||||
conn.close()
|
||||
return render_template('dashboard.html', jobs=recent, stats=stats, low_stock=low_stock)
|
||||
|
||||
# ─── Clients ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route('/clients')
|
||||
def clients():
|
||||
conn = get_db()
|
||||
@@ -206,6 +309,8 @@ def delete_client(id):
|
||||
conn.commit(); conn.close()
|
||||
return redirect(url_for('clients'))
|
||||
|
||||
# ─── Matières ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route('/materials')
|
||||
def materials():
|
||||
conn = get_db()
|
||||
@@ -277,6 +382,188 @@ def delete_material(id):
|
||||
conn.commit(); conn.close()
|
||||
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')
|
||||
def jobs():
|
||||
conn = get_db()
|
||||
@@ -291,9 +578,14 @@ def jobs():
|
||||
@app.route('/jobs/new', methods=['GET','POST'])
|
||||
def new_job():
|
||||
conn = get_db()
|
||||
clients = conn.execute('SELECT * FROM clients ORDER BY name').fetchall()
|
||||
mats = conn.execute('SELECT * FROM materials ORDER BY type,name').fetchall()
|
||||
settings = get_settings()
|
||||
clients = conn.execute('SELECT * FROM clients ORDER BY name').fetchall()
|
||||
mats = conn.execute('SELECT * FROM materials ORDER BY type,name').fetchall()
|
||||
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':
|
||||
weight_g = float(request.form['weight_g'])
|
||||
hours = int(request.form.get('hours',0))
|
||||
@@ -305,37 +597,80 @@ def new_job():
|
||||
client_id = request.form.get('client_id') or None
|
||||
material_id = request.form.get('material_id') or None
|
||||
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
|
||||
if material_id:
|
||||
mat = conn.execute('SELECT price_per_kg FROM materials WHERE id=?',(material_id,)).fetchone()
|
||||
if mat:
|
||||
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,
|
||||
material_cost,material_margin,design_cost,handling_cost,wear_cost,
|
||||
electricity_cost,subtotal,margin_amount,tax_amount,final_price,notes)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
|
||||
(client_id,material_id,source_file,request.form['name'],request.form.get('description',''),
|
||||
material_cost,material_margin,design_cost,handling_cost,wear_cost,electricity_cost,
|
||||
cout_fixe,total_marge,subtotal,margin_amount,
|
||||
cotisations_amount,vfl_amount,tva_amount,tax_amount,final_price,notes)
|
||||
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,
|
||||
r['material_cost'],r['material_margin'],r['design_cost'],r['handling_cost'],
|
||||
r['wear_cost'],r['electricity_cost'],r['subtotal'],r['margin_amount'],
|
||||
r['tax_amount'],r['final_price'],request.form.get('notes','')))
|
||||
r['wear_cost'],r['electricity_cost'],
|
||||
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:
|
||||
conn.execute('UPDATE materials SET stock_g=MAX(0,stock_g-?) WHERE id=?', (weight_g, material_id))
|
||||
conn.commit(); conn.close()
|
||||
return redirect(url_for('jobs'))
|
||||
|
||||
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>')
|
||||
def job_detail(id):
|
||||
conn = get_db()
|
||||
job = conn.execute('''
|
||||
SELECT j.*, c.name as client_name,
|
||||
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
|
||||
m.name as material_name, m.color as material_color, m.type as material_type,
|
||||
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 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()
|
||||
conn.close()
|
||||
if not job:
|
||||
@@ -349,22 +684,49 @@ def delete_job(id):
|
||||
conn.commit(); conn.close()
|
||||
return redirect(url_for('jobs'))
|
||||
|
||||
# ─── API ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route('/api/calculate', methods=['POST'])
|
||||
def api_calculate():
|
||||
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
|
||||
if d.get('material_id'):
|
||||
conn = get_db()
|
||||
mat = conn.execute('SELECT price_per_kg FROM materials WHERE id=?',(d['material_id'],)).fetchone()
|
||||
conn.close()
|
||||
if mat:
|
||||
price_kg = mat['price_per_kg']
|
||||
conn.close()
|
||||
|
||||
result = calc(
|
||||
weight_g=float(d['weight_g']),
|
||||
print_time_s=int(d.get('hours',0))*3600 + int(d.get('minutes',0))*60,
|
||||
design_mult=float(d['design_multiplier']),
|
||||
gross_margin_pct=float(d['gross_margin_pct']),
|
||||
design_mult=float(d.get('design_multiplier',1.80)),
|
||||
gross_margin_pct=float(d.get('gross_margin_pct',27.5)),
|
||||
discount_pct=float(d.get('discount_pct',0)),
|
||||
price_per_kg_override=price_kg, s=s)
|
||||
return jsonify(result)
|
||||
@@ -389,16 +751,20 @@ def api_parse_3mf():
|
||||
result['filename'] = os.path.splitext(f.filename)[0]
|
||||
return jsonify(result)
|
||||
|
||||
# ─── Paramètres ───────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route('/settings', methods=['GET','POST'])
|
||||
def settings():
|
||||
if request.method == 'POST':
|
||||
conn = get_db()
|
||||
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()
|
||||
return redirect(url_for('settings'))
|
||||
return render_template('settings.html', settings=get_settings())
|
||||
|
||||
# ─── Statistiques ─────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route('/stats')
|
||||
def stats():
|
||||
conn = get_db()
|
||||
@@ -419,6 +785,8 @@ def stats():
|
||||
conn.close()
|
||||
return render_template('stats.html', by_client=by_client, by_month=by_month, by_material=by_material)
|
||||
|
||||
# ─── Export CSV ───────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route('/export/csv')
|
||||
def export_csv():
|
||||
conn = get_db()
|
||||
@@ -427,9 +795,9 @@ def export_csv():
|
||||
COALESCE(m.name,'') as material, m.type as material_type,
|
||||
j.source_file, j.name, j.weight_g, j.print_time_s,
|
||||
j.design_multiplier, j.gross_margin_pct, j.discount_pct,
|
||||
j.material_cost, j.material_margin, j.design_cost, j.handling_cost,
|
||||
j.wear_cost, j.electricity_cost, j.subtotal, j.margin_amount,
|
||||
j.tax_amount, j.final_price
|
||||
j.cout_fixe, j.handling_cost, j.design_cost, j.total_marge,
|
||||
j.margin_amount, j.cotisations_amount, j.vfl_amount, j.tva_amount,
|
||||
j.final_price
|
||||
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()
|
||||
conn.close()
|
||||
@@ -437,8 +805,8 @@ def export_csv():
|
||||
w = csv.writer(out, delimiter=';')
|
||||
w.writerow(['Date','Client','Matiere','Type','Fichier source','Nom',
|
||||
'Poids(g)','Temps(s)','Coeff Design','Marge(%)','Remise(%)',
|
||||
'Matiere EUR','Marge Mat.','Design','Manutention','Usure',
|
||||
'Electricite','Sous-total','Marge Brute','Cotisations','Prix Final'])
|
||||
'Cout fixe','Manutention','Design','Total marge',
|
||||
'Marge brute','Cotisations','VFL','TVA','Prix Final'])
|
||||
for j in jobs:
|
||||
w.writerow(list(j))
|
||||
return Response(out.getvalue(), mimetype='text/csv',
|
||||
|
||||
Reference in New Issue
Block a user