feat: export Indy — selection commandes, CSV produits/services, acronyme client
Deploy via Portainer / deploy (push) Successful in 0s
Deploy via Portainer / deploy (push) Successful in 0s
This commit is contained in:
@@ -309,6 +309,7 @@ def init_db():
|
|||||||
'ALTER TABLE jobs ADD COLUMN meshy_credits_used INTEGER DEFAULT 0',
|
'ALTER TABLE jobs ADD COLUMN meshy_credits_used INTEGER DEFAULT 0',
|
||||||
'ALTER TABLE jobs ADD COLUMN meshy_cost REAL DEFAULT 0',
|
'ALTER TABLE jobs ADD COLUMN meshy_cost REAL DEFAULT 0',
|
||||||
'ALTER TABLE jobs ADD COLUMN meshy_margin_pct REAL DEFAULT 0',
|
'ALTER TABLE jobs ADD COLUMN meshy_margin_pct REAL DEFAULT 0',
|
||||||
|
'ALTER TABLE clients ADD COLUMN acronyme TEXT DEFAULT ""',
|
||||||
]
|
]
|
||||||
for sql in migrations:
|
for sql in migrations:
|
||||||
try:
|
try:
|
||||||
@@ -845,10 +846,11 @@ def new_client():
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
def _int(k): return int(request.form[k]) if request.form.get(k) else None
|
def _int(k): return int(request.form[k]) if request.form.get(k) else None
|
||||||
conn.execute('''INSERT INTO clients
|
conn.execute('''INSERT INTO clients
|
||||||
(name,email,notes,default_machine_profile_id,default_handling_profile_id,
|
(name,acronyme,email,notes,default_machine_profile_id,default_handling_profile_id,
|
||||||
default_material_profile_id,default_pricing_profile_id)
|
default_material_profile_id,default_pricing_profile_id)
|
||||||
VALUES(?,?,?,?,?,?,?)''',
|
VALUES(?,?,?,?,?,?,?,?)''',
|
||||||
(request.form['name'], request.form.get('email',''), request.form.get('notes',''),
|
(request.form['name'], request.form.get('acronyme','').upper().strip(),
|
||||||
|
request.form.get('email',''), request.form.get('notes',''),
|
||||||
_int('default_machine_profile_id'), _int('default_handling_profile_id'),
|
_int('default_machine_profile_id'), _int('default_handling_profile_id'),
|
||||||
_int('default_material_profile_id'), _int('default_pricing_profile_id')))
|
_int('default_material_profile_id'), _int('default_pricing_profile_id')))
|
||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
@@ -863,10 +865,11 @@ def edit_client(id):
|
|||||||
client = conn.execute('SELECT * FROM clients WHERE id=?',(id,)).fetchone()
|
client = conn.execute('SELECT * FROM clients WHERE id=?',(id,)).fetchone()
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
def _int(k): return int(request.form[k]) if request.form.get(k) else None
|
def _int(k): return int(request.form[k]) if request.form.get(k) else None
|
||||||
conn.execute('''UPDATE clients SET name=?,email=?,notes=?,
|
conn.execute('''UPDATE clients SET name=?,acronyme=?,email=?,notes=?,
|
||||||
default_machine_profile_id=?,default_handling_profile_id=?,
|
default_machine_profile_id=?,default_handling_profile_id=?,
|
||||||
default_material_profile_id=?,default_pricing_profile_id=? WHERE id=?''',
|
default_material_profile_id=?,default_pricing_profile_id=? WHERE id=?''',
|
||||||
(request.form['name'], request.form.get('email',''), request.form.get('notes',''),
|
(request.form['name'], request.form.get('acronyme','').upper().strip(),
|
||||||
|
request.form.get('email',''), request.form.get('notes',''),
|
||||||
_int('default_machine_profile_id'), _int('default_handling_profile_id'),
|
_int('default_machine_profile_id'), _int('default_handling_profile_id'),
|
||||||
_int('default_material_profile_id'), _int('default_pricing_profile_id'), id))
|
_int('default_material_profile_id'), _int('default_pricing_profile_id'), id))
|
||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
@@ -897,6 +900,25 @@ def delete_client(id):
|
|||||||
conn.commit(); conn.close()
|
conn.commit(); conn.close()
|
||||||
return redirect(url_for('clients'))
|
return redirect(url_for('clients'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/export/indy')
|
||||||
|
def export_indy():
|
||||||
|
"""Page d'export des commandes au format Indy (produits/services)."""
|
||||||
|
conn = get_db()
|
||||||
|
jobs = conn.execute('''
|
||||||
|
SELECT j.id, j.name, j.created_at, j.final_price, j.discount_pct,
|
||||||
|
j.tva_amount, j.order_qty, j.pieces_per_plate, j.plate_name,
|
||||||
|
j.description,
|
||||||
|
c.name as client_name, c.acronyme as client_acronyme
|
||||||
|
FROM jobs j
|
||||||
|
LEFT JOIN clients c ON j.client_id = c.id
|
||||||
|
ORDER BY j.created_at DESC
|
||||||
|
''').fetchall()
|
||||||
|
s = get_settings()
|
||||||
|
tva_pct = float(s.get('tva_rate_pct', 20.0))
|
||||||
|
conn.close()
|
||||||
|
return render_template('export_indy.html', jobs=jobs, tva_pct=tva_pct)
|
||||||
|
|
||||||
# ─── Projets ──────────────────────────────────────────────────────────────────
|
# ─── Projets ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.route('/projects')
|
@app.route('/projects')
|
||||||
|
|||||||
@@ -213,6 +213,9 @@
|
|||||||
<a href="{{ url_for('stats') }}" class="{% if request.endpoint=='stats' %}active{% endif %}" title="Statistiques">
|
<a href="{{ url_for('stats') }}" class="{% if request.endpoint=='stats' %}active{% endif %}" title="Statistiques">
|
||||||
<i class="bi bi-bar-chart-line"></i><span class="nav-label"> Statistiques</span>
|
<i class="bi bi-bar-chart-line"></i><span class="nav-label"> Statistiques</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('export_indy') }}" class="{% if request.endpoint == 'export_indy' %}active{% endif %}" title="Export Indy">
|
||||||
|
<i class="bi bi-file-earmark-arrow-down"></i><span class="nav-label"> Export Indy</span>
|
||||||
|
</a>
|
||||||
<a href="{{ url_for('export_csv') }}" title="Export CSV">
|
<a href="{{ url_for('export_csv') }}" title="Export CSV">
|
||||||
<i class="bi bi-download"></i><span class="nav-label"> Export CSV</span>
|
<i class="bi bi-download"></i><span class="nav-label"> Export CSV</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -15,11 +15,20 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-8">
|
||||||
<label class="form-label fw-semibold">Nom *</label>
|
<label class="form-label fw-semibold">Nom *</label>
|
||||||
<input type="text" name="name" class="form-control" required
|
<input type="text" name="name" class="form-control" required
|
||||||
value="{{ client.name if client else '' }}" placeholder="ex: Blockcorp">
|
value="{{ client.name if client else '' }}" placeholder="ex: Blockcorp">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label fw-semibold">Acronyme <span class="text-muted fw-normal">(export Indy)</span></label>
|
||||||
|
<input type="text" name="acronyme" class="form-control text-uppercase"
|
||||||
|
maxlength="10" value="{{ client.acronyme if client else '' }}"
|
||||||
|
placeholder="ex: BLK"
|
||||||
|
oninput="this.value=this.value.toUpperCase()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Email</label>
|
<label class="form-label fw-semibold">Email</label>
|
||||||
<input type="email" name="email" class="form-control"
|
<input type="email" name="email" class="form-control"
|
||||||
|
|||||||
@@ -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 & 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 %}
|
||||||
Reference in New Issue
Block a user