Fix migration DB, detail recap, TVA BIC vente, docker-compose sans traefik
This commit is contained in:
@@ -98,11 +98,30 @@ def init_db():
|
||||
('handling_rate_per_hour','40.0'),('handling_minutes_per_plate','5'),
|
||||
('material_margin_pct','20.0'),
|
||||
('cotisations_rate_pct','12.3'),
|
||||
('vfl_rate_pct','1.7'),
|
||||
('tva_rate_pct','0.0'),
|
||||
('vfl_rate_pct','1.0'),
|
||||
('tva_rate_pct','20.0'),
|
||||
('printer_power_kw','0.35'),
|
||||
]
|
||||
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()
|
||||
|
||||
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_margin': round(mat_margin, 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),
|
||||
'kwh_used': round(s['printer_power_kw'] * (print_time_s / 3600), 4),
|
||||
'cout_fixe': round(cout_fixe, 4),
|
||||
'handling_cost': round(handling, 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
|
||||
'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),
|
||||
# Taux et paramètres 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),
|
||||
'_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):
|
||||
@@ -284,7 +311,7 @@ def new_client():
|
||||
conn = get_db()
|
||||
conn.execute('INSERT INTO clients(name,email,design_multiplier,notes) VALUES(?,?,?,?)',
|
||||
(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()
|
||||
return redirect(url_for('clients'))
|
||||
return render_template('client_form.html', client=None)
|
||||
@@ -296,7 +323,7 @@ def edit_client(id):
|
||||
if request.method == 'POST':
|
||||
conn.execute('UPDATE clients SET name=?,email=?,design_multiplier=?,notes=? WHERE id=?',
|
||||
(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()
|
||||
return redirect(url_for('clients'))
|
||||
conn.close()
|
||||
|
||||
@@ -24,20 +24,6 @@
|
||||
<input type="email" name="email" class="form-control"
|
||||
value="{{ client.email if client else '' }}">
|
||||
</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">
|
||||
<label class="form-label fw-semibold">Notes</label>
|
||||
<textarea name="notes" class="form-control" rows="3">{{ client.notes if client else '' }}</textarea>
|
||||
@@ -55,10 +41,4 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function setDM(val) {
|
||||
document.getElementById('dm').value = val;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
+29
-13
@@ -61,7 +61,8 @@
|
||||
<option value="">Prix global (parametres)</option>
|
||||
{% 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 }}">
|
||||
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
|
||||
{% if m.stock_g <= 0 %}[RUPTURE]{% elif m.stock_g <= m.low_stock_threshold_g %}[stock bas]{% endif %}
|
||||
</option>
|
||||
@@ -367,33 +368,48 @@ function fmt2(n) { return parseFloat(n).toFixed(2) + ' €'; }
|
||||
function renderBreakdown(d, body) {
|
||||
const dm = parseFloat(body.design_multiplier).toFixed(2);
|
||||
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
|
||||
? `<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>` : '';
|
||||
? `<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>` : '';
|
||||
|
||||
// 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
|
||||
? `<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 = `
|
||||
<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">Marge matiere</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">Electricite</span><span>${fmt(d.electricity_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 (${d._material_margin_pct}%)</span><span>${fmt(d.material_margin)}</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">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 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">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>
|
||||
<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}
|
||||
${tvaRow}
|
||||
<div class="breakdown-row ${tvaClass}"><span class="ps-2">${tvaLabel}</span></div>
|
||||
${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
|
||||
|
||||
@@ -35,14 +35,20 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<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 }} %
|
||||
</span>
|
||||
</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"
|
||||
min="20" max="35" step="0.5"
|
||||
min="0" max="100" 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 class="d-flex justify-content-between" style="font-size:.75rem;color:#6b7280"><span>0%</span><span>100%</span></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Notes</label>
|
||||
@@ -66,8 +72,26 @@
|
||||
function setDM(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>
|
||||
{% endblock %}
|
||||
|
||||
+10
-8
@@ -116,15 +116,18 @@
|
||||
<label class="form-label fw-semibold">Cotisations sociales (%)</label>
|
||||
<input type="number" name="cotisations_rate_pct" class="form-control"
|
||||
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 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).
|
||||
<strong>1%</strong> — BIC vente (si option VFL activee).
|
||||
Mettre <strong>0</strong> si vous n'avez pas opte pour le VFL (impot sur declaration classique).
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
@@ -133,18 +136,17 @@
|
||||
<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.
|
||||
<strong>20%</strong> — taux normal applicable a la vente de biens imprimes 3D.
|
||||
La TVA s'ajoute au prix HT et est collectee pour l'Etat (non conservee).
|
||||
</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>
|
||||
Charges sur CA HT : <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
|
||||
— TVA {{ settings.tva_rate_pct }}% collectee et reversee a l'Etat
|
||||
{% else %}
|
||||
— franchise TVA active
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user