297 lines
11 KiB
HTML
297 lines
11 KiB
HTML
{% 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 & 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 %}
|