Files
3d-pricing/templates/export_indy.html
T
2026-07-17 00:15:26 +02:00

297 lines
11 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'base.html' %}
{% block title %}Export Indy{% endblock %}
{% block content %}
<div class="page-header d-flex justify-content-between align-items-center">
<h1><i class="bi bi-file-earmark-arrow-down me-2 text-primary"></i>Export Indy</h1>
<a href="{{ url_for('jobs') }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Retour commandes
</a>
</div>
<div class="row g-4">
<!-- COLONNE GAUCHE : selection des commandes -->
<div class="col-lg-5">
<div class="card h-100">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<span class="fw-semibold"><i class="bi bi-check2-square me-1"></i>Selection des commandes</span>
<div class="d-flex gap-2">
<button id="selAll" class="btn btn-sm btn-outline-secondary py-0">Tout</button>
<button id="selNone" class="btn btn-sm btn-outline-secondary py-0">Aucun</button>
</div>
</div>
<!-- Filtres -->
<div class="card-body border-bottom py-2 px-3">
<div class="row g-2">
<div class="col">
<input type="text" id="fSearch" class="form-control form-control-sm" placeholder="Rechercher...">
</div>
<div class="col-auto">
<select id="fClientSel" class="form-select form-select-sm">
<option value="">Tous clients</option>
{% set seen = [] %}
{% for j in jobs %}
{% if j.client_name and j.client_name not in seen %}
<option value="{{ j.client_name }}">{{ j.client_name }}</option>
{% if seen.append(j.client_name) %}{% endif %}
{% endif %}
{% endfor %}
</select>
</div>
</div>
</div>
<!-- Liste jobs -->
<div class="card-body p-0" style="max-height:520px;overflow-y:auto">
<table class="table table-sm table-hover mb-0" id="jobSelTable">
<thead class="table-light sticky-top">
<tr>
<th style="width:32px"><input type="checkbox" id="chkAll" class="form-check-input"></th>
<th>Commande</th>
<th class="text-end">Prix HT</th>
</tr>
</thead>
<tbody id="jobSelBody">
{% for j in jobs %}
{% set prix_ttc = (j.final_price / (1 - j.discount_pct / 100)) if (j.discount_pct and j.discount_pct > 0 and j.discount_pct < 100) else (j.final_price or 0) %}
{% set prix_ht_net = prix_ttc - (j.tva_amount or 0) %}
{% set qty = j.order_qty or 1 %}
{% set acro = j.client_acronyme or '' %}
{% set default_nom = (acro ~ ' - ' ~ j.name) if acro else j.name %}
<tr class="sel-row"
data-id="{{ j.id }}"
data-client="{{ j.client_name or '' }}"
data-nom="{{ default_nom }}"
data-prix-ht="{{ '%.2f'|format(prix_ht_net) }}"
data-tva="{{ tva_pct }}"
data-qty="{{ qty }}"
data-desc="{{ j.description or '' }}"
data-date="{{ j.created_at[:10] }}"
>
<td><input type="checkbox" class="form-check-input job-chk"></td>
<td>
<div class="fw-semibold" style="font-size:.85rem">{{ j.name }}</div>
<div class="text-muted" style="font-size:.72rem">
{{ j.created_at[:10] }}{% if j.client_name %} · {{ j.client_name }}{% if acro %} <span class="badge bg-light text-dark border">{{ acro }}</span>{% endif %}{% endif %}
</div>
</td>
<td class="text-end small fw-semibold">{{ '%.2f'|format(prix_ht_net) }} €</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- COLONNE DROITE : tableau d'export éditable -->
<div class="col-lg-7">
<div class="card">
<div class="card-header py-2 d-flex justify-content-between align-items-center">
<span class="fw-semibold"><i class="bi bi-table me-1"></i>Produits Indy <span id="exportCount" class="badge bg-secondary ms-1">0</span></span>
<button id="btnDownload" class="btn btn-sm btn-success" disabled>
<i class="bi bi-download me-1"></i>Télécharger CSV Indy
</button>
</div>
<div class="card-body p-0" style="max-height:600px;overflow-y:auto">
<table class="table table-sm mb-0" id="exportTable">
<thead class="table-light sticky-top">
<tr>
<th>Nom produit <small class="text-muted fw-normal">(éditable)</small></th>
<th style="width:110px">Nature</th>
<th style="width:90px">Prix HT</th>
<th style="width:70px">TVA %</th>
<th style="width:60px">Unité</th>
<th style="width:28px"></th>
</tr>
</thead>
<tbody id="exportBody">
<tr id="exportEmpty">
<td colspan="6" class="text-center text-muted py-4">
<i class="bi bi-arrow-left me-1"></i>Sélectionnez des commandes
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="form-text mt-2 ps-1">
<i class="bi bi-info-circle me-1"></i>
Les colonnes <strong>Nom</strong>, <strong>Nature</strong>, <strong>Prix HT</strong>, <strong>TVA</strong> et <strong>Unité</strong> sont éditables directement dans le tableau.
Le CSV généré est compatible avec l'import <em>Produits &amp; Services</em> d'Indy.
</div>
</div>
</div>
<script>
(function() {
var rows = Array.from(document.querySelectorAll('.sel-row'));
var expBody = document.getElementById('exportBody');
var expEmpty = document.getElementById('exportEmpty');
var expCount = document.getElementById('exportCount');
var btnDl = document.getElementById('btnDownload');
var fSearch = document.getElementById('fSearch');
var fClient = document.getElementById('fClientSel');
var chkAll = document.getElementById('chkAll');
// Map jobId -> export row element
var exportRows = {};
function filterRows() {
var q = fSearch.value.toLowerCase();
var cli = fClient.value;
rows.forEach(function(r) {
var name = (r.dataset.nom + ' ' + r.dataset.client + ' ' + r.dataset.date).toLowerCase();
var show = (!q || name.includes(q)) && (!cli || r.dataset.client === cli);
r.style.display = show ? '' : 'none';
});
}
function makeInput(val, cls) {
var inp = document.createElement('input');
inp.type = 'text';
inp.className = 'form-control form-control-sm border-0 px-1 ' + (cls||'');
inp.value = val;
inp.style.minWidth = '0';
return inp;
}
function makeSelect(opts, selected) {
var sel = document.createElement('select');
sel.className = 'form-select form-select-sm border-0 px-1';
opts.forEach(function(o) {
var opt = document.createElement('option');
opt.value = o; opt.textContent = o;
if (o === selected) opt.selected = true;
sel.appendChild(opt);
});
return sel;
}
function addExportRow(r) {
if (exportRows[r.dataset.id]) return;
var tr = document.createElement('tr');
tr.dataset.id = r.dataset.id;
// Nom
var tdNom = document.createElement('td');
tdNom.appendChild(makeInput(r.dataset.nom, 'nom-inp'));
tr.appendChild(tdNom);
// Nature
var tdNat = document.createElement('td');
tdNat.appendChild(makeSelect(['Prestation de services', 'Vente de produits'], 'Prestation de services'));
tr.appendChild(tdNat);
// Prix HT
var tdPrix = document.createElement('td');
var prixInp = makeInput(r.dataset.prixHt, 'text-end');
tdPrix.appendChild(prixInp);
tr.appendChild(tdPrix);
// TVA
var tdTva = document.createElement('td');
tdTva.appendChild(makeInput(r.dataset.tva, 'text-end'));
tr.appendChild(tdTva);
// Unite
var tdUnit = document.createElement('td');
tdUnit.appendChild(makeSelect(['unite', 'piece', 'lot', 'heure'], 'unite'));
tr.appendChild(tdUnit);
// Remove
var tdDel = document.createElement('td');
var btn = document.createElement('button');
btn.className = 'btn btn-sm btn-link text-danger p-0';
btn.innerHTML = '<i class="bi bi-x-lg"></i>';
btn.onclick = function() {
// uncheck the source row
var srcRow = rows.find(function(rr) { return rr.dataset.id === r.dataset.id; });
if (srcRow) srcRow.querySelector('.job-chk').checked = false;
removeExportRow(r.dataset.id);
};
tdDel.appendChild(btn);
tr.appendChild(tdDel);
exportRows[r.dataset.id] = tr;
expBody.appendChild(tr);
updateEmpty();
}
function removeExportRow(id) {
var tr = exportRows[id];
if (tr) { tr.remove(); delete exportRows[id]; }
updateEmpty();
}
function updateEmpty() {
var count = Object.keys(exportRows).length;
expCount.textContent = count;
btnDl.disabled = count === 0;
if (count === 0) {
if (!expEmpty.parentNode) expBody.appendChild(expEmpty);
} else {
if (expEmpty.parentNode) expEmpty.remove();
}
}
// Checkbox on job rows
rows.forEach(function(r) {
var chk = r.querySelector('.job-chk');
chk.addEventListener('change', function() {
if (chk.checked) addExportRow(r);
else removeExportRow(r.dataset.id);
});
});
// Select all / none visible
document.getElementById('selAll').addEventListener('click', function() {
rows.filter(function(r) { return r.style.display !== 'none'; }).forEach(function(r) {
var chk = r.querySelector('.job-chk');
if (!chk.checked) { chk.checked = true; addExportRow(r); }
});
});
document.getElementById('selNone').addEventListener('click', function() {
rows.forEach(function(r) {
var chk = r.querySelector('.job-chk');
if (chk.checked) { chk.checked = false; removeExportRow(r.dataset.id); }
});
});
chkAll.addEventListener('change', function() {
if (chkAll.checked) document.getElementById('selAll').click();
else document.getElementById('selNone').click();
});
// Filters
fSearch.addEventListener('input', filterRows);
fClient.addEventListener('change', filterRows);
// CSV download
btnDl.addEventListener('click', function() {
var lines = ['"Nom*","Nature*","Prix unitaire HT*","Taux de TVA*","Description","Unite","Reference"'];
Object.values(exportRows).forEach(function(tr) {
var inputs = tr.querySelectorAll('input,select');
var nom = inputs[0].value.replace(/"/g,'""');
var nature = inputs[1].value.replace(/"/g,'""');
var prix = parseFloat(inputs[2].value).toFixed(2).replace('.',',');
var tva = inputs[3].value.replace(/"/g,'""');
var unite = inputs[4].value.replace(/"/g,'""');
lines.push('"'+nom+'","'+nature+'","'+prix+'","'+tva+'%","","'+unite+'",""');
});
var csv = '' + lines.join('\r\n');
var blob = new Blob([csv], {type:'text/csv;charset=utf-8;'});
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'indy_produits_' + new Date().toISOString().slice(0,10) + '.csv';
a.click();
URL.revokeObjectURL(url);
});
updateEmpty();
})();
</script>
{% endblock %}