v0.0.6: massa-acties en CSV-export in de lijsten van contacten, relaties, producten en offertes

Rijen aanvinken (shift-klik voor een reeks, of alles op de pagina) en in de balk onderin een actie kiezen
(templates/_bulk.html, static/bulk.js). Contacten: status wijzigen, toewijzen, omzetten naar klant, exporteren,
verwijderen; relaties: type wijzigen, exporteren, verwijderen; producten: actief/inactief, exporteren, verwijderen
(in offertes gebruikt → inactief); offertes: status wijzigen (met de gevolgen voor het contact), exporteren,
verwijderen. Exporteer CSV neemt de filters van de lijst mee en levert alle velden; bij contacten ook labels,
toegewezen gebruiker, relatie, model, portaaltoegang en de extra formuliervelden als eigen kolom. Nieuwe
leadstatus Vervallen (telt als afgesloten).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-16 19:12:39 +02:00
co-authored by Claude Opus 5.5
parent 3cf53bc248
commit db3e26d258
9 changed files with 471 additions and 74 deletions
+328 -67
View File
@@ -4,7 +4,7 @@ Starten: python app.py
E-mail ophalen: python app.py sync (bijv. elke 10 minuten via cron)
Demoleads: python app.py demo (standaardmodellen uit demo/modellen als gekozen model)
"""
__version__ = '0.0.5'
__version__ = '0.0.6'
import csv
import hashlib
@@ -48,7 +48,8 @@ DEFAULT_RELATION_TYPES = {'klant': 'Huishouden / familie, Particulier, Zakelijk,
'partner': 'Leverancier, Aannemer, Installateur, Architect / adviseur, Gemeente, Overig'}
RELATION_FIELDS = ['name', 'type', 'email', 'phone', 'website', 'address', 'postcode', 'city', 'kvk', 'notes']
STATUSES = [('nieuw', 'Nieuw'), ('contact', 'In behandeling'), ('offerte', 'Offerte'),
('gewonnen', 'Gewonnen'), ('verloren', 'Verloren')]
('gewonnen', 'Gewonnen'), ('verloren', 'Verloren'), ('vervallen', 'Vervallen')]
CLOSED_STATUSES = ('gewonnen', 'verloren', 'vervallen')
QUOTE_STATUSES = [('concept', 'Concept'), ('verzonden', 'Verzonden'),
('geaccepteerd', 'Geaccepteerd'), ('afgewezen', 'Afgewezen')]
ROLES = [('admin', 'Beheerder'), ('user', 'Medewerker')]
@@ -135,6 +136,43 @@ def back(default):
return ref if ref.startswith(request.host_url) else default
def csv_cell(value):
if value is None:
return ''
if isinstance(value, float):
return fmt_number(value)
return value
def csv_response(name, header, rows):
"""CSV met ; en BOM, zodat Excel kolommen en tekens direct goed leest (en de import hem terugneemt)."""
out = io.StringIO()
writer = csv.writer(out, delimiter=';')
writer.writerow(header)
writer.writerows([csv_cell(v) for v in row] for row in rows)
resp = make_response('\ufeff' + out.getvalue())
resp.headers['Content-Type'] = 'text/csv; charset=utf-8'
resp.headers['Content-Disposition'] = f'attachment; filename={name}-{date.today()}.csv'
return resp
def table_columns(table, hidden=()):
return [r[1] for r in q(f'PRAGMA table_info({table})') if r[1] not in hidden]
def bulk_ids():
"""De aangevinkte rijen van een massa-actie."""
return sorted({i for i in request.form.getlist('ids', type=int) if i})
def id_list(ids):
return f"({', '.join('?' * len(ids))})"
def bulk_back(default):
return redirect(safe_next(request.form.get('next', '')) or default)
# ---------------------------------------------------------------- database
# Kolommen die later zijn bijgekomen; bestaande databases krijgen ze bij het starten.
@@ -712,7 +750,7 @@ def user_edit(uid):
def dashboard():
since = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
line_sum = 'SELECT COALESCE(SUM(l.qty*l.price), 0) FROM quote_lines l JOIN quotes qu ON qu.id=l.quote_id'
open_leads = "type='lead' AND status NOT IN ('gewonnen', 'verloren')"
open_leads = f"type='lead' AND status NOT IN {CLOSED_STATUSES}"
stats = {
'new30': scalar("SELECT COUNT(*) FROM contacts WHERE type='lead' AND created_at >= ?", since),
'open_leads': scalar(f'SELECT COUNT(*) FROM contacts WHERE {open_leads}'),
@@ -756,14 +794,16 @@ def dashboard():
# ---------------------------------------------------------------- contacten
@app.route('/contacts')
def contacts():
term = request.args.get('q', '').strip()
args = {k: request.args.get(k, '') for k in ('type', 'status', 'owner')}
sql = ('SELECT c.*, u.name AS user_name, r.name AS relation_name, r.kind AS relation_kind, f.title AS design_title '
'FROM contacts c LEFT JOIN users u ON u.id=c.assigned_user_id LEFT JOIN relations r ON r.id=c.relation_id '
'LEFT JOIN files f ON f.id=c.design_file_id WHERE 1=1')
params = []
CONTACT_FROM = ('FROM contacts c LEFT JOIN users u ON u.id=c.assigned_user_id '
'LEFT JOIN relations r ON r.id=c.relation_id LEFT JOIN files f ON f.id=c.design_file_id')
CONTACT_HIDDEN = ('portal_token', 'portal_password_hash')
def contacts_filter(source):
"""De filters van de contactenlijst (zoekterm, soort, status, eigenaar) als SQL; ook voor de export."""
term = source.get('q', '').strip()
args = {k: source.get(k, '') for k in ('type', 'status', 'owner')}
sql, params = '', []
if term:
sql += (' AND (c.name LIKE ? OR c.email LIKE ? OR c.company LIKE ? OR c.phone LIKE ? OR c.city LIKE ?'
' OR r.name LIKE ?)')
@@ -779,22 +819,97 @@ def contacts():
params.append(g.user['id'])
elif args['owner'] == 'none':
sql += ' AND c.assigned_user_id IS NULL'
rows = db().execute(sql + ' ORDER BY c.created_at DESC LIMIT 500', params).fetchall()
return term, args, sql, params
@app.route('/contacts')
def contacts():
term, args, where, params = contacts_filter(request.args)
rows = db().execute('SELECT c.*, u.name AS user_name, r.name AS relation_name, r.kind AS relation_kind, '
f'f.title AS design_title {CONTACT_FROM} WHERE 1=1{where} ORDER BY c.created_at DESC LIMIT 500',
params).fetchall()
return render_template('contacts.html', rows=rows, term=term, a=args)
def contacts_csv(where, params):
"""Alle velden van de contacten, met de namen bij de koppelingen en de extra formuliervelden als eigen kolom."""
rows = db().execute(f'SELECT c.*, u.name AS user_name, r.name AS relation_name, f.title AS design_title '
f'{CONTACT_FROM} WHERE 1=1{where} ORDER BY c.id', params).fetchall()
cols = [c for c in table_columns('contacts', CONTACT_HIDDEN) if c != 'extra']
extras = []
for r in rows:
try:
e = json.loads(r['extra'] or '{}')
except ValueError:
e = {}
extras.append(e if isinstance(e, dict) else {})
keys = list(dict.fromkeys(k for e in extras for k in e))
taken = set(cols) | {'soort', 'status_label', 'toegewezen', 'relatie', 'model', 'portaal'}
header = [*cols, 'soort', 'status_label', 'toegewezen', 'relatie', 'model', 'portaal',
*[f'extra_{k}' if k in taken else k for k in keys]]
data = []
for r, e in zip(rows, extras):
data.append([*(r[c] for c in cols), dict(TYPES).get(r['type'], r['type']), dict(STATUSES).get(r['status'], r['status']),
r['user_name'], r['relation_name'], r['design_title'],
'ja' if r['portal_password_hash'] else ('uitgenodigd' if r['portal_token'] else ''),
*(e.get(k) if isinstance(e.get(k), (str, int, float)) or e.get(k) is None
else json.dumps(e[k], ensure_ascii=False) for k in keys)])
return csv_response('contacten', header, data)
@app.route('/contacts/export.csv')
def contacts_export():
out = io.StringIO()
writer = csv.writer(out, delimiter=';')
cols = ['id', 'type', *CONTACT_FIELDS, 'status', 'value', 'created_at']
writer.writerow(cols)
for r in q(f'SELECT {", ".join(cols)} FROM contacts ORDER BY id'):
writer.writerow(list(r))
resp = make_response('' + out.getvalue())
resp.headers['Content-Type'] = 'text/csv; charset=utf-8'
resp.headers['Content-Disposition'] = f'attachment; filename=contacten-{date.today()}.csv'
return resp
_, _, where, params = contacts_filter(request.args)
return contacts_csv(where, params)
def delete_contact(cid):
for f in q('SELECT stored FROM files WHERE contact_id=?', cid):
remove_upload(f['stored'])
db().execute('DELETE FROM contacts WHERE id=?', (cid,))
@app.post('/contacts/bulk')
def contacts_bulk():
ids, action = bulk_ids(), request.form.get('action')
if not ids:
flash('Vink eerst een of meer contacten aan.', 'error')
return bulk_back(url_for('contacts'))
if action == 'export':
return contacts_csv(f' AND c.id IN {id_list(ids)}', ids)
existing = [r['id'] for r in q(f'SELECT id FROM contacts WHERE id IN {id_list(ids)}', *ids)]
if not existing:
flash('Die contacten bestaan niet meer.', 'error')
return bulk_back(url_for('contacts'))
if action == 'status' and request.form.get('status') in dict(STATUSES):
status = request.form['status']
for cid in existing:
set_contact_status(cid, status)
msg = f"{len(existing)} contact(en) op {dict(STATUSES)[status]} gezet."
elif action == 'assign':
uid = request.form.get('user_id', type=int)
user = q1('SELECT id, name FROM users WHERE id=?', uid) if uid else None
changed = [r['id'] for r in q(f'SELECT id FROM contacts WHERE id IN {id_list(existing)} '
'AND assigned_user_id IS NOT ?', *existing, user['id'] if user else None)]
for cid in changed:
db().execute('UPDATE contacts SET assigned_user_id=?, updated_at=? WHERE id=?',
(user['id'] if user else None, now(), cid))
log(cid, 'status', f"Toegewezen aan {user['name'] if user else 'niemand'}")
msg = f"{len(changed)} contact(en) toegewezen aan {user['name'] if user else 'niemand'}."
elif action == 'convert':
for cid in existing:
convert_contact(cid)
msg = f'{len(existing)} contact(en) omgezet naar klant.'
elif action == 'delete':
for cid in existing:
delete_contact(cid)
msg = f'{len(existing)} contact(en) verwijderd.'
else:
flash('Kies een actie.', 'error')
return bulk_back(url_for('contacts'))
db().commit()
flash(msg)
return bulk_back(url_for('contacts'))
def contact_form_values():
@@ -991,9 +1106,7 @@ def contact_portal(cid):
@app.post('/contacts/<int:cid>/delete')
def contact_delete(cid):
for f in q('SELECT stored FROM files WHERE contact_id=?', cid):
remove_upload(f['stored'])
db().execute('DELETE FROM contacts WHERE id=?', (cid,))
delete_contact(cid)
db().commit()
flash('Contact verwijderd.')
return redirect(url_for('contacts'))
@@ -1118,15 +1231,10 @@ def upsert_relation(raw, kind, update_existing=True):
return cur.lastrowid, 'created'
@app.route('/relations')
def relations():
kind = request.args.get('kind') if request.args.get('kind') in dict(RELATION_KINDS) else 'klant'
type_, term = request.args.get('type', ''), request.args.get('q', '').strip()
sql = ('SELECT r.*, (SELECT COUNT(*) FROM contacts WHERE relation_id=r.id) AS members, '
'(SELECT GROUP_CONCAT(name, ", ") FROM (SELECT name FROM contacts WHERE relation_id=r.id LIMIT 4)) AS member_names, '
'(SELECT COUNT(*) FROM products WHERE supplier_id=r.id) AS products '
'FROM relations r WHERE r.kind=?')
params = [kind]
def relations_filter(source):
kind = source.get('kind') if source.get('kind') in dict(RELATION_KINDS) else 'klant'
type_, term = source.get('type', ''), source.get('q', '').strip()
sql, params = ' AND r.kind=?', [kind]
if type_:
sql += ' AND r.type=?'
params.append(type_)
@@ -1134,7 +1242,17 @@ def relations():
sql += (' AND (r.name LIKE ? OR r.city LIKE ? OR r.email LIKE ? OR EXISTS '
'(SELECT 1 FROM contacts c WHERE c.relation_id=r.id AND c.name LIKE ?))')
params += [f'%{term}%'] * 4
rows = db().execute(sql + ' ORDER BY r.name LIMIT 500', params).fetchall()
return kind, type_, term, sql, params
@app.route('/relations')
def relations():
kind, type_, term, where, params = relations_filter(request.args)
sql = ('SELECT r.*, (SELECT COUNT(*) FROM contacts WHERE relation_id=r.id) AS members, '
'(SELECT GROUP_CONCAT(name, ", ") FROM (SELECT name FROM contacts WHERE relation_id=r.id LIMIT 4)) AS member_names, '
'(SELECT COUNT(*) FROM products WHERE supplier_id=r.id) AS products '
'FROM relations r WHERE 1=1')
rows = db().execute(sql + where + ' ORDER BY r.name LIMIT 500', params).fetchall()
counts = {r[0]: r[1] for r in q('SELECT type, COUNT(*) FROM relations WHERE kind=? GROUP BY type', kind)}
return render_template('relations.html', rows=rows, kind=kind, type=type_, term=term,
types=relation_types(kind), counts=counts)
@@ -1210,18 +1328,73 @@ def relation_member_remove(rid, cid):
return redirect(url_for('relation_view', rid=rid))
@app.post('/relations/<int:rid>/delete')
def relation_delete(rid):
r = get_or_404('relations', rid)
def delete_relation(rid):
for c in q('SELECT id FROM contacts WHERE relation_id=?', rid):
link_relation(c['id'], None, '')
db().execute("UPDATE products SET supplier_id=NULL WHERE supplier_id=?", (rid,))
db().execute('DELETE FROM relations WHERE id=?', (rid,))
@app.post('/relations/<int:rid>/delete')
def relation_delete(rid):
r = get_or_404('relations', rid)
delete_relation(rid)
db().commit()
flash('Relatie verwijderd; de contactpersonen en producten zijn bewaard.')
return redirect(url_for('relations', kind=r['kind']))
def relations_csv(where, params):
"""Alle velden van de relaties, met hun contactpersonen (naam, rol, e-mail) en het aantal producten."""
cols = table_columns('relations')
rows = db().execute(
'SELECT r.*, (SELECT GROUP_CONCAT(name || CASE WHEN COALESCE(relation_role, \'\') <> \'\' '
'THEN \' (\' || relation_role || \')\' ELSE \'\' END || CASE WHEN COALESCE(email, \'\') <> \'\' '
'THEN \' <\' || email || \'>\' ELSE \'\' END, \', \') FROM contacts WHERE relation_id=r.id) AS member_list, '
'(SELECT COUNT(*) FROM contacts WHERE relation_id=r.id) AS members, '
'(SELECT COUNT(*) FROM products WHERE supplier_id=r.id) AS products '
f'FROM relations r WHERE 1=1{where} ORDER BY r.kind, r.name', params).fetchall()
header = [*cols, 'aantal_contactpersonen', 'contactpersonen', 'aantal_producten']
return csv_response('relaties', header, [[*(r[c] for c in cols), r['members'], r['member_list'], r['products']]
for r in rows])
@app.route('/relations/export.csv')
def relations_export():
*_, where, params = relations_filter(request.args)
return relations_csv(where, params)
@app.post('/relations/bulk')
def relations_bulk():
ids, action = bulk_ids(), request.form.get('action')
default = url_for('relations', kind=request.form.get('kind') or None)
if not ids:
flash('Vink eerst een of meer relaties aan.', 'error')
return bulk_back(default)
if action == 'export':
return relations_csv(f' AND r.id IN {id_list(ids)}', ids)
rows = q(f'SELECT id, kind FROM relations WHERE id IN {id_list(ids)}', *ids)
if action == 'type':
type_ = request.form.get('type', '').strip()
n = 0
for r in rows:
if type_ in relation_types(r['kind']) or not type_:
db().execute('UPDATE relations SET type=?, updated_at=? WHERE id=?', (type_, now(), r['id']))
n += 1
msg = f"{n} relatie(s) op type {type_ or '(geen)'} gezet."
elif action == 'delete':
for r in rows:
delete_relation(r['id'])
msg = f'{len(rows)} relatie(s) verwijderd; de contactpersonen en producten zijn bewaard.'
else:
flash('Kies een actie.', 'error')
return bulk_back(default)
db().commit()
flash(msg)
return bulk_back(default)
# ---------------------------------------------------------------- import
IMPORT_NAME = re.compile(r'[0-9a-f]{32}\.(csv|json)')
@@ -2379,34 +2552,43 @@ def import_summary(r):
return 'Import klaar: ' + ', '.join(parts) + '.'
@app.route('/products')
def products():
term = request.args.get('q', '').strip()
show = request.args.get('show', 'active')
sql, params = 'SELECT * FROM products WHERE 1=1', []
def products_filter(source):
term = source.get('q', '').strip()
show = source.get('show', 'active')
sql, params = '', []
if term:
sql += ' AND (name LIKE ? OR sku LIKE ? OR supplier LIKE ?)'
params += [f'%{term}%'] * 3
if show in ('active', 'inactive'):
sql += ' AND active=?'
params.append(1 if show == 'active' else 0)
rows = db().execute(sql + ' ORDER BY name', params).fetchall()
return term, show, sql, params
@app.route('/products')
def products():
term, show, where, params = products_filter(request.args)
rows = db().execute(f'SELECT * FROM products WHERE 1=1{where} ORDER BY name', params).fetchall()
return render_template('products.html', rows=rows, term=term, show=show)
def products_csv(where, params):
"""In het formaat van de import (op SKU terug te lezen), met daarachter de overige velden."""
header = ['sku', 'naam', 'omschrijving', 'eenheid', 'prijs', 'btw', 'leverancier', 'actief', 'bijgewerkt',
'id', 'leverancier_id', 'afbeelding', 'aangemaakt', 'in_offertes']
rows = db().execute('SELECT p.*, (SELECT COUNT(DISTINCT quote_id) FROM quote_lines WHERE product_id=p.id) AS used '
f'FROM products p WHERE 1=1{where} ORDER BY sku, name', params).fetchall()
return csv_response('producten', header, [
[p['sku'], p['name'], p['description'], p['unit'], f"{p['price']:.2f}".replace('.', ','),
fmt_number(p['vat']), p['supplier'], 'ja' if p['active'] else 'nee', (p['updated_at'] or p['created_at'])[:10],
p['id'], p['supplier_id'], p['image'], p['created_at'], p['used']] for p in rows])
@app.route('/products/export.csv')
def products_export():
out = io.StringIO()
writer = csv.writer(out, delimiter=';')
writer.writerow(['sku', 'naam', 'omschrijving', 'eenheid', 'prijs', 'btw', 'leverancier', 'actief', 'bijgewerkt'])
for p in q('SELECT * FROM products ORDER BY sku, name'):
writer.writerow([p['sku'], p['name'], p['description'], p['unit'], f"{p['price']:.2f}".replace('.', ','),
fmt_number(p['vat']), p['supplier'], 'ja' if p['active'] else 'nee',
(p['updated_at'] or p['created_at'])[:10]])
resp = make_response('' + out.getvalue())
resp.headers['Content-Type'] = 'text/csv; charset=utf-8'
resp.headers['Content-Disposition'] = f'attachment; filename=producten-{date.today()}.csv'
return resp
# zonder filters (zoals vroeger) alle producten, ook de inactieve
_, _, where, params = products_filter({'show': 'all', **request.args.to_dict()})
return products_csv(where, params)
@app.route('/products/new', methods=['GET', 'POST'], defaults={'pid': None})
@@ -2459,24 +2641,59 @@ def history_for(pid):
'WHERE h.product_id=? ORDER BY h.created_at DESC, h.id DESC LIMIT 100', pid)
def set_product_active(p, active, source):
if bool(p['active']) != active:
db().execute('UPDATE products SET active=?, updated_at=? WHERE id=?', (int(active), now(), p['id']))
product_history(p['id'], 'active', p['active'], int(active), source)
return True
return False
def delete_product(p):
"""Verwijder een product; staat het in offertes, dan wordt het gearchiveerd (koppeling en historie blijven)."""
if scalar('SELECT COUNT(*) FROM quote_lines WHERE product_id=?', p['id']):
set_product_active(p, False, 'Handmatig (gearchiveerd)')
return False
remove_upload(p['image'])
db().execute('DELETE FROM products WHERE id=?', (p['id'],))
return True
@app.post('/products/<int:pid>/delete')
def product_delete(pid):
p = get_or_404('products', pid)
if scalar('SELECT COUNT(*) FROM quote_lines WHERE product_id=?', pid):
# in gebruik in offertes: archiveren zodat de koppeling en de historie blijven bestaan
if p['active']:
db().execute('UPDATE products SET active=0, updated_at=? WHERE id=?', (now(), pid))
product_history(pid, 'active', 1, 0, 'Handmatig (gearchiveerd)')
db().commit()
flash('Dit product staat in offertes en is daarom op inactief gezet in plaats van verwijderd.')
return redirect(url_for('products'))
remove_upload(p['image'])
db().execute('DELETE FROM products WHERE id=?', (pid,))
deleted = delete_product(p)
db().commit()
flash('Product verwijderd.')
flash('Product verwijderd.' if deleted else
'Dit product staat in offertes en is daarom op inactief gezet in plaats van verwijderd.')
return redirect(url_for('products'))
@app.post('/products/bulk')
def products_bulk():
ids, action = bulk_ids(), request.form.get('action')
if not ids:
flash('Vink eerst een of meer producten aan.', 'error')
return bulk_back(url_for('products'))
if action == 'export':
return products_csv(f' AND p.id IN {id_list(ids)}', ids)
rows = q(f'SELECT * FROM products WHERE id IN {id_list(ids)}', *ids)
if action in ('activate', 'deactivate'):
n = sum(set_product_active(p, action == 'activate', 'Handmatig (massa-actie)') for p in rows)
msg = f"{n} product(en) op {'actief' if action == 'activate' else 'inactief'} gezet."
elif action == 'delete':
deleted = sum(delete_product(p) for p in rows)
msg = f'{deleted} product(en) verwijderd.'
if deleted < len(rows):
msg += f' {len(rows) - deleted} staan in offertes en zijn op inactief gezet.'
else:
flash('Kies een actie.', 'error')
return bulk_back(url_for('products'))
db().commit()
flash(msg)
return bulk_back(url_for('products'))
# ---------------------------------------------------------------- offertes
def next_quote_number():
@@ -2496,6 +2713,50 @@ def quotes():
return render_template('quotes.html', rows=rows, status=status)
def quotes_csv(rows):
"""Alle velden van de offertes (zonder het bestek zelf), met klant, aantal regels en de bedragen."""
cols = table_columns('quotes', ('bestek',))
header = [*cols, 'status_label', 'klant', 'klant_email', 'regels', 'bestek', 'subtotaal', 'btw', 'totaal']
extra = {r['id']: r for r in q('SELECT qu.id, c.email, (SELECT COUNT(*) FROM quote_lines WHERE quote_id=qu.id) AS n, '
"COALESCE(qu.bestek, '') <> '' AS has_bestek "
'FROM quotes qu LEFT JOIN contacts c ON c.id=qu.contact_id')}
return csv_response('offertes', header, [
[*(r[c] for c in cols), dict(QUOTE_STATUSES).get(r['status'], r['status']), r['contact_name'],
extra[r['id']]['email'], extra[r['id']]['n'], 'ja' if extra[r['id']]['has_bestek'] else '',
round(r['subtotal'], 2), round(r['total'] - r['subtotal'], 2), round(r['total'], 2)] for r in rows])
@app.route('/quotes/export.csv')
def quotes_export():
status = request.args.get('status', '')
return quotes_csv(quotes_with_totals('WHERE qu.status=?', status) if status else quotes_with_totals())
@app.post('/quotes/bulk')
def quotes_bulk():
ids, action = bulk_ids(), request.form.get('action')
if not ids:
flash('Vink eerst een of meer offertes aan.', 'error')
return bulk_back(url_for('quotes'))
if action == 'export':
return quotes_csv(quotes_with_totals(f'WHERE qu.id IN {id_list(ids)}', *ids))
rows = q(f'SELECT * FROM quotes WHERE id IN {id_list(ids)}', *ids)
if action == 'status' and request.form.get('status') in dict(QUOTE_STATUSES):
status = request.form['status']
for qt in rows:
change_quote_status(qt, status)
msg = f"{len(rows)} offerte(s) op {dict(QUOTE_STATUSES)[status]} gezet."
elif action == 'delete':
db().execute(f'DELETE FROM quotes WHERE id IN {id_list(ids)}', ids)
msg = f'{len(rows)} offerte(s) verwijderd.'
else:
flash('Kies een actie.', 'error')
return bulk_back(url_for('quotes'))
db().commit()
flash(msg)
return bulk_back(url_for('quotes'))
@app.post('/quotes/new')
def quote_new():
cid = request.form.get('contact_id', type=int)
@@ -2930,7 +3191,7 @@ def api_leads():
body += [f'{k}: {v if isinstance(v, str) else json.dumps(v, ensure_ascii=False)}' for k, v in extra.items()]
log(cid, 'form', '\n'.join(body))
if outcome == 'updated':
db().execute("UPDATE contacts SET status='nieuw' WHERE id=? AND status='verloren'", (cid,))
db().execute("UPDATE contacts SET status='nieuw' WHERE id=? AND status IN ('verloren', 'vervallen')", (cid,))
for design in designs:
save_design_json(design, cid)