feat: export Indy — selection commandes, CSV produits/services, acronyme client
Deploy via Portainer / deploy (push) Successful in 0s

This commit is contained in:
Jo
2026-07-17 00:15:26 +02:00
parent 38fc4ed565
commit a82dcc0c92
4 changed files with 339 additions and 9 deletions
+296
View File
@@ -0,0 +1,296 @@
{% 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 %}