149 lines
8.6 KiB
HTML
149 lines
8.6 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}Offerte {{ qt.number }}{% endblock %}
|
|
{% macro row(l) %}
|
|
<tr>
|
|
<td data-label="Product">
|
|
<select name="l_product" class="prod form-select form-select-sm" aria-label="Product">
|
|
<option value="">Vrije regel</option>
|
|
{% for p in products %}
|
|
<option value="{{ p.id }}" {{ 'selected' if l.product_id == p.id }}
|
|
data-desc="{{ p.name }}{% if p.description %} {{ p.description }}{% endif %}"
|
|
data-price="{{ p.price }}" data-vat="{{ p.vat|nr|replace(',', '.') }}" data-unit="{{ p.unit }}">{{ p.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</td>
|
|
<td data-label="Omschrijving"><textarea class="form-control form-control-sm" name="l_desc" rows="1" aria-label="Omschrijving">{{ l.description }}</textarea></td>
|
|
<td data-label="Aantal"><input class="form-control form-control-sm" type="number" step="any" inputmode="decimal" name="l_qty" value="{{ l.qty|default(1) }}" aria-label="Aantal"></td>
|
|
<td data-label="Eenheid"><input class="form-control form-control-sm" name="l_unit" value="{{ l.unit }}" aria-label="Eenheid"></td>
|
|
<td data-label="Prijs excl."><input class="form-control form-control-sm" type="number" step="0.01" inputmode="decimal" name="l_price" value="{{ l.price }}" aria-label="Prijs">
|
|
{% if l.product_id and prices.get(l.product_id) is not none and prices[l.product_id] != l.price %}<p class="hint" style="margin:.2rem 0 0">Actueel: {{ prices[l.product_id]|euro }}</p>{% endif %}</td>
|
|
<td data-label="Btw"><select class="form-select form-select-sm" name="l_vat" aria-label="Btw">{% for v in [21, 9, 0] %}<option value="{{ v }}" {{ 'selected' if l.vat|default(21) == v }}>{{ v }}%</option>{% endfor %}</select></td>
|
|
<td class="num amt" data-label="Totaal"></td>
|
|
<td class="del-cell"><button type="button" class="linkbtn del" aria-label="Regel verwijderen">✕</button></td>
|
|
</tr>
|
|
{% endmacro %}
|
|
{% block content %}
|
|
<form method="post">
|
|
<div class="head">
|
|
<div><a class="label" href="{{ url_for('quotes') }}">← Offertes</a><h1>Offerte {{ qt.number }}</h1></div>
|
|
<div class="actions">
|
|
<button class="btn btn-outline-primary" name="then" value="print">Opslaan en afdrukken</button>
|
|
<button class="btn btn-primary">Opslaan</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card"><div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6"><label class="form-label" for="contact">Klant</label>
|
|
<select class="form-select" id="contact" name="contact_id">
|
|
<option value="">— Kies een contact —</option>
|
|
{% for c in contacts %}<option value="{{ c.id }}" {{ 'selected' if c.id == qt.contact_id }}>{{ c.name }}{% if c.company %} ({{ c.company }}){% endif %}</option>{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6"><label class="form-label" for="status">Status</label>
|
|
<select class="form-select" id="status" name="status">{% for k, l in QUOTE_STATUSES %}<option value="{{ k }}" {{ 'selected' if qt.status == k }}>{{ l }}</option>{% endfor %}</select>
|
|
<p class="hint">Op "Verzonden" zetten = zichtbaar in het klantportaal.</p>
|
|
</div>
|
|
<div class="col-6"><label class="form-label" for="date">Datum</label><input class="form-control" id="date" type="date" name="date" value="{{ qt.date }}"></div>
|
|
<div class="col-6"><label class="form-label" for="valid">Geldig tot</label><input class="form-control" id="valid" type="date" name="valid_until" value="{{ qt.valid_until or '' }}"></div>
|
|
<div class="col-12"><label class="form-label" for="title">Onderwerp</label><input class="form-control" id="title" name="title" value="{{ qt.title }}" placeholder="Bijv. Mantelzorgwoning model Amstel"></div>
|
|
<div class="col-12"><label class="form-label" for="intro">Inleiding</label><textarea class="form-control" id="intro" name="intro">{{ qt.intro }}</textarea></div>
|
|
</div>
|
|
</div></div>
|
|
|
|
<div class="card"><div class="card-body">
|
|
<h2>Regels</h2>
|
|
<div class="table-responsive"><table class="table lines">
|
|
<thead><tr><th style="min-width:170px">Product</th><th style="min-width:240px">Omschrijving</th><th style="width:90px">Aantal</th><th style="width:100px">Eenheid</th><th style="width:130px">Prijs excl.</th><th style="width:90px">Btw</th><th class="num" style="width:120px">Totaal</th><th></th></tr></thead>
|
|
<tbody id="rows">{% for l in lines %}{{ row(l) }}{% endfor %}</tbody>
|
|
</table></div>
|
|
<div class="actions"><button type="button" class="btn btn-outline-primary btn-sm" id="add">Regel toevoegen</button></div>
|
|
<table class="totals">
|
|
<tr><td>Subtotaal</td><td class="num" id="t-sub"></td></tr>
|
|
<tbody id="t-vat"></tbody>
|
|
<tr class="grand"><td>Totaal</td><td class="num" id="t-total"></td></tr>
|
|
</table>
|
|
</div></div>
|
|
|
|
<div class="card"><div class="card-body">
|
|
<label class="form-label" for="notes">Opmerkingen en voorwaarden</label>
|
|
<textarea class="form-control" id="notes" name="notes">{{ qt.notes }}</textarea>
|
|
</div></div>
|
|
</form>
|
|
|
|
<div class="card"><div class="card-body">
|
|
<div class="card-top">
|
|
<h2>Bestek uit ontwerp</h2>
|
|
<div class="actions">
|
|
{% if bestek %}
|
|
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('quote_print', qid=qt.id) }}#bestek">Bekijken</a>
|
|
{% if designs %}<a class="btn btn-outline-primary btn-sm" href="{{ url_for('quote_bestek', qid=qt.id) }}">Opnieuw berekenen</a>{% endif %}
|
|
<form method="post" action="{{ url_for('quote_bestek_delete', qid=qt.id) }}" class="inline" onsubmit="return confirm('Bestek loskoppelen? De offerteregels blijven staan.')"><button class="btn btn-outline-secondary btn-sm">Loskoppelen</button></form>
|
|
{% elif designs %}
|
|
<a class="btn btn-primary btn-sm" href="{{ url_for('quote_bestek', qid=qt.id) }}">Bestek maken</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% if bestek %}
|
|
<p class="muted" style="margin:0">{{ bestek.design.title }} · berekend {{ bestek.computedAt[:19]|dt }} · {{ bestek.chapters|length }} hoofdstukken ·
|
|
{{ bestek.totals.exBtw|euro }} excl. btw ({{ bestek.totals.inclBtw|euro }} incl.). Het bestek staat als bijlage bij de offerte, ook in het klantportaal.</p>
|
|
{% elif designs %}
|
|
<p class="muted" style="margin:0">Hoeveelheden en prijzen uit het gekozen model van de klant (gevel, dak, kozijnen, stopcontacten, leidingen, …) als offerteregels en bijlage.</p>
|
|
{% else %}
|
|
<p class="empty" style="padding:0">Kies (en sla op) een klant met een gekozen model om een bestek uit het ontwerp te maken.</p>
|
|
{% endif %}
|
|
</div></div>
|
|
|
|
<form method="post" action="{{ url_for('quote_delete', qid=qt.id) }}" onsubmit="return confirm('Offerte verwijderen?')">
|
|
<button class="btn btn-outline-secondary btn-sm">Offerte verwijderen</button>
|
|
</form>
|
|
|
|
<template id="tpl">{{ row({}) }}</template>
|
|
<script>
|
|
const rows = document.getElementById('rows'), tpl = document.getElementById('tpl');
|
|
const fmt = n => '€ ' + n.toLocaleString('nl-NL', {minimumFractionDigits: 2, maximumFractionDigits: 2});
|
|
const val = (tr, name) => parseFloat(tr.querySelector(`[name=${name}]`).value) || 0;
|
|
function recalc() {
|
|
let sub = 0; const vat = {};
|
|
rows.querySelectorAll('tr').forEach(tr => {
|
|
const amount = val(tr, 'l_qty') * val(tr, 'l_price'), rate = val(tr, 'l_vat');
|
|
sub += amount;
|
|
vat[rate] = (vat[rate] || 0) + amount * rate / 100;
|
|
tr.querySelector('.amt').textContent = fmt(amount);
|
|
});
|
|
document.getElementById('t-sub').textContent = fmt(sub);
|
|
const tv = document.getElementById('t-vat');
|
|
tv.innerHTML = '';
|
|
let total = sub;
|
|
Object.keys(vat).sort((a, b) => b - a).forEach(rate => {
|
|
total += vat[rate];
|
|
const r = tv.insertRow();
|
|
r.insertCell().textContent = `Btw ${rate}%`;
|
|
const c = r.insertCell(); c.className = 'num'; c.textContent = fmt(vat[rate]);
|
|
});
|
|
document.getElementById('t-total').textContent = fmt(total);
|
|
}
|
|
rows.addEventListener('input', recalc);
|
|
rows.addEventListener('change', e => {
|
|
if (e.target.classList.contains('prod') && e.target.value) {
|
|
const o = e.target.selectedOptions[0], tr = e.target.closest('tr');
|
|
tr.querySelector('[name=l_desc]').value = o.dataset.desc;
|
|
tr.querySelector('[name=l_price]').value = o.dataset.price;
|
|
tr.querySelector('[name=l_vat]').value = o.dataset.vat;
|
|
tr.querySelector('[name=l_unit]').value = o.dataset.unit;
|
|
}
|
|
recalc();
|
|
});
|
|
rows.addEventListener('click', e => {
|
|
if (e.target.closest('.del')) { e.target.closest('tr').remove(); recalc(); }
|
|
});
|
|
document.getElementById('add').addEventListener('click', () => {
|
|
const tr = tpl.content.firstElementChild.cloneNode(true);
|
|
rows.appendChild(tr);
|
|
document.dispatchEvent(new CustomEvent('crm:row', { detail: tr }));
|
|
recalc();
|
|
});
|
|
if (!rows.children.length) document.getElementById('add').click(); else recalc();
|
|
</script>
|
|
{% endblock %}
|