A local web app for a small room-rental business in Austria: guest register, room ledger, occupancy plan, invoices, the monthly tourist-tax report and the quarterly VAT list. Application code only; it ships no data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
5.7 KiB
HTML
86 lines
5.7 KiB
HTML
{% extends "base.html" %}
|
|
{% block content %}
|
|
<h1>{{ t('quality_title') }}</h1>
|
|
<p class="lead">{{ t('quality_intro') }}</p>
|
|
|
|
<h2 style="margin:26px 0 6px">{{ t('quality_gaps') }}</h2>
|
|
{% macro gaptable(title, rows, cols) %}
|
|
{# rows carrying _kind/_key (set in quality.py) get a per-row "Passt so" button: the finding is
|
|
reviewed-as-correct and stays hidden — keyed by natural key, not by row id #}
|
|
{% set dismissable = rows and rows[0]._kind is defined %}
|
|
<details style="margin:8px 0" {% if rows|length and rows|length <= 12 %}open{% endif %}>
|
|
<summary style="cursor:pointer; font-weight:600">{{ title }}
|
|
<span class="pill {{ 'open' if rows|length else 'paid' }}">{{ rows|length }}</span></summary>
|
|
{% if rows %}
|
|
<div class="tablewrap" style="max-height:280px; margin-top:6px">
|
|
<table>
|
|
<thead><tr>{% for c in cols %}<th>{{ c }}</th>{% endfor %}{% if dismissable %}<th></th>{% endif %}</tr></thead>
|
|
<tbody>
|
|
{% for r in rows[:80] %}
|
|
<tr>{% for c in cols %}<td>{{ r[c] if r[c] is not none else '' }}</td>{% endfor %}
|
|
{% if dismissable %}
|
|
<td><form method="post" action="{{ url_for('quality_dismiss') }}" style="margin:0">
|
|
<input type="hidden" name="kind" value="{{ r._kind }}"><input type="hidden" name="key" value="{{ r._key }}">
|
|
<button class="btn secondary small" type="submit">{{ t('btn_dismiss') }}</button></form></td>
|
|
{% endif %}</tr>
|
|
{% endfor %}
|
|
{% if rows|length > 80 %}<tr><td colspan="{{ cols|length + (1 if dismissable else 0) }}">{{ t('quality_more', n=rows|length - 80) }}</td></tr>{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endif %}
|
|
</details>
|
|
{% endmacro %}
|
|
{{ gaptable(t('gap_depart_before_arrival'), q.gaps.depart_before_arrival,
|
|
['stay_id','source_sheet','name','arrival','departure']) }}
|
|
{{ gaptable(t('gap_charges_without_money'), q.gaps.charges_without_money,
|
|
['charge_id','room','name','period_from','period_to']) }}
|
|
{{ gaptable(t('gap_client_without_address'), q.gaps.client_without_address,
|
|
['name','as_payer','as_employer']) }}
|
|
{{ gaptable(t('gap_address_has_name'), q.gaps.address_has_name,
|
|
['client_id','company','name','address','postcode','city']) }}
|
|
{{ gaptable(t('gap_implausible_qty'), q.gaps.implausible_quantities,
|
|
['charge_id','room','name','period_from','period_to','quantity','amount']) }}
|
|
{{ gaptable(t('gap_rnr_unlinked'), q.gaps.rnr_unlinked, ['charge_id','room','name','period_from','rnr']) }}
|
|
{{ gaptable(t('gap_duplicate_numbers'), q.gaps.duplicate_invoice_numbers, ['unit','number','n','ids']) }}
|
|
{{ gaptable(t('gap_number_year'), q.gaps.number_year_mismatch, ['unit','number','date']) }}
|
|
{{ gaptable(t('gap_open_items'), q.gaps.open_items, ['room','name','period_from','amount']) }}
|
|
{{ gaptable(t('gap_register_ledger'), q.gaps.register_ledger_mismatch,
|
|
['room','name','sheet_number','arrival','departure','ledger_from','ledger_to']) }}
|
|
{{ gaptable(t('gap_gb_ref_unknown'), q.gaps.gb_ref_unknown,
|
|
['charge_id','room','name','period_from','gb_ref','missing']) }}
|
|
{{ gaptable(t('gap_never_billed'), q.gaps.register_never_billed,
|
|
['sheet_number','room','name','arrival','departure']) }}
|
|
{{ gaptable(t('gap_betrag_formula'), q.gaps.betrag_off_formula,
|
|
['room','name','period_from','quantity','gross_per_unit','persons','betrag','calc']) }}
|
|
{{ gaptable(t('gap_tvb_stale_open'), q.gaps.tvb_stale_open, ['room','name','sheet_number','arrival']) }}
|
|
{{ gaptable(t('gap_no_departure'), q.gaps.register_no_departure, ['name','arrival']) }}
|
|
|
|
<h2 style="margin:26px 0 6px">{{ t('quality_suggestions') }}</h2>
|
|
<p style="color:#6b756e; font-size:13px">{{ t('quality_suggestions_hint') }}</p>
|
|
{% for s in q.suggestions.companies %}
|
|
<form method="post" action="{{ url_for('quality_merge_companies') }}" style="margin:4px 0; display:flex; gap:8px; align-items:center"
|
|
onsubmit="return confirm('{{ t('confirm_merge') }}')">
|
|
<input type="hidden" name="keep" value="{{ s.a_id }}"><input type="hidden" name="drop" value="{{ s.b_id }}">
|
|
<span><b>{{ s.a }}</b> ~ <b>{{ s.b }}</b></span>
|
|
<button class="btn secondary small" type="submit">{{ t('btn_merge') }} → {{ s.a }}</button>
|
|
<a class="btn secondary small" href="{{ url_for('client_edit', company_id=s.a_id) }}">{{ t('btn_open') }}</a>
|
|
<button class="btn secondary small" type="submit" form="dp-c{{ loop.index }}">{{ t('btn_dismiss') }}</button>
|
|
</form>
|
|
<form id="dp-c{{ loop.index }}" method="post" action="{{ url_for('quality_dismiss') }}" style="display:none">
|
|
<input type="hidden" name="kind" value="{{ s._kind }}"><input type="hidden" name="key" value="{{ s._key }}"></form>
|
|
{% else %}<p>{{ t('quality_no_company_suggestions') }}</p>{% endfor %}
|
|
|
|
{% for s in q.suggestions.guests %}
|
|
<form method="post" action="{{ url_for('quality_merge_guests') }}" style="margin:4px 0; display:flex; gap:8px; align-items:center"
|
|
onsubmit="return confirm('{{ t('confirm_merge') }}')">
|
|
<input type="hidden" name="keep" value="{{ s.a_id }}"><input type="hidden" name="drop" value="{{ s.b_id }}">
|
|
<span>{{ t('quality_guest') }}: <b>{{ s.a }}</b> ~ <b>{{ s.b }}</b></span>
|
|
<button class="btn secondary small" type="submit">{{ t('btn_merge') }} → {{ s.a }}</button>
|
|
<button class="btn secondary small" type="submit" form="dp-g{{ loop.index }}">{{ t('btn_dismiss') }}</button>
|
|
</form>
|
|
<form id="dp-g{{ loop.index }}" method="post" action="{{ url_for('quality_dismiss') }}" style="display:none">
|
|
<input type="hidden" name="kind" value="{{ s._kind }}"><input type="hidden" name="key" value="{{ s._key }}"></form>
|
|
{% endfor %}
|
|
{% endblock %}
|