Fix migration DB, detail recap, TVA BIC vente, docker-compose sans traefik

This commit is contained in:
Jo
2026-07-06 13:09:07 +02:00
parent 8434f3745a
commit 274ec01239
5 changed files with 105 additions and 56 deletions
+32 -5
View File
@@ -98,11 +98,30 @@ def init_db():
('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'), ('material_margin_pct','20.0'),
('cotisations_rate_pct','12.3'), ('cotisations_rate_pct','12.3'),
('vfl_rate_pct','1.7'), ('vfl_rate_pct','1.0'),
('tva_rate_pct','0.0'), ('tva_rate_pct','20.0'),
('printer_power_kw','0.35'), ('printer_power_kw','0.35'),
] ]
conn.executemany('INSERT OR IGNORE INTO settings VALUES (?,?)', defaults) conn.executemany('INSERT OR IGNORE INTO settings VALUES (?,?)', defaults)
# Migrations : ajout de colonnes si elles n'existent pas (upgrade DB existante)
migrations = [
'ALTER TABLE jobs ADD COLUMN machine_profile_id INTEGER',
'ALTER TABLE jobs ADD COLUMN handling_profile_id INTEGER',
'ALTER TABLE jobs ADD COLUMN material_profile_id INTEGER',
'ALTER TABLE jobs ADD COLUMN pricing_profile_id INTEGER',
'ALTER TABLE jobs ADD COLUMN cout_fixe REAL',
'ALTER TABLE jobs ADD COLUMN total_marge REAL',
'ALTER TABLE jobs ADD COLUMN cotisations_amount REAL',
'ALTER TABLE jobs ADD COLUMN vfl_amount REAL',
'ALTER TABLE jobs ADD COLUMN tva_amount REAL',
]
for sql in migrations:
try:
conn.execute(sql)
except Exception:
pass # colonne deja existante
conn.commit(); conn.close() conn.commit(); conn.close()
def get_settings(): def get_settings():
@@ -218,7 +237,11 @@ def calc(weight_g, print_time_s, design_mult, gross_margin_pct, discount_pct,
'material_cost': round(mat, 4), 'material_cost': round(mat, 4),
'material_margin': round(mat_margin, 4), 'material_margin': round(mat_margin, 4),
'wear_cost': round(wear, 4), 'wear_cost': round(wear, 4),
'printer_wear': round(printer_w, 4),
'nozzle_wear': round(nozzle_w, 4),
'plate_wear': round(plate_w, 4),
'electricity_cost': round(elec, 4), 'electricity_cost': round(elec, 4),
'kwh_used': round(s['printer_power_kw'] * (print_time_s / 3600), 4),
'cout_fixe': round(cout_fixe, 4), 'cout_fixe': round(cout_fixe, 4),
'handling_cost': round(handling, 4), 'handling_cost': round(handling, 4),
'design_cost': round(design, 4), 'design_cost': round(design, 4),
@@ -235,10 +258,14 @@ def calc(weight_g, print_time_s, design_mult, gross_margin_pct, discount_pct,
# Compat # Compat
'tax_amount': round(cot_amount + vfl_amount, 4), 'tax_amount': round(cot_amount + vfl_amount, 4),
'final_price': round(final, 2), 'final_price': round(final, 2),
# Taux appliqués (pour affichage) # Taux et paramètres appliqués (pour affichage)
'_cot_pct': round(cot_rate * 100, 2), '_cot_pct': round(cot_rate * 100, 2),
'_vfl_pct': round(vfl_rate * 100, 2), '_vfl_pct': round(vfl_rate * 100, 2),
'_tva_pct': round(tva_rate * 100, 2), '_tva_pct': round(tva_rate * 100, 2),
'_material_margin_pct': s['material_margin_pct'],
'_handling_rate': s['handling_rate_per_hour'],
'_handling_minutes': s['handling_minutes_per_plate'],
'_elec_price': s['electricity_price_kwh'],
} }
def fmt_time(s): def fmt_time(s):
@@ -284,7 +311,7 @@ def new_client():
conn = get_db() conn = get_db()
conn.execute('INSERT INTO clients(name,email,design_multiplier,notes) VALUES(?,?,?,?)', conn.execute('INSERT INTO clients(name,email,design_multiplier,notes) VALUES(?,?,?,?)',
(request.form['name'], request.form.get('email',''), (request.form['name'], request.form.get('email',''),
float(request.form['design_multiplier']), request.form.get('notes',''))) float(request.form.get('design_multiplier', 1.80)), request.form.get('notes','')))
conn.commit(); conn.close() conn.commit(); conn.close()
return redirect(url_for('clients')) return redirect(url_for('clients'))
return render_template('client_form.html', client=None) return render_template('client_form.html', client=None)
@@ -296,7 +323,7 @@ def edit_client(id):
if request.method == 'POST': if request.method == 'POST':
conn.execute('UPDATE clients SET name=?,email=?,design_multiplier=?,notes=? WHERE id=?', conn.execute('UPDATE clients SET name=?,email=?,design_multiplier=?,notes=? WHERE id=?',
(request.form['name'], request.form.get('email',''), (request.form['name'], request.form.get('email',''),
float(request.form['design_multiplier']), request.form.get('notes',''), id)) float(request.form.get('design_multiplier', client['design_multiplier'])), request.form.get('notes',''), id))
conn.commit(); conn.close() conn.commit(); conn.close()
return redirect(url_for('clients')) return redirect(url_for('clients'))
conn.close() conn.close()
+1 -21
View File
@@ -24,20 +24,6 @@
<input type="email" name="email" class="form-control" <input type="email" name="email" class="form-control"
value="{{ client.email if client else '' }}"> value="{{ client.email if client else '' }}">
</div> </div>
<div class="mb-4">
<label class="form-label fw-semibold">Coefficient design par défaut</label>
<div class="d-flex gap-2 mb-2">
<button type="button" class="btn btn-sm btn-outline-success" onclick="setDM(0.80)">
×0.80 — STL fourni
</button>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="setDM(1.80)">
×1.80 — Création complète
</button>
</div>
<input type="number" name="design_multiplier" id="dm" class="form-control"
step="0.05" min="0" value="{{ client.design_multiplier if client else '1.80' }}">
<div class="form-text">0.80 = client qui fournit ses STL. 1.80 = vous faites tout le design.</div>
</div>
<div class="mb-4"> <div class="mb-4">
<label class="form-label fw-semibold">Notes</label> <label class="form-label fw-semibold">Notes</label>
<textarea name="notes" class="form-control" rows="3">{{ client.notes if client else '' }}</textarea> <textarea name="notes" class="form-control" rows="3">{{ client.notes if client else '' }}</textarea>
@@ -55,10 +41,4 @@
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}{% endblock %}
<script>
function setDM(val) {
document.getElementById('dm').value = val;
}
</script>
{% endblock %}
+29 -13
View File
@@ -61,7 +61,8 @@
<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 }}" <option value="{{ m.id }}" data-price="{{ m.price_per_kg }}"
data-stock="{{ m.stock_g }}" data-threshold="{{ m.low_stock_threshold_g }}"> data-stock="{{ m.stock_g }}" data-threshold="{{ m.low_stock_threshold_g }}"
data-name="{{ m.name }} ({{ m.type }})">
{{ m.name }} ({{ m.type }}) — {{ m.price_per_kg }} €/kg {{ 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>
@@ -367,33 +368,48 @@ function fmt2(n) { return parseFloat(n).toFixed(2) + ' €'; }
function renderBreakdown(d, body) { function renderBreakdown(d, body) {
const dm = parseFloat(body.design_multiplier).toFixed(2); const dm = parseFloat(body.design_multiplier).toFixed(2);
const mgn = parseFloat(body.gross_margin_pct).toFixed(1); const mgn = parseFloat(body.gross_margin_pct).toFixed(1);
// Nom du materiau selectionne
const matSel = document.getElementById('materialSelect');
const matOpt = matSel.options[matSel.selectedIndex];
const matName = matOpt.value ? (matOpt.dataset.name || matOpt.text.split('—')[0].trim()) : 'global';
// Remise
const dis = body.discount_pct > 0 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>` : ''; ? `<div class="breakdown-row text-danger"><span class="ps-2">Remise (${body.discount_pct}%)</span><span>-${fmt2(d.prix_ttc * 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>` : ''; // TVA — toujours affichee
const tvaLabel = d._tva_pct > 0
? `TVA ${d._tva_pct}% — ${fmt2(d.tva_amount)}`
: `TVA — franchise en base`;
const tvaClass = d._tva_pct > 0 ? '' : 'text-muted fst-italic';
// VFL
const vflRow = d._vfl_pct > 0 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>` : ''; ? `<div class="breakdown-row"><span class="breakdown-muted ps-2">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 section-header"><span>Cout fixe</span></div> <div class="breakdown-row section-header"><span>Cout fixe</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 ps-2">Matiere <small class="text-secondary">${matName}</small></span><span>${fmt(d.material_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 ps-2">Marge matiere (${d._material_margin_pct}%)</span><span>${fmt(d.material_margin)}</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 ps-2">Usure — imprimante</span><span>${fmt(d.printer_wear)}</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 ps-2">Usure — buse</span><span>${fmt(d.nozzle_wear)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Usure — plateau</span><span>${fmt(d.plate_wear)}</span></div>
<div class="breakdown-row"><span class="breakdown-muted ps-2">Electricite <small class="text-secondary">${parseFloat(d.kwh_used).toFixed(3)} kWh × ${d._elec_price} €</small></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">Total cout fixe</span><span class="fw-semibold">${fmt2(d.cout_fixe)}</span></div>
<div class="breakdown-row section-header"><span>Partie variable</span></div> <div class="breakdown-row section-header"><span>Partie variable</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">Manutention <small class="text-secondary">${d._handling_minutes} min × ${d._handling_rate} €/h</small></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">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"><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 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 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> <div class="breakdown-row"><span class="breakdown-muted ps-2">Cotisations BIC vente (${d._cot_pct}%)</span><span>${fmt2(d.cotisations_amount)}</span></div>
${vflRow} ${vflRow}
${tvaRow} <div class="breakdown-row ${tvaClass}"><span class="ps-2">${tvaLabel}</span></div>
${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 TTC</span><span class="final-price">${fmt2(d.final_price)}</span></div>`;
} }
// Init // Init
+29 -5
View File
@@ -35,14 +35,20 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<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"> Marge brute <span id="marginDisplay" class="fw-bold" style="color:#ff6b35">
{{ profile.gross_margin_pct if profile else 27.5 }} % {{ profile.gross_margin_pct if profile else 27.5 }} %
</span> </span>
</label> </label>
<div class="d-flex gap-2 mb-2" id="marginButtons">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="setMargin(20)">20%</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="setMargin(25)">25%</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="setMargin(30)">30%</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="setMargin(35)">35%</button>
</div>
<input type="range" name="gross_margin_pct" id="gross_margin_pct" class="form-range" <input type="range" name="gross_margin_pct" id="gross_margin_pct" class="form-range"
min="20" max="35" step="0.5" min="0" max="100" step="0.5"
value="{{ profile.gross_margin_pct if profile else 27.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 class="d-flex justify-content-between" style="font-size:.75rem;color:#6b7280"><span>0%</span><span>100%</span></div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Notes</label> <label class="form-label fw-semibold">Notes</label>
@@ -66,8 +72,26 @@
function setDM(val) { function setDM(val) {
document.getElementById('design_multiplier').value = val; document.getElementById('design_multiplier').value = val;
} }
document.getElementById('gross_margin_pct').addEventListener('input', function() {
document.getElementById('marginDisplay').textContent = this.value + ' %'; function setMargin(val) {
document.getElementById('gross_margin_pct').value = val;
updateMarginDisplay(val);
}
function updateMarginDisplay(val) {
val = parseFloat(val);
document.getElementById('marginDisplay').textContent = val.toFixed(1) + ' %';
[20, 25, 30, 35].forEach(function(v) {
const btn = document.querySelector('#marginButtons button[onclick="setMargin(' + v + ')"]');
if (btn) btn.className = 'btn btn-sm ' + (Math.abs(val - v) < 0.01 ? 'btn-secondary' : 'btn-outline-secondary');
}); });
}
const slider = document.getElementById('gross_margin_pct');
slider.addEventListener('input', function() { updateMarginDisplay(this.value); });
slider.addEventListener('change', function() { updateMarginDisplay(this.value); });
// Init
updateMarginDisplay(slider.value);
</script> </script>
{% endblock %} {% endblock %}
+10 -8
View File
@@ -116,15 +116,18 @@
<label class="form-label fw-semibold">Cotisations sociales (%)</label> <label class="form-label fw-semibold">Cotisations sociales (%)</label>
<input type="number" name="cotisations_rate_pct" class="form-control" <input type="number" name="cotisations_rate_pct" class="form-control"
step="0.1" value="{{ settings.cotisations_rate_pct }}"> step="0.1" value="{{ settings.cotisations_rate_pct }}">
<div class="form-text">12.3% pour prestation de services (2024). Calculees sur le CA HT.</div> <div class="form-text">
<strong>12.3%</strong> — micro-entrepreneur BIC vente (fabrication/vente de marchandises, 2024).
Calculees sur le CA HT.
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-semibold">Versement Forfaitaire Liberatoire — impot (%)</label> <label class="form-label fw-semibold">Versement Forfaitaire Liberatoire — impot (%)</label>
<input type="number" name="vfl_rate_pct" class="form-control" <input type="number" name="vfl_rate_pct" class="form-control"
step="0.1" value="{{ settings.vfl_rate_pct }}"> step="0.1" value="{{ settings.vfl_rate_pct }}">
<div class="form-text"> <div class="form-text">
1.7% pour prestation de services (option VFL). Mettre 0 si vous n'etes pas au VFL <strong>1%</strong> — BIC vente (si option VFL activee).
(impot calcule separement sur declaration classique). Mettre <strong>0</strong> si vous n'avez pas opte pour le VFL (impot sur declaration classique).
</div> </div>
</div> </div>
<hr> <hr>
@@ -133,18 +136,17 @@
<input type="number" name="tva_rate_pct" class="form-control" <input type="number" name="tva_rate_pct" class="form-control"
step="0.1" value="{{ settings.tva_rate_pct }}"> step="0.1" value="{{ settings.tva_rate_pct }}">
<div class="form-text"> <div class="form-text">
<strong>0</strong> si vous etes en franchise en base de TVA (CA &lt; seuil). <strong>20%</strong> — taux normal applicable a la vente de biens imprimes 3D.
<strong>20</strong> si vous avez depasse le seuil et devez facturer la TVA. La TVA s'ajoute au prix HT et est collectee pour l'Etat (non conservee).
La TVA est affichee separement et s'ajoute au prix HT.
</div> </div>
</div> </div>
{% set total_charges = settings.cotisations_rate_pct + settings.vfl_rate_pct %} {% set total_charges = settings.cotisations_rate_pct + settings.vfl_rate_pct %}
<div class="alert alert-info py-2 mt-3 mb-0 small"> <div class="alert alert-info py-2 mt-3 mb-0 small">
<i class="bi bi-info-circle me-1"></i> <i class="bi bi-info-circle me-1"></i>
Charges totales sur CA : <strong>{{ total_charges }} %</strong> Charges sur CA HT : <strong>{{ total_charges }} %</strong>
(cotisations {{ settings.cotisations_rate_pct }}% + VFL {{ settings.vfl_rate_pct }}%) (cotisations {{ settings.cotisations_rate_pct }}% + VFL {{ settings.vfl_rate_pct }}%)
{% if settings.tva_rate_pct > 0 %} {% if settings.tva_rate_pct > 0 %}
+ TVA {{ settings.tva_rate_pct }}% ajoutee au prix HT TVA {{ settings.tva_rate_pct }}% collectee et reversee a l'Etat
{% else %} {% else %}
— franchise TVA active — franchise TVA active
{% endif %} {% endif %}